From 55c6008ff06de501c4028a7d3788d45d35a1eee6 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 21 Jun 2025 15:38:34 -0500 Subject: [PATCH] Fix birdseye crash when dynamically adding a camera (#18821) --- frigate/app.py | 1 + frigate/camera/maintainer.py | 6 ++- frigate/output/birdseye.py | 73 ++++++++++++++++++++++++------------ frigate/output/output.py | 8 +++- 4 files changed, 60 insertions(+), 28 deletions(-) diff --git a/frigate/app.py b/frigate/app.py index 8f7fa71ef..ac9c7bdf2 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -423,6 +423,7 @@ class FrigateApp: self.camera_metrics, self.ptz_metrics, self.stop_event, + self.metrics_manager, ) self.camera_maintainer.start() diff --git a/frigate/camera/maintainer.py b/frigate/camera/maintainer.py index dd978bbfc..dd122d4fd 100644 --- a/frigate/camera/maintainer.py +++ b/frigate/camera/maintainer.py @@ -6,7 +6,7 @@ import os import shutil import threading from multiprocessing import Queue -from multiprocessing.managers import DictProxy +from multiprocessing.managers import DictProxy, SyncManager from multiprocessing.synchronize import Event as MpEvent from frigate.camera import CameraMetrics, PTZMetrics @@ -35,6 +35,7 @@ class CameraMaintainer(threading.Thread): camera_metrics: DictProxy, ptz_metrics: dict[str, PTZMetrics], stop_event: MpEvent, + metrics_manager: SyncManager, ): super().__init__(name="camera_processor") self.config = config @@ -56,6 +57,7 @@ class CameraMaintainer(threading.Thread): self.shm_count = self.__calculate_shm_frame_count() self.camera_processes: dict[str, mp.Process] = {} self.capture_processes: dict[str, mp.Process] = {} + self.metrics_manager = metrics_manager def __init_historical_regions(self) -> None: # delete region grids for removed or renamed cameras @@ -128,7 +130,7 @@ class CameraMaintainer(threading.Thread): return if runtime: - self.camera_metrics[name] = CameraMetrics() + self.camera_metrics[name] = CameraMetrics(self.metrics_manager) self.ptz_metrics[name] = PTZMetrics(autotracker_enabled=False) self.region_grids[name] = get_camera_regions_grid( name, diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index a19436d5e..0939b5ce4 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -319,35 +319,48 @@ class BirdsEyeFrameManager: self.frame[:] = self.blank_frame self.cameras = {} - for camera, settings in self.config.cameras.items(): - # precalculate the coordinates for all the channels - y, u1, u2, v1, v2 = get_yuv_crop( - settings.frame_shape_yuv, - ( - 0, - 0, - settings.frame_shape[1], - settings.frame_shape[0], - ), - ) - self.cameras[camera] = { - "dimensions": [settings.detect.width, settings.detect.height], - "last_active_frame": 0.0, - "current_frame": 0.0, - "layout_frame": 0.0, - "channel_dims": { - "y": y, - "u1": u1, - "u2": u2, - "v1": v1, - "v2": v2, - }, - } + for camera in self.config.cameras.keys(): + self.add_camera(camera) self.camera_layout = [] self.active_cameras = set() self.last_output_time = 0.0 + def add_camera(self, cam: str): + """Add a camera to self.cameras with the correct structure.""" + settings = self.config.cameras[cam] + # precalculate the coordinates for all the channels + y, u1, u2, v1, v2 = get_yuv_crop( + settings.frame_shape_yuv, + ( + 0, + 0, + settings.frame_shape[1], + settings.frame_shape[0], + ), + ) + self.cameras[cam] = { + "dimensions": [ + settings.detect.width, + settings.detect.height, + ], + "last_active_frame": 0.0, + "current_frame": 0.0, + "layout_frame": 0.0, + "channel_dims": { + "y": y, + "u1": u1, + "u2": u2, + "v1": v1, + "v2": v2, + }, + } + + def remove_camera(self, cam: str): + """Remove a camera from self.cameras.""" + if cam in self.cameras: + del self.cameras[cam] + def clear_frame(self): logger.debug("Clearing the birdseye frame") self.frame[:] = self.blank_frame @@ -774,7 +787,7 @@ class Birdseye: self.broadcaster = BroadcastThread( "birdseye", self.converter, websocket_server, stop_event ) - self.birdseye_manager = BirdsEyeFrameManager(config, stop_event) + self.birdseye_manager = BirdsEyeFrameManager(self.config, stop_event) self.frame_manager = SharedMemoryFrameManager() self.stop_event = stop_event self.requestor = InterProcessRequestor() @@ -804,6 +817,16 @@ class Birdseye: self.birdseye_manager.clear_frame() self.__send_new_frame() + def add_camera(self, camera: str) -> None: + """Add a camera to the birdseye manager.""" + self.birdseye_manager.add_camera(camera) + logger.debug(f"Added camera {camera} to birdseye") + + def remove_camera(self, camera: str) -> None: + """Remove a camera from the birdseye manager.""" + self.birdseye_manager.remove_camera(camera) + logger.debug(f"Removed camera {camera} from birdseye") + def write_data( self, camera: str, diff --git a/frigate/output/output.py b/frigate/output/output.py index da5906e78..f176b2e4c 100644 --- a/frigate/output/output.py +++ b/frigate/output/output.py @@ -133,7 +133,7 @@ class OutputProcess(FrigateProcess): # check if there is an updated config updates = config_subscriber.check_for_updates() - if "add" in updates: + if CameraConfigUpdateEnum.add in updates: for camera in updates["add"]: jsmpeg_cameras[camera] = JsmpegCamera( cam_config, self.stop_event, websocket_server @@ -141,6 +141,12 @@ class OutputProcess(FrigateProcess): preview_recorders[camera] = PreviewRecorder(cam_config) preview_write_times[camera] = 0 + if ( + self.config.birdseye.enabled + and self.config.cameras[camera].birdseye.enabled + ): + birdseye.add_camera(camera) + (topic, data) = detection_subscriber.check_for_update(timeout=1) now = datetime.datetime.now().timestamp()