mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Tweaks and fixes (#11372)
* Ensure camera activity is up to date * Persist playback rate between cameras * Add setting for default playback rate * Fix audio events saving image * Formatting * Use select component
This commit is contained in:
		
							parent
							
								
									b451d0a4f1
								
							
						
					
					
						commit
						b10ae68c1f
					
				| @ -68,7 +68,8 @@ class PendingReviewSegment: | ||||
|         self.last_update = frame_time | ||||
| 
 | ||||
|         # thumbnail | ||||
|         self.frame = np.zeros((THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8) | ||||
|         self._frame = np.zeros((THUMB_HEIGHT * 3 // 2, THUMB_WIDTH), np.uint8) | ||||
|         self.has_frame = False | ||||
|         self.frame_active_count = 0 | ||||
|         self.frame_path = os.path.join( | ||||
|             CLIPS_DIR, f"review/thumb-{self.camera}-{self.id}.webp" | ||||
| @ -101,25 +102,27 @@ class PendingReviewSegment: | ||||
|         color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) | ||||
|         color_frame = color_frame[region[1] : region[3], region[0] : region[2]] | ||||
|         width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0]) | ||||
|         self.frame = cv2.resize( | ||||
|         self._frame = cv2.resize( | ||||
|             color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA | ||||
|         ) | ||||
| 
 | ||||
|         if self.frame is not None: | ||||
|         if self._frame is not None: | ||||
|             self.has_frame = True | ||||
|             cv2.imwrite( | ||||
|                 self.frame_path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] | ||||
|                 self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] | ||||
|             ) | ||||
| 
 | ||||
|     def save_full_frame(self, camera_config: CameraConfig, frame): | ||||
|         color_frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420) | ||||
|         width = int(THUMB_HEIGHT * color_frame.shape[1] / color_frame.shape[0]) | ||||
|         self.frame = cv2.resize( | ||||
|         self._frame = cv2.resize( | ||||
|             color_frame, dsize=(width, THUMB_HEIGHT), interpolation=cv2.INTER_AREA | ||||
|         ) | ||||
| 
 | ||||
|         if self.frame is not None: | ||||
|         if self._frame is not None: | ||||
|             self.has_frame = True | ||||
|             cv2.imwrite( | ||||
|                 self.frame_path, self.frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] | ||||
|                 self.frame_path, self._frame, [int(cv2.IMWRITE_WEBP_QUALITY), 60] | ||||
|             ) | ||||
| 
 | ||||
|     def get_data(self, ended: bool) -> dict: | ||||
| @ -194,7 +197,10 @@ class ReviewSegmentMaintainer(threading.Thread): | ||||
|     ) -> None: | ||||
|         """Update segment.""" | ||||
|         prev_data = segment.get_data(ended=False) | ||||
|         segment.update_frame(camera_config, frame, objects) | ||||
| 
 | ||||
|         if frame is not None: | ||||
|             segment.update_frame(camera_config, frame, objects) | ||||
| 
 | ||||
|         new_data = segment.get_data(ended=False) | ||||
|         self.requestor.send_data(UPSERT_REVIEW_SEGMENT, new_data) | ||||
|         self.requestor.send_data( | ||||
| @ -282,33 +288,23 @@ class ReviewSegmentMaintainer(threading.Thread): | ||||
|                 except FileNotFoundError: | ||||
|                     return | ||||
|         else: | ||||
|             if not segment.has_frame: | ||||
|                 try: | ||||
|                     frame_id = f"{camera_config.name}{frame_time}" | ||||
|                     yuv_frame = self.frame_manager.get( | ||||
|                         frame_id, camera_config.frame_shape_yuv | ||||
|                     ) | ||||
|                     segment.save_full_frame(camera_config, yuv_frame) | ||||
|                     self.frame_manager.close(frame_id) | ||||
|                     self.update_segment(segment, camera_config, None, []) | ||||
|                 except FileNotFoundError: | ||||
|                     return | ||||
| 
 | ||||
|             if segment.severity == SeverityEnum.alert and frame_time > ( | ||||
|                 segment.last_update + THRESHOLD_ALERT_ACTIVITY | ||||
|             ): | ||||
|                 if segment.frame is None: | ||||
|                     try: | ||||
|                         frame_id = f"{camera_config.name}{frame_time}" | ||||
|                         yuv_frame = self.frame_manager.get( | ||||
|                             frame_id, camera_config.frame_shape_yuv | ||||
|                         ) | ||||
|                         segment.save_full_frame(camera_config, yuv_frame) | ||||
|                         self.frame_manager.close(frame_id) | ||||
|                     except FileNotFoundError: | ||||
|                         return | ||||
| 
 | ||||
|                 self.end_segment(segment) | ||||
|             elif frame_time > (segment.last_update + THRESHOLD_DETECTION_ACTIVITY): | ||||
|                 if segment.frame is None: | ||||
|                     try: | ||||
|                         frame_id = f"{camera_config.name}{frame_time}" | ||||
|                         yuv_frame = self.frame_manager.get( | ||||
|                             frame_id, camera_config.frame_shape_yuv | ||||
|                         ) | ||||
|                         segment.save_full_frame(camera_config, yuv_frame) | ||||
|                         self.frame_manager.close(frame_id) | ||||
|                     except FileNotFoundError: | ||||
|                         return | ||||
| 
 | ||||
|                 self.end_segment(segment) | ||||
| 
 | ||||
|     def check_if_new_segment( | ||||
|  | ||||
| @ -204,13 +204,26 @@ export function useFrigateStats(): { payload: FrigateStats } { | ||||
|   return { payload: JSON.parse(payload as string) }; | ||||
| } | ||||
| 
 | ||||
| export function useInitialCameraState(camera: string): { | ||||
| export function useInitialCameraState( | ||||
|   camera: string, | ||||
|   refreshOnStart: boolean, | ||||
| ): { | ||||
|   payload: FrigateCameraState; | ||||
| } { | ||||
|   const { | ||||
|     value: { payload }, | ||||
|   } = useWs("camera_activity", ""); | ||||
|     send: sendCommand, | ||||
|   } = useWs("camera_activity", "onConnect"); | ||||
|   const data = JSON.parse(payload as string); | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (refreshOnStart) { | ||||
|       sendCommand("onConnect"); | ||||
|     } | ||||
|     // only refresh when onRefresh value changes
 | ||||
|     // eslint-disable-next-line react-hooks/exhaustive-deps
 | ||||
|   }, [refreshOnStart]); | ||||
| 
 | ||||
|   return { payload: data ? data[camera] : undefined }; | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -15,6 +15,7 @@ import { FrigateConfig } from "@/types/frigateConfig"; | ||||
| import { AxiosResponse } from "axios"; | ||||
| import { toast } from "sonner"; | ||||
| import { useOverlayState } from "@/hooks/use-overlay-state"; | ||||
| import { usePersistence } from "@/hooks/use-persistence"; | ||||
| 
 | ||||
| // Android native hls does not seek correctly
 | ||||
| const USE_NATIVE_HLS = !isAndroid; | ||||
| @ -111,6 +112,11 @@ export default function HlsVideoPlayer({ | ||||
|   const [isPlaying, setIsPlaying] = useState(true); | ||||
|   const [muted, setMuted] = useOverlayState("playerMuted", true); | ||||
|   const [volume, setVolume] = useOverlayState("playerVolume", 1.0); | ||||
|   const [defaultPlaybackRate] = usePersistence("playbackRate", 1); | ||||
|   const [playbackRate, setPlaybackRate] = useOverlayState( | ||||
|     "playbackRate", | ||||
|     defaultPlaybackRate ?? 1, | ||||
|   ); | ||||
|   const [mobileCtrlTimeout, setMobileCtrlTimeout] = useState<NodeJS.Timeout>(); | ||||
|   const [controls, setControls] = useState(isMobile); | ||||
|   const [controlsOpen, setControlsOpen] = useState(false); | ||||
| @ -162,7 +168,7 @@ export default function HlsVideoPlayer({ | ||||
|         }} | ||||
|         setControlsOpen={setControlsOpen} | ||||
|         setMuted={(muted) => setMuted(muted, true)} | ||||
|         playbackRate={videoRef.current?.playbackRate ?? 1} | ||||
|         playbackRate={playbackRate ?? 1} | ||||
|         hotKeys={hotKeys} | ||||
|         onPlayPause={(play) => { | ||||
|           if (!videoRef.current) { | ||||
| @ -184,9 +190,13 @@ export default function HlsVideoPlayer({ | ||||
| 
 | ||||
|           videoRef.current.currentTime = Math.max(0, currentTime + diff); | ||||
|         }} | ||||
|         onSetPlaybackRate={(rate) => | ||||
|           videoRef.current ? (videoRef.current.playbackRate = rate) : null | ||||
|         } | ||||
|         onSetPlaybackRate={(rate) => { | ||||
|           setPlaybackRate(rate); | ||||
| 
 | ||||
|           if (videoRef.current) { | ||||
|             videoRef.current.playbackRate = rate; | ||||
|           } | ||||
|         }} | ||||
|         onUploadFrame={async () => { | ||||
|           if (videoRef.current && onUploadFrame) { | ||||
|             const resp = await onUploadFrame(videoRef.current.currentTime); | ||||
| @ -255,8 +265,14 @@ export default function HlsVideoPlayer({ | ||||
|           onLoadedMetadata={() => { | ||||
|             handleLoadedMetadata(); | ||||
| 
 | ||||
|             if (videoRef.current && volume) { | ||||
|               videoRef.current.volume = volume; | ||||
|             if (videoRef.current) { | ||||
|               if (playbackRate) { | ||||
|                 videoRef.current.playbackRate = playbackRate; | ||||
|               } | ||||
| 
 | ||||
|               if (volume) { | ||||
|                 videoRef.current.volume = volume; | ||||
|               } | ||||
|             } | ||||
|           }} | ||||
|           onEnded={onClipEnded} | ||||
|  | ||||
| @ -9,6 +9,17 @@ import { Button } from "../ui/button"; | ||||
| import useSWR from "swr"; | ||||
| import { FrigateConfig } from "@/types/frigateConfig"; | ||||
| import { del as delData } from "idb-keyval"; | ||||
| import { usePersistence } from "@/hooks/use-persistence"; | ||||
| import { isSafari } from "react-device-detect"; | ||||
| import { | ||||
|   Select, | ||||
|   SelectContent, | ||||
|   SelectGroup, | ||||
|   SelectItem, | ||||
|   SelectTrigger, | ||||
| } from "../ui/select"; | ||||
| 
 | ||||
| const PLAYBACK_RATE_DEFAULT = isSafari ? [0.5, 1, 2] : [0.5, 1, 2, 4, 8, 16]; | ||||
| 
 | ||||
| export default function General() { | ||||
|   const { data: config } = useSWR<FrigateConfig>("config"); | ||||
| @ -38,6 +49,8 @@ export default function General() { | ||||
|     document.title = "General Settings - Frigate"; | ||||
|   }, []); | ||||
| 
 | ||||
|   const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1); | ||||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="flex flex-col md:flex-row size-full"> | ||||
| @ -64,6 +77,36 @@ export default function General() { | ||||
|               </div> | ||||
|             </div> | ||||
|             <Separator className="flex my-2 bg-secondary" /> | ||||
|             <div className="mt-2 space-y-6"> | ||||
|               <div className="space-y-0.5"> | ||||
|                 <div className="text-md">Default Playback Rate</div> | ||||
|                 <div className="text-sm text-muted-foreground my-2"> | ||||
|                   <p>Default playback rate for recordings playback.</p> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|             <Select | ||||
|               value={playbackRate?.toString()} | ||||
|               onValueChange={(value) => setPlaybackRate(parseFloat(value))} | ||||
|             > | ||||
|               <SelectTrigger className="w-20"> | ||||
|                 {`${playbackRate}x`} | ||||
|               </SelectTrigger> | ||||
|               <SelectContent> | ||||
|                 <SelectGroup> | ||||
|                   {PLAYBACK_RATE_DEFAULT.map((rate) => ( | ||||
|                     <SelectItem | ||||
|                       key={rate} | ||||
|                       className="cursor-pointer" | ||||
|                       value={rate.toString()} | ||||
|                     > | ||||
|                       {rate}x | ||||
|                     </SelectItem> | ||||
|                   ))} | ||||
|                 </SelectGroup> | ||||
|               </SelectContent> | ||||
|             </Select> | ||||
|             <Separator className="flex my-2 bg-secondary" /> | ||||
|             <div className="mt-2 space-y-6"> | ||||
|               <div className="space-y-0.5"> | ||||
|                 <div className="text-md">Low Data Mode</div> | ||||
|  | ||||
| @ -19,12 +19,16 @@ type useCameraActivityReturn = { | ||||
| 
 | ||||
| export function useCameraActivity( | ||||
|   camera: CameraConfig, | ||||
|   refreshOnStart: boolean = true, | ||||
| ): useCameraActivityReturn { | ||||
|   const [objects, setObjects] = useState<ObjectType[]>([]); | ||||
| 
 | ||||
|   // init camera activity
 | ||||
| 
 | ||||
|   const { payload: initialCameraState } = useInitialCameraState(camera.name); | ||||
|   const { payload: initialCameraState } = useInitialCameraState( | ||||
|     camera.name, | ||||
|     refreshOnStart, | ||||
|   ); | ||||
| 
 | ||||
|   const updatedCameraState = useDeepMemo(initialCameraState); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user