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
This commit is contained in:
Francesco Durighetto 2025-10-18 15:14:41 +02:00
parent ce9dd7d7f7
commit 3e102b1bdf
2 changed files with 10 additions and 42 deletions

View File

@ -58,7 +58,8 @@ class BirdseyeConfig(FrigateBaseModel):
idle_heartbeat_fps: float = Field( idle_heartbeat_fps: float = Field(
default=0.0, default=0.0,
ge=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 # uses BaseModel because some global attributes are not available at the camera level

View File

@ -792,16 +792,9 @@ class Birdseye:
self.frame_manager = SharedMemoryFrameManager() self.frame_manager = SharedMemoryFrameManager()
self.stop_event = stop_event self.stop_event = stop_event
self.requestor = InterProcessRequestor() self.requestor = InterProcessRequestor()
self._heartbeat_thread = None self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps
# --- 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_interval: Optional[float] = (1.0 / self.idle_fps) if self.idle_fps > 0 else None self._idle_interval: Optional[float] = (1.0 / self.idle_fps) if self.idle_fps > 0 else None
if config.birdseye.restream: if config.birdseye.restream:
self.birdseye_buffer = self.frame_manager.create( self.birdseye_buffer = self.frame_manager.create(
"birdseye", "birdseye",
@ -811,16 +804,6 @@ class Birdseye:
self.converter.start() self.converter.start()
self.broadcaster.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: def __send_new_frame(self) -> None:
frame_bytes = self.birdseye_manager.frame.tobytes() frame_bytes = self.birdseye_manager.frame.tobytes()
@ -868,28 +851,12 @@ class Birdseye:
if frame_layout_changed: if frame_layout_changed:
coordinates = self.birdseye_manager.get_camera_coordinates() coordinates = self.birdseye_manager.get_camera_coordinates()
self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates) self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates)
if self._idle_interval:
def _idle_heartbeat_loop(self) -> None: now = time.monotonic()
""" is_idle = (len(self.birdseye_manager.camera_layout) == 0)
Periodically re-send the last composed frame when idle. if is_idle and (now - self.birdseye_manager.last_output_time) >= self._idle_interval:
Active only if FRIGATE_BIRDSEYE_IDLE_FPS > 0. self.__send_new_frame()
"""
# 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)
def stop(self) -> None: def stop(self) -> None:
self.converter.join() self.converter.join()
self.broadcaster.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)