From 6a9a6626f4fa07316e53c102c113f68ba6d44f0f Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 9 Mar 2024 08:58:48 -0700 Subject: [PATCH] Fix handling of recordings and switching cameras --- .../components/player/DynamicVideoPlayer.tsx | 101 ++++++++++-------- web/src/views/events/RecordingView.tsx | 16 +-- 2 files changed, 62 insertions(+), 55 deletions(-) diff --git a/web/src/components/player/DynamicVideoPlayer.tsx b/web/src/components/player/DynamicVideoPlayer.tsx index 13911d8c3..1e099b383 100644 --- a/web/src/components/player/DynamicVideoPlayer.tsx +++ b/web/src/components/player/DynamicVideoPlayer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import VideoPlayer from "./VideoPlayer"; import Player from "video.js/dist/types/player"; import TimelineEventOverlay from "../overlay/TimelineDataOverlay"; @@ -24,6 +24,7 @@ type DynamicVideoPlayerProps = { timeRange: { start: number; end: number }; cameraPreviews: Preview[]; previewOnly?: boolean; + startTime?: number; onControllerReady: (controller: DynamicVideoController) => void; onClick?: () => void; }; @@ -33,6 +34,7 @@ export default function DynamicVideoPlayer({ timeRange, cameraPreviews, previewOnly = false, + startTime, onControllerReady, onClick, }: DynamicVideoPlayerProps) { @@ -59,7 +61,7 @@ export default function DynamicVideoPlayer({ // controlling playback - const playerRef = useRef(null); + const [playerRef, setPlayerRef] = useState(null); const [previewController, setPreviewController] = useState(null); const [isScrubbing, setIsScrubbing] = useState(previewOnly); @@ -67,13 +69,13 @@ export default function DynamicVideoPlayer({ undefined, ); const controller = useMemo(() => { - if (!config || !playerRef.current || !previewController) { + if (!config || !playerRef || !previewController) { return undefined; } return new DynamicVideoController( camera, - playerRef.current, + playerRef, previewController, (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, previewOnly ? "scrubbing" : "playback", @@ -82,7 +84,7 @@ export default function DynamicVideoPlayer({ ); // we only want to fire once when players are ready // eslint-disable-next-line react-hooks/exhaustive-deps - }, [camera, config, playerRef.current, previewController]); + }, [camera, config, playerRef, previewController]); useEffect(() => { if (!controller) { @@ -100,7 +102,7 @@ export default function DynamicVideoPlayer({ const [initPreviewOnly, setInitPreviewOnly] = useState(previewOnly); useEffect(() => { - if (!controller || !playerRef.current) { + if (!controller || !playerRef) { return; } @@ -109,7 +111,7 @@ export default function DynamicVideoPlayer({ } if (!previewOnly) { - controller.seekToTimestamp(playerRef.current.currentTime() || 0, true); + controller.seekToTimestamp(playerRef.currentTime() || 0, true); } setInitPreviewOnly(previewOnly); @@ -121,40 +123,40 @@ export default function DynamicVideoPlayer({ const onKeyboardShortcut = useCallback( (key: string, down: boolean, repeat: boolean) => { - if (!playerRef.current || previewOnly) { + if (!playerRef || previewOnly) { return; } switch (key) { case "ArrowLeft": if (down) { - const currentTime = playerRef.current.currentTime(); + const currentTime = playerRef.currentTime(); if (currentTime) { - playerRef.current.currentTime(Math.max(0, currentTime - 5)); + playerRef.currentTime(Math.max(0, currentTime - 5)); } } break; case "ArrowRight": if (down) { - const currentTime = playerRef.current.currentTime(); + const currentTime = playerRef.currentTime(); if (currentTime) { - playerRef.current.currentTime(currentTime + 5); + playerRef.currentTime(currentTime + 5); } } break; case "m": if (down && !repeat && playerRef) { - playerRef.current.muted(!playerRef.current.muted()); + playerRef.muted(!playerRef.muted()); } break; case " ": if (down && playerRef) { - if (playerRef.current.paused()) { - playerRef.current.play(); + if (playerRef.paused()) { + playerRef.play(); } else { - playerRef.current.pause(); + playerRef.pause(); } } break; @@ -162,7 +164,7 @@ export default function DynamicVideoPlayer({ }, // only update when preview only changes // eslint-disable-next-line react-hooks/exhaustive-deps - [playerRef.current, previewOnly], + [playerRef, previewOnly], ); useKeyboardListener( ["ArrowLeft", "ArrowRight", "m", " "], @@ -186,6 +188,35 @@ export default function DynamicVideoPlayer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // start at correct time + + useEffect(() => { + if (previewOnly || !startTime) { + return; + } + + const player = playerRef; + + if (!player) { + return; + } + + if (player.isReady_) { + controller?.seekToTimestamp(startTime, true); + return; + } + + const callback = () => { + controller?.seekToTimestamp(startTime, true); + }; + player.on("loadeddata", callback); + return () => { + player.off("loadeddata", callback); + }; + // we only want to calculate this once + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [previewOnly, startTime, controller]); + // state of playback player const recordingParams = useMemo(() => { @@ -236,7 +267,7 @@ export default function DynamicVideoPlayer({ { - playerRef.current = player; + setPlayerRef(player); }} onDispose={() => { - playerRef.current = null; + setPlayerRef(null); }} > {config && focusedItem && ( @@ -289,12 +320,10 @@ export class DynamicVideoController { private recordings: Recording[] = []; private annotationOffset: number; private timeToStart: number | undefined = undefined; - private canPlay: boolean = false; // listeners private playerProgressListener: (() => void) | null = null; private playerEndedListener: (() => void) | null = null; - private canPlayListener: (() => void) | null = null; constructor( camera: string, @@ -320,7 +349,6 @@ export class DynamicVideoController { src: newPlayback.playbackUri, type: "application/vnd.apple.mpegurl", }); - this.canPlay = false; if (this.timeToStart) { this.seekToTimestamp(this.timeToStart); @@ -328,10 +356,6 @@ export class DynamicVideoController { } } - autoPlay(play: boolean) { - this.playerController.autoplay(play); - } - seekToTimestamp(time: number, play: boolean = false) { if (this.playerMode != "playback") { this.playerMode = "playback"; @@ -391,21 +415,6 @@ export class DynamicVideoController { return timestamp; } - onCanPlay(listener: (() => void) | null) { - if (this.canPlayListener) { - this.playerController.off("canplay", this.canPlayListener); - this.canPlayListener = null; - } - - if (listener) { - this.canPlayListener = () => { - this.canPlay = true; - listener(); - }; - this.playerController.on("canplay", this.canPlayListener); - } - } - onPlayerTimeUpdate(listener: ((timestamp: number) => void) | null) { if (this.playerProgressListener) { this.playerController.off("timeupdate", this.playerProgressListener); @@ -414,9 +423,13 @@ export class DynamicVideoController { if (listener) { this.playerProgressListener = () => { - if (this.canPlay) { - listener(this.getProgress(this.playerController.currentTime() || 0)); + const progress = this.playerController.currentTime() || 0; + + if (progress == 0) { + return; } + + listener(this.getProgress(progress)); }; this.playerController.on("timeupdate", this.playerProgressListener); } diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 89dff8b40..8a4bf5f4e 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -40,6 +40,8 @@ export function DesktopRecordingView({ {}, ); + const [playbackStart, setPlaybackStart] = useState(startTime); + // timeline time const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]); @@ -119,13 +121,7 @@ export function DesktopRecordingView({ const newController = videoPlayersRef.current[newCam]; lastController.onPlayerTimeUpdate(null); lastController.onClipChangedEvent(null); - lastController.autoPlay(false); lastController.scrubToTimestamp(currentTime); - newController.autoPlay(true); - newController.onCanPlay(() => { - newController.seekToTimestamp(currentTime, true); - newController.onCanPlay(null); - }); newController.onPlayerTimeUpdate((timestamp: number) => { setCurrentTime(timestamp); @@ -137,6 +133,8 @@ export function DesktopRecordingView({ } }); }); + newController.seekToTimestamp(currentTime, true); + setPlaybackStart(currentTime); setMainCamera(newCam); }, [allCameras, currentTime, mainCamera], @@ -179,6 +177,7 @@ export function DesktopRecordingView({ camera={cam} timeRange={currentTimeRange} cameraPreviews={allPreviews ?? []} + startTime={playbackStart} onControllerReady={(controller) => { videoPlayersRef.current[cam] = controller; controller.onPlayerTimeUpdate((timestamp: number) => { @@ -192,11 +191,6 @@ export function DesktopRecordingView({ } }); }); - - controller.onCanPlay(() => { - controller.seekToTimestamp(startTime, true); - controller.onCanPlay(null); - }); }} />