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"; /** * 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>; toggleFullscreen: () => void; containerRef?: React.MutableRefObject; }; export default function DynamicVideoPlayer({ className, camera, timeRange, cameraPreviews, startTimestamp, isScrubbing, hotKeys, fullscreen, onControllerReady, onTimestampUpdate, onClipEnded, setFullResolution, toggleFullscreen, containerRef, }: 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 [isBuffering, setIsBuffering] = 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); } if (isBuffering) { setIsBuffering(false); } onTimestampUpdate(controller.getProgress(time)); }, [controller, onTimestampUpdate, isBuffering, isLoading, isScrubbing], ); 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( () => ({ before: timeRange.before, after: timeRange.after, }), [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); } setNoRecording(false); }} setFullResolution={setFullResolution} onUploadFrame={onUploadFrameToPlus} toggleFullscreen={toggleFullscreen} onError={(error) => { if (error == "stalled" && !isScrubbing) { setIsBuffering(true); } }} /> setPreviewController(previewController) } /> {!isScrubbing && (isLoading || isBuffering) && !noRecording && ( )} {!isScrubbing && !isLoading && noRecording && (
No recordings found for this time
)} ); }