From 0e2e95714f9fcbdc1e7190067f956882b4d5ab52 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 15 Jul 2026 07:09:01 -0500
Subject: [PATCH 1/4] 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
---
frigate/output/birdseye.py | 26 ++++---
frigate/test/test_birdseye.py | 71 ++++++++++++++++++-
.../sectionExtras/BirdseyeCameraReorder.tsx | 1 +
3 files changed, 89 insertions(+), 9 deletions(-)
diff --git a/frigate/output/birdseye.py b/frigate/output/birdseye.py
index cd29dfe949..a38669cbf9 100644
--- a/frigate/output/birdseye.py
+++ b/frigate/output/birdseye.py
@@ -335,6 +335,7 @@ class BirdsEyeFrameManager:
self.camera_layout: list[Any] = []
self.active_cameras: set[str] = set()
+ self.layout_camera_order: list[str] = []
self.last_output_time = 0.0
def add_camera(self, cam: str) -> None:
@@ -372,6 +373,13 @@ class BirdsEyeFrameManager:
if cam in self.cameras:
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:
logger.debug("Clearing the birdseye frame")
self.frame[:] = self.blank_frame
@@ -482,6 +490,7 @@ class BirdsEyeFrameManager:
# if the layout needs to be cleared
self.camera_layout = []
self.active_cameras = set()
+ self.layout_camera_order = []
self.clear_frame()
frame_changed = True
layout_changed = True
@@ -500,21 +509,21 @@ class BirdsEyeFrameManager:
else:
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:
logger.debug("Resetting Birdseye layout...")
self.clear_frame()
self.active_cameras = active_cameras
+ self.layout_camera_order = sorted_active_cameras
layout_changed = True # Layout is changing due to reset
# this also converts added_cameras from a set to a list since we need
# to pop elements in order
- active_cameras_to_add = sorted(
- 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,
- ),
- )
+ active_cameras_to_add = sorted_active_cameras
if len(active_cameras) == 1:
# show single camera as fullscreen
camera = active_cameras_to_add[0]
@@ -780,6 +789,7 @@ class BirdsEyeFrameManager:
frame_changed, layout_changed = False, False
self.active_cameras = set()
self.camera_layout = []
+ self.layout_camera_order = []
print(traceback.format_exc())
# if the frame was updated or the fps is too low, send frame
diff --git a/frigate/test/test_birdseye.py b/frigate/test/test_birdseye.py
index 33683f5c4b..bd70e37efe 100644
--- a/frigate/test/test_birdseye.py
+++ b/frigate/test/test_birdseye.py
@@ -1,8 +1,10 @@
"""Test camera user and password cleanup."""
+import multiprocessing as mp
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):
@@ -45,3 +47,70 @@ class TestBirdseye(unittest.TestCase):
canvas_width, canvas_height = get_canvas_shape(width, height)
assert canvas_width == width # width will be the same
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"]
diff --git a/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx b/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx
index b481e9a86f..f448310230 100644
--- a/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx
+++ b/web/src/components/config-form/sectionExtras/BirdseyeCameraReorder.tsx
@@ -91,6 +91,7 @@ export default function BirdseyeCameraReorder({
try {
await axios.put("config/set", {
requires_restart: 0,
+ update_topic: "config/cameras/*/birdseye",
config_data: { cameras: cameraUpdates },
});
await updateConfig();
From da635749ba1056c91ce2a04caec334c86aa078fb Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 15 Jul 2026 07:18:03 -0500
Subject: [PATCH 2/4] clarify config/set endpoint response message
this legacy message is not consumed anywhere by the frontend so this change is just to avoid user confusion
---
frigate/api/app.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/frigate/api/app.py b/frigate/api/app.py
index ac839158e2..49b555606d 100644
--- a/frigate/api/app.py
+++ b/frigate/api/app.py
@@ -971,7 +971,11 @@ def config_set(request: Request, body: AppConfigSetBody):
content=(
{
"success": True,
- "message": "Config successfully updated, restart to apply",
+ "message": (
+ "Config successfully updated"
+ if body.requires_restart == 0
+ else "Config successfully updated, restart to apply"
+ ),
}
),
status_code=200,
From 6e9b9f35a4bf58298581c2ccafc6b9435de12b37 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 15 Jul 2026 07:32:40 -0500
Subject: [PATCH 3/4] add note about fd and pid limits to installation docs
---
docs/docs/frigate/installation.md | 27 +++++++++++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md
index 5cdbc13627..14c85ec4f3 100644
--- a/docs/docs/frigate/installation.md
+++ b/docs/docs/frigate/installation.md
@@ -78,7 +78,7 @@ Users of the Snapcraft build of Docker cannot use storage locations outside your
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**.
-The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose).
+The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). If raising the shm size does not help, check your [process and file limits](#process-and-file-limits) as well.
The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well.
@@ -86,6 +86,30 @@ The Frigate container also stores logs in shm, which can take up to **40MB**, so
The shm size cannot be set per container for Home Assistant Apps. However, this is probably not required since by default Home Assistant Supervisor allocates `/dev/shm` with half the size of your total memory. If your machine has 8GB of memory, chances are that Frigate will have access to up to 4GB without any additional configuration.
+### Process and file limits
+
+Frigate runs many processes and opens a number of shared memory files. Installs with a large number of cameras can exceed the default limits your container runtime applies.
+
+Hitting the PID limit logs `RuntimeError: can't start new thread`, often followed by a "Bus error" that makes it look like an shm sizing problem. Compare the current count against the max from inside the container:
+
+```bash
+cat /sys/fs/cgroup/pids.current
+cat /sys/fs/cgroup/pids.max
+```
+
+If these are close, raise the limit with [`--pids-limit`](https://docs.docker.com/engine/containers/resource_constraints/) (or `service.pids_limit` in Docker Compose).
+
+Running out of file descriptors logs `OSError: [Errno 24] Too many open files`. Raise the limit in Docker Compose:
+
+```yaml
+services:
+ frigate:
+ ulimits:
+ nofile:
+ soft: 65535
+ hard: 65535
+```
+
## Extra Steps for Specific Hardware
The following sections contain additional setup steps that are only required if you are using specific hardware. If you are not using any of these hardware types, you can skip to the [Docker](#docker) installation section.
@@ -484,7 +508,6 @@ Generate a Frigate Docker Compose configuration based on your hardware and requi
-
```yaml
From 4c5ea3a8907b731da44bb6139c83db5ec0276ed9 Mon Sep 17 00:00:00 2001
From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
Date: Wed, 15 Jul 2026 08:21:09 -0500
Subject: [PATCH 4/4] update intel gpu error message to include kernel version
---
frigate/util/services.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frigate/util/services.py b/frigate/util/services.py
index 62929d32df..cc25de1fb7 100644
--- a/frigate/util/services.py
+++ b/frigate/util/services.py
@@ -433,7 +433,8 @@ def get_intel_gpu_stats(
if not snapshot_a:
logger.warning(
"Unable to collect Intel GPU stats: no DRM fdinfo entries found"
- "%s. Check that /proc is readable and the i915/xe driver is loaded",
+ "%s. Check kernel version (required >= 6.5) and that /proc is readable"
+ " and the i915/xe driver is loaded",
f" for pdev {target_pdev}" if target_pdev else "",
)
return None