diff --git a/web/src/components/Wrapper.tsx b/web/src/components/Wrapper.tsx index 0726d280e..860f41296 100644 --- a/web/src/components/Wrapper.tsx +++ b/web/src/components/Wrapper.tsx @@ -5,7 +5,7 @@ type TWrapperProps = { }; const Wrapper = ({ children }: TWrapperProps) => { - return
{children}
; + return
{children}
; }; export default Wrapper; diff --git a/web/src/components/player/DynamicVideoPlayer.tsx b/web/src/components/player/DynamicVideoPlayer.tsx index 13911d8c3..77eb51ee8 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) { @@ -97,64 +99,44 @@ export default function DynamicVideoPlayer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [controller]); - const [initPreviewOnly, setInitPreviewOnly] = useState(previewOnly); - - useEffect(() => { - if (!controller || !playerRef.current) { - return; - } - - if (previewOnly == initPreviewOnly) { - return; - } - - if (!previewOnly) { - controller.seekToTimestamp(playerRef.current.currentTime() || 0, true); - } - - setInitPreviewOnly(previewOnly); - // we only want to fire once when players are ready - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [controller, previewOnly]); - // keyboard control 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 +144,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 +168,42 @@ export default function DynamicVideoPlayer({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // start at correct time + + useEffect(() => { + const player = playerRef; + + if (!player) { + return; + } + + if (previewOnly) { + player.autoplay(false); + return; + } + + player.autoplay(true); + + if (!startTime) { + 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 +254,7 @@ export default function DynamicVideoPlayer({ { - playerRef.current = player; + setPlayerRef(player); }} onDispose={() => { - playerRef.current = null; + setPlayerRef(null); }} > {config && focusedItem && ( @@ -289,12 +307,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 +336,6 @@ export class DynamicVideoController { src: newPlayback.playbackUri, type: "application/vnd.apple.mpegurl", }); - this.canPlay = false; if (this.timeToStart) { this.seekToTimestamp(this.timeToStart); @@ -328,10 +343,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 +402,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 +410,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/pages/Events.tsx b/web/src/pages/Events.tsx index 0d0eae1cc..2fa4229da 100644 --- a/web/src/pages/Events.tsx +++ b/web/src/pages/Events.tsx @@ -270,6 +270,7 @@ export default function Events() { reviewItems={selectedReviewData.cameraSegments} startCamera={selectedReviewData.camera} startTime={selectedReviewData.start_time} + allCameras={selectedReviewData.allCameras} severity={selectedReviewData.severity} relevantPreviews={allPreviews} /> diff --git a/web/src/views/events/RecordingView.tsx b/web/src/views/events/RecordingView.tsx index 89dff8b40..737caec82 100644 --- a/web/src/views/events/RecordingView.tsx +++ b/web/src/views/events/RecordingView.tsx @@ -4,6 +4,13 @@ import DynamicVideoPlayer, { import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Preview } from "@/types/preview"; import { MotionData, ReviewSegment, ReviewSeverity } from "@/types/review"; import { getChunkedTimeDay } from "@/utils/timelineUtil"; @@ -40,6 +47,8 @@ export function DesktopRecordingView({ {}, ); + const [playbackStart, setPlaybackStart] = useState(startTime); + // timeline time const timeRange = useMemo(() => getChunkedTimeDay(startTime), [startTime]); @@ -119,13 +128,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 +140,8 @@ export function DesktopRecordingView({ } }); }); + newController.seekToTimestamp(currentTime, true); + setPlaybackStart(currentTime); setMainCamera(newCam); }, [allCameras, currentTime, mainCamera], @@ -179,6 +184,7 @@ export function DesktopRecordingView({ camera={cam} timeRange={currentTimeRange} cameraPreviews={allPreviews ?? []} + startTime={playbackStart} onControllerReady={(controller) => { videoPlayersRef.current[cam] = controller; controller.onPlayerTimeUpdate((timestamp: number) => { @@ -192,11 +198,6 @@ export function DesktopRecordingView({ } }); }); - - controller.onCanPlay(() => { - controller.seekToTimestamp(startTime, true); - controller.onCanPlay(null); - }); }} /> @@ -264,6 +265,7 @@ type MobileRecordingViewProps = { severity: ReviewSeverity; reviewItems: ReviewSegment[]; relevantPreviews?: Preview[]; + allCameras: string[]; }; export function MobileRecordingView({ startCamera, @@ -271,6 +273,7 @@ export function MobileRecordingView({ severity, reviewItems, relevantPreviews, + allCameras, }: MobileRecordingViewProps) { const navigate = useNavigate(); const contentRef = useRef(null); @@ -279,6 +282,8 @@ export function MobileRecordingView({ const [playerReady, setPlayerReady] = useState(false); const controllerRef = useRef(undefined); + const [playbackCamera, setPlaybackCamera] = useState(startCamera); + const [playbackStart, setPlaybackStart] = useState(startTime); // timeline time @@ -361,16 +366,45 @@ export function MobileRecordingView({ return (
- +
+ + + + + + + { + setPlaybackStart(currentTime); + setPlaybackCamera(cam); + }} + > + {allCameras.map((cam) => ( + + {cam.replaceAll("_", " ")} + + ))} + + + +
{ controllerRef.current = controller; setPlayerReady(true);