From 39a29d148e80e49ada8142e976922d4c95e57197 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:28:06 -0500 Subject: [PATCH] Recording scrubbing fixes (#10439) * use a single source of truth for scrubbing * simplify controller state * Cleanup scrubbing logic * Apply same logic to mobile --------- Co-authored-by: Nicolas Mowen --- .../player/dynamic/DynamicVideoController.ts | 13 +-- .../player/dynamic/DynamicVideoPlayer.tsx | 10 ++- web/src/views/events/RecordingView.tsx | 90 +++++++++++-------- 3 files changed, 63 insertions(+), 50 deletions(-) diff --git a/web/src/components/player/dynamic/DynamicVideoController.ts b/web/src/components/player/dynamic/DynamicVideoController.ts index 45101c27f..2b3956db7 100644 --- a/web/src/components/player/dynamic/DynamicVideoController.ts +++ b/web/src/components/player/dynamic/DynamicVideoController.ts @@ -9,7 +9,6 @@ export class DynamicVideoController { public camera = ""; private playerController: HTMLVideoElement; private previewController: PreviewController; - private setScrubbing: (isScrubbing: boolean) => void; private setFocusedItem: (timeline: Timeline) => void; private playerMode: PlayerMode = "playback"; @@ -24,7 +23,6 @@ export class DynamicVideoController { previewController: PreviewController, annotationOffset: number, defaultMode: PlayerMode, - setScrubbing: (isScrubbing: boolean) => void, setFocusedItem: (timeline: Timeline) => void, ) { this.camera = camera; @@ -32,7 +30,6 @@ export class DynamicVideoController { this.previewController = previewController; this.annotationOffset = annotationOffset; this.playerMode = defaultMode; - this.setScrubbing = setScrubbing; this.setFocusedItem = setFocusedItem; } @@ -50,11 +47,6 @@ export class DynamicVideoController { } seekToTimestamp(time: number, play: boolean = false) { - if (this.playerMode != "playback") { - this.playerMode = "playback"; - this.setScrubbing(false); - } - if ( this.recordings.length == 0 || time < this.recordings[0].start_time || @@ -64,6 +56,10 @@ export class DynamicVideoController { return; } + if (this.playerMode != "playback") { + this.playerMode = "playback"; + } + let seekSeconds = 0; (this.recordings || []).every((segment) => { // if the next segment is past the desired time, stop calculating @@ -126,7 +122,6 @@ export class DynamicVideoController { if (scrubResult && this.playerMode != "scrubbing") { this.playerMode = "scrubbing"; this.playerController.pause(); - this.setScrubbing(true); } } diff --git a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx index 96fbc80fa..da169a838 100644 --- a/web/src/components/player/dynamic/DynamicVideoPlayer.tsx +++ b/web/src/components/player/dynamic/DynamicVideoPlayer.tsx @@ -21,6 +21,7 @@ type DynamicVideoPlayerProps = { onControllerReady: (controller: DynamicVideoController) => void; onTimestampUpdate?: (timestamp: number) => void; onClipEnded?: () => void; + isScrubbing: boolean; }; export default function DynamicVideoPlayer({ className, @@ -31,6 +32,7 @@ export default function DynamicVideoPlayer({ onControllerReady, onTimestampUpdate, onClipEnded, + isScrubbing, }: DynamicVideoPlayerProps) { const apiHost = useApiHost(); const { data: config } = useSWR("config"); @@ -53,7 +55,6 @@ export default function DynamicVideoPlayer({ const playerRef = useRef(null); const [previewController, setPreviewController] = useState(null); - const [isScrubbing, setIsScrubbing] = useState(false); const [focusedItem, setFocusedItem] = useState( undefined, ); @@ -67,8 +68,7 @@ export default function DynamicVideoPlayer({ playerRef.current, previewController, (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, - "playback", - setIsScrubbing, + isScrubbing ? "scrubbing" : "playback", setFocusedItem, ); // we only want to fire once when players are ready @@ -133,6 +133,10 @@ export default function DynamicVideoPlayer({ return; } + if (playerRef.current) { + playerRef.current.autoplay = !isScrubbing; + } + setSource( `${apiHost}vod/${camera}/start/${timeRange.start}/end/${timeRange.end}/master.m3u8`, ); diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 2bf96daaf..ff5bfbac6 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -89,13 +89,16 @@ export function DesktopRecordingView({ const [playerTime, setPlayerTime] = useState(startTime); const updateSelectedSegment = useCallback( - (currentTime: number) => { + (currentTime: number, updateStartTime: boolean) => { const index = timeRange.ranges.findIndex( (seg) => seg.start <= currentTime && seg.end >= currentTime, ); if (index != -1) { - setPlaybackStart(currentTime); + if (updateStartTime) { + setPlaybackStart(currentTime); + } + setSelectedRangeIdx(index); } }, @@ -108,7 +111,7 @@ export function DesktopRecordingView({ currentTime > currentTimeRange.end + 60 || currentTime < currentTimeRange.start - 60 ) { - updateSelectedSegment(currentTime); + updateSelectedSegment(currentTime, false); return; } @@ -126,40 +129,22 @@ export function DesktopRecordingView({ updateSelectedSegment, ]); - useEffect(() => { - if (!scrubbing) { - if ( - currentTimeRange.start <= currentTime && - currentTimeRange.end >= currentTime - ) { - mainControllerRef.current?.seekToTimestamp(currentTime, true); - } else { - updateSelectedSegment(currentTime); - } - } - - // we only want to seek when user stops scrubbing - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scrubbing]); - useEffect(() => { if (!scrubbing) { if (Math.abs(currentTime - playerTime) > 10) { - mainControllerRef.current?.pause(); - if ( currentTimeRange.start <= currentTime && currentTimeRange.end >= currentTime ) { mainControllerRef.current?.seekToTimestamp(currentTime, true); } else { - updateSelectedSegment(currentTime); + updateSelectedSegment(currentTime, true); } } } // we only want to seek when current time doesn't match the player update time // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentTime]); + }, [currentTime, scrubbing]); const onSelectCamera = useCallback( (newCam: string) => { @@ -234,6 +219,7 @@ export function DesktopRecordingView({ onControllerReady={(controller) => { mainControllerRef.current = controller; }} + isScrubbing={scrubbing} />
@@ -357,8 +343,24 @@ export function MobileRecordingView({ // scrubbing and timeline state const [scrubbing, setScrubbing] = useState(false); - const [currentTime, setCurrentTime] = useState( - startTime || Date.now() / 1000, + const [currentTime, setCurrentTime] = useState(startTime); + const [playerTime, setPlayerTime] = useState(startTime); + + const updateSelectedSegment = useCallback( + (currentTime: number, updateStartTime: boolean) => { + const index = timeRange.ranges.findIndex( + (seg) => seg.start <= currentTime && seg.end >= currentTime, + ); + + if (index != -1) { + if (updateStartTime) { + setPlaybackStart(currentTime); + } + + setSelectedRangeIdx(index); + } + }, + [timeRange], ); useEffect(() => { @@ -367,28 +369,36 @@ export function MobileRecordingView({ currentTime > currentTimeRange.end + 60 || currentTime < currentTimeRange.start - 60 ) { - const index = timeRange.ranges.findIndex( - (seg) => seg.start <= currentTime && seg.end >= currentTime, - ); - - if (index != -1) { - setSelectedRangeIdx(index); - } + updateSelectedSegment(currentTime, false); return; } controllerRef.current?.scrubToTimestamp(currentTime); } - }, [currentTime, scrubbing, currentTimeRange, timeRange]); + }, [ + currentTime, + scrubbing, + timeRange, + currentTimeRange, + updateSelectedSegment, + ]); useEffect(() => { if (!scrubbing) { - controllerRef.current?.seekToTimestamp(currentTime, true); + if (Math.abs(currentTime - playerTime) > 10) { + if ( + currentTimeRange.start <= currentTime && + currentTimeRange.end >= currentTime + ) { + controllerRef.current?.seekToTimestamp(currentTime, true); + } else { + updateSelectedSegment(currentTime, true); + } + } } - - // we only want to seek when user stops scrubbing + // we only want to seek when current time doesn't match the player update time // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scrubbing]); + }, [currentTime, scrubbing]); // motion timeline data @@ -450,8 +460,12 @@ export function MobileRecordingView({ onControllerReady={(controller) => { controllerRef.current = controller; }} - onTimestampUpdate={setCurrentTime} + onTimestampUpdate={(timestamp) => { + setPlayerTime(timestamp); + setCurrentTime(timestamp); + }} onClipEnded={onClipEnded} + isScrubbing={scrubbing} />