mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Fix motion review (#10329)
* Break preview only video player out * Simplify * Load after current preview changes * Clear out waiting for seek state * Start at correct time of hour * Fix layout for tall video
This commit is contained in:
		
							parent
							
								
									ea5cb4fd8b
								
							
						
					
					
						commit
						3d539c93eb
					
				
							
								
								
									
										217
									
								
								web/src/components/player/PreviewVideoPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								web/src/components/player/PreviewVideoPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,217 @@ | ||||
| import { | ||||
|   MutableRefObject, | ||||
|   useCallback, | ||||
|   useEffect, | ||||
|   useMemo, | ||||
|   useRef, | ||||
|   useState, | ||||
| } from "react"; | ||||
| import useSWR from "swr"; | ||||
| import { FrigateConfig } from "@/types/frigateConfig"; | ||||
| import { Preview } from "@/types/preview"; | ||||
| import { PreviewPlayback } from "@/types/playback"; | ||||
| 
 | ||||
| type PreviewVideoPlayerProps = { | ||||
|   className?: string; | ||||
|   camera: string; | ||||
|   timeRange: { start: number; end: number }; | ||||
|   cameraPreviews: Preview[]; | ||||
|   onControllerReady: (controller: PreviewVideoController) => void; | ||||
|   onClick?: () => void; | ||||
| }; | ||||
| export default function PreviewVideoPlayer({ | ||||
|   className, | ||||
|   camera, | ||||
|   timeRange, | ||||
|   cameraPreviews, | ||||
|   onControllerReady, | ||||
|   onClick, | ||||
| }: PreviewVideoPlayerProps) { | ||||
|   const { data: config } = useSWR<FrigateConfig>("config"); | ||||
| 
 | ||||
|   // controlling playback
 | ||||
| 
 | ||||
|   const previewRef = useRef<HTMLVideoElement | null>(null); | ||||
|   const controller = useMemo(() => { | ||||
|     if (!config || !previewRef.current) { | ||||
|       return undefined; | ||||
|     } | ||||
| 
 | ||||
|     return new PreviewVideoController(camera, previewRef); | ||||
|     // we only care when preview is ready
 | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [camera, config, previewRef.current]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!controller) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (controller) { | ||||
|       onControllerReady(controller); | ||||
|     } | ||||
|     // we only want to fire once when players are ready
 | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [controller]); | ||||
| 
 | ||||
|   // initial state
 | ||||
| 
 | ||||
|   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(); | ||||
|   }, [controller]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!controller) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const preview = cameraPreviews.find( | ||||
|       (preview) => | ||||
|         preview.camera == camera && | ||||
|         Math.round(preview.start) >= timeRange.start && | ||||
|         Math.floor(preview.end) <= timeRange.end, | ||||
|     ); | ||||
|     setCurrentPreview(preview); | ||||
| 
 | ||||
|     controller.newPlayback({ | ||||
|       preview, | ||||
|       timeRange, | ||||
|     }); | ||||
| 
 | ||||
|     // we only want this to change when recordings update
 | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [controller, timeRange]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (!currentPreview || !previewRef.current) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     previewRef.current.load(); | ||||
|   }, [currentPreview, previewRef]); | ||||
| 
 | ||||
|   return ( | ||||
|     <div | ||||
|       className={`relative w-full ${className ?? ""} ${onClick ? "cursor-pointer" : ""}`} | ||||
|       onClick={onClick} | ||||
|     > | ||||
|       <video | ||||
|         ref={previewRef} | ||||
|         className={`size-full rounded-2xl bg-black`} | ||||
|         preload="auto" | ||||
|         autoPlay | ||||
|         playsInline | ||||
|         muted | ||||
|         disableRemotePlayback | ||||
|         onSeeked={onPreviewSeeked} | ||||
|         onLoadedData={() => { | ||||
|           if (controller) { | ||||
|             controller.previewReady(); | ||||
|           } else { | ||||
|             previewRef.current?.pause(); | ||||
|           } | ||||
|         }} | ||||
|       > | ||||
|         {currentPreview != undefined && ( | ||||
|           <source src={currentPreview.src} type={currentPreview.type} /> | ||||
|         )} | ||||
|       </video> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| 
 | ||||
| export class PreviewVideoController { | ||||
|   // main state
 | ||||
|   public camera = ""; | ||||
|   private previewRef: MutableRefObject<HTMLVideoElement | null>; | ||||
|   private timeRange: { start: number; end: number } | undefined = undefined; | ||||
| 
 | ||||
|   // preview
 | ||||
|   private preview: Preview | undefined = undefined; | ||||
|   private timeToSeek: number | undefined = undefined; | ||||
|   private seeking = false; | ||||
| 
 | ||||
|   constructor( | ||||
|     camera: string, | ||||
|     previewRef: MutableRefObject<HTMLVideoElement | null>, | ||||
|   ) { | ||||
|     this.camera = camera; | ||||
|     this.previewRef = previewRef; | ||||
|   } | ||||
| 
 | ||||
|   newPlayback(newPlayback: PreviewPlayback) { | ||||
|     this.preview = newPlayback.preview; | ||||
|     this.seeking = false; | ||||
| 
 | ||||
|     this.timeRange = newPlayback.timeRange; | ||||
|   } | ||||
| 
 | ||||
|   scrubToTimestamp(time: number) { | ||||
|     if (!this.preview || !this.timeRange) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (time < this.preview.start || time > this.preview.end) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     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; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setNewPreviewStartTime(time: number) { | ||||
|     this.timeToSeek = time; | ||||
|   } | ||||
| 
 | ||||
|   finishedSeeking() { | ||||
|     if (!this.previewRef.current || !this.preview) { | ||||
|       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.seeking = false; | ||||
|     this.previewRef.current?.pause(); | ||||
| 
 | ||||
|     if (this.timeToSeek) { | ||||
|       this.finishedSeeking(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -7,3 +7,8 @@ export type DynamicPlayback = { | ||||
|   preview: Preview | undefined; | ||||
|   timeRange: { end: number; start: number }; | ||||
| }; | ||||
| 
 | ||||
| export type PreviewPlayback = { | ||||
|   preview: Preview | undefined; | ||||
|   timeRange: { end: number; start: number }; | ||||
| }; | ||||
|  | ||||
| @ -2,9 +2,6 @@ import Logo from "@/components/Logo"; | ||||
| import NewReviewData from "@/components/dynamic/NewReviewData"; | ||||
| import ReviewActionGroup from "@/components/filter/ReviewActionGroup"; | ||||
| import ReviewFilterGroup from "@/components/filter/ReviewFilterGroup"; | ||||
| import DynamicVideoPlayer, { | ||||
|   DynamicVideoController, | ||||
| } from "@/components/player/DynamicVideoPlayer"; | ||||
| import PreviewThumbnailPlayer from "@/components/player/PreviewThumbnailPlayer"; | ||||
| import EventReviewTimeline from "@/components/timeline/EventReviewTimeline"; | ||||
| import ActivityIndicator from "@/components/indicators/activity-indicator"; | ||||
| @ -36,6 +33,9 @@ import { MdCircle } from "react-icons/md"; | ||||
| import useSWR from "swr"; | ||||
| import MotionReviewTimeline from "@/components/timeline/MotionReviewTimeline"; | ||||
| import { Button } from "@/components/ui/button"; | ||||
| import PreviewVideoPlayer, { | ||||
|   PreviewVideoController, | ||||
| } from "@/components/player/PreviewVideoPlayer"; | ||||
| 
 | ||||
| type EventViewProps = { | ||||
|   reviews?: ReviewSegment[]; | ||||
| @ -531,7 +531,6 @@ function MotionReview({ | ||||
| }: MotionReviewProps) { | ||||
|   const segmentDuration = 30; | ||||
|   const { data: config } = useSWR<FrigateConfig>("config"); | ||||
|   const [playerReady, setPlayerReady] = useState(false); | ||||
| 
 | ||||
|   const reviewCameras = useMemo(() => { | ||||
|     if (!config) { | ||||
| @ -552,7 +551,7 @@ function MotionReview({ | ||||
|     return cameras.sort((a, b) => a.ui.order - b.ui.order); | ||||
|   }, [config, filter]); | ||||
| 
 | ||||
|   const videoPlayersRef = useRef<{ [camera: string]: DynamicVideoController }>( | ||||
|   const videoPlayersRef = useRef<{ [camera: string]: PreviewVideoController }>( | ||||
|     {}, | ||||
|   ); | ||||
| 
 | ||||
| @ -593,27 +592,6 @@ function MotionReview({ | ||||
| 
 | ||||
|   // move to next clip
 | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       !videoPlayersRef.current && | ||||
|       Object.values(videoPlayersRef.current).length > 0 | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     const firstController = Object.values(videoPlayersRef.current)[0]; | ||||
| 
 | ||||
|     if (firstController) { | ||||
|       firstController.onClipChangedEvent((dir) => { | ||||
|         if (dir == "forward") { | ||||
|           if (selectedRangeIdx < timeRangeSegments.ranges.length - 1) { | ||||
|             setSelectedRangeIdx(selectedRangeIdx + 1); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }, [selectedRangeIdx, timeRangeSegments, videoPlayersRef, playerReady]); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if ( | ||||
|       currentTime > currentTimeRange.end + 60 || | ||||
| @ -624,6 +602,9 @@ function MotionReview({ | ||||
|       ); | ||||
| 
 | ||||
|       if (index != -1) { | ||||
|         Object.values(videoPlayersRef.current).forEach((controller) => { | ||||
|           controller.setNewPreviewStartTime(currentTime); | ||||
|         }); | ||||
|         setSelectedRangeIdx(index); | ||||
|       } | ||||
|       return; | ||||
| @ -656,17 +637,14 @@ function MotionReview({ | ||||
|               grow = "aspect-video"; | ||||
|             } | ||||
|             return ( | ||||
|               <DynamicVideoPlayer | ||||
|               <PreviewVideoPlayer | ||||
|                 key={camera.name} | ||||
|                 className={`${grow}`} | ||||
|                 camera={camera.name} | ||||
|                 timeRange={currentTimeRange} | ||||
|                 cameraPreviews={relevantPreviews || []} | ||||
|                 previewOnly | ||||
|                 preloadRecordings={false} | ||||
|                 onControllerReady={(controller) => { | ||||
|                   videoPlayersRef.current[camera.name] = controller; | ||||
|                   setPlayerReady(true); | ||||
|                 }} | ||||
|                 onClick={() => | ||||
|                   onSelectReview(`motion,${camera.name},${currentTime}`, false) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user