mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-11 05:11:13 +03:00
fix birdseye reordering widget in settings
cameras are only sorted while the layout is built, so a config change that reorders them needs a fresh layout even when the active set is unchanged, which is always the case in continuous mode
This commit is contained in:
@@ -335,6 +335,7 @@ class BirdsEyeFrameManager:
|
|||||||
|
|
||||||
self.camera_layout: list[Any] = []
|
self.camera_layout: list[Any] = []
|
||||||
self.active_cameras: set[str] = set()
|
self.active_cameras: set[str] = set()
|
||||||
|
self.layout_camera_order: list[str] = []
|
||||||
self.last_output_time = 0.0
|
self.last_output_time = 0.0
|
||||||
|
|
||||||
def add_camera(self, cam: str) -> None:
|
def add_camera(self, cam: str) -> None:
|
||||||
@@ -372,6 +373,13 @@ class BirdsEyeFrameManager:
|
|||||||
if cam in self.cameras:
|
if cam in self.cameras:
|
||||||
del self.cameras[cam]
|
del self.cameras[cam]
|
||||||
|
|
||||||
|
def sort_cameras(self, cameras: set[str]) -> list[str]:
|
||||||
|
"""Sort cameras by birdseye order, falling back to name when tied."""
|
||||||
|
return sorted(
|
||||||
|
cameras,
|
||||||
|
key=lambda camera: (self.config.cameras[camera].birdseye.order, camera),
|
||||||
|
)
|
||||||
|
|
||||||
def clear_frame(self) -> None:
|
def clear_frame(self) -> None:
|
||||||
logger.debug("Clearing the birdseye frame")
|
logger.debug("Clearing the birdseye frame")
|
||||||
self.frame[:] = self.blank_frame
|
self.frame[:] = self.blank_frame
|
||||||
@@ -482,6 +490,7 @@ class BirdsEyeFrameManager:
|
|||||||
# if the layout needs to be cleared
|
# if the layout needs to be cleared
|
||||||
self.camera_layout = []
|
self.camera_layout = []
|
||||||
self.active_cameras = set()
|
self.active_cameras = set()
|
||||||
|
self.layout_camera_order = []
|
||||||
self.clear_frame()
|
self.clear_frame()
|
||||||
frame_changed = True
|
frame_changed = True
|
||||||
layout_changed = True
|
layout_changed = True
|
||||||
@@ -500,21 +509,21 @@ class BirdsEyeFrameManager:
|
|||||||
else:
|
else:
|
||||||
reset_layout = True
|
reset_layout = True
|
||||||
|
|
||||||
|
sorted_active_cameras = self.sort_cameras(active_cameras)
|
||||||
|
|
||||||
|
if not reset_layout and sorted_active_cameras != self.layout_camera_order:
|
||||||
|
logger.debug("Birdseye camera order changed")
|
||||||
|
reset_layout = True
|
||||||
|
|
||||||
if reset_layout:
|
if reset_layout:
|
||||||
logger.debug("Resetting Birdseye layout...")
|
logger.debug("Resetting Birdseye layout...")
|
||||||
self.clear_frame()
|
self.clear_frame()
|
||||||
self.active_cameras = active_cameras
|
self.active_cameras = active_cameras
|
||||||
|
self.layout_camera_order = sorted_active_cameras
|
||||||
layout_changed = True # Layout is changing due to reset
|
layout_changed = True # Layout is changing due to reset
|
||||||
# this also converts added_cameras from a set to a list since we need
|
# this also converts added_cameras from a set to a list since we need
|
||||||
# to pop elements in order
|
# to pop elements in order
|
||||||
active_cameras_to_add = sorted(
|
active_cameras_to_add = sorted_active_cameras
|
||||||
active_cameras,
|
|
||||||
# sort cameras by order and by name if the order is the same
|
|
||||||
key=lambda active_camera: (
|
|
||||||
self.config.cameras[active_camera].birdseye.order,
|
|
||||||
active_camera,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if len(active_cameras) == 1:
|
if len(active_cameras) == 1:
|
||||||
# show single camera as fullscreen
|
# show single camera as fullscreen
|
||||||
camera = active_cameras_to_add[0]
|
camera = active_cameras_to_add[0]
|
||||||
@@ -780,6 +789,7 @@ class BirdsEyeFrameManager:
|
|||||||
frame_changed, layout_changed = False, False
|
frame_changed, layout_changed = False, False
|
||||||
self.active_cameras = set()
|
self.active_cameras = set()
|
||||||
self.camera_layout = []
|
self.camera_layout = []
|
||||||
|
self.layout_camera_order = []
|
||||||
print(traceback.format_exc())
|
print(traceback.format_exc())
|
||||||
|
|
||||||
# if the frame was updated or the fps is too low, send frame
|
# if the frame was updated or the fps is too low, send frame
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""Test camera user and password cleanup."""
|
"""Test camera user and password cleanup."""
|
||||||
|
|
||||||
|
import multiprocessing as mp
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from frigate.output.birdseye import get_canvas_shape
|
from frigate.config import FrigateConfig
|
||||||
|
from frigate.output.birdseye import BirdsEyeFrameManager, get_canvas_shape
|
||||||
|
|
||||||
|
|
||||||
class TestBirdseye(unittest.TestCase):
|
class TestBirdseye(unittest.TestCase):
|
||||||
@@ -45,3 +47,70 @@ class TestBirdseye(unittest.TestCase):
|
|||||||
canvas_width, canvas_height = get_canvas_shape(width, height)
|
canvas_width, canvas_height = get_canvas_shape(width, height)
|
||||||
assert canvas_width == width # width will be the same
|
assert canvas_width == width # width will be the same
|
||||||
assert canvas_height != height
|
assert canvas_height != height
|
||||||
|
|
||||||
|
|
||||||
|
class TestBirdseyeCameraOrder(unittest.TestCase):
|
||||||
|
"""Test that birdseye reacts to camera order changes without a restart."""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
config = {
|
||||||
|
"mqtt": {"enabled": False},
|
||||||
|
"birdseye": {"enabled": True, "mode": "continuous"},
|
||||||
|
"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", "side")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
self.config = FrigateConfig(**config)
|
||||||
|
self.manager = BirdsEyeFrameManager(self.config, mp.Event())
|
||||||
|
|
||||||
|
# mark every camera as continuously active with no frame to draw, which
|
||||||
|
# exercises the layout without needing real yuv frames
|
||||||
|
for camera_data in self.manager.cameras.values():
|
||||||
|
camera_data["current_frame"] = None
|
||||||
|
camera_data["current_frame_time"] = 1.0
|
||||||
|
camera_data["last_active_frame"] = 1.0
|
||||||
|
|
||||||
|
def layout_order(self) -> list[str]:
|
||||||
|
"""Return the cameras in the order the current layout renders them."""
|
||||||
|
return [position[0] for row in self.manager.camera_layout for position in row]
|
||||||
|
|
||||||
|
def test_layout_uses_configured_order(self):
|
||||||
|
"""Test the layout is sorted by order, then by name when tied."""
|
||||||
|
self.config.cameras["side"].birdseye.order = 0
|
||||||
|
self.config.cameras["back"].birdseye.order = 10
|
||||||
|
self.config.cameras["front"].birdseye.order = 20
|
||||||
|
|
||||||
|
self.manager.update_frame()
|
||||||
|
|
||||||
|
assert self.layout_order() == ["side", "back", "front"]
|
||||||
|
|
||||||
|
def test_order_change_rebuilds_layout(self):
|
||||||
|
"""Test a reorder relayouts even though the active cameras are unchanged."""
|
||||||
|
self.manager.update_frame()
|
||||||
|
assert self.layout_order() == ["back", "front", "side"]
|
||||||
|
|
||||||
|
# a stable active set means only an order change can reset the layout,
|
||||||
|
# which is what a settings reorder publishes to this process
|
||||||
|
self.config.cameras["side"].birdseye.order = -10
|
||||||
|
|
||||||
|
_, layout_changed = self.manager.update_frame()
|
||||||
|
|
||||||
|
assert layout_changed
|
||||||
|
assert self.layout_order() == ["side", "back", "front"]
|
||||||
|
|
||||||
|
def test_unchanged_order_keeps_layout(self):
|
||||||
|
"""Test a repeat update with no order change doesn't reset the layout."""
|
||||||
|
self.manager.update_frame()
|
||||||
|
|
||||||
|
_, layout_changed = self.manager.update_frame()
|
||||||
|
|
||||||
|
assert not layout_changed
|
||||||
|
assert self.layout_order() == ["back", "front", "side"]
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export default function BirdseyeCameraReorder({
|
|||||||
try {
|
try {
|
||||||
await axios.put("config/set", {
|
await axios.put("config/set", {
|
||||||
requires_restart: 0,
|
requires_restart: 0,
|
||||||
|
update_topic: "config/cameras/*/birdseye",
|
||||||
config_data: { cameras: cameraUpdates },
|
config_data: { cameras: cameraUpdates },
|
||||||
});
|
});
|
||||||
await updateConfig();
|
await updateConfig();
|
||||||
|
|||||||
Reference in New Issue
Block a user