import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useApiHost } from "@/api"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { Recording } from "@/types/record"; import { Preview } from "@/types/preview"; import PreviewPlayer, { PreviewController } from "../PreviewPlayer"; import { DynamicVideoController } from "./DynamicVideoController"; import HlsVideoPlayer from "../HlsVideoPlayer"; import { TimeRange } from "@/types/timeline"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { VideoResolutionType } from "@/types/live"; import axios from "axios"; import { cn } from "@/lib/utils"; import { getTimestampOffset } from "@/utils/dateUtil"; /** * Dynamically switches between video playback and scrubbing preview player. */ type DynamicVideoPlayerProps = { className?: string; camera: string; timeRange: TimeRange; cameraPreviews: Preview[]; startTimestamp?: number; isScrubbing: boolean; hotKeys: boolean; fullscreen: boolean; onControllerReady: (controller: DynamicVideoController) => void; onTimestampUpdate?: (timestamp: number) => void; onClipEnded?: () => void; setFullResolution: React.Dispatch>; setFullscreen: (full: boolean) => void; }; export default function DynamicVideoPlayer({ className, camera, timeRange, cameraPreviews, startTimestamp, isScrubbing, hotKeys, fullscreen, onControllerReady, onTimestampUpdate, onClipEnded, setFullResolution, setFullscreen, }: DynamicVideoPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); // controlling playback const playerRef = useRef(null); const [previewController, setPreviewController] = useState(null); const [noRecording, setNoRecording] = useState(false); const controller = useMemo(() => { if (!config || !playerRef.current || !previewController) { return undefined; } return new DynamicVideoController( camera, playerRef.current, previewController, (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, isScrubbing ? "scrubbing" : "playback", setNoRecording, () => {}, ); // we only want to fire once when players are ready // eslint-disable-next-line react-hooks/exhaustive-deps }, [camera, config, playerRef.current, previewController]); useEffect(() => { if (!controller) { return; } if (controller) { onControllerReady(controller); } // we only want to fire once when players are ready // eslint-disable-next-line react-hooks/exhaustive-deps }, [controller]); // initial state const [isLoading, setIsLoading] = useState(false); const [loadingTimeout, setLoadingTimeout] = useState(); const [source, setSource] = useState( `${apiHost}vod/${camera}/start/${timeRange.after}/end/${timeRange.before}/master.m3u8`, ); // start at correct time useEffect(() => { if (!isScrubbing) { setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000)); } return () => { if (loadingTimeout) { clearTimeout(loadingTimeout); } }; // we only want trigger when scrubbing state changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [camera, isScrubbing]); const onPlayerLoaded = useCallback(() => { if (!controller || !startTimestamp) { return; } controller.seekToTimestamp(startTimestamp, true); }, [startTimestamp, controller]); const onTimeUpdate = useCallback( (time: number) => { if (isScrubbing || !controller || !onTimestampUpdate || time == 0) { return; } if (isLoading) { setIsLoading(false); } onTimestampUpdate(controller.getProgress(time)); }, [controller, onTimestampUpdate, isScrubbing, isLoading], ); const onUploadFrameToPlus = useCallback( (playTime: number) => { if (!controller) { return; } const time = controller.getProgress(playTime); return axios.post(`/${camera}/plus/${time}`); }, [camera, controller], ); // state of playback player const recordingParams = useMemo(() => { const timeRangeOffset = getTimestampOffset(timeRange.before); return { before: timeRange.before + timeRangeOffset, after: timeRange.after + timeRangeOffset, }; }, [timeRange]); const { data: recordings } = useSWR( [`${camera}/recordings`, recordingParams], { revalidateOnFocus: false }, ); useEffect(() => { if (!controller || !recordings) { return; } if (playerRef.current) { playerRef.current.autoplay = !isScrubbing; } setSource( `${apiHost}vod/${camera}/start/${recordingParams.after}/end/${recordingParams.before}/master.m3u8`, ); setLoadingTimeout(setTimeout(() => setIsLoading(true), 1000)); controller.newPlayback({ recordings: recordings ?? [], timeRange, }); // we only want this to change when recordings update // eslint-disable-next-line react-hooks/exhaustive-deps }, [controller, recordings]); return ( <> { if (isScrubbing) { playerRef.current?.pause(); } if (loadingTimeout) { clearTimeout(loadingTimeout); } setIsLoading(false); setNoRecording(false); }} setFullResolution={setFullResolution} onUploadFrame={onUploadFrameToPlus} setFullscreen={setFullscreen} /> { setPreviewController(previewController); }} /> {!isScrubbing && isLoading && !noRecording && ( )} {!isScrubbing && noRecording && (
No recordings found for this time
)} ); }