From 3e102b1bdf6a23eb49022994292ae3c0db9a7e5f Mon Sep 17 00:00:00 2001 From: Francesco Durighetto Date: Sat, 18 Oct 2025 15:14:41 +0200 Subject: [PATCH] Refactors Birdseye idle frame broadcasting Simplifies the idle frame broadcasting logic by removing the dedicated thread. The idle frame is now resent directly within the main loop, improving efficiency and reducing complexity. Also, limits the idle heartbeat FPS to a maximum of 10 since the framebuffer is limited to 10 anyway --- frigate/config/camera/birdseye.py | 3 +- frigate/output/birdseye.py | 49 +++++-------------------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/frigate/config/camera/birdseye.py b/frigate/config/camera/birdseye.py index e968956b9..407e500c6 100644 --- a/frigate/config/camera/birdseye.py +++ b/frigate/config/camera/birdseye.py @@ -58,7 +58,8 @@ class BirdseyeConfig(FrigateBaseModel): idle_heartbeat_fps: float = Field( default=0.0, ge=0.0, - title="Idle heartbeat FPS (0 disables)", + le=10.0, + title="Idle heartbeat FPS (0 disables, max 10)", ) # uses BaseModel because some global attributes are not available at the camera level diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py index 770078b69..f3243fd58 100644 --- a/frigate/output/birdseye.py +++ b/frigate/output/birdseye.py @@ -792,16 +792,9 @@ class Birdseye: self.frame_manager = SharedMemoryFrameManager() self.stop_event = stop_event self.requestor = InterProcessRequestor() - self._heartbeat_thread = None - - # --- Optional idle heartbeat (disabled by default) --- - # If FRIGATE_BIRDSEYE_IDLE_FPS > 0, periodically re-send the last frame - # when no frames have been output recently. This improves client attach times - # without altering default behavior. - self.idle_fps = float(self.config.birdseye.idle_heartbeat_fps or 0.0) - self.idle_fps = max(0.0, self.idle_fps) + self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps self._idle_interval: Optional[float] = (1.0 / self.idle_fps) if self.idle_fps > 0 else None - + if config.birdseye.restream: self.birdseye_buffer = self.frame_manager.create( "birdseye", @@ -811,16 +804,6 @@ class Birdseye: self.converter.start() self.broadcaster.start() - - # Start heartbeat loop only if enabled - if self._idle_interval: - self._heartbeat_thread = threading.Thread( - target=self._idle_heartbeat_loop, - name="birdseye_idle_heartbeat", - daemon=True, - ) - self._heartbeat_thread.start() - def __send_new_frame(self) -> None: frame_bytes = self.birdseye_manager.frame.tobytes() @@ -868,28 +851,12 @@ class Birdseye: if frame_layout_changed: coordinates = self.birdseye_manager.get_camera_coordinates() self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates) - - def _idle_heartbeat_loop(self) -> None: - """ - Periodically re-send the last composed frame when idle. - Active only if FRIGATE_BIRDSEYE_IDLE_FPS > 0. - """ - # Small sleep granularity to check often without busy-spinning. - min_sleep = 0.2 - while not self.stop_event.is_set(): - try: - if self._idle_interval: - now = datetime.datetime.now().timestamp() - if (now - self.birdseye_manager.last_output_time) >= self._idle_interval: - self.__send_new_frame() - finally: - # Sleep at the smaller of idle interval or a safe minimum - sleep_for = self._idle_interval if self._idle_interval and self._idle_interval < min_sleep else min_sleep - time.sleep(sleep_for) + if self._idle_interval: + now = time.monotonic() + is_idle = (len(self.birdseye_manager.camera_layout) == 0) + if is_idle and (now - self.birdseye_manager.last_output_time) >= self._idle_interval: + self.__send_new_frame() def stop(self) -> None: self.converter.join() - self.broadcaster.join() - if self._heartbeat_thread and self._heartbeat_thread.is_alive(): - # the thread is daemon=True; join a moment just for cleanliness - self._heartbeat_thread.join(timeout=0.2) + self.broadcaster.join() \ No newline at end of file