Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/docs/configuration/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ birdseye:
scaling_factor: 2.0
# Optional: Maximum number of cameras to show at one time, showing the most recent (default: show all cameras)
max_cameras: 1
# Optional: Frames-per-second to re-send the last composed Birdseye frame when idle (no motion or active updates). (default: shown below)
idle_heartbeat_fps: 10.0

# Optional: ffmpeg configuration
# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets
Expand Down
8 changes: 7 additions & 1 deletion docs/docs/configuration/restream.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ birdseye:
restream: True
```

**Tip:** To improve connection speed when using Birdseye via RTSP or WebRTC,
you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps`
to a low value (e.g. `1–2`).
This makes Frigate periodically push the last frame even when no motion
is detected, reducing initial connection latency.

### Securing Restream With Authentication

The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
Expand Down Expand Up @@ -164,4 +170,4 @@ NOTE: The output will need to be passed with two curly braces `{{output}}`
go2rtc:
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}}
```
```
6 changes: 5 additions & 1 deletion frigate/config/camera/birdseye.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ class BirdseyeConfig(FrigateBaseModel):
layout: BirdseyeLayoutConfig = Field(
default_factory=BirdseyeLayoutConfig, title="Birdseye Layout Config"
)

idle_heartbeat_fps: float = Field(
default=0.0,
ge=0.0,
title="Idle heartbeat FPS (0 disables)",
)

# uses BaseModel because some global attributes are not available at the camera level
class BirdseyeCameraConfig(BaseModel):
Expand Down
43 changes: 42 additions & 1 deletion frigate/output/birdseye.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import threading
import traceback
from typing import Any, Optional
import time

import cv2
import numpy as np
Expand Down Expand Up @@ -791,7 +792,16 @@ def __init__(
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_interval: Optional[float] = (1.0 / self.idle_fps) if self.idle_fps > 0 else None
Comment on lines +801 to +803
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant. We know it will be a valid float, with a default of 0.0


if config.birdseye.restream:
self.birdseye_buffer = self.frame_manager.create(
"birdseye",
Expand All @@ -801,6 +811,16 @@ def __init__(
self.converter.start()
self.broadcaster.start()


# Start heartbeat loop only if enabled
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a thread for this, we should be able to just add a last_update check, and if the last update was older than the idle_heartbeat it should manually update with the same frame that was already sent.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but from my understanding this code runs only when we receive an update.
the purpose is to overcome this limit with a timed thread

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, that's not the case. Birdseye will always get updates even if there is no motion or objects. It just won't send an update in the case that no cameras match the criteria to be displayed. That's the part that just needs a check to resend the blank image anyway

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()

Expand Down Expand Up @@ -849,6 +869,27 @@ def write_data(
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)

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)