import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import TimelineEventOverlay from "../../overlay/TimelineDataOverlay"; 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 { Timeline } from "@/types/timeline"; /** * Dynamically switches between video playback and scrubbing preview player. */ type DynamicVideoPlayerProps = { className?: string; camera: string; timeRange: { start: number; end: number }; cameraPreviews: Preview[]; startTimestamp?: number; isScrubbing: boolean; onControllerReady: (controller: DynamicVideoController) => void; onTimestampUpdate?: (timestamp: number) => void; onClipEnded?: () => void; }; export default function DynamicVideoPlayer({ className, camera, timeRange, cameraPreviews, startTimestamp, isScrubbing, onControllerReady, onTimestampUpdate, onClipEnded, }: DynamicVideoPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); // playback behavior const grow = useMemo(() => { if (!config) { return "aspect-video"; } const aspectRatio = config.cameras[camera].detect.width / config.cameras[camera].detect.height; if (aspectRatio > 2) { return ""; } else if (aspectRatio < 16 / 9) { return "aspect-tall"; } else { return "aspect-video"; } }, [camera, config]); // controlling playback const playerRef = useRef(null); const [previewController, setPreviewController] = useState(null); const [focusedItem, setFocusedItem] = useState( undefined, ); 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", setFocusedItem, ); // 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 [source, setSource] = useState( `${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`, ); // start at correct time useEffect(() => { if (isScrubbing) { setIsLoading(true); } }, [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; } onTimestampUpdate(controller.getProgress(time)); }, [controller, onTimestampUpdate, isScrubbing], ); // state of playback player const recordingParams = useMemo(() => { return { before: timeRange.end, after: timeRange.start, }; }, [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/${timeRange.start}/end/${timeRange.end}/master.m3u8`, ); setIsLoading(true); controller.newPlayback({ recordings: recordings ?? [], }); // 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(); } setIsLoading(false); }} > {config && focusedItem && ( )} { setPreviewController(previewController); }} />
); }