diff --git a/frigate/output/output.py b/frigate/output/output.py index e0e64e298..30900a5ab 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -41,22 +41,30 @@ def check_disabled_camera_update( has_enabled_camera = False for camera, last_update in write_times.items(): + offline_time = now - last_update + if config.cameras[camera].enabled: has_enabled_camera = True + else: + # flag camera as offline when it is disabled + previews[camera].flag_offline(now) - if now - last_update > 1: - # last camera update was more than one second ago - # need to send empty data to updaters because current + if offline_time > 1: + # last camera update was more than 1 second ago + # need to send empty data to birdseye because current # frame is now out of date - frame = get_blank_yuv_frame( - config.cameras[camera].detect.width, - config.cameras[camera].detect.height, - ) - - if birdseye: - birdseye.write_data(camera, [], [], now, frame) - - previews[camera].write_data([], [], now, frame) + if birdseye and offline_time < 10: + # we only need to send blank frames to birdseye at the beginning of a camera being offline + birdseye.write_data( + camera, + [], + [], + now, + get_blank_yuv_frame( + config.cameras[camera].detect.width, + config.cameras[camera].detect.height, + ), + ) if not has_enabled_camera and birdseye: birdseye.all_cameras_disabled() @@ -170,6 +178,12 @@ def output_frames( else: failed_frame_requests[camera] = 0 + # send frames for low fps recording + preview_recorders[camera].write_data( + current_tracked_objects, motion_boxes, frame_time, frame + ) + preview_write_times[camera] = frame_time + # send camera frame to ffmpeg process if websockets are connected if any( ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager @@ -193,11 +207,6 @@ def output_frames( frame, ) - # send frames for low fps recording - preview_recorders[camera].write_data( - current_tracked_objects, motion_boxes, frame_time, frame - ) - preview_write_times[camera] = frame_time frame_manager.close(frame_name) move_preview_frames("clips") diff --git a/frigate/output/preview.py b/frigate/output/preview.py index 4f8796d39..247886bfd 100644 --- a/frigate/output/preview.py +++ b/frigate/output/preview.py @@ -23,7 +23,7 @@ from frigate.ffmpeg_presets import ( ) from frigate.models import Previews from frigate.object_processing import TrackedObject -from frigate.util.image import copy_yuv_to_position, get_yuv_crop +from frigate.util.image import copy_yuv_to_position, get_blank_yuv_frame, get_yuv_crop logger = logging.getLogger(__name__) @@ -153,6 +153,7 @@ class PreviewRecorder: self.config = config self.start_time = 0 self.last_output_time = 0 + self.offline = False self.output_frames = [] if config.detect.width > config.detect.height: @@ -241,6 +242,17 @@ class PreviewRecorder: self.last_output_time = ts self.output_frames.append(ts) + def reset_frame_cache(self, frame_time: float) -> None: + 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: list[float] = [] + def should_write_frame( self, current_tracked_objects: list[dict[str, any]], @@ -307,7 +319,9 @@ class PreviewRecorder: motion_boxes: list[list[int]], frame_time: float, frame: np.ndarray, - ) -> bool: + ) -> None: + self.offline = False + # check for updated record config _, updated_record_config = self.config_subscriber.check_for_update() @@ -319,7 +333,7 @@ class PreviewRecorder: self.start_time = frame_time self.output_frames.append(frame_time) self.write_frame_to_cache(frame_time, frame) - return False + return # check if PREVIEW clip should be generated and cached frames reset if frame_time >= self.segment_end: @@ -340,32 +354,35 @@ class PreviewRecorder: f"Not saving preview for {self.config.name} because there are no saved frames." ) - # 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: list[float] = [] + self.reset_frame_cache(frame_time) # include first frame to ensure consistent duration if self.config.record.enabled: self.output_frames.append(frame_time) self.write_frame_to_cache(frame_time, frame) - return True + return 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 + return def flag_offline(self, frame_time: float) -> None: + if not self.offline: + self.write_frame_to_cache( + frame_time, + get_blank_yuv_frame( + self.config.detect.width, self.config.detect.height + ), + ) + self.offline = True + # check if PREVIEW clip should be generated and cached frames reset if frame_time >= self.segment_end: if len(self.output_frames) == 0: + # camera has been offline for entire hour + # we have no preview to create + self.reset_frame_cache(frame_time) return old_frame_path = get_cache_image_name( @@ -382,16 +399,7 @@ class PreviewRecorder: 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 = [] + self.reset_frame_cache(frame_time) def stop(self) -> None: self.requestor.stop()