diff --git a/frigate/review/maintainer.py b/frigate/review/maintainer.py index 77d3d2a6b..4f419b36b 100644 --- a/frigate/review/maintainer.py +++ b/frigate/review/maintainer.py @@ -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( diff --git a/web/src/api/ws.tsx b/web/src/api/ws.tsx index 34fe4b2f0..5e347e1d4 100644 --- a/web/src/api/ws.tsx +++ b/web/src/api/ws.tsx @@ -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 }; } diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index c52bddb59..023e94a6c 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -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(); 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} diff --git a/web/src/components/settings/General.tsx b/web/src/components/settings/General.tsx index bdd30fdb7..9af7c17df 100644 --- a/web/src/components/settings/General.tsx +++ b/web/src/components/settings/General.tsx @@ -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("config"); @@ -38,6 +49,8 @@ export default function General() { document.title = "General Settings - Frigate"; }, []); + const [playbackRate, setPlaybackRate] = usePersistence("playbackRate", 1); + return ( <>
@@ -64,6 +77,36 @@ export default function General() {
+
+
+
Default Playback Rate
+
+

Default playback rate for recordings playback.

+
+
+
+ +
Low Data Mode
diff --git a/web/src/hooks/use-camera-activity.ts b/web/src/hooks/use-camera-activity.ts index 074c2be52..7019dafda 100644 --- a/web/src/hooks/use-camera-activity.ts +++ b/web/src/hooks/use-camera-activity.ts @@ -19,12 +19,16 @@ type useCameraActivityReturn = { export function useCameraActivity( camera: CameraConfig, + refreshOnStart: boolean = true, ): useCameraActivityReturn { const [objects, setObjects] = useState([]); // init camera activity - const { payload: initialCameraState } = useInitialCameraState(camera.name); + const { payload: initialCameraState } = useInitialCameraState( + camera.name, + refreshOnStart, + ); const updatedCameraState = useDeepMemo(initialCameraState);