mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-10 08:42:49 +03:00
Fix Birdseye rapid camera switching when max_cameras is set (#24591)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* Fix Birdseye rapid camera switching in single-camera view (#10845) Add a configurable min_camera_hold (default 5s) to prevent Birdseye from rapidly flipping between cameras when multiple have simultaneous activity. Also fix the max_cameras cooldown which was bypassed whenever more cameras were active than the configured limit. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Remove min_camera_hold config, use hardcoded CAMERA_HOLD_SECONDS constant Replace the configurable min_camera_hold field with a module-level CAMERA_HOLD_SECONDS = 5 constant to keep behavior simple and avoid adding config complexity. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix max_cameras hold blocking count changes, add count-increase test The >= guard could prevent cameras from appearing when the displayed count was below max_cameras. Now the hold only applies when the limited count matches the currently displayed count, so count changes are always immediate. Adds a dedicated test for this case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Apply reviewer feedback: simplify max_cameras hold, remove single-camera hold - Use hawkeye217's suggested condition for max_cameras cooldown: only hold when currently showing exactly max_cameras and there are at least max_cameras active - Remove single-camera hold entirely — without max_cameras set, the hold only triggers when one camera expires as another activates, keeping the inactive camera visible unnecessarily - Remove last_layout_change_time (no longer used) - Remove single-camera hold tests (feature removed) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f084e6acc7
commit
f1d1748fb3
@@ -30,6 +30,9 @@ from frigate.util.image import (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Minimum seconds to hold the current camera before switching to a different one
|
||||||
|
CAMERA_HOLD_SECONDS = 5
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class BirdseyeActivity:
|
class BirdseyeActivity:
|
||||||
@@ -496,8 +499,11 @@ class BirdsEyeFrameManager:
|
|||||||
if max_cameras:
|
if max_cameras:
|
||||||
now = datetime.datetime.now().timestamp()
|
now = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
if len(active_cameras) == max_cameras and now - self.last_refresh_time < 10:
|
if (
|
||||||
# don't refresh cameras too often
|
len(self.active_cameras) == max_cameras
|
||||||
|
and len(active_cameras) >= max_cameras
|
||||||
|
and now - self.last_refresh_time < CAMERA_HOLD_SECONDS
|
||||||
|
):
|
||||||
active_cameras = self.active_cameras
|
active_cameras = self.active_cameras
|
||||||
else:
|
else:
|
||||||
limited_active_cameras = sorted(
|
limited_active_cameras = sorted(
|
||||||
|
|||||||
@@ -500,6 +500,80 @@ class TestBirdseyeLiveActivity(unittest.TestCase):
|
|||||||
assert self.manager.active_cameras == {"front"}
|
assert self.manager.active_cameras == {"front"}
|
||||||
|
|
||||||
|
|
||||||
|
class TestBirdseyeCameraHold(unittest.TestCase):
|
||||||
|
"""Test that CAMERA_HOLD_SECONDS prevents rapid camera switching."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
config = {
|
||||||
|
"mqtt": {"enabled": False},
|
||||||
|
"birdseye": {
|
||||||
|
"enabled": True,
|
||||||
|
"modes": ["motion"],
|
||||||
|
"inactivity_threshold": 30,
|
||||||
|
},
|
||||||
|
"cameras": {
|
||||||
|
camera: {
|
||||||
|
"ffmpeg": {
|
||||||
|
"inputs": [
|
||||||
|
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"detect": {"height": 1080, "width": 1920, "fps": 5},
|
||||||
|
}
|
||||||
|
for camera in ("back", "front")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.config = FrigateConfig(**config)
|
||||||
|
self.manager = BirdsEyeFrameManager(self.config, mp.Event())
|
||||||
|
|
||||||
|
for camera_data in self.manager.cameras.values():
|
||||||
|
camera_data["current_frame"] = None
|
||||||
|
camera_data["current_frame_time"] = 100.0
|
||||||
|
camera_data["last_active_frame"] = 0.0
|
||||||
|
camera_data["live_active"] = False
|
||||||
|
|
||||||
|
def test_max_cameras_cooldown_applies_when_more_active_than_max(self):
|
||||||
|
"""The max_cameras cooldown should apply even when more cameras are active than max."""
|
||||||
|
self.config.birdseye.layout.max_cameras = 1
|
||||||
|
self.manager.cameras["front"]["last_active_frame"] = 95.0
|
||||||
|
self.manager.cameras["front"]["current_frame_time"] = 100.0
|
||||||
|
self.manager.update_frame()
|
||||||
|
assert "front" in self.manager.active_cameras
|
||||||
|
|
||||||
|
self.manager.cameras["back"]["last_active_frame"] = 99.0
|
||||||
|
self.manager.cameras["back"]["current_frame_time"] = 100.0
|
||||||
|
self.manager.update_frame()
|
||||||
|
|
||||||
|
assert "front" in self.manager.active_cameras
|
||||||
|
|
||||||
|
def test_camera_count_change_ignores_hold(self):
|
||||||
|
"""Adding a camera (count change) should not be blocked by the hold period."""
|
||||||
|
self.manager.cameras["front"]["last_active_frame"] = 95.0
|
||||||
|
self.manager.update_frame()
|
||||||
|
assert self.manager.active_cameras == {"front"}
|
||||||
|
|
||||||
|
self.manager.cameras["back"]["last_active_frame"] = 99.0
|
||||||
|
self.manager.cameras["back"]["current_frame_time"] = 100.0
|
||||||
|
self.manager.cameras["front"]["last_active_frame"] = 95.0
|
||||||
|
self.manager.update_frame()
|
||||||
|
|
||||||
|
assert self.manager.active_cameras == {"front", "back"}
|
||||||
|
|
||||||
|
def test_max_cameras_count_increase_not_blocked_by_hold(self):
|
||||||
|
"""With max_cameras=2 showing 1, a second active camera must appear immediately."""
|
||||||
|
self.config.birdseye.layout.max_cameras = 2
|
||||||
|
self.manager.cameras["front"]["last_active_frame"] = 95.0
|
||||||
|
self.manager.cameras["front"]["current_frame_time"] = 100.0
|
||||||
|
self.manager.update_frame()
|
||||||
|
assert self.manager.active_cameras == {"front"}
|
||||||
|
|
||||||
|
self.manager.cameras["back"]["last_active_frame"] = 99.0
|
||||||
|
self.manager.cameras["back"]["current_frame_time"] = 100.0
|
||||||
|
self.manager.update_frame()
|
||||||
|
|
||||||
|
assert self.manager.active_cameras == {"front", "back"}
|
||||||
|
|
||||||
|
|
||||||
class TestBirdseyeModePayload(unittest.TestCase):
|
class TestBirdseyeModePayload(unittest.TestCase):
|
||||||
"""Test the MQTT payload contract for Birdseye activity modes."""
|
"""Test the MQTT payload contract for Birdseye activity modes."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user