mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-24 10:18:53 -05:00
Compare commits
3 Commits
fix-go2rtc
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
352d271fe4 | ||
|
|
a6e11a59d6 | ||
|
|
a7d8d13d9a |
@@ -9,4 +9,25 @@ Snapshots are accessible in the UI in the Explore pane. This allows for quick su
|
||||
|
||||
To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones)
|
||||
|
||||
Snapshots sent via MQTT are configured in the [config file](https://docs.frigate.video/configuration/) under `cameras -> your_camera -> mqtt`
|
||||
Snapshots sent via MQTT are configured in the [config file](/configuration) under `cameras -> your_camera -> mqtt`
|
||||
|
||||
## Frame Selection
|
||||
|
||||
Frigate does not save every frame — it picks a single "best" frame for each tracked object and uses it for both the snapshot and clean copy. As the object is tracked across frames, Frigate continuously evaluates whether the current frame is better than the previous best based on detection confidence, object size, and the presence of key attributes like faces or license plates. Frames where the object touches the edge of the frame are deprioritized. The snapshot is written to disk once tracking ends using whichever frame was determined to be the best.
|
||||
|
||||
MQTT snapshots are published more frequently — each time a better thumbnail frame is found during tracking, or when the current best image is older than `best_image_timeout` (default: 60s). These use their own annotation settings configured under `cameras -> your_camera -> mqtt`.
|
||||
|
||||
## Clean Copy
|
||||
|
||||
Frigate can produce up to two snapshot files per event, each used in different places:
|
||||
|
||||
| Version | File | Annotations | Used by |
|
||||
| --- | --- | --- | --- |
|
||||
| **Regular snapshot** | `<camera>-<id>.jpg` | Respects your `timestamp`, `bounding_box`, `crop`, and `height` settings | API (`/api/events/<id>/snapshot.jpg`), MQTT (`<camera>/<label>/snapshot`), Explore pane in the UI |
|
||||
| **Clean copy** | `<camera>-<id>-clean.webp` | Always unannotated — no bounding box, no timestamp, no crop, full resolution | API (`/api/events/<id>/snapshot-clean.webp`), [Frigate+](/plus/first_model) submissions, "Download Clean Snapshot" in the UI |
|
||||
|
||||
MQTT snapshots are configured separately under `cameras -> your_camera -> mqtt` and are unrelated to the clean copy.
|
||||
|
||||
The clean copy is required for submitting events to [Frigate+](/plus/first_model) — if you plan to use Frigate+, keep `clean_copy` enabled regardless of your other snapshot settings.
|
||||
|
||||
If you are not using Frigate+ and `timestamp`, `bounding_box`, and `crop` are all disabled, the regular snapshot is already effectively clean, so `clean_copy` provides no benefit and only uses additional disk space. You can safely set `clean_copy: False` in this case.
|
||||
|
||||
@@ -16,7 +16,15 @@ See the [MQTT integration
|
||||
documentation](https://www.home-assistant.io/integrations/mqtt/) for more
|
||||
details.
|
||||
|
||||
In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function.
|
||||
In addition, MQTT must be enabled in your Frigate configuration file and Frigate must be connected to the same MQTT server as Home Assistant for many of the entities created by the integration to function, e.g.:
|
||||
|
||||
```yaml
|
||||
mqtt:
|
||||
enabled: True
|
||||
host: mqtt.server.com # the address of your HA server that's running the MQTT integration
|
||||
user: your_mqtt_broker_username
|
||||
password: your_mqtt_broker_password
|
||||
```
|
||||
|
||||
### Integration installation
|
||||
|
||||
@@ -95,12 +103,12 @@ services:
|
||||
|
||||
If you are using Home Assistant Add-on, the URL should be one of the following depending on which Add-on variant you are using. Note that if you are using the Proxy Add-on, you should NOT point the integration at the proxy URL. Just enter the same URL used to access Frigate directly from your network.
|
||||
|
||||
| Add-on Variant | URL |
|
||||
| -------------------------- | ----------------------------------------- |
|
||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
| Add-on Variant | URL |
|
||||
| -------------------------- | -------------------------------------- |
|
||||
| Frigate | `http://ccab4aaf-frigate:5000` |
|
||||
| Frigate (Full Access) | `http://ccab4aaf-frigate-fa:5000` |
|
||||
| Frigate Beta | `http://ccab4aaf-frigate-beta:5000` |
|
||||
| Frigate Beta (Full Access) | `http://ccab4aaf-frigate-fa-beta:5000` |
|
||||
|
||||
### Frigate running on a separate machine
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ Message published for each changed tracked object. The first message is publishe
|
||||
|
||||
### `frigate/tracked_object_update`
|
||||
|
||||
Message published for updates to tracked object metadata, for example:
|
||||
Message published for updates to tracked object metadata. All messages include an `id` field which is the tracked object's event ID, and can be used to look up the event via the API or match it to items in the UI.
|
||||
|
||||
#### Generative AI Description Update
|
||||
|
||||
@@ -134,12 +134,14 @@ Message published for updates to tracked object metadata, for example:
|
||||
|
||||
#### Face Recognition Update
|
||||
|
||||
Published after each recognition attempt, regardless of whether the score meets `recognition_threshold`. See the [Face Recognition](/configuration/face_recognition) documentation for details on how scoring works.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "face",
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"name": "John",
|
||||
"score": 0.95,
|
||||
"name": "John", // best matching person, or null if no match
|
||||
"score": 0.95, // running weighted average across all recognition attempts
|
||||
"camera": "front_door_cam",
|
||||
"timestamp": 1607123958.748393
|
||||
}
|
||||
@@ -147,11 +149,13 @@ Message published for updates to tracked object metadata, for example:
|
||||
|
||||
#### License Plate Recognition Update
|
||||
|
||||
Published when a license plate is recognized on a car object. See the [License Plate Recognition](/configuration/license_plate_recognition) documentation for details.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "lpr",
|
||||
"id": "1607123955.475377-mxklsc",
|
||||
"name": "John's Car",
|
||||
"name": "John's Car", // known name for the plate, or null
|
||||
"plate": "123ABC",
|
||||
"score": 0.95,
|
||||
"camera": "driveway_cam",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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µphone"
|
||||
)
|
||||
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")
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user