From f5c01b7926976c754c462144fc3df7b98a6aa5c3 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Fri, 8 Mar 2024 11:48:58 -0700 Subject: [PATCH] Cleanup to use new preview video player --- .../components/player/DynamicVideoPlayer.tsx | 338 ++++++------------ .../components/player/PreviewVideoPlayer.tsx | 8 +- web/src/types/playback.ts | 2 - web/src/views/events/RecordingView.tsx | 16 +- 4 files changed, 126 insertions(+), 238 deletions(-) diff --git a/web/src/components/player/DynamicVideoPlayer.tsx b/web/src/components/player/DynamicVideoPlayer.tsx index e090c208c..01d826d47 100644 --- a/web/src/components/player/DynamicVideoPlayer.tsx +++ b/web/src/components/player/DynamicVideoPlayer.tsx @@ -1,11 +1,4 @@ -import { - MutableRefObject, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import VideoPlayer from "./VideoPlayer"; import Player from "video.js/dist/types/player"; import TimelineEventOverlay from "../overlay/TimelineDataOverlay"; @@ -16,6 +9,9 @@ import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { Recording } from "@/types/record"; import { Preview } from "@/types/preview"; import { DynamicPlayback } from "@/types/playback"; +import PreviewVideoPlayer, { + PreviewVideoController, +} from "./PreviewVideoPlayer"; type PlayerMode = "playback" | "scrubbing"; @@ -28,7 +24,6 @@ type DynamicVideoPlayerProps = { timeRange: { start: number; end: number }; cameraPreviews: Preview[]; previewOnly?: boolean; - preloadRecordings: boolean; onControllerReady: (controller: DynamicVideoController) => void; onClick?: () => void; }; @@ -38,7 +33,6 @@ export default function DynamicVideoPlayer({ timeRange, cameraPreviews, previewOnly = false, - preloadRecordings = true, onControllerReady, onClick, }: DynamicVideoPlayerProps) { @@ -51,35 +45,36 @@ export default function DynamicVideoPlayer({ ); // playback behavior - const tallVideo = useMemo(() => { + const wideVideo = useMemo(() => { if (!config) { return false; } return ( config.cameras[camera].detect.width / - config.cameras[camera].detect.height < - 1 + config.cameras[camera].detect.height > + 1.7 ); }, [camera, config]); // controlling playback - const [playerRef, setPlayerRef] = useState(undefined); - const previewRef = useRef(null); + const playerRef = useRef(null); + const [previewController, setPreviewController] = + useState(null); const [isScrubbing, setIsScrubbing] = useState(previewOnly); const [focusedItem, setFocusedItem] = useState( undefined, ); const controller = useMemo(() => { - if (!config || !playerRef || !previewRef.current) { + if (!config || !playerRef.current || !previewController) { return undefined; } return new DynamicVideoController( camera, - playerRef, - previewRef, + playerRef.current, + previewController, (config.cameras[camera]?.detect?.annotation_offset || 0) / 1000, previewOnly ? "scrubbing" : "playback", setIsScrubbing, @@ -87,7 +82,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, previewRef]); + }, [camera, config, playerRef.current, previewController]); useEffect(() => { if (!controller) { @@ -105,7 +100,7 @@ export default function DynamicVideoPlayer({ const [initPreviewOnly, setInitPreviewOnly] = useState(previewOnly); useEffect(() => { - if (!controller || !playerRef) { + if (!controller || !playerRef.current) { return; } @@ -113,10 +108,8 @@ export default function DynamicVideoPlayer({ return; } - if (previewOnly) { - playerRef.autoplay(false); - } else { - controller.seekToTimestamp(playerRef.currentTime() || 0, true); + if (!previewOnly) { + controller.seekToTimestamp(playerRef.current.currentTime() || 0, true); } setInitPreviewOnly(previewOnly); @@ -124,48 +117,52 @@ export default function DynamicVideoPlayer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [controller, previewOnly]); - const [hasRecordingAtTime, setHasRecordingAtTime] = useState(true); - // keyboard control const onKeyboardShortcut = useCallback( (key: string, down: boolean, repeat: boolean) => { + if (!playerRef.current || previewOnly) { + return; + } + switch (key) { case "ArrowLeft": if (down) { - const currentTime = playerRef?.currentTime(); + const currentTime = playerRef.current.currentTime(); if (currentTime) { - playerRef?.currentTime(Math.max(0, currentTime - 5)); + playerRef.current.currentTime(Math.max(0, currentTime - 5)); } } break; case "ArrowRight": if (down) { - const currentTime = playerRef?.currentTime(); + const currentTime = playerRef.current.currentTime(); if (currentTime) { - playerRef?.currentTime(currentTime + 5); + playerRef.current.currentTime(currentTime + 5); } } break; case "m": if (down && !repeat && playerRef) { - playerRef.muted(!playerRef.muted()); + playerRef.current.muted(!playerRef.current.muted()); } break; case " ": if (down && playerRef) { - if (playerRef.paused()) { - playerRef.play(); + if (playerRef.current.paused()) { + playerRef.current.play(); } else { - playerRef.pause(); + playerRef.current.pause(); } } break; } }, - [playerRef], + // only update when preview only changes + // eslint-disable-next-line react-hooks/exhaustive-deps + [playerRef.current, previewOnly], ); useKeyboardListener( ["ArrowLeft", "ArrowRight", "m", " "], @@ -188,35 +185,6 @@ export default function DynamicVideoPlayer({ // we only want to calculate this once // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const initialPreview = useMemo(() => { - return cameraPreviews.find( - (preview) => - preview.camera == camera && - Math.round(preview.start) >= timeRange.start && - Math.floor(preview.end) <= timeRange.end, - ); - - // we only want to calculate this once - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const [currentPreview, setCurrentPreview] = useState(initialPreview); - - const onPreviewSeeked = useCallback(() => { - if (!controller) { - return; - } - - controller.finishedSeeking(); - - if (currentPreview && previewOnly && previewRef.current && onClick) { - setHasRecordingAtTime( - controller.hasRecordingAtTime( - currentPreview.start + previewRef.current.currentTime, - ), - ); - } - }, [controller, currentPreview, onClick, previewOnly]); // state of playback player @@ -246,23 +214,9 @@ export default function DynamicVideoPlayer({ ",", )}/master.m3u8`; - const preview = cameraPreviews.find( - (preview) => - preview.camera == camera && - Math.round(preview.start) >= timeRange.start && - Math.floor(preview.end) <= timeRange.end, - ); - setCurrentPreview(preview); - - if (preview && previewRef.current) { - previewRef.current.load(); - } - controller.newPlayback({ recordings: recordings ?? [], playbackUri, - preview, - timeRange, }); // we only want this to change when recordings update @@ -271,62 +225,53 @@ export default function DynamicVideoPlayer({ return (
- {preloadRecordings && ( -
- { - setPlayerRef(player); - }} - onDispose={() => { - setPlayerRef(undefined); - }} - > - {config && focusedItem && ( - - )} - -
- )} -
); } @@ -334,23 +279,17 @@ export default function DynamicVideoPlayer({ export class DynamicVideoController { // main state public camera = ""; - private playerRef: Player; - private previewRef: MutableRefObject; + private playerController: Player; + private previewController: PreviewVideoController; private setScrubbing: (isScrubbing: boolean) => void; private setFocusedItem: (timeline: Timeline) => void; private playerMode: PlayerMode = "playback"; - private timeRange: { start: number; end: number } | undefined = undefined; // playback private recordings: Recording[] = []; private annotationOffset: number; private timeToStart: number | undefined = undefined; - // preview - private preview: Preview | undefined = undefined; - private timeToSeek: number | undefined = undefined; - private seeking = false; - // listeners private playerProgressListener: (() => void) | null = null; private playerEndedListener: (() => void) | null = null; @@ -358,16 +297,16 @@ export class DynamicVideoController { constructor( camera: string, - playerRef: Player, - previewRef: MutableRefObject, + playerController: Player, + previewController: PreviewVideoController, annotationOffset: number, defaultMode: PlayerMode, setScrubbing: (isScrubbing: boolean) => void, setFocusedItem: (timeline: Timeline) => void, ) { this.camera = camera; - this.playerRef = playerRef; - this.previewRef = previewRef; + this.playerController = playerController; + this.previewController = previewController; this.annotationOffset = annotationOffset; this.playerMode = defaultMode; this.setScrubbing = setScrubbing; @@ -377,7 +316,7 @@ export class DynamicVideoController { newPlayback(newPlayback: DynamicPlayback) { this.recordings = newPlayback.recordings; - this.playerRef.src({ + this.playerController.src({ src: newPlayback.playbackUri, type: "application/vnd.apple.mpegurl", }); @@ -386,23 +325,12 @@ export class DynamicVideoController { this.seekToTimestamp(this.timeToStart); this.timeToStart = undefined; } - - this.preview = newPlayback.preview; - - if (this.preview) { - this.seeking = false; - this.timeToSeek = undefined; - } - - this.timeRange = newPlayback.timeRange; } seekToTimestamp(time: number, play: boolean = false) { if (this.playerMode != "playback") { this.playerMode = "playback"; this.setScrubbing(false); - this.timeToSeek = undefined; - this.seeking = false; } if (this.recordings.length == 0) { @@ -425,29 +353,17 @@ export class DynamicVideoController { segment.end_time - segment.start_time - (segment.end_time - time); return true; }); - this.playerRef.currentTime(seekSeconds); + this.playerController.currentTime(seekSeconds); if (play) { - this.playerRef.play(); + this.playerController.play(); } else { - this.playerRef.pause(); - } - } - - onCanPlay(listener: (() => void) | null) { - if (listener) { - this.canPlayListener = listener; - this.playerRef.on("canplay", this.canPlayListener); - } else { - if (this.canPlayListener) { - this.playerRef.off("canplay", this.canPlayListener); - this.canPlayListener = null; - } + this.playerController.pause(); } } seekToTimelineItem(timeline: Timeline) { - this.playerRef.pause(); + this.playerController.pause(); this.seekToTimestamp(timeline.timestamp + this.annotationOffset); this.setFocusedItem(timeline); } @@ -470,79 +386,55 @@ export class DynamicVideoController { return timestamp; } - onPlayerTimeUpdate(listener: ((timestamp: number) => void) | undefined) { + onCanPlay(listener: (() => void) | null) { + if (this.canPlayListener) { + this.playerController.off("canplay", this.canPlayListener); + this.canPlayListener = null; + } + + if (listener) { + this.canPlayListener = listener; + this.playerController.on("canplay", this.canPlayListener); + } + } + + onPlayerTimeUpdate(listener: ((timestamp: number) => void) | null) { if (this.playerProgressListener) { - this.playerRef.off("timeupdate", this.playerProgressListener); + this.playerController.off("timeupdate", this.playerProgressListener); + this.playerProgressListener = null; } if (listener) { this.playerProgressListener = () => - listener(this.getProgress(this.playerRef.currentTime() || 0)); - this.playerRef.on("timeupdate", this.playerProgressListener); + listener(this.getProgress(this.playerController.currentTime() || 0)); + this.playerController.on("timeupdate", this.playerProgressListener); } } - onClipChangedEvent(listener: ((dir: "forward") => void) | undefined) { + onClipChangedEvent(listener: ((dir: "forward") => void) | null) { if (this.playerEndedListener) { - this.playerRef.off("ended", this.playerEndedListener); + this.playerController.off("ended", this.playerEndedListener); + this.playerEndedListener = null; } if (listener) { this.playerEndedListener = () => listener("forward"); - this.playerRef.on("ended", this.playerEndedListener); + this.playerController.on("ended", this.playerEndedListener); } } - scrubToTimestamp(time: number) { - if (!this.preview || !this.timeRange) { - return; + scrubToTimestamp(time: number, saveIfNotReady: boolean = false) { + const scrubResult = this.previewController.scrubToTimestamp(time); + + if (!scrubResult && saveIfNotReady) { + this.previewController.setNewPreviewStartTime(time); } - if (time < this.preview.start || time > this.preview.end) { - return; - } - - if (this.playerMode != "scrubbing") { + if (scrubResult && this.playerMode != "scrubbing") { this.playerMode = "scrubbing"; - this.playerRef.pause(); + this.playerController.pause(); this.setScrubbing(true); } - - if (this.seeking) { - this.timeToSeek = time; - } else { - if (this.previewRef.current) { - this.previewRef.current.currentTime = Math.max( - 0, - time - this.preview.start, - ); - this.seeking = true; - } - } - } - - finishedSeeking() { - if ( - !this.previewRef.current || - !this.preview || - this.playerMode == "playback" - ) { - return; - } - - if ( - this.timeToSeek && - this.timeToSeek != this.previewRef.current?.currentTime - ) { - this.previewRef.current.currentTime = - this.timeToSeek - this.preview.start; - } else { - this.seeking = false; - } - } - - previewReady() { - this.previewRef.current?.pause(); } hasRecordingAtTime(time: number): boolean { diff --git a/web/src/components/player/PreviewVideoPlayer.tsx b/web/src/components/player/PreviewVideoPlayer.tsx index 8f3f170d2..b10a3c137 100644 --- a/web/src/components/player/PreviewVideoPlayer.tsx +++ b/web/src/components/player/PreviewVideoPlayer.tsx @@ -164,13 +164,13 @@ export class PreviewVideoController { this.timeRange = newPlayback.timeRange; } - scrubToTimestamp(time: number) { + scrubToTimestamp(time: number): boolean { if (!this.preview || !this.timeRange) { - return; + return false; } if (time < this.preview.start || time > this.preview.end) { - return; + return false; } if (this.seeking) { @@ -184,6 +184,8 @@ export class PreviewVideoController { this.seeking = true; } } + + return true; } setNewPreviewStartTime(time: number) { diff --git a/web/src/types/playback.ts b/web/src/types/playback.ts index ea1d901b7..b1efeed37 100644 --- a/web/src/types/playback.ts +++ b/web/src/types/playback.ts @@ -4,8 +4,6 @@ import { Recording } from "./record"; export type DynamicPlayback = { recordings: Recording[]; playbackUri: string; - preview: Preview | undefined; - timeRange: { end: number; start: number }; }; export type PreviewPlayback = { diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 3eb7f776c..2af834cd6 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -35,7 +35,6 @@ export function DesktopRecordingView({ // controller state - const [playerReady, setPlayerReady] = useState(false); const [mainCamera, setMainCamera] = useState(startCamera); const videoPlayersRef = useRef<{ [camera: string]: DynamicVideoController }>( {}, @@ -74,7 +73,9 @@ export function DesktopRecordingView({ } }); } - }, [selectedRangeIdx, timeRange, videoPlayersRef, playerReady, mainCamera]); + // we only want to fire once when players are ready + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedRangeIdx, timeRange, videoPlayersRef.current, mainCamera]); // scrubbing and timeline state @@ -116,8 +117,8 @@ export function DesktopRecordingView({ (newCam: string) => { const lastController = videoPlayersRef.current[mainCamera]; const newController = videoPlayersRef.current[newCam]; - lastController.onPlayerTimeUpdate(undefined); - lastController.onClipChangedEvent(undefined); + lastController.onPlayerTimeUpdate(null); + lastController.onClipChangedEvent(null); lastController.scrubToTimestamp(currentTime); newController.onCanPlay(() => { newController.seekToTimestamp(currentTime, true); @@ -176,10 +177,8 @@ export function DesktopRecordingView({ camera={cam} timeRange={currentTimeRange} cameraPreviews={allPreviews ?? []} - preloadRecordings onControllerReady={(controller) => { videoPlayersRef.current[cam] = controller; - setPlayerReady(true); controller.onPlayerTimeUpdate((timestamp: number) => { setCurrentTime(timestamp); @@ -210,11 +209,9 @@ export function DesktopRecordingView({ timeRange={currentTimeRange} cameraPreviews={allPreviews ?? []} previewOnly - preloadRecordings onControllerReady={(controller) => { videoPlayersRef.current[cam] = controller; - setPlayerReady(true); - controller.scrubToTimestamp(startTime); + controller.scrubToTimestamp(startTime, true); }} onClick={() => onSelectCamera(cam)} /> @@ -372,7 +369,6 @@ export function MobileRecordingView({ camera={startCamera} timeRange={currentTimeRange} cameraPreviews={relevantPreviews || []} - preloadRecordings onControllerReady={(controller) => { controllerRef.current = controller; setPlayerReady(true);