diff --git a/frigate/output/output.py b/frigate/output/output.py index e0e7d0cac..b29144b00 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -64,6 +64,7 @@ def output_frames( jsmpeg_cameras: dict[str, JsmpegCamera] = {} birdseye: Optional[Birdseye] = None preview_recorders: dict[str, PreviewRecorder] = {} + preview_write_times: dict[str, float] = {} move_preview_frames("cache") @@ -73,6 +74,7 @@ def output_frames( jsmpeg_cameras[camera] = JsmpegCamera(cam_config, stop_event, websocket_server) preview_recorders[camera] = PreviewRecorder(cam_config) + preview_write_times[camera] = 0 if config.birdseye.enabled: birdseye = Birdseye(config, frame_manager, stop_event, websocket_server) @@ -121,14 +123,24 @@ def output_frames( ) # send frames for low fps recording - preview_recorders[camera].write_data( + generated_preview = preview_recorders[camera].write_data( current_tracked_objects, motion_boxes, frame_time, frame ) + preview_write_times[camera] = frame_time # delete frames after they have been used for output if camera in previous_frames: frame_manager.delete(f"{camera}{previous_frames[camera]}") + # if another camera generated a preview, + # check for any cameras that are currently offline + # and need to generate a preview + if generated_preview: + for camera, time in preview_write_times.copy().items(): + if time != 0 and frame_time - time > 10: + preview_recorders[camera].flag_offline(frame_time) + preview_write_times[camera] = frame_time + previous_frames[camera] = frame_time move_preview_frames("clips") diff --git a/frigate/output/preview.py b/frigate/output/preview.py index 650805750..87efa3f11 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -3,6 +3,7 @@ import datetime import logging import os +import shutil import subprocess as sp import threading import time @@ -298,13 +299,13 @@ class PreviewRecorder: motion_boxes: list[list[int]], frame_time: float, frame, - ) -> None: + ) -> bool: # always write the first frame if self.start_time == 0: self.start_time = frame_time self.output_frames.append(frame_time) self.write_frame_to_cache(frame_time, frame) - return + return False # check if PREVIEW clip should be generated and cached frames reset if frame_time >= self.segment_end: @@ -326,14 +327,47 @@ class PreviewRecorder: ) self.start_time = frame_time self.last_output_time = frame_time - self.output_frames = [] + self.output_frames: list[float] = [] # include first frame to ensure consistent duration self.output_frames.append(frame_time) self.write_frame_to_cache(frame_time, frame) + return True elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time): self.output_frames.append(frame_time) self.write_frame_to_cache(frame_time, frame) + return False + + def flag_offline(self, frame_time: float) -> None: + # check if PREVIEW clip should be generated and cached frames reset + if frame_time >= self.segment_end: + if len(self.output_frames) == 0: + return + + old_frame_path = get_cache_image_name( + self.config.name, self.output_frames[-1] + ) + new_frame_path = get_cache_image_name(self.config.name, frame_time) + shutil.copy(old_frame_path, new_frame_path) + + # save last frame to ensure consistent duration + self.output_frames.append(frame_time) + FFMpegConverter( + self.config, + self.output_frames, + self.requestor, + ).start() + + # reset frame cache + self.segment_end = ( + (datetime.datetime.now() + datetime.timedelta(hours=1)) + .astimezone(datetime.timezone.utc) + .replace(minute=0, second=0, microsecond=0) + .timestamp() + ) + self.start_time = frame_time + self.last_output_time = frame_time + self.output_frames = [] def stop(self) -> None: self.requestor.stop()