Compare commits

..
Author SHA1 Message Date
Blake Blackshear cb943022f9 updates for yolov9 coral support 2026-02-27 01:35:35 +00:00
20 changed files with 57 additions and 442 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
default_target: local
COMMIT_HASH := $(shell git log -1 --pretty=format:"%h"|tail -1)
VERSION = 0.17.1
VERSION = 0.17.0
IMAGE_REPO ?= ghcr.io/blakeblackshear/frigate
GITHUB_REF_NAME ?= $(shell git rev-parse --abbrev-ref HEAD)
BOARDS= #Initialized empty
@@ -7,7 +7,7 @@ Object classification allows you to train a custom MobileNetV2 classification mo
## Minimum System Requirements
Object classification models are lightweight and run very fast on CPU.
Object classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate.
Training the model does briefly use a high amount of system resources for about 13 minutes per training run. On lower-power devices, training may take longer.
@@ -7,7 +7,7 @@ State classification allows you to train a custom MobileNetV2 classification mod
## Minimum System Requirements
State classification models are lightweight and run very fast on CPU.
State classification models are lightweight and run very fast on CPU. Inference should be usable on virtually any machine that can run Frigate.
Training the model does briefly use a high amount of system resources for about 13 minutes per training run. On lower-power devices, training may take longer.
@@ -32,8 +32,6 @@ All of these features run locally on your system.
## Minimum System Requirements
A CPU with AVX instructions is required to run Face Recognition.
The `small` model is optimized for efficiency and runs on the CPU, most CPUs should run the model efficiently.
The `large` model is optimized for accuracy, an integrated or discrete GPU / NPU is required. See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_enrichments.md) documentation.
@@ -30,7 +30,7 @@ In the default mode, Frigate's LPR needs to first detect a `car` or `motorcycle`
## Minimum System Requirements
License plate recognition works by running AI models locally on your system. The YOLOv9 plate detector model and the OCR models ([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)) are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM and a CPU with AVX instructions is required.
License plate recognition works by running AI models locally on your system. The YOLOv9 plate detector model and the OCR models ([PaddleOCR](https://github.com/PaddlePaddle/PaddleOCR)) are relatively lightweight and can run on your CPU or GPU, depending on your configuration. At least 4GB of RAM is required.
## Configuration
+4 -4
View File
@@ -161,7 +161,7 @@ YOLOv9 models that are compiled for TensorFlow Lite and properly quantized are s
:::tip
**Frigate+ Users:** Follow the [instructions](/integrations/plus#use-models) to set a model ID in your config file.
**Frigate+ Users:** Follow the [instructions](../integrations/plus#use-models) to set a model ID in your config file.
:::
@@ -1571,12 +1571,12 @@ YOLOv9 model can be exported as ONNX using the command below. You can copy and p
```sh
docker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'
FROM python:3.11 AS build
RUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/
RUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/
WORKDIR /yolov9
ADD https://github.com/WongKinYiu/yolov9.git .
RUN uv pip install --system -r requirements.txt
RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript
RUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier>=0.4.1 onnxscript
ARG MODEL_SIZE
ARG IMG_SIZE
ADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt
+1 -1
View File
@@ -13,7 +13,7 @@ Semantic Search is accessed via the _Explore_ view in the Frigate UI.
Semantic Search works by running a large AI model locally on your system. Small or underpowered systems like a Raspberry Pi will not run Semantic Search reliably or at all.
A minimum of 8GB of RAM is required to use Semantic Search. A CPU with AVX instructions is required to run Semantic Search. A GPU is not strictly required but will provide a significant performance increase over CPU-only systems.
A minimum of 8GB of RAM is required to use Semantic Search. A GPU is not strictly required but will provide a significant performance increase over CPU-only systems.
For best performance, 16GB or more of RAM and a dedicated GPU are recommended.
+1 -1
View File
@@ -26,7 +26,7 @@ I may earn a small commission for my endorsement, recommendation, testimonial, o
## Server
My current favorite is the Beelink EQ13 because of the efficient N100 CPU and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU (with AVX instructions) and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral, Hailo, or other AI accelerators.
My current favorite is the Beelink EQ13 because of the efficient N100 CPU and dual NICs that allow you to setup a dedicated private network for your cameras where they can be blocked from accessing the internet. There are many used workstation options on eBay that work very well. Anything with an Intel CPU and capable of running Debian should work fine. As a bonus, you may want to look for devices with a M.2 or PCIe express slot that is compatible with the Google Coral, Hailo, or other AI accelerators.
Note that many of these mini PCs come with Windows pre-installed, and you will need to install Linux according to the [getting started guide](../guides/getting_started.md).
-3
View File
@@ -38,9 +38,6 @@ There are many different hardware options for object detection depending on prio
Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings.
### CPU
Frigate requires a CPU with AVX instructions. Most modern CPUs (post-2011) support AVX, but it is generally absent in low-power or budget-oriented processors, particularly older Intel Pentium, Celeron, and Atom-based chips. Specifically, Intel Celeron and Pentium models prior to the 2020 Tiger Lake generation typically lack AVX.
#### SSDs (Solid State Drives)
+8 -8
View File
@@ -37,18 +37,18 @@ The following diagram adds a lot more detail than the simple view explained befo
%%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%%
flowchart TD
RecStore[(Recording<br>store)]
SnapStore[(Snapshot<br>store)]
RecStore[(Recording\nstore)]
SnapStore[(Snapshot\nstore)]
subgraph Acquisition
Cam["Camera"] -->|FFmpeg supported| Stream
Cam -->|"Other streaming<br>protocols"| go2rtc
Cam -->|"Other streaming\nprotocols"| go2rtc
go2rtc("go2rtc") --> Stream
Stream[Capture main and<br>sub streams] --> |detect stream|Decode(Decode and<br>downscale)
Stream[Capture main and\nsub streams] --> |detect stream|Decode(Decode and\ndownscale)
end
subgraph Motion
Decode --> MotionM(Apply<br>motion masks)
MotionM --> MotionD(Motion<br>detection)
Decode --> MotionM(Apply\nmotion masks)
MotionM --> MotionD(Motion\ndetection)
end
subgraph Detection
MotionD --> |motion regions| ObjectD(Object detection)
@@ -60,8 +60,8 @@ flowchart TD
MotionD --> |motion event|Birdseye
ObjectZ --> |object event|Birdseye
MotionD --> |"video segments<br>(retain motion)"|RecStore
MotionD --> |"video segments\n(retain motion)"|RecStore
ObjectZ --> |detection clip|RecStore
Stream -->|"video segments<br>(retain all)"| RecStore
Stream -->|"video segments\n(retain all)"| RecStore
ObjectZ --> |detection snapshot|SnapStore
```
+1 -42
View File
@@ -174,47 +174,8 @@ cameras:
### Step 4: Configure detectors
By default, Frigate will use a single CPU detector.
By default, Frigate will use a single CPU detector. If you have a USB Coral, you will need to add a detectors section to your config.
Generally speaking, integrated graphics can meet the needs of most users. Intel integrated graphics users can refer to the following configuration.
<details>
<summary>Use Intel openvino detector</summary>
You need to refer to **Configure hardware acceleration** above to enable the container to use the GPU.
```yaml
mqtt: ...
detectors: # <---- add detectors
ov:
type: openvino # <---- use openvino detector
device: GPU
# We will use the default MobileNet_v2 model from OpenVINO.
model:
width: 300
height: 300
input_tensor: nhwc
input_pixel_format: bgr
path: /openvino-model/ssdlite_mobilenet_v2.xml
labelmap_path: /openvino-model/coco_91cl_bkgr.txt
cameras:
name_of_your_camera:
ffmpeg: ...
detect:
enabled: True # <---- turn on detection
...
```
</details>
If you have a USB Coral, you will need to add a detectors section to your config.
<details>
<summary>Use USB Coral detector</summary>
`docker-compose.yml` (after modifying, you will need to run `docker compose up -d` to apply changes)
```yaml
@@ -243,8 +204,6 @@ cameras:
...
```
</details>
More details on available detectors can be found [here](../configuration/object_detectors.md).
Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they will need to be added according to the [configuration file reference](../configuration/reference.md).
+1 -65
View File
@@ -986,16 +986,7 @@ async def require_camera_access(
current_user = await get_current_user(request)
if isinstance(current_user, JSONResponse):
detail = "Authentication required"
try:
error_payload = json.loads(current_user.body)
detail = (
error_payload.get("message") or error_payload.get("detail") or detail
)
except Exception:
pass
raise HTTPException(status_code=current_user.status_code, detail=detail)
return current_user
role = current_user["role"]
all_camera_names = set(request.app.frigate_config.cameras.keys())
@@ -1013,61 +1004,6 @@ async def require_camera_access(
)
def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
owner_cameras: set[str] = set()
for camera_name, camera in request.app.frigate_config.cameras.items():
if stream_name == camera_name:
owner_cameras.add(camera_name)
continue
if stream_name in camera.live.streams.values():
owner_cameras.add(camera_name)
return owner_cameras
async def require_go2rtc_stream_access(
stream_name: Optional[str] = None,
request: Request = None,
):
"""Dependency to enforce go2rtc stream access based on owning camera access."""
if stream_name is None:
return
current_user = await get_current_user(request)
if isinstance(current_user, JSONResponse):
detail = "Authentication required"
try:
error_payload = json.loads(current_user.body)
detail = (
error_payload.get("message") or error_payload.get("detail") or detail
)
except Exception:
pass
raise HTTPException(status_code=current_user.status_code, detail=detail)
role = current_user["role"]
all_camera_names = set(request.app.frigate_config.cameras.keys())
roles_dict = request.app.frigate_config.auth.roles
allowed_cameras = User.get_allowed_cameras(role, roles_dict, all_camera_names)
# Admin or full access bypasses
if role == "admin" or not roles_dict.get(role):
return
owner_cameras = _get_stream_owner_cameras(request, stream_name)
if owner_cameras & set(allowed_cameras):
return
raise HTTPException(
status_code=403,
detail=f"Access denied to camera '{stream_name}'. Allowed: {allowed_cameras}",
)
async def get_allowed_cameras_for_filter(request: Request):
"""Dependency to get allowed_cameras for filtering lists."""
current_user = await get_current_user(request)
+5 -18
View File
@@ -17,7 +17,7 @@ from zeep.transports import AsyncTransport
from frigate.api.auth import (
allow_any_authenticated,
require_go2rtc_stream_access,
require_camera_access,
require_role,
)
from frigate.api.defs.tags import Tags
@@ -71,27 +71,14 @@ def go2rtc_streams():
@router.get(
"/go2rtc/streams/{stream_name}",
dependencies=[Depends(require_go2rtc_stream_access)],
"/go2rtc/streams/{camera_name}", dependencies=[Depends(require_camera_access)]
)
def go2rtc_camera_stream(request: Request, stream_name: str):
def go2rtc_camera_stream(request: Request, camera_name: str):
r = requests.get(
"http://127.0.0.1:1984/api/streams",
params={
"src": stream_name,
"video": "all",
"audio": "all",
"microphone": "",
},
f"http://127.0.0.1:1984/api/streams?src={camera_name}&video=all&audio=all&microphone"
)
if not r.ok:
camera_config = request.app.frigate_config.cameras.get(stream_name)
if camera_config is None:
for camera_name, camera in request.app.frigate_config.cameras.items():
if stream_name in camera.live.streams.values():
camera_config = request.app.frigate_config.cameras.get(camera_name)
break
camera_config = request.app.frigate_config.cameras.get(camera_name)
if camera_config and camera_config.enabled:
logger.error("Failed to fetch streams from go2rtc")
@@ -103,19 +103,16 @@ class ObjectDescriptionProcessor(PostProcessorApi):
logger.debug(f"{camera} sending early request to GenAI")
self.early_request_sent[data["id"]] = True
# Copy thumbnails to avoid holding references after cleanup
thumbnails_copy = [
data["thumbnail"][:] if data.get("thumbnail") else None
for data in self.tracked_events[data["id"]]
if data.get("thumbnail")
]
threading.Thread(
target=self._genai_embed_description,
name=f"_genai_embed_description_{event.id}",
daemon=True,
args=(
event,
thumbnails_copy,
[
data["thumbnail"]
for data in self.tracked_events[data["id"]]
],
),
).start()
@@ -175,13 +172,8 @@ class ObjectDescriptionProcessor(PostProcessorApi):
embed_image = (
[snapshot_image]
if event.has_snapshot and source == "snapshot"
# Copy thumbnails to avoid holding references
else (
[
data["thumbnail"][:] if data.get("thumbnail") else None
for data in self.tracked_events[event_id]
if data.get("thumbnail")
]
[data["thumbnail"] for data in self.tracked_events[event_id]]
if len(self.tracked_events.get(event_id, [])) > 0
else [thumbnail]
)
@@ -284,13 +276,8 @@ class ObjectDescriptionProcessor(PostProcessorApi):
embed_image = (
[snapshot_image]
if event.has_snapshot and camera_config.objects.genai.use_snapshot
# Copy thumbnails to avoid holding references after cleanup
else (
[
data["thumbnail"][:] if data.get("thumbnail") else None
for data in self.tracked_events[event.id]
if data.get("thumbnail")
]
[data["thumbnail"] for data in self.tracked_events[event.id]]
if num_thumbnails > 0
else [thumbnail]
)
+1 -4
View File
@@ -679,7 +679,4 @@ class EmbeddingMaintainer(threading.Thread):
if not self.config.semantic_search.enabled:
return
try:
self.embeddings.embed_thumbnail(event_id, thumbnail)
except ValueError:
logger.warning(f"Failed to embed thumbnail for event {event_id}")
self.embeddings.embed_thumbnail(event_id, thumbnail)
@@ -1,7 +1,6 @@
from unittest.mock import patch
from fastapi import HTTPException, Request
from fastapi.testclient import TestClient
from frigate.api.auth import (
get_allowed_cameras_for_filter,
@@ -10,33 +9,6 @@ from frigate.api.auth import (
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
# Minimal multi-camera config used by go2rtc stream access tests.
# front_door has a stream alias "front_door_main"; back_door uses its own name.
# The "limited_user" role is restricted to front_door only.
_MULTI_CAMERA_CONFIG = {
"mqtt": {"host": "mqtt"},
"auth": {
"roles": {
"limited_user": ["front_door"],
}
},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
"live": {"streams": {"default": "front_door_main"}},
},
"back_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
},
}
class TestCameraAccessEventReview(BaseTestHttp):
def setUp(self):
@@ -218,179 +190,3 @@ class TestCameraAccessEventReview(BaseTestHttp):
resp = client.get("/events/summary")
summary_list = resp.json()
assert len(summary_list) == 2
class TestGo2rtcStreamAccess(BaseTestHttp):
"""Tests for require_go2rtc_stream_access — the auth dependency on
GET /go2rtc/streams/{stream_name}.
go2rtc is not running in unit tests, so an authorized request returns
500 (the proxy call fails), while an unauthorized request returns 401/403
before the proxy is ever reached.
"""
def _make_app(self, config_override: dict | None = None):
"""Build a test app, optionally replacing self.minimal_config."""
if config_override is not None:
self.minimal_config = config_override
app = super().create_app()
# Allow tests to control the current user via request headers.
async def mock_get_current_user(request: Request):
username = request.headers.get("remote-user")
role = request.headers.get("remote-role")
if not username or not role:
from fastapi.responses import JSONResponse
return JSONResponse(
content={"message": "No authorization headers."},
status_code=401,
)
return {"username": username, "role": role}
app.dependency_overrides[get_current_user] = mock_get_current_user
return app
def setUp(self):
super().setUp([Event, ReviewSegment, Recordings])
def tearDown(self):
super().tearDown()
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _get_stream(
self, app, stream_name: str, role: str = "admin", user: str = "test"
):
"""Issue GET /go2rtc/streams/{stream_name} with the given role."""
with AuthTestClient(app) as client:
return client.get(
f"/go2rtc/streams/{stream_name}",
headers={"remote-user": user, "remote-role": role},
)
# ------------------------------------------------------------------
# Tests
# ------------------------------------------------------------------
def test_admin_can_access_any_stream(self):
"""Admin role bypasses camera restrictions."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
# front_door stream — go2rtc is not running so expect 500, not 401/403
resp = self._get_stream(app, "front_door", role="admin")
assert resp.status_code not in (401, 403), (
f"Admin should not be blocked; got {resp.status_code}"
)
# back_door stream
resp = self._get_stream(app, "back_door", role="admin")
assert resp.status_code not in (401, 403)
def test_missing_auth_headers_returns_401(self):
"""Requests without auth headers must be rejected with 401."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
# Use plain TestClient (not AuthTestClient) so no headers are injected.
with TestClient(app, raise_server_exceptions=False) as client:
resp = client.get("/go2rtc/streams/front_door")
assert resp.status_code == 401, f"Expected 401, got {resp.status_code}"
def test_unconfigured_role_can_access_any_stream(self):
"""When no camera restrictions are configured for a role the user
should have access to all streams (no roles_dict entry ⇒ no restriction)."""
no_roles_config = {
"mqtt": {"host": "mqtt"},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"back_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
},
}
app = self._make_app(no_roles_config)
# "myuser" role is not listed in roles_dict — should be allowed everywhere
for stream in ("front_door", "back_door"):
resp = self._get_stream(app, stream, role="myuser")
assert resp.status_code not in (401, 403), (
f"Unconfigured role should not be blocked on '{stream}'; "
f"got {resp.status_code}"
)
def test_restricted_role_can_access_allowed_camera(self):
"""limited_user role (restricted to front_door) can access front_door stream."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
resp = self._get_stream(app, "front_door", role="limited_user")
assert resp.status_code not in (401, 403), (
f"limited_user should be allowed on front_door; got {resp.status_code}"
)
def test_restricted_role_blocked_from_disallowed_camera(self):
"""limited_user role (restricted to front_door) cannot access back_door stream."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
resp = self._get_stream(app, "back_door", role="limited_user")
assert resp.status_code == 403, (
f"limited_user should be denied on back_door; got {resp.status_code}"
)
def test_stream_alias_allowed_for_owning_camera(self):
"""Stream alias 'front_door_main' is owned by front_door; limited_user (who
is allowed front_door) should be permitted."""
app = self._make_app(_MULTI_CAMERA_CONFIG)
# front_door_main is the alias defined in live.streams for front_door
resp = self._get_stream(app, "front_door_main", role="limited_user")
assert resp.status_code not in (401, 403), (
f"limited_user should be allowed on alias front_door_main; "
f"got {resp.status_code}"
)
def test_stream_alias_blocked_when_owning_camera_disallowed(self):
"""limited_user cannot access a stream alias that belongs to a camera they
are not allowed to see."""
# Give back_door a stream alias and restrict limited_user to front_door only
config = {
"mqtt": {"host": "mqtt"},
"auth": {
"roles": {
"limited_user": ["front_door"],
}
},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}
]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"back_door": {
"ffmpeg": {
"inputs": [
{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}
]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
"live": {"streams": {"default": "back_door_main"}},
},
},
}
app = self._make_app(config)
resp = self._get_stream(app, "back_door_main", role="limited_user")
assert resp.status_code == 403, (
f"limited_user should be denied on alias back_door_main; "
f"got {resp.status_code}"
)
@@ -77,7 +77,6 @@ import { useStreamingSettings } from "@/context/streaming-settings-provider";
import { Trans, useTranslation } from "react-i18next";
import { CameraNameLabel } from "../camera/FriendlyNameLabel";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useHasFullCameraAccess } from "@/hooks/use-has-full-camera-access";
import { useIsAdmin } from "@/hooks/use-is-admin";
import { useUserPersistedOverlayState } from "@/hooks/use-overlay-state";
@@ -678,7 +677,7 @@ export function CameraGroupEdit({
);
const allowedCameras = useAllowedCameras();
const hasFullCameraAccess = useHasFullCameraAccess();
const isAdmin = useIsAdmin();
const [openCamera, setOpenCamera] = useState<string | null>();
@@ -867,7 +866,8 @@ export function CameraGroupEdit({
<FormDescription>{t("group.cameras.desc")}</FormDescription>
<FormMessage />
{[
...(birdseyeConfig?.enabled && hasFullCameraAccess
...(birdseyeConfig?.enabled &&
(isAdmin || "birdseye" in allowedCameras)
? ["birdseye"]
: []),
...Object.keys(config?.cameras ?? {})
+16 -30
View File
@@ -18,25 +18,18 @@ export default function useCameraLiveMode(
const streamNames = new Set<string>();
cameras.forEach((camera) => {
if (activeStreams && activeStreams[camera.name]) {
const selectedStreamName = activeStreams[camera.name];
const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes(
selectedStreamName,
);
const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes(
Object.values(camera.live.streams)[0],
);
if (isRestreamed) {
streamNames.add(selectedStreamName);
}
} else {
Object.values(camera.live.streams).forEach((streamName) => {
const isRestreamed = Object.keys(
config.go2rtc.streams || {},
).includes(streamName);
if (isRestreamed) {
if (isRestreamed) {
if (activeStreams && activeStreams[camera.name]) {
streamNames.add(activeStreams[camera.name]);
} else {
Object.values(camera.live.streams).forEach((streamName) => {
streamNames.add(streamName);
}
});
});
}
}
});
@@ -73,11 +66,11 @@ export default function useCameraLiveMode(
} = {};
cameras.forEach((camera) => {
const selectedStreamName =
activeStreams?.[camera.name] ?? Object.values(camera.live.streams)[0];
const isRestreamed =
config &&
Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName);
Object.keys(config.go2rtc.streams || {}).includes(
Object.values(camera.live.streams)[0],
);
newIsRestreamedStates[camera.name] = isRestreamed ?? false;
@@ -108,21 +101,14 @@ export default function useCameraLiveMode(
setPreferredLiveModes(newPreferredLiveModes);
setIsRestreamedStates(newIsRestreamedStates);
setSupportsAudioOutputStates(newSupportsAudioOutputStates);
}, [activeStreams, cameras, config, windowVisible, streamMetadata]);
}, [cameras, config, windowVisible, streamMetadata]);
const resetPreferredLiveMode = useCallback(
(cameraName: string) => {
const mseSupported =
"MediaSource" in window || "ManagedMediaSource" in window;
const cameraConfig = cameras.find((camera) => camera.name === cameraName);
const selectedStreamName =
activeStreams?.[cameraName] ??
(cameraConfig
? Object.values(cameraConfig.live.streams)[0]
: cameraName);
const isRestreamed =
config &&
Object.keys(config.go2rtc.streams || {}).includes(selectedStreamName);
config && Object.keys(config.go2rtc.streams || {}).includes(cameraName);
setPreferredLiveModes((prevModes) => {
const newModes = { ...prevModes };
@@ -136,7 +122,7 @@ export default function useCameraLiveMode(
return newModes;
});
},
[activeStreams, cameras, config],
[config],
);
return {
@@ -1,26 +0,0 @@
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
/**
* Returns true if the current user has access to all cameras.
* This is used to determine birdseye access — users who can see
* all cameras should also be able to see the birdseye view.
*/
export function useHasFullCameraAccess() {
const allowedCameras = useAllowedCameras();
const { data: config } = useSWR<FrigateConfig>("config", {
revalidateOnFocus: false,
});
if (!config?.cameras) return false;
const enabledCameraNames = Object.entries(config.cameras)
.filter(([, cam]) => cam.enabled_in_config)
.map(([name]) => name);
return (
enabledCameraNames.length > 0 &&
enabledCameraNames.every((name) => allowedCameras.includes(name))
);
}
+6 -8
View File
@@ -11,12 +11,12 @@ import { useTranslation } from "react-i18next";
import { useEffect, useMemo, useRef } from "react";
import useSWR from "swr";
import { useAllowedCameras } from "@/hooks/use-allowed-cameras";
import { useHasFullCameraAccess } from "@/hooks/use-has-full-camera-access";
import { useIsAdmin } from "@/hooks/use-is-admin";
function Live() {
const { t } = useTranslation(["views/live"]);
const { data: config } = useSWR<FrigateConfig>("config");
const hasFullCameraAccess = useHasFullCameraAccess();
const isAdmin = useIsAdmin();
// selection
@@ -90,8 +90,8 @@ function Live() {
const allowedCameras = useAllowedCameras();
const includesBirdseye = useMemo(() => {
// Users without access to all cameras should not have access to birdseye
if (!hasFullCameraAccess) {
// Restricted users should never have access to birdseye
if (!isAdmin) {
return false;
}
@@ -106,7 +106,7 @@ function Live() {
} else {
return false;
}
}, [config, cameraGroup, hasFullCameraAccess]);
}, [config, cameraGroup, isAdmin]);
const cameras = useMemo(() => {
if (!config) {
@@ -151,9 +151,7 @@ function Live() {
return (
<div className="size-full" ref={mainRef}>
{selectedCameraName === "birdseye" &&
hasFullCameraAccess &&
config?.birdseye?.enabled ? (
{selectedCameraName === "birdseye" ? (
<LiveBirdseyeView
supportsFullscreen={supportsFullScreen}
fullscreen={fullscreen}