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