mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Fix handling of recordings and switching cameras (#10351)
* Fix handling of recordings and switching cameras * mobile switch * Cleanup * Cleanup autoplay * Remove vite
This commit is contained in:
		
							parent
							
								
									9fc1286568
								
							
						
					
					
						commit
						b910db4f05
					
				| @ -5,7 +5,7 @@ type TWrapperProps = { | ||||
| }; | ||||
| 
 | ||||
| const Wrapper = ({ children }: TWrapperProps) => { | ||||
|   return <main className="w-screen h-screen overflow-hidden">{children}</main>; | ||||
|   return <main className="w-screen h-dvh overflow-hidden">{children}</main>; | ||||
| }; | ||||
| 
 | ||||
| export default Wrapper; | ||||
|  | ||||
| @ -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<Player | null>(null); | ||||
|   const [playerRef, setPlayerRef] = useState<Player | null>(null); | ||||
|   const [previewController, setPreviewController] = | ||||
|     useState<PreviewVideoController | null>(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({ | ||||
|         <VideoPlayer | ||||
|           options={{ | ||||
|             preload: "auto", | ||||
|             autoplay: !previewOnly, | ||||
|             autoplay: false, | ||||
|             sources: [initialPlaybackSource], | ||||
|             aspectRatio: wideVideo ? undefined : "16:9", | ||||
|             controlBar: { | ||||
| @ -248,10 +266,10 @@ export default function DynamicVideoPlayer({ | ||||
|           }} | ||||
|           seekOptions={{ forward: 10, backward: 5 }} | ||||
|           onReady={(player) => { | ||||
|             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); | ||||
|     } | ||||
|  | ||||
| @ -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} | ||||
|         /> | ||||
|  | ||||
| @ -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); | ||||
|                     }); | ||||
|                   }} | ||||
|                 /> | ||||
|               </div> | ||||
| @ -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<HTMLDivElement | null>(null); | ||||
| @ -279,6 +282,8 @@ export function MobileRecordingView({ | ||||
| 
 | ||||
|   const [playerReady, setPlayerReady] = useState(false); | ||||
|   const controllerRef = useRef<DynamicVideoController | undefined>(undefined); | ||||
|   const [playbackCamera, setPlaybackCamera] = useState(startCamera); | ||||
|   const [playbackStart, setPlaybackStart] = useState(startTime); | ||||
| 
 | ||||
|   // timeline time
 | ||||
| 
 | ||||
| @ -361,16 +366,45 @@ export function MobileRecordingView({ | ||||
| 
 | ||||
|   return ( | ||||
|     <div ref={contentRef} className="flex flex-col relative w-full h-full"> | ||||
|       <Button className="rounded-lg" onClick={() => navigate(-1)}> | ||||
|         <IoMdArrowRoundBack className="size-5 mr-[10px]" /> | ||||
|         Back | ||||
|       </Button> | ||||
|       <div className="flex justify-evenly items-center"> | ||||
|         <Button className="rounded-lg" onClick={() => navigate(-1)}> | ||||
|           <IoMdArrowRoundBack className="size-5 mr-[10px]" /> | ||||
|           Back | ||||
|         </Button> | ||||
|         <DropdownMenu> | ||||
|           <DropdownMenuTrigger asChild> | ||||
|             <Button className="capitalize"> | ||||
|               {playbackCamera.replaceAll("_", " ")} | ||||
|             </Button> | ||||
|           </DropdownMenuTrigger> | ||||
|           <DropdownMenuContent> | ||||
|             <DropdownMenuRadioGroup | ||||
|               value={playbackCamera} | ||||
|               onValueChange={(cam) => { | ||||
|                 setPlaybackStart(currentTime); | ||||
|                 setPlaybackCamera(cam); | ||||
|               }} | ||||
|             > | ||||
|               {allCameras.map((cam) => ( | ||||
|                 <DropdownMenuRadioItem | ||||
|                   key={cam} | ||||
|                   className="capitalize" | ||||
|                   value={cam} | ||||
|                 > | ||||
|                   {cam.replaceAll("_", " ")} | ||||
|                 </DropdownMenuRadioItem> | ||||
|               ))} | ||||
|             </DropdownMenuRadioGroup> | ||||
|           </DropdownMenuContent> | ||||
|         </DropdownMenu> | ||||
|       </div> | ||||
| 
 | ||||
|       <div> | ||||
|         <DynamicVideoPlayer | ||||
|           camera={startCamera} | ||||
|           camera={playbackCamera} | ||||
|           timeRange={currentTimeRange} | ||||
|           cameraPreviews={relevantPreviews || []} | ||||
|           startTime={playbackStart} | ||||
|           onControllerReady={(controller) => { | ||||
|             controllerRef.current = controller; | ||||
|             setPlayerReady(true); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user