Fix previews failing when disabled (#16962)

* Fix previews failing when offline

* Simplify frame cache handling
This commit is contained in:
Nicolas Mowen 2025-03-05 07:07:48 -07:00 committed by GitHub
parent ad0e89e147
commit 73c2c34127
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 59 additions and 42 deletions

View File

@ -41,22 +41,30 @@ def check_disabled_camera_update(
has_enabled_camera = False has_enabled_camera = False
for camera, last_update in write_times.items(): for camera, last_update in write_times.items():
offline_time = now - last_update
if config.cameras[camera].enabled: if config.cameras[camera].enabled:
has_enabled_camera = True has_enabled_camera = True
else:
# flag camera as offline when it is disabled
previews[camera].flag_offline(now)
if now - last_update > 1: if offline_time > 1:
# last camera update was more than one second ago # last camera update was more than 1 second ago
# need to send empty data to updaters because current # need to send empty data to birdseye because current
# frame is now out of date # frame is now out of date
frame = get_blank_yuv_frame( if birdseye and offline_time < 10:
config.cameras[camera].detect.width, # we only need to send blank frames to birdseye at the beginning of a camera being offline
config.cameras[camera].detect.height, birdseye.write_data(
) camera,
[],
if birdseye: [],
birdseye.write_data(camera, [], [], now, frame) now,
get_blank_yuv_frame(
previews[camera].write_data([], [], now, frame) config.cameras[camera].detect.width,
config.cameras[camera].detect.height,
),
)
if not has_enabled_camera and birdseye: if not has_enabled_camera and birdseye:
birdseye.all_cameras_disabled() birdseye.all_cameras_disabled()
@ -170,6 +178,12 @@ def output_frames(
else: else:
failed_frame_requests[camera] = 0 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 # send camera frame to ffmpeg process if websockets are connected
if any( if any(
ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager ws.environ["PATH_INFO"].endswith(camera) for ws in websocket_server.manager
@ -193,11 +207,6 @@ def output_frames(
frame, 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) frame_manager.close(frame_name)
move_preview_frames("clips") move_preview_frames("clips")

View File

@ -23,7 +23,7 @@ from frigate.ffmpeg_presets import (
) )
from frigate.models import Previews from frigate.models import Previews
from frigate.object_processing import TrackedObject 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__) logger = logging.getLogger(__name__)
@ -153,6 +153,7 @@ class PreviewRecorder:
self.config = config self.config = config
self.start_time = 0 self.start_time = 0
self.last_output_time = 0 self.last_output_time = 0
self.offline = False
self.output_frames = [] self.output_frames = []
if config.detect.width > config.detect.height: if config.detect.width > config.detect.height:
@ -241,6 +242,17 @@ class PreviewRecorder:
self.last_output_time = ts self.last_output_time = ts
self.output_frames.append(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( def should_write_frame(
self, self,
current_tracked_objects: list[dict[str, any]], current_tracked_objects: list[dict[str, any]],
@ -307,7 +319,9 @@ class PreviewRecorder:
motion_boxes: list[list[int]], motion_boxes: list[list[int]],
frame_time: float, frame_time: float,
frame: np.ndarray, frame: np.ndarray,
) -> bool: ) -> None:
self.offline = False
# check for updated record config # check for updated record config
_, updated_record_config = self.config_subscriber.check_for_update() _, updated_record_config = self.config_subscriber.check_for_update()
@ -319,7 +333,7 @@ class PreviewRecorder:
self.start_time = frame_time self.start_time = frame_time
self.output_frames.append(frame_time) self.output_frames.append(frame_time)
self.write_frame_to_cache(frame_time, frame) self.write_frame_to_cache(frame_time, frame)
return False return
# check if PREVIEW clip should be generated and cached frames reset # check if PREVIEW clip should be generated and cached frames reset
if frame_time >= self.segment_end: 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." f"Not saving preview for {self.config.name} because there are no saved frames."
) )
# reset frame cache self.reset_frame_cache(frame_time)
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] = []
# include first frame to ensure consistent duration # include first frame to ensure consistent duration
if self.config.record.enabled: if self.config.record.enabled:
self.output_frames.append(frame_time) self.output_frames.append(frame_time)
self.write_frame_to_cache(frame_time, frame) self.write_frame_to_cache(frame_time, frame)
return True return
elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time): elif self.should_write_frame(current_tracked_objects, motion_boxes, frame_time):
self.output_frames.append(frame_time) self.output_frames.append(frame_time)
self.write_frame_to_cache(frame_time, frame) self.write_frame_to_cache(frame_time, frame)
return False return
def flag_offline(self, frame_time: float) -> None: 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 # check if PREVIEW clip should be generated and cached frames reset
if frame_time >= self.segment_end: if frame_time >= self.segment_end:
if len(self.output_frames) == 0: 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 return
old_frame_path = get_cache_image_name( old_frame_path = get_cache_image_name(
@ -382,16 +399,7 @@ class PreviewRecorder:
self.requestor, self.requestor,
).start() ).start()
# reset frame cache self.reset_frame_cache(frame_time)
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: def stop(self) -> None:
self.requestor.stop() self.requestor.stop()