mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-01 01:21:14 +03:00
Compare commits
21 Commits
e0cbf50cc4
...
effb5d80cd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
effb5d80cd | ||
|
|
61c03c7de1 | ||
|
|
2bfc634836 | ||
|
|
3cb168f05f | ||
|
|
693bd048cc | ||
|
|
e7714a3b61 | ||
|
|
8841976b81 | ||
|
|
f79f568fac | ||
|
|
94e5ab9edb | ||
|
|
efe2ef027a | ||
|
|
2ed7775481 | ||
|
|
6c0a9ee7ec | ||
|
|
d7a127adc6 | ||
|
|
dea75b8c2a | ||
|
|
9afd487e98 | ||
|
|
66b9036d55 | ||
|
|
ec39b36aa8 | ||
|
|
be8d6c1c21 | ||
|
|
bb2cc60d79 | ||
|
|
703e6a44c0 | ||
|
|
259b6324c1 |
@ -143,11 +143,6 @@ If your ONVIF camera does not require authentication credentials, you may still
|
||||
|
||||
:::
|
||||
|
||||
If a camera connects but fails to authenticate, two optional fields can help:
|
||||
|
||||
- `tls_insecure`: Skips TLS certificate verification and sends the ONVIF password as plaintext (`PasswordText`) instead of a hashed digest (`PasswordDigest`). Some cameras reject the digest token and only accept plaintext. This weakens connection security, so only enable it on a trusted local network.
|
||||
- `ignore_time_mismatch`: ONVIF authentication tokens include a timestamp, and a camera will reject the token if its clock differs too much from Frigate's. Enabling this makes Frigate compensate for the time offset so authentication can still succeed. Running NTP on both the camera and the Frigate host is the recommended fix; only use this in a "safe" environment, as it slightly weakens token validation.
|
||||
|
||||
If your camera has multiple ONVIF profiles, you can specify which one to use for PTZ control with the `profile` option, matched by token or name. When not set, Frigate selects the first profile with a valid PTZ configuration. Check the Frigate debug logs (`frigate.ptz.onvif: debug`) to see available profile names and tokens for your camera.
|
||||
|
||||
An ONVIF-capable camera that supports relative movement within the field of view (FOV) can also be configured to automatically track moving objects and keep them in the center of the frame. For autotracking setup, see the [autotracking](autotracking.md) docs.
|
||||
|
||||
@ -153,7 +153,7 @@ Clicking a preview clip seeks the recording player to that timestamp so you can
|
||||
|
||||
Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked).
|
||||
|
||||
To start a search, open the Actions menu in History or click the kebab menu on a camera in the <NavPath path="Review > Motion" /> page and choose **Motion Search**. In the dialog:
|
||||
To start a search, click the kebab menu on a camera in the <NavPath path="Review > Motion" /> page and choose **Motion Search**. In the dialog:
|
||||
|
||||
1. Pick the camera and time range to scan.
|
||||
2. Draw a polygon on the camera frame to define the region of interest.
|
||||
@ -170,21 +170,3 @@ To start a search, open the Actions menu in History or click the kebab menu on a
|
||||
Once running, Frigate scans the recording segments that overlap the time range and reports timestamps where changes were detected inside the polygon, along with the percentage of the ROI that changed. Clicking a result seeks the player to that moment so you can review what happened.
|
||||
|
||||
The status panel shows live progress and metrics such as how many segments were scanned, how many were skipped because no motion was recorded for that segment (using the stored motion heatmap), how many frames were decoded, and the total wall-clock time. Segments with no recorded motion in the selected ROI are skipped automatically, which is what makes searching long time ranges practical.
|
||||
|
||||
#### Common use cases
|
||||
|
||||
Frigate's main use case is to record and surface tracked objects, so Motion Search is most useful for the cases where object detection produced nothing — there is no object to find in Explore, but you suspect something happened.
|
||||
|
||||
- **Locating an unattributed change.** You know something appeared, disappeared, or moved in a window of footage — a package now gone, a gate left open — but no detection points to it. A search returns the candidate timestamps instead of scrubbing the timeline by hand.
|
||||
- **An object that was never detected.** Something Frigate doesn't have a model label for, an object too small or distant to be detected, or movement in a region where detection isn't running. The activity left no tracked object but did change the pixels, so a search can still find it.
|
||||
- **Activity while detection was effectively paused.** Changes that occurred while object detection was disabled, motion was suppressed by `skip_motion_threshold`, or inside an area covered by a motion mask, won't appear as review items or tracked objects but can be recovered by searching the recordings directly.
|
||||
|
||||
#### Expected performance
|
||||
|
||||
Motion Search analyzes the saved recordings on demand rather than reading a pre-built index, so a search over a long range takes longer than browsing Motion Previews. Cost scales mainly with how much footage has to be examined: segments with no recorded motion in your ROI are skipped using the stored motion heatmap (shown as "segments skipped" in the status panel), so a quiet range finishes quickly while a busy one takes longer.
|
||||
|
||||
To increase the speed of searches:
|
||||
|
||||
- Draw a tight ROI. Because **Minimum Change Area** is measured as a percentage of the region you draw, a tight ROI around where you expect the change makes the object fill a larger share of the area, so it clears the threshold more easily. A loose ROI makes the same object a small fraction of the region, so it can fall below the threshold and be missed — forcing you to lower Minimum Change Area, which lets in more noise.
|
||||
- Keep Frame Skip high. A higher value samples fewer frames and speeds up the search considerably, while still landing within a few seconds of when the motion or object appeared — close enough to seek to in the recording. Only lower it when you need to pinpoint the exact frame something appears or disappears.
|
||||
- Use Parallel mode to shorten wall-clock time on multi-core systems, at the cost of higher CPU usage while it runs.
|
||||
|
||||
@ -529,68 +529,6 @@ def _extract_fps(r_frame_rate: str) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
def _build_digest_transport(username: str, password: str) -> AsyncTransport:
|
||||
"""Build a zeep transport backed by an httpx client using HTTP digest auth."""
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
client = httpx.AsyncClient(auth=auth, timeout=10.0)
|
||||
return AsyncTransport(client=client)
|
||||
|
||||
|
||||
async def _connect_onvif_camera(
|
||||
host: str,
|
||||
port: int,
|
||||
username: str,
|
||||
password: str,
|
||||
wsdl_base: str | None,
|
||||
auth_type: str,
|
||||
) -> ONVIFCamera:
|
||||
"""Connect to an ONVIF device, trying both WS-Security password encodings.
|
||||
|
||||
Cameras disagree on whether the WS-Security UsernameToken should carry a
|
||||
hashed PasswordDigest or a plaintext PasswordText. The wizard can't know
|
||||
which a given camera expects, so we try PasswordDigest first (the common
|
||||
case) and fall back to PasswordText when the device rejects the token. This
|
||||
is independent of auth_type, which controls HTTP transport-level auth.
|
||||
"""
|
||||
first_error: Fault | None = None
|
||||
|
||||
# encrypt=True -> PasswordDigest, encrypt=False -> PasswordText
|
||||
for encrypt in (True, False):
|
||||
onvif_camera = ONVIFCamera(
|
||||
host,
|
||||
port,
|
||||
username or "",
|
||||
password or "",
|
||||
wsdl_dir=wsdl_base,
|
||||
encrypt=encrypt,
|
||||
)
|
||||
|
||||
try:
|
||||
await onvif_camera.update_xaddrs()
|
||||
except Fault as e:
|
||||
# A SOAP fault here is how a camera signals the wrong password
|
||||
# encoding, so retry with the other encoding before giving up.
|
||||
logger.debug(
|
||||
"ONVIF connect with %s rejected, trying alternate encoding",
|
||||
"PasswordDigest" if encrypt else "PasswordText",
|
||||
)
|
||||
if first_error is None:
|
||||
first_error = e
|
||||
continue
|
||||
|
||||
if auth_type == "digest" and username and password:
|
||||
transport = _build_digest_transport(username, password)
|
||||
for service in ("devicemgmt", "media", "ptz"):
|
||||
if hasattr(onvif_camera, service):
|
||||
getattr(onvif_camera, service).zeep_client.transport = transport
|
||||
logger.debug("Configured digest authentication")
|
||||
|
||||
return onvif_camera
|
||||
|
||||
# Both encodings failed authentication; surface the original fault.
|
||||
raise first_error
|
||||
|
||||
|
||||
@router.get(
|
||||
"/onvif/probe",
|
||||
dependencies=[Depends(require_role(["admin"]))],
|
||||
@ -667,10 +605,34 @@ async def onvif_probe(
|
||||
except Exception:
|
||||
wsdl_base = None
|
||||
|
||||
onvif_camera = await _connect_onvif_camera(
|
||||
host, port, username, password, wsdl_base, auth_type
|
||||
onvif_camera = ONVIFCamera(
|
||||
host, port, username or "", password or "", wsdl_dir=wsdl_base
|
||||
)
|
||||
|
||||
# Configure digest authentication if requested
|
||||
if auth_type == "digest" and username and password:
|
||||
# Create httpx client with digest auth
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
client = httpx.AsyncClient(auth=auth, timeout=10.0)
|
||||
|
||||
# Replace the transport in the zeep client
|
||||
transport = AsyncTransport(client=client)
|
||||
|
||||
# Update the xaddr before setting transport
|
||||
await onvif_camera.update_xaddrs()
|
||||
|
||||
# Replace transport in all services
|
||||
if hasattr(onvif_camera, "devicemgmt"):
|
||||
onvif_camera.devicemgmt.zeep_client.transport = transport
|
||||
if hasattr(onvif_camera, "media"):
|
||||
onvif_camera.media.zeep_client.transport = transport
|
||||
if hasattr(onvif_camera, "ptz"):
|
||||
onvif_camera.ptz.zeep_client.transport = transport
|
||||
|
||||
logger.debug("Configured digest authentication")
|
||||
else:
|
||||
await onvif_camera.update_xaddrs()
|
||||
|
||||
# Get device information
|
||||
device_info = {
|
||||
"manufacturer": "Unknown",
|
||||
@ -682,9 +644,10 @@ async def onvif_probe(
|
||||
|
||||
# Update transport for device service if digest auth
|
||||
if auth_type == "digest" and username and password:
|
||||
device_service.zeep_client.transport = _build_digest_transport(
|
||||
username, password
|
||||
)
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
client = httpx.AsyncClient(auth=auth, timeout=10.0)
|
||||
transport = AsyncTransport(client=client)
|
||||
device_service.zeep_client.transport = transport
|
||||
|
||||
device_info_resp = await device_service.GetDeviceInformation()
|
||||
manufacturer = getattr(device_info_resp, "Manufacturer", None) or (
|
||||
@ -722,9 +685,10 @@ async def onvif_probe(
|
||||
|
||||
# Update transport for media service if digest auth
|
||||
if auth_type == "digest" and username and password:
|
||||
media_service.zeep_client.transport = _build_digest_transport(
|
||||
username, password
|
||||
)
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
client = httpx.AsyncClient(auth=auth, timeout=10.0)
|
||||
transport = AsyncTransport(client=client)
|
||||
media_service.zeep_client.transport = transport
|
||||
|
||||
profiles = await media_service.GetProfiles()
|
||||
profiles_count = len(profiles) if profiles else 0
|
||||
@ -756,9 +720,10 @@ async def onvif_probe(
|
||||
|
||||
# Update transport for PTZ service if digest auth
|
||||
if auth_type == "digest" and username and password:
|
||||
ptz_service.zeep_client.transport = _build_digest_transport(
|
||||
username, password
|
||||
)
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
client = httpx.AsyncClient(auth=auth, timeout=10.0)
|
||||
transport = AsyncTransport(client=client)
|
||||
ptz_service.zeep_client.transport = transport
|
||||
|
||||
# Check if PTZ service is available
|
||||
try:
|
||||
@ -911,9 +876,10 @@ async def onvif_probe(
|
||||
|
||||
# Update transport for media service if digest auth
|
||||
if auth_type == "digest" and username and password:
|
||||
media_service.zeep_client.transport = _build_digest_transport(
|
||||
username, password
|
||||
)
|
||||
auth = httpx.DigestAuth(username, password)
|
||||
client = httpx.AsyncClient(auth=auth, timeout=10.0)
|
||||
transport = AsyncTransport(client=client)
|
||||
media_service.zeep_client.transport = transport
|
||||
|
||||
if profiles_count and media_service:
|
||||
for p in profiles or []:
|
||||
|
||||
@ -42,9 +42,9 @@ class MotionSearchRequest(BaseModel):
|
||||
description="Minimum change area as a percentage of the ROI",
|
||||
)
|
||||
frame_skip: int = Field(
|
||||
default=30,
|
||||
default=5,
|
||||
ge=1,
|
||||
le=120,
|
||||
le=30,
|
||||
description="Process every Nth frame (1=all frames, 5=every 5th frame)",
|
||||
)
|
||||
parallel: bool = Field(
|
||||
|
||||
@ -343,21 +343,13 @@ class FrigateApp:
|
||||
)
|
||||
self.dispatcher.profile_manager = self.profile_manager
|
||||
|
||||
def restore_active_profile(self) -> None:
|
||||
"""Re-activate the persisted profile after subscribers are connected.
|
||||
|
||||
ZMQ PUB/SUB drops messages with no subscribers, so activation must
|
||||
run after every config_updater subscriber is up.
|
||||
"""
|
||||
if self.profile_manager is None:
|
||||
return
|
||||
|
||||
persisted = ProfileManager.load_persisted_profile()
|
||||
if persisted and any(
|
||||
persisted in cam.profiles for cam in self.config.cameras.values()
|
||||
):
|
||||
logger.info("Restoring persisted profile '%s'", persisted)
|
||||
# runtime overrides are layered on top via restore_runtime_state()
|
||||
# don't clear runtime overrides here, restore_runtime_state() later
|
||||
# in startup replays it on top of the activated profile
|
||||
self.profile_manager.activate_profile(
|
||||
persisted, clear_runtime_overrides=False
|
||||
)
|
||||
@ -625,7 +617,6 @@ class FrigateApp:
|
||||
self.start_watchdog()
|
||||
|
||||
# restore persisted runtime overrides on top of config
|
||||
self.restore_active_profile()
|
||||
self.dispatcher.restore_runtime_state()
|
||||
|
||||
self.init_auth()
|
||||
|
||||
@ -1,124 +0,0 @@
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from zeep.exceptions import Fault, TransportError
|
||||
from zeep.transports import AsyncTransport
|
||||
|
||||
from frigate.api.camera import _build_digest_transport, _connect_onvif_camera
|
||||
|
||||
|
||||
def _make_camera(update_side_effect=None):
|
||||
"""Build a mock ONVIFCamera whose update_xaddrs can raise or succeed."""
|
||||
camera = MagicMock()
|
||||
camera.update_xaddrs = AsyncMock(side_effect=update_side_effect)
|
||||
return camera
|
||||
|
||||
|
||||
class TestConnectOnvifCamera(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_password_digest_succeeds_first(self):
|
||||
# Cameras that accept PasswordDigest authenticate on the first attempt
|
||||
# and should never trigger the PasswordText fallback.
|
||||
camera = _make_camera()
|
||||
|
||||
with patch("frigate.api.camera.ONVIFCamera", return_value=camera) as mock_cls:
|
||||
result = await _connect_onvif_camera(
|
||||
"cam.local", 80, "user", "pass", None, "basic"
|
||||
)
|
||||
|
||||
self.assertIs(result, camera)
|
||||
mock_cls.assert_called_once()
|
||||
self.assertTrue(mock_cls.call_args.kwargs["encrypt"])
|
||||
|
||||
async def test_falls_back_to_password_text(self):
|
||||
# A PasswordDigest rejection should retry once with PasswordText.
|
||||
camera_digest = _make_camera(update_side_effect=Fault("token rejected"))
|
||||
camera_text = _make_camera()
|
||||
|
||||
with patch(
|
||||
"frigate.api.camera.ONVIFCamera",
|
||||
side_effect=[camera_digest, camera_text],
|
||||
) as mock_cls:
|
||||
result = await _connect_onvif_camera(
|
||||
"cam.local", 80, "user", "pass", None, "basic"
|
||||
)
|
||||
|
||||
self.assertIs(result, camera_text)
|
||||
self.assertEqual(mock_cls.call_count, 2)
|
||||
self.assertTrue(mock_cls.call_args_list[0].kwargs["encrypt"])
|
||||
self.assertFalse(mock_cls.call_args_list[1].kwargs["encrypt"])
|
||||
|
||||
async def test_both_encodings_fail_raises_first_fault(self):
|
||||
# When both encodings fault, the original (PasswordDigest) fault is
|
||||
# surfaced so the caller's existing Fault handler reports it.
|
||||
first_fault = Fault("digest rejected")
|
||||
camera_digest = _make_camera(update_side_effect=first_fault)
|
||||
camera_text = _make_camera(update_side_effect=Fault("text rejected"))
|
||||
|
||||
with patch(
|
||||
"frigate.api.camera.ONVIFCamera",
|
||||
side_effect=[camera_digest, camera_text],
|
||||
) as mock_cls:
|
||||
with self.assertRaises(Fault) as ctx:
|
||||
await _connect_onvif_camera(
|
||||
"cam.local", 80, "user", "pass", None, "basic"
|
||||
)
|
||||
|
||||
self.assertIs(ctx.exception, first_fault)
|
||||
self.assertEqual(mock_cls.call_count, 2)
|
||||
|
||||
async def test_transport_error_is_not_retried(self):
|
||||
# Connection-level errors (timeout, refused, unreachable) should
|
||||
# propagate immediately without doubling latency on a second encoding.
|
||||
camera = _make_camera(update_side_effect=TransportError("unreachable"))
|
||||
|
||||
with patch("frigate.api.camera.ONVIFCamera", side_effect=[camera]) as mock_cls:
|
||||
with self.assertRaises(TransportError):
|
||||
await _connect_onvif_camera(
|
||||
"cam.local", 80, "user", "pass", None, "basic"
|
||||
)
|
||||
|
||||
mock_cls.assert_called_once()
|
||||
|
||||
async def test_digest_auth_replaces_service_transports(self):
|
||||
# auth_type "digest" wires an HTTP digest transport onto each service,
|
||||
# independently of the WS-Security encoding.
|
||||
camera = _make_camera()
|
||||
|
||||
with (
|
||||
patch("frigate.api.camera.ONVIFCamera", return_value=camera),
|
||||
patch(
|
||||
"frigate.api.camera._build_digest_transport",
|
||||
return_value="TRANSPORT",
|
||||
) as mock_transport,
|
||||
):
|
||||
result = await _connect_onvif_camera(
|
||||
"cam.local", 80, "user", "pass", None, "digest"
|
||||
)
|
||||
|
||||
self.assertIs(result, camera)
|
||||
mock_transport.assert_called_once_with("user", "pass")
|
||||
self.assertEqual(camera.devicemgmt.zeep_client.transport, "TRANSPORT")
|
||||
self.assertEqual(camera.media.zeep_client.transport, "TRANSPORT")
|
||||
self.assertEqual(camera.ptz.zeep_client.transport, "TRANSPORT")
|
||||
|
||||
async def test_basic_auth_does_not_replace_transports(self):
|
||||
# Without digest auth, no transport override is built.
|
||||
camera = _make_camera()
|
||||
|
||||
with (
|
||||
patch("frigate.api.camera.ONVIFCamera", return_value=camera),
|
||||
patch("frigate.api.camera._build_digest_transport") as mock_transport,
|
||||
):
|
||||
await _connect_onvif_camera("cam.local", 80, "user", "pass", None, "basic")
|
||||
|
||||
mock_transport.assert_not_called()
|
||||
|
||||
|
||||
class TestBuildDigestTransport(unittest.TestCase):
|
||||
def test_returns_async_transport(self):
|
||||
transport = _build_digest_transport("user", "pass")
|
||||
self.assertIsInstance(transport, AsyncTransport)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@ -55,5 +55,5 @@
|
||||
"goToReplay": "Ves a la repetició"
|
||||
}
|
||||
},
|
||||
"description": "Reprodueix els enregistraments de la càmera per a la depuració. La llista d'objectes mostra un resum retardat en el temps dels objectes detectats i la pestanya Missatges mostra un flux de missatges interns de frigate a partir del metratge de reproducció."
|
||||
"description": "Reprodueix els enregistraments de la càmera per a la depuració. La llista d'objectes mostra un resum retardat en el temps dels objectes detectats i la pestanya Missatges mostra un flux de missatges interns de la fragata a partir del metratge de reproducció."
|
||||
}
|
||||
|
||||
@ -1059,7 +1059,7 @@
|
||||
"brands": {
|
||||
"reolink-rtsp": "No es recomana Reolink RST. Es recomana habilitar HTTP a la configuració de la càmera i reiniciar l'assistent de la càmera."
|
||||
},
|
||||
"customUrlRtspRequired": "Els URL personalitzats han de començar amb \"rtsp://\" o \"rtsps://\". Es requereix configuració manual per a fluxos de càmera no RTSP."
|
||||
"customUrlRtspRequired": "Els URL personalitzats han de començar amb \"rtsp://\". Es requereix configuració manual per a fluxos de càmera no RTSP."
|
||||
},
|
||||
"selectBrand": "Seleccioneu la marca de la càmera per a la plantilla d'URL",
|
||||
"customUrl": "URL de flux personalitzat",
|
||||
@ -1309,7 +1309,7 @@
|
||||
"enableDesc": "Inhabilita temporalment una càmera habilitada fins que es reiniciï Frigate. La inhabilitació d'una càmera atura completament el processament de Frigate dels fluxos d'aquesta càmera. La detecció, l'enregistrament i la depuració no estaran disponibles.<br /> <em>Nota: això no inhabilita els restreams go2rtc.</em><br /><br />Drag el handle per reordenar les càmeres tal com apareixen a la interfície d'usuari. L'ordre de les càmeres habilitades es reflectirà en tota la interfície d'usuari, incloent el tauler en viu i els desplegables de selecció de càmeres.",
|
||||
"disableLabel": "Càmeres inhabilitades",
|
||||
"disableDesc": "Habilita una càmera que actualment no és visible a la interfície d'usuari i està desactivada a la configuració. Es requereix un reinici de Frigate després d'activar-la.",
|
||||
"enableSuccess": "{{cameraName}} activat. Reinicia Frigate a aplicar.",
|
||||
"enableSuccess": "{{cameraName}} activat. Reinicia la fragata a aplicar.",
|
||||
"friendlyName": {
|
||||
"edit": "Edita el nom de la pantalla de la càmera",
|
||||
"title": "Edita el nom de la pantalla",
|
||||
@ -1330,7 +1330,7 @@
|
||||
"webuiUrlInvalid": "Ha de ser un URL vàlid (p. ex., https://example.com)."
|
||||
},
|
||||
"label": "Estat de la càmera",
|
||||
"description": "Estableix l'estat operatiu de cada càmera.<br /><br /><strong>A</strong>: els fluxos es processen normalment.<br /><strong>Off</strong>: pausa temporalment el processament. No persisteix a través de reinicis de Frigate.<br /><strong>Inhabilitat</strong>: deixa de processar i desa el canvi a la configuració. Es requereix un reinici per a tornar a habilitar una càmera inhabilitada.<br /><br /><em>Nota: La inhabilitació no afecta els restreams de go2rtc.</em><br /><br />Arrossegueu l'ansa per a reordenar les càmeres actives a mesura que apareguin a tota la interfície d'usuari, inclosos els desplegables de selecció de quadres en viu i de càmera.",
|
||||
"description": "Estableix l'estat operatiu de cada càmera.<br /><br /><strong>A</strong>: els fluxos es processen normalment.<br /><strong>Off</strong>: pausa temporalment el processament. No persisteix a través de reinicis de la fragata.<br /><strong>Inhabilitat</strong>: deixa de processar i desa el canvi a la configuració. Es requereix un reinici per a tornar a habilitar una càmera inhabilitada.<br /><br /><em>Nota: La inhabilitació no afecta els restreams de go2rtc.</em><br /><br />Arrossegueu l'ansa per a reordenar les càmeres actives a mesura que apareguin a tota la interfície d'usuari, inclosos els desplegables de selecció de quadres en viu i de càmera.",
|
||||
"disabledSubheading": "Desactivat en la configuració",
|
||||
"status": {
|
||||
"on": "Engegat",
|
||||
@ -1395,98 +1395,10 @@
|
||||
"label": "Tipus de càmera",
|
||||
"description": "Estableix el tipus per a cada càmera. Les càmeres LPR dedicades són càmeres d'un sol ús amb un potent zoom òptic per capturar matrícules en vehicles distants. La majoria de les càmeres haurien d'utilitzar el tipus de càmera normal llevat que la càmera sigui específicament per a LPR i tingui una vista molt centrada en les matrícules.",
|
||||
"dedicatedLpr": "LPR dedicat",
|
||||
"saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia Frigate per aplicar els canvis.",
|
||||
"saveSuccess": "Tipus de càmera actualitzat per {{cameraName}}. Reinicia la fragata per aplicar els canvis.",
|
||||
"normal": "Normal"
|
||||
},
|
||||
"description": "Afegiu, editeu i suprimiu les càmeres, controleu l'estat de cada càmera, i configureu les superposicions per perfil i tipus de càmera. Per a configurar fluxos, detecció, moviment i altres paràmetres específics de la càmera, trieu la secció específica a Configuració de la càmera.",
|
||||
"clone": {
|
||||
"sectionTitle": "Clona la configuració",
|
||||
"sectionDescription": "Copia la configuració d'una càmera a una altra càmera o una de nova.",
|
||||
"button": "Clona la configuració",
|
||||
"title": "Clona la configuració de la càmera",
|
||||
"description": "Copia la configuració d'una càmera a una o més càmeres o a una càmera nova. La identitat (nom, nom amigable, URL de la interfície d'usuari web, ordre de visualització) no es copia mai.",
|
||||
"source": {
|
||||
"label": "Càmera d'origen",
|
||||
"placeholder": "Seleccioneu una càmera d'origen",
|
||||
"required": "Seleccioneu una càmera d'origen"
|
||||
},
|
||||
"target": {
|
||||
"legend": "Objectiu",
|
||||
"newRadio": "Càmara nova",
|
||||
"newNameLabel": "Nom de la càmera",
|
||||
"newNamePlaceholder": "p. ex., porta enrere orporta o porta posterior",
|
||||
"newNameInvalid": "Es requereix el nom de la càmera",
|
||||
"newNameCollision": "Ja existeix una càmera amb aquest nom",
|
||||
"newStreamsForced": "Els fluxos sempre es copien per a una càmera nova.",
|
||||
"existingCamerasRadio": "Càmeres existents",
|
||||
"allCameras": "Totes les càmeres",
|
||||
"existingPlaceholder": "Selecciona almenys una càmera",
|
||||
"existingDisabled": "No hi ha cap altra càmera a la qual copiar",
|
||||
"newNameRequired": "Es requereix el nom de la càmera"
|
||||
},
|
||||
"categories": {
|
||||
"legend": "Configuració per clonar",
|
||||
"description": "Trieu quina configuració voleu copiar de la càmera d'origen.",
|
||||
"selectAll": "Selecciona-ho tot",
|
||||
"selectNone": "No en seleccioneu cap",
|
||||
"resetDefaults": "Restableix als valors predeterminats",
|
||||
"general": "General",
|
||||
"spatial": "Paràmetres espacials",
|
||||
"streams": "Fluxos",
|
||||
"spatialWarningTitle": "La resolució no coincideix",
|
||||
"spatialWarning": "La càmera d'origen {{srcCamera}} detecta la resolució ({{srcWidth}}.{{srcHeight}}) difereix de: {{cameras}}. És possible que els polígons no s'alineïn en aquestes càmeres. Aquests valors predeterminats estan desactivats; habiliteu-ho per a copiar tal qual.",
|
||||
"restartHint": "Reinicia requerit",
|
||||
"items": {
|
||||
"record": "Enregistrament",
|
||||
"snapshots": "Instantànies",
|
||||
"review": "Revisió",
|
||||
"motion": "Detecció de moviment",
|
||||
"objects": "Objectes",
|
||||
"audio": "Detecció d'àudio",
|
||||
"audio_transcription": "Transcripció d'àudio",
|
||||
"notifications": "Notificacions",
|
||||
"birdseye": "Birdseye",
|
||||
"timestamp_style": "Estil de la marca horària",
|
||||
"lpr": "Reconeixement de la matrícula",
|
||||
"face_recognition": "Reconeixement de cares",
|
||||
"semantic_search": "Cerca semàntica",
|
||||
"genai": "IA generativa",
|
||||
"type": "Tipus de càmera (LPR normal / dedicat)",
|
||||
"profiles": "Perfils",
|
||||
"detect": "Detecta les dimensions",
|
||||
"zones": "Zones",
|
||||
"motion_mask": "Màscares de moviment",
|
||||
"object_masks": "Màscares d'objecte",
|
||||
"ffmpeg_live": "URL i rols de flux",
|
||||
"mqtt": "MQTT",
|
||||
"onvif": "ONVIF"
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"changeCount_one": "{{count}} s'aplicarà el canvi",
|
||||
"changeCount_many": "{{count}} canvis s'aplicaran",
|
||||
"changeCount_other": "{{count}} canvis s'aplicaran",
|
||||
"restartNeeded": "Es requerirà reiniciar per a alguns canvis.",
|
||||
"liveOnly": "Tots els canvis s'aplicaran en viu sense reiniciar.",
|
||||
"submit": "Clona",
|
||||
"submitting": "S'està clonant…"
|
||||
},
|
||||
"toast": {
|
||||
"success": "Configuració copiada a {{cameraName}}",
|
||||
"successWithRestart": "Configuració copiada a {{cameraName}}. Reinicia Frigate per aplicar tots els canvis.",
|
||||
"successMulti_one": "Configuració copiada a la càmera {{count}}",
|
||||
"successMulti_many": "Configuració copiada a {{count}} càmeres",
|
||||
"successMulti_other": "Configuració copiada a {{count}} càmeres",
|
||||
"successMultiWithRestart_one": "Configuració copiada a la càmera {{count}}. Reinicia Frigate per aplicar tots els canvis.",
|
||||
"successMultiWithRestart_many": "Configuració copiada a {{count}} càmeres. Reinicia Frigate per aplicar tots els canvis.",
|
||||
"successMultiWithRestart_other": "Configuració copiada a {{count}} càmeres. Reinicia la fragata per aplicar tots els canvis.",
|
||||
"partialFailure": "{{successCount}} seccions aplicades; «{{failedSection}}» ha fallat: {{errorMessage}}",
|
||||
"partialFailureMulti": "S'ha copiat a {{successCount}} càmera(es); ha fallat {{failed}}: {{errorMessage}}",
|
||||
"newCameraPartialFailure": "S'ha creat la càmera {{cameraName}} però no s'han pogut copiar alguns paràmetres: {{errorMessage}}",
|
||||
"sourceMissing": "La càmera d'origen ja no existeix",
|
||||
"submitError": "No s'ha pogut clonar la càmera: {{errorMessage}}"
|
||||
}
|
||||
}
|
||||
"description": "Afegiu, editeu i suprimiu les càmeres, controleu l'estat de cada càmera, i configureu les superposicions per perfil i tipus de càmera. Per a configurar fluxos, detecció, moviment i altres paràmetres específics de la càmera, trieu la secció específica a Configuració de la càmera."
|
||||
},
|
||||
"cameraReview": {
|
||||
"object_descriptions": {
|
||||
@ -1608,7 +1520,7 @@
|
||||
"desc": "La quadrícula de regions és una optimització que aprèn on solen aparèixer objectes de diferents mides en el camp de visió de cada càmera. Frigate utilitza aquestes dades per detectar regions de mida eficient. La quadrícula es construeix automàticament amb el temps a partir de dades d'objectes rastrejats.",
|
||||
"clear": "Neteja la quadrícula de la regió",
|
||||
"clearConfirmTitle": "Neteja la quadrícula de la regió",
|
||||
"clearConfirmDesc": "No es recomana netejar la quadrícula de la regió tret que hagi canviat recentment la mida del model del detector o hagi canviat la posició física de la càmera i tingui problemes de seguiment d'objectes. La quadrícula es reconstruirà automàticament amb el temps a mesura que els objectes siguin rastrejats. Es requereix un reinici de Frigate perquè els canvis tinguin efecte.",
|
||||
"clearConfirmDesc": "No es recomana netejar la quadrícula de la regió tret que hagi canviat recentment la mida del model del detector o hagi canviat la posició física de la càmera i tingui problemes de seguiment d'objectes. La quadrícula es reconstruirà automàticament amb el temps a mesura que els objectes siguin rastrejats. Es requereix un reinici de la fragata perquè els canvis tinguin efecte.",
|
||||
"clearSuccess": "La quadrícula de la regió s'ha netejat correctament",
|
||||
"clearError": "Ha fallat en netejar la graella de la regió",
|
||||
"restartRequired": "Cal reiniciar per a que els canvis de la quadrícula de la regió tinguin efecte"
|
||||
@ -1864,9 +1776,9 @@
|
||||
"saveAllPartial_other": "{{successCount}} de {{totalCount}} seccions desades. {{failCount}} ha fallat.",
|
||||
"saveAllFailure": "Ha fallat en desar totes les seccions.",
|
||||
"applied": "La configuració s'ha aplicat correctament",
|
||||
"saveAllSuccessRestartRequired_one": "S'ha desat la secció {{count}} correctament. Reinicia Frigate per aplicar els canvis.",
|
||||
"saveAllSuccessRestartRequired_many": "Totes les {{count}} seccions s'han desat correctament. Reinicia Frigate per aplicar els canvis.",
|
||||
"saveAllSuccessRestartRequired_other": "Totes les {{count}} seccions s'han desat correctament. Reinicia Frigate per aplicar els canvis."
|
||||
"saveAllSuccessRestartRequired_one": "S'ha desat la secció {{count}} correctament. Reinicia la fragata per aplicar els canvis.",
|
||||
"saveAllSuccessRestartRequired_many": "Totes les {{count}} seccions s'han desat correctament. Reinicia la fragata per aplicar els canvis.",
|
||||
"saveAllSuccessRestartRequired_other": "Totes les {{count}} seccions s'han desat correctament. Reinicia la fragata per aplicar els canvis."
|
||||
},
|
||||
"unsavedChanges": "Teniu canvis sense desar",
|
||||
"confirmReset": "Confirma el restabliment",
|
||||
@ -2017,7 +1929,7 @@
|
||||
"recordDisabled": "L'enregistrament està desactivat, els elements de revisió no es generaran.",
|
||||
"detectDisabled": "La detecció d'objectes està desactivada. Els elements de revisió requereixen objectes detectats per categoritzar alertes i deteccions.",
|
||||
"allNonAlertDetections": "Totes les activitats no alertes s'inclouran com a deteccions.",
|
||||
"genaiImageSourceRecordingsRecordDisabled": "La font d'imatges està configurada com a 'enregistraments', però l'enregistrament està desactivat. Frigate tornarà a la vista prèvia de les imatges."
|
||||
"genaiImageSourceRecordingsRecordDisabled": "La font d'imatges està configurada com a 'enregistraments', però l'enregistrament està desactivat. La fragata tornarà a la vista prèvia de les imatges."
|
||||
},
|
||||
"audio": {
|
||||
"noAudioRole": "Cap flux té definit el rol d'àudio. Heu d'habilitar el rol d'àudio per a la detecció d'àudio perquè funcioni."
|
||||
|
||||
@ -67,7 +67,7 @@
|
||||
"needsReview": "Needs review",
|
||||
"securityConcern": "Security concern",
|
||||
"motionSearch": {
|
||||
"menuItem": "Motion Search",
|
||||
"menuItem": "Motion search",
|
||||
"openMenu": "Camera options"
|
||||
},
|
||||
"motionPreviews": {
|
||||
|
||||
@ -24,9 +24,7 @@
|
||||
"points_one": "{{count}} point",
|
||||
"points_other": "{{count}} points",
|
||||
"undo": "Undo last point",
|
||||
"reset": "Reset polygon",
|
||||
"drawMode": "Draw",
|
||||
"moveMode": "Move"
|
||||
"reset": "Reset polygon"
|
||||
},
|
||||
"motionHeatmapLabel": "Motion Heatmap",
|
||||
"dialog": {
|
||||
|
||||
@ -320,7 +320,7 @@
|
||||
"nameLength": "Camera name must be 64 characters or less",
|
||||
"invalidCharacters": "Camera name contains invalid characters",
|
||||
"nameExists": "Camera name already exists",
|
||||
"customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\" or \"rtsps://\". Manual configuration is required for non-RTSP camera streams."
|
||||
"customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\". Manual configuration is required for non-RTSP camera streams."
|
||||
}
|
||||
},
|
||||
"step2": {
|
||||
|
||||
@ -1091,7 +1091,7 @@
|
||||
"nameLength": "El nombre de la cámara debe tener 64 caracteres o menos",
|
||||
"invalidCharacters": "El nombre de la cámara contiene caracteres no válidos",
|
||||
"nameExists": "El nombre de la cámara ya existe",
|
||||
"customUrlRtspRequired": "Las URL personalizadas deben comenzar por “rtsp://” o “rtsps://”. Se requiere configuración manual para flujos de cámara que no sean RTSP.",
|
||||
"customUrlRtspRequired": "Las URL personalizadas deben comenzar con \"rtsp://\". Se requiere configuración manual para transmisiones de cámara sin RTSP.",
|
||||
"brandOrCustomUrlRequired": "Seleccione una marca de cámara con host/IP o elija \"Otro\" con una URL personalizada"
|
||||
},
|
||||
"description": "Ingrese los detalles de su cámara y elija probar la cámara o seleccionar manualmente la marca.",
|
||||
|
||||
@ -224,7 +224,7 @@
|
||||
},
|
||||
"retry_interval": {
|
||||
"label": "FFmpeg 再試行間隔",
|
||||
"description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10 秒。"
|
||||
"description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10。"
|
||||
},
|
||||
"apple_compatibility": {
|
||||
"label": "Apple 互換性",
|
||||
|
||||
@ -743,7 +743,7 @@
|
||||
},
|
||||
"retry_interval": {
|
||||
"label": "FFmpeg 再試行間隔",
|
||||
"description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10 秒。"
|
||||
"description": "カメラストリームの失敗後、再接続を試みるまでの待機秒数。デフォルトは 10。"
|
||||
},
|
||||
"apple_compatibility": {
|
||||
"label": "Apple 互換性",
|
||||
|
||||
@ -119,7 +119,7 @@
|
||||
},
|
||||
"liveFallbackTimeout": {
|
||||
"label": "ライブプレイヤーのフォールバック タイムアウト",
|
||||
"desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルトは 3 秒。"
|
||||
"desc": "カメラの高画質ライブストリームが利用できない場合、指定した秒数後に低帯域モードへ切り替えます。デフォルト:3 秒。"
|
||||
}
|
||||
},
|
||||
"storedLayouts": {
|
||||
|
||||
@ -124,12 +124,9 @@
|
||||
"next": "Volgende",
|
||||
"deleteNow": "Nu verwijderen",
|
||||
"continue": "Doorgaan",
|
||||
"add": "Toevoegen",
|
||||
"add": "Voeg toe",
|
||||
"undo": "Ongedaan maken",
|
||||
"copiedToClipboard": "Gekopieerd naar klembord",
|
||||
"applying": "Verwerken…",
|
||||
"modified": "Gewijzigd",
|
||||
"overridden": "Overschreven"
|
||||
"copiedToClipboard": "Gekopieerd naar het klembord"
|
||||
},
|
||||
"unit": {
|
||||
"speed": {
|
||||
|
||||
@ -82,7 +82,6 @@
|
||||
"zones": "Zones",
|
||||
"boundingBox": "Objectkader",
|
||||
"timestamp": "Tijdstempel",
|
||||
"regions": "Regio's",
|
||||
"paths": "Paden"
|
||||
"regions": "Regio's"
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +48,5 @@
|
||||
"error": {
|
||||
"submitFrigatePlusFailed": "Het is niet gelukt om een frame naar Frigate+ te sturen"
|
||||
}
|
||||
},
|
||||
"cameraOff": "De camera staat uit"
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,15 +13,15 @@
|
||||
"description": "Geactiveerd"
|
||||
},
|
||||
"audio": {
|
||||
"label": "Audio events",
|
||||
"description": "Instellingen voor audio-gebaseerde detectie voor deze camera.",
|
||||
"label": "Geluiddetectie",
|
||||
"description": "Audio-instellingen voor gebeurtenisdetectie van deze camera.",
|
||||
"enabled": {
|
||||
"label": "Geluiddetectie inschakelen",
|
||||
"description": "Schakel de detectie van audio-events voor deze camera in of uit."
|
||||
"description": "Audio‑gebeurtenisdetectie voor deze camera in- of uitschakelen."
|
||||
},
|
||||
"max_not_heard": {
|
||||
"label": "Einde time-out",
|
||||
"description": "Aantal seconden zonder het geconfigureerde audiotype voordat de audio-event wordt beëindigd."
|
||||
"label": "Einde timeout",
|
||||
"description": "Aantal seconden zonder het geconfigureerde audiotype, voordat de geluidsgebeurtenis is beëindigd."
|
||||
},
|
||||
"min_volume": {
|
||||
"label": "Minimumvolume",
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"audio": {
|
||||
"label": "Audio events",
|
||||
"label": "Geluiddetectie",
|
||||
"enabled": {
|
||||
"label": "Geluiddetectie inschakelen",
|
||||
"description": "Audioeventdetectie voor alle camera's in- of uitschakelen; kan per camera worden overschreven."
|
||||
},
|
||||
"max_not_heard": {
|
||||
"label": "Einde time-out",
|
||||
"description": "Aantal seconden zonder het geconfigureerde audiotype voordat de audio-event wordt beëindigd."
|
||||
"label": "Einde timeout",
|
||||
"description": "Aantal seconden zonder het geconfigureerde audiotype, voordat de geluidsgebeurtenis is beëindigd."
|
||||
},
|
||||
"min_volume": {
|
||||
"label": "Minimumvolume",
|
||||
|
||||
@ -28,8 +28,5 @@
|
||||
"header_map": {
|
||||
"roleHeaderRequired": "Rol titel is vereist wanneer rol bindingen zijn geconfigureerd."
|
||||
}
|
||||
},
|
||||
"detect": {
|
||||
"dimensionMustBeEven": "Het moet een even getal zijn."
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,10 +120,5 @@
|
||||
"kangaroo": "Kangoeroe",
|
||||
"skunk": "Stinkdier",
|
||||
"school_bus": "Schoolbus",
|
||||
"royal_mail": "Royal Mail",
|
||||
"canada_post": "Canada Post",
|
||||
"baby": "Baby",
|
||||
"baby_stroller": "Kinderwagen",
|
||||
"rickshaw": "Riksja",
|
||||
"rodent": "Knaagdier"
|
||||
"royal_mail": "Royal Mail"
|
||||
}
|
||||
|
||||
@ -33,8 +33,7 @@
|
||||
"deleteModelFailed": "Model verwijderen mislukt: {{errorMessage}}",
|
||||
"updateModelFailed": "Bijwerken van model mislukt: {{errorMessage}}",
|
||||
"renameCategoryFailed": "Hernoemen van klasse mislukt: {{errorMessage}}",
|
||||
"trainingFailedToStart": "Het is niet gelukt om het model te trainen: {{errorMessage}}",
|
||||
"reclassifyFailed": "Opnieuw classificeren van afbeelding mislukt: {{errorMessage}}"
|
||||
"trainingFailedToStart": "Het is niet gelukt om het model te trainen: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"deleteCategory": {
|
||||
@ -156,13 +155,8 @@
|
||||
"allImagesRequired_other": "Classificeer alle afbeeldingen. {{count}} afbeeldingen resterend.",
|
||||
"modelCreated": "Model succesvol aangemaakt. Gebruik de weergave Recente classificaties om afbeeldingen voor ontbrekende statussen toe te voegen en train vervolgens het model.",
|
||||
"missingStatesWarning": {
|
||||
"title": "Ontbrekende klassevoorbeelden",
|
||||
"description": "Niet alle klassen hebben voorbeelden. Probeer nieuwe voorbeelden te genereren om de ontbrekende klasse te vinden, of ga verder en gebruik de weergave 'Recente classificaties' om later afbeeldingen toe te voegen."
|
||||
},
|
||||
"refreshExamples": "Nieuwe voorbeelden genereren",
|
||||
"refreshConfirm": {
|
||||
"title": "Nieuwe voorbeelden genereren?",
|
||||
"description": "Dit genereert een nieuwe set afbeeldingen en wist alle selecties, inclusief eerdere klassen. Je moet opnieuw voorbeelden selecteren voor alle klassen."
|
||||
"title": "Voorbeelden van ontbrekende staten",
|
||||
"description": "Het wordt aanbevolen om voor alle staten voorbeelden te selecteren voor het beste resultaat. Je kunt doorgaan zonder alle staten te selecteren, maar het model wordt pas getraind zodra alle staten afbeeldingen hebben. Na het doorgaan kun je in de weergave ‘Recente Classificaties’ de ontbrekende staten van afbeeldingen voorzien, en daarna het model trainen."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -193,7 +187,5 @@
|
||||
"modelNotReady": "Model is niet klaar voor training",
|
||||
"noChanges": "Geen wijzigingen in de dataset sinds de laatste training."
|
||||
},
|
||||
"none": "Geen overeenkomst",
|
||||
"reclassifyImageAs": "Afbeelding opnieuw classificeren als:",
|
||||
"reclassifyImage": "Afbeelding opnieuw classificeren"
|
||||
"none": "Geen overeenkomst"
|
||||
}
|
||||
|
||||
@ -170,8 +170,7 @@
|
||||
"attributes": "Classificatie-kenmerken",
|
||||
"title": {
|
||||
"label": "Titel"
|
||||
},
|
||||
"scoreInfo": "Informatie over de score"
|
||||
}
|
||||
},
|
||||
"itemMenu": {
|
||||
"downloadVideo": {
|
||||
@ -222,13 +221,6 @@
|
||||
"downloadCleanSnapshot": {
|
||||
"label": "Download schone snapshot",
|
||||
"aria": "Download schone snapshot"
|
||||
},
|
||||
"debugReplay": {
|
||||
"label": "Debug-herhaling",
|
||||
"aria": "Bekijk dit gevolgde object in de weergave voor het afspelen van foutopsporing"
|
||||
},
|
||||
"more": {
|
||||
"aria": "Meer"
|
||||
}
|
||||
},
|
||||
"noTrackedObjects": "Geen gevolgde objecten gevonden",
|
||||
@ -249,9 +241,6 @@
|
||||
"confirmDelete": {
|
||||
"title": "Bevestig Verwijderen",
|
||||
"desc": "Het verwijderen van dit gevolgde object verwijdert de snapshot, alle opgeslagen embeddings en eventuele bijbehorende trackinggegevens van het object. Opgenomen videobeelden van dit object in de Geschiedenisweergave worden <em>NIET</em> verwijderd.<br /><br />Weet je zeker dat je wilt doorgaan?"
|
||||
},
|
||||
"toast": {
|
||||
"error": "Fout bij het verwijderen van dit bijgehouden object: {{errorMessage}}"
|
||||
}
|
||||
},
|
||||
"fetchingTrackedObjectsFailed": "Fout bij het ophalen van gevolgde objecten: {{errorMessage}}",
|
||||
@ -287,10 +276,7 @@
|
||||
"zones": "Zones",
|
||||
"ratio": "Verhouding",
|
||||
"area": "Gebied",
|
||||
"score": "Score",
|
||||
"computedScore": "Berekende score",
|
||||
"topScore": "Hoogste score",
|
||||
"toggleAdvancedScores": "Geavanceerde scores weergeven"
|
||||
"score": "Score"
|
||||
}
|
||||
},
|
||||
"annotationSettings": {
|
||||
|
||||
@ -21,11 +21,7 @@
|
||||
"title": "Recente herkenningen",
|
||||
"aria": "Selecteer recente herkenningen",
|
||||
"empty": "Er zijn geen recente pogingen tot gezichtsherkenning",
|
||||
"titleShort": "Recent",
|
||||
"emptyNoLibrary": {
|
||||
"title": "Een gezicht uploaden",
|
||||
"description": "U moet ten minste één gezicht aan de bibliotheek toevoegen om gezichtsherkenning te laten werken."
|
||||
}
|
||||
"titleShort": "Recent"
|
||||
},
|
||||
"selectFace": "Selecteer gezicht",
|
||||
"toast": {
|
||||
@ -36,8 +32,7 @@
|
||||
"updateFaceScoreFailed": "Niet gelukt om gezichtsscore bij te werken: {{errorMessage}}",
|
||||
"uploadingImageFailed": "Afbeelding uploaden mislukt: {{errorMessage}}",
|
||||
"trainFailed": "Trainen mislukt: {{errorMessage}}",
|
||||
"renameFaceFailed": "Het is niet gelukt om het gezicht te hernoemen: {{errorMessage}}",
|
||||
"reclassifyFailed": "Opnieuw classificeren van gezicht mislukt: {{errorMessage}}"
|
||||
"renameFaceFailed": "Het is niet gelukt om het gezicht te hernoemen: {{errorMessage}}"
|
||||
},
|
||||
"success": {
|
||||
"deletedFace_one": "{{count}} gezicht is succesvol verwijderd.",
|
||||
@ -48,8 +43,7 @@
|
||||
"deletedName_other": "{{count}} gezichten zijn succesvol verwijderd.",
|
||||
"uploadedImage": "Afbeelding succesvol geüpload.",
|
||||
"addFaceLibrary": "{{name}} is succesvol toegevoegd aan de Gezichtenbibliotheek!",
|
||||
"renamedFace": "Gezicht succesvol hernoemd naar {{name}}",
|
||||
"reclassifiedFace": "Gezicht succesvol geherclassificeerd."
|
||||
"renamedFace": "Gezicht succesvol hernoemd naar {{name}}"
|
||||
}
|
||||
},
|
||||
"imageEntry": {
|
||||
@ -104,7 +98,5 @@
|
||||
},
|
||||
"collections": "Collecties",
|
||||
"nofaces": "Geen gezichten beschikbaar",
|
||||
"pixels": "{{area}}px",
|
||||
"reclassifyFaceAs": "Herclassificeer ‘Face’ als:",
|
||||
"reclassifyFace": "Gezicht opnieuw classificeren"
|
||||
"pixels": "{{area}}px"
|
||||
}
|
||||
|
||||
@ -54,9 +54,7 @@
|
||||
},
|
||||
"camera": {
|
||||
"enable": "Camera inschakelen",
|
||||
"disable": "Camera uitschakelen",
|
||||
"turnOn": "Camera inschakelen",
|
||||
"turnOff": "Camera uitschakelen"
|
||||
"disable": "Camera uitschakelen"
|
||||
},
|
||||
"muteCameras": {
|
||||
"enable": "Alle camera's dempen",
|
||||
@ -110,8 +108,7 @@
|
||||
},
|
||||
"recording": {
|
||||
"disable": "Opname uitschakelen",
|
||||
"enable": "Opname inschakelen",
|
||||
"disabledInConfig": "De opnamefunctie moet eerst worden ingeschakeld in de instellingen van deze camera."
|
||||
"enable": "Opname inschakelen"
|
||||
},
|
||||
"suspend": {
|
||||
"forTime": "Onderbreken voor: "
|
||||
@ -153,8 +150,7 @@
|
||||
"autotracking": "Automatisch volgen",
|
||||
"snapshots": "Momentopnames",
|
||||
"cameraEnabled": "Camera ingeschakeld",
|
||||
"transcription": "Audiotranscriptie",
|
||||
"camera": "Camera"
|
||||
"transcription": "Audiotranscriptie"
|
||||
},
|
||||
"history": {
|
||||
"label": "Historische beelden weergeven"
|
||||
|
||||
@ -1077,7 +1077,7 @@
|
||||
"brands": {
|
||||
"reolink-rtsp": "RTSP Reolink nu este recomandat. Activează HTTP în setările firmware ale camerei și repornește asistentul."
|
||||
},
|
||||
"customUrlRtspRequired": "URL-urile personalizate trebuie să înceapă cu „rtsp://” sau „rtsps://”. Configurarea manuală este necesară pentru stream-urile care nu sunt RTSP."
|
||||
"customUrlRtspRequired": "URL-urile personalizate trebuie să înceapă cu „rtsp://”. Configurarea manuală este necesară pentru stream-urile care nu sunt RTSP."
|
||||
},
|
||||
"docs": {
|
||||
"reolink": "https://docs.frigate.video/configuration/camera_specific.html#reolink-cameras"
|
||||
|
||||
@ -14,8 +14,8 @@ const BlurredIconButton = forwardRef<HTMLDivElement, BlurredIconButtonProps>(
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-30 blur-md transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||
<div className="relative z-10 cursor-pointer text-white/85 drop-shadow-[0_1px_1px_rgba(0,0,0,0.9)] hover:text-white">
|
||||
<div className="pointer-events-none absolute inset-0 m-auto size-5 scale-95 rounded-full bg-black opacity-0 blur-sm transition-all duration-200 group-hover:scale-100 group-hover:opacity-100 group-hover:blur-xl" />
|
||||
<div className="relative z-10 cursor-pointer text-white/85 hover:text-white">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -12,21 +12,14 @@ type ActionsDropdownProps = {
|
||||
onDebugReplayClick?: () => void;
|
||||
onExportClick: () => void;
|
||||
onShareTimestampClick: () => void;
|
||||
onMotionSearchClick?: () => void;
|
||||
};
|
||||
|
||||
export default function ActionsDropdown({
|
||||
onDebugReplayClick,
|
||||
onExportClick,
|
||||
onShareTimestampClick,
|
||||
onMotionSearchClick,
|
||||
}: Readonly<ActionsDropdownProps>) {
|
||||
const { t } = useTranslation([
|
||||
"components/dialog",
|
||||
"views/replay",
|
||||
"views/events",
|
||||
"common",
|
||||
]);
|
||||
const { t } = useTranslation(["components/dialog", "views/replay", "common"]);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@ -49,11 +42,6 @@ export default function ActionsDropdown({
|
||||
<DropdownMenuItem onClick={onShareTimestampClick}>
|
||||
{t("recording.shareTimestamp.label", { ns: "components/dialog" })}
|
||||
</DropdownMenuItem>
|
||||
{onMotionSearchClick && (
|
||||
<DropdownMenuItem onClick={onMotionSearchClick}>
|
||||
{t("motionSearch.menuItem", { ns: "views/events" })}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{onDebugReplayClick && (
|
||||
<DropdownMenuItem onClick={onDebugReplayClick}>
|
||||
{t("title", { ns: "views/replay" })}
|
||||
|
||||
@ -3,7 +3,7 @@ import { baseUrl } from "@/api/baseUrl";
|
||||
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
|
||||
import { Button } from "../ui/button";
|
||||
import { FaArrowDown, FaCalendarAlt, FaCog, FaFilter } from "react-icons/fa";
|
||||
import { LuBug, LuSearch, LuShare2 } from "react-icons/lu";
|
||||
import { LuBug, LuShare2 } from "react-icons/lu";
|
||||
import { TimeRange } from "@/types/timeline";
|
||||
import { ExportContent, ExportPreviewDialog, ExportTab } from "./ExportDialog";
|
||||
import {
|
||||
@ -46,7 +46,6 @@ const DRAWER_FEATURES = [
|
||||
"filter",
|
||||
"debug-replay",
|
||||
"share-timestamp",
|
||||
"motion-search",
|
||||
] as const;
|
||||
export type DrawerFeatures = (typeof DRAWER_FEATURES)[number];
|
||||
const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [
|
||||
@ -55,7 +54,6 @@ const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [
|
||||
"filter",
|
||||
"debug-replay",
|
||||
"share-timestamp",
|
||||
"motion-search",
|
||||
];
|
||||
|
||||
type MobileReviewSettingsDrawerProps = {
|
||||
@ -77,7 +75,6 @@ type MobileReviewSettingsDrawerProps = {
|
||||
setDebugReplayMode?: (mode: ExportMode) => void;
|
||||
setDebugReplayRange?: (range: TimeRange | undefined) => void;
|
||||
onShareTimestamp?: (timestamp: number) => void;
|
||||
onMotionSearch?: () => void;
|
||||
onUpdateFilter: (filter: ReviewFilter) => void;
|
||||
setRange: (range: TimeRange | undefined) => void;
|
||||
setMode: (mode: ExportMode) => void;
|
||||
@ -102,7 +99,6 @@ export default function MobileReviewSettingsDrawer({
|
||||
setDebugReplayMode = () => {},
|
||||
setDebugReplayRange = () => {},
|
||||
onShareTimestamp = () => {},
|
||||
onMotionSearch,
|
||||
onUpdateFilter,
|
||||
setRange,
|
||||
setMode,
|
||||
@ -112,7 +108,6 @@ export default function MobileReviewSettingsDrawer({
|
||||
"views/recording",
|
||||
"components/dialog",
|
||||
"views/replay",
|
||||
"views/events",
|
||||
"common",
|
||||
]);
|
||||
const isAdmin = useIsAdmin();
|
||||
@ -348,6 +343,27 @@ export default function MobileReviewSettingsDrawer({
|
||||
{t("export")}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("share-timestamp") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label={t("recording.shareTimestamp.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
onClick={() => {
|
||||
const initialTimestamp = Math.floor(currentTime);
|
||||
|
||||
setShareTimestampAtOpen(initialTimestamp);
|
||||
setCustomShareTimestamp(initialTimestamp);
|
||||
setSelectedShareOption("current");
|
||||
setDrawerMode("share-timestamp");
|
||||
}}
|
||||
>
|
||||
<LuShare2 className="size-5 rounded-md bg-secondary-foreground stroke-secondary p-1" />
|
||||
{t("recording.shareTimestamp.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("calendar") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
@ -374,40 +390,6 @@ export default function MobileReviewSettingsDrawer({
|
||||
{t("filter")}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("share-timestamp") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label={t("recording.shareTimestamp.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
onClick={() => {
|
||||
const initialTimestamp = Math.floor(currentTime);
|
||||
|
||||
setShareTimestampAtOpen(initialTimestamp);
|
||||
setCustomShareTimestamp(initialTimestamp);
|
||||
setSelectedShareOption("current");
|
||||
setDrawerMode("share-timestamp");
|
||||
}}
|
||||
>
|
||||
<LuShare2 className="size-5 rounded-md bg-secondary-foreground stroke-secondary p-1" />
|
||||
{t("recording.shareTimestamp.label", {
|
||||
ns: "components/dialog",
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
{features.includes("motion-search") && onMotionSearch && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
aria-label={t("motionSearch.menuItem", { ns: "views/events" })}
|
||||
onClick={() => {
|
||||
onMotionSearch();
|
||||
setDrawerMode("none");
|
||||
}}
|
||||
>
|
||||
<LuSearch className="size-5 rounded-md bg-secondary-foreground stroke-secondary p-1" />
|
||||
{t("motionSearch.menuItem", { ns: "views/events" })}
|
||||
</Button>
|
||||
)}
|
||||
{isAdmin && features.includes("debug-replay") && (
|
||||
<Button
|
||||
className="flex w-full items-center justify-center gap-2"
|
||||
|
||||
@ -87,8 +87,7 @@ export default function Step1NameCamera({
|
||||
.string()
|
||||
.optional()
|
||||
.refine(
|
||||
(val) =>
|
||||
!val || val.startsWith("rtsp://") || val.startsWith("rtsps://"),
|
||||
(val) => !val || val.startsWith("rtsp://"),
|
||||
t("cameraWizard.step1.errors.customUrlRtspRequired"),
|
||||
),
|
||||
})
|
||||
|
||||
@ -56,9 +56,11 @@ export default function Events() {
|
||||
false,
|
||||
);
|
||||
|
||||
const [recording, setRecording] = useOverlayState<
|
||||
RecordingStartingPoint | undefined
|
||||
>("recording", undefined, false);
|
||||
const [recording, setRecording] = useOverlayState<RecordingStartingPoint>(
|
||||
"recording",
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const [motionPreviewsCamera, setMotionPreviewsCamera] = useOverlayState<
|
||||
string | undefined
|
||||
>("motionPreviewsCamera", undefined);
|
||||
@ -666,10 +668,6 @@ export default function Events() {
|
||||
filter={reviewFilter}
|
||||
updateFilter={onUpdateFilter}
|
||||
refreshData={reloadData}
|
||||
onMotionSearch={(camera) => {
|
||||
setMotionSearchCamera(camera);
|
||||
setRecording(undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,11 +3,9 @@ import { useTranslation } from "react-i18next";
|
||||
import { isDesktop, isIOS, isMobile } from "react-device-detect";
|
||||
import { FaArrowRight, FaCalendarAlt, FaCheckCircle } from "react-icons/fa";
|
||||
import { MdOutlineRestartAlt, MdUndo } from "react-icons/md";
|
||||
import { LuHand, LuPencil } from "react-icons/lu";
|
||||
|
||||
import { FrigateConfig } from "@/types/frigateConfig";
|
||||
import { TimeRange } from "@/types/timeline";
|
||||
import { ASPECT_PORTRAIT_LAYOUT, ASPECT_WIDE_LAYOUT } from "@/types/record";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@ -114,35 +112,40 @@ export default function MotionSearchDialog({
|
||||
}: MotionSearchDialogProps) {
|
||||
const { t } = useTranslation(["views/motionSearch", "common"]);
|
||||
const apiHost = useApiHost();
|
||||
const [containerNode, setContainerNode] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [containerSize, setContainerSize] = useState({ width: 0, height: 0 });
|
||||
const containerWidth = containerSize.width;
|
||||
const containerHeight = containerSize.height;
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
const [panMode, setPanMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const measure = () => {
|
||||
const rect = containerNode.getBoundingClientRect();
|
||||
setContainerSize((prev) =>
|
||||
prev.width === rect.width && prev.height === rect.height
|
||||
? prev
|
||||
: { width: rect.width, height: rect.height },
|
||||
);
|
||||
};
|
||||
|
||||
measure();
|
||||
|
||||
const observer = new ResizeObserver(() => measure());
|
||||
observer.observe(containerNode);
|
||||
return () => observer.disconnect();
|
||||
}, [containerNode]);
|
||||
|
||||
const cameraConfig = useMemo(() => {
|
||||
if (!selectedCamera) return undefined;
|
||||
return config.cameras[selectedCamera];
|
||||
}, [config, selectedCamera]);
|
||||
|
||||
const aspectRatio = useMemo(() => {
|
||||
if (!cameraConfig) {
|
||||
return 16 / 9;
|
||||
}
|
||||
|
||||
return cameraConfig.detect.width / cameraConfig.detect.height;
|
||||
}, [cameraConfig]);
|
||||
|
||||
// Determine camera aspect ratio category
|
||||
const cameraAspect = useMemo(() => {
|
||||
if (!aspectRatio) {
|
||||
return "normal";
|
||||
} else if (aspectRatio > ASPECT_WIDE_LAYOUT) {
|
||||
return "wide";
|
||||
} else if (aspectRatio < ASPECT_PORTRAIT_LAYOUT) {
|
||||
return "tall";
|
||||
} else {
|
||||
return "normal";
|
||||
}
|
||||
}, [aspectRatio]);
|
||||
|
||||
const polygonClosed = useMemo(
|
||||
() => !isDrawingROI && polygonPoints.length >= 3,
|
||||
[isDrawingROI, polygonPoints.length],
|
||||
@ -166,9 +169,30 @@ export default function MotionSearchDialog({
|
||||
setIsDrawingROI(true);
|
||||
}, [isSearching, polygonPoints.length, setIsDrawingROI, setPolygonPoints]);
|
||||
|
||||
const imageSize = useMemo(() => {
|
||||
if (!containerWidth || !containerHeight || !cameraConfig) {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
|
||||
const cameraAspectRatio =
|
||||
cameraConfig.detect.width / cameraConfig.detect.height;
|
||||
const availableAspectRatio = containerWidth / containerHeight;
|
||||
|
||||
if (availableAspectRatio >= cameraAspectRatio) {
|
||||
return {
|
||||
width: containerHeight * cameraAspectRatio,
|
||||
height: containerHeight,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: containerWidth,
|
||||
height: containerWidth / cameraAspectRatio,
|
||||
};
|
||||
}, [containerWidth, containerHeight, cameraConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
setImageLoaded(false);
|
||||
setPanMode(false);
|
||||
}, [selectedCamera]);
|
||||
|
||||
const Overlay = isDesktop ? Dialog : Drawer;
|
||||
@ -243,13 +267,7 @@ export default function MotionSearchDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TransformWrapper
|
||||
minScale={1.0}
|
||||
wheel={{ smoothStep: 0.005 }}
|
||||
panning={{ disabled: !isDesktop && !panMode }}
|
||||
pinch={{ disabled: !isDesktop && !panMode }}
|
||||
doubleClick={{ disabled: !isDesktop && !panMode }}
|
||||
>
|
||||
<TransformWrapper minScale={1.0} wheel={{ smoothStep: 0.005 }}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<TransformComponent
|
||||
wrapperStyle={{
|
||||
@ -263,16 +281,18 @@ export default function MotionSearchDialog({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"relative mx-auto flex items-center justify-center overflow-hidden rounded-lg border bg-secondary",
|
||||
cameraAspect === "tall"
|
||||
? "max-h-[50dvh] lg:max-h-[60dvh]"
|
||||
: "w-full",
|
||||
)}
|
||||
style={{ aspectRatio }}
|
||||
ref={setContainerNode}
|
||||
className="relative flex w-full items-center justify-center overflow-hidden rounded-lg border bg-secondary"
|
||||
style={{ aspectRatio: "16 / 9" }}
|
||||
>
|
||||
{selectedCamera && cameraConfig ? (
|
||||
<div className="relative h-full w-full">
|
||||
<div
|
||||
className="relative"
|
||||
style={{
|
||||
width: imageSize.width || "100%",
|
||||
height: imageSize.height || "100%",
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={t("dialog.previewAlt", {
|
||||
camera: selectedCamera,
|
||||
@ -300,7 +320,6 @@ export default function MotionSearchDialog({
|
||||
isDrawing={isDrawingROI}
|
||||
setIsDrawing={setIsDrawingROI}
|
||||
isInteractive={true}
|
||||
panMode={panMode}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@ -322,41 +341,11 @@ export default function MotionSearchDialog({
|
||||
{polygonClosed && <FaCheckCircle className="ml-2 size-5" />}
|
||||
</div>
|
||||
<div className="flex flex-row justify-center gap-2">
|
||||
{!isDesktop && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant={panMode ? "select" : "default"}
|
||||
className="size-8 rounded-md p-1.5"
|
||||
aria-label={
|
||||
panMode
|
||||
? t("polygonControls.moveMode")
|
||||
: t("polygonControls.drawMode")
|
||||
}
|
||||
onClick={() => setPanMode((prev) => !prev)}
|
||||
>
|
||||
{panMode ? (
|
||||
<LuHand className="text-selected-foreground" />
|
||||
) : (
|
||||
<LuPencil className="text-secondary-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{panMode
|
||||
? t("polygonControls.moveMode")
|
||||
: t("polygonControls.drawMode")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className={cn(
|
||||
"rounded-md",
|
||||
isDesktop ? "size-6 p-1" : "size-8 p-1.5",
|
||||
)}
|
||||
className="size-6 rounded-md p-1"
|
||||
aria-label={t("polygonControls.undo")}
|
||||
disabled={polygonPoints.length === 0 || isSearching}
|
||||
onClick={undoPolygonPoint}
|
||||
@ -372,10 +361,7 @@ export default function MotionSearchDialog({
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className={cn(
|
||||
"rounded-md",
|
||||
isDesktop ? "size-6 p-1" : "size-8 p-1.5",
|
||||
)}
|
||||
className="size-6 rounded-md p-1"
|
||||
aria-label={t("polygonControls.reset")}
|
||||
disabled={polygonPoints.length === 0 || isSearching}
|
||||
onClick={resetPolygon}
|
||||
@ -443,7 +429,7 @@ export default function MotionSearchDialog({
|
||||
<Slider
|
||||
id="frameSkip"
|
||||
min={1}
|
||||
max={120}
|
||||
max={60}
|
||||
step={1}
|
||||
value={[frameSkip]}
|
||||
onValueChange={([value]) => setFrameSkip(value)}
|
||||
|
||||
@ -14,7 +14,6 @@ type MotionSearchROICanvasProps = {
|
||||
isDrawing: boolean;
|
||||
setIsDrawing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isInteractive?: boolean;
|
||||
panMode?: boolean;
|
||||
motionHeatmap?: Record<string, number> | null;
|
||||
showMotionHeatmap?: boolean;
|
||||
};
|
||||
@ -27,7 +26,6 @@ export default function MotionSearchROICanvas({
|
||||
isDrawing,
|
||||
setIsDrawing,
|
||||
isInteractive = true,
|
||||
panMode = false,
|
||||
motionHeatmap,
|
||||
showMotionHeatmap = false,
|
||||
}: MotionSearchROICanvasProps) {
|
||||
@ -343,9 +341,7 @@ export default function MotionSearchROICanvas({
|
||||
ref={setContainerNode}
|
||||
className={cn(
|
||||
"absolute inset-0 z-10",
|
||||
isInteractive && !panMode
|
||||
? "pointer-events-auto"
|
||||
: "pointer-events-none",
|
||||
isInteractive ? "pointer-events-auto" : "pointer-events-none",
|
||||
)}
|
||||
style={{ cursor: isDrawing ? "crosshair" : "default" }}
|
||||
>
|
||||
|
||||
@ -146,7 +146,7 @@ export default function MotionSearchView({
|
||||
const [parallelMode, setParallelMode] = useState(false);
|
||||
const [threshold, setThreshold] = useState(30);
|
||||
const [minArea, setMinArea] = useState(20);
|
||||
const [frameSkip, setFrameSkip] = useState(30);
|
||||
const [frameSkip, setFrameSkip] = useState(10);
|
||||
const [maxResults, setMaxResults] = useState(25);
|
||||
|
||||
// Job state
|
||||
@ -846,13 +846,7 @@ export default function MotionSearchView({
|
||||
responseData.errors;
|
||||
|
||||
if (Array.isArray(apiMessage)) {
|
||||
errorMessage = apiMessage
|
||||
.map((item) =>
|
||||
typeof item === "string"
|
||||
? item
|
||||
: ((item as { msg?: string })?.msg ?? JSON.stringify(item)),
|
||||
)
|
||||
.join(", ");
|
||||
errorMessage = apiMessage.join(", ");
|
||||
} else if (typeof apiMessage === "string") {
|
||||
errorMessage = apiMessage;
|
||||
} else if (apiMessage) {
|
||||
|
||||
@ -95,7 +95,6 @@ type RecordingViewProps = {
|
||||
filter?: ReviewFilter;
|
||||
updateFilter: (newFilter: ReviewFilter) => void;
|
||||
refreshData?: () => void;
|
||||
onMotionSearch?: (camera: string) => void;
|
||||
};
|
||||
export function RecordingView({
|
||||
startCamera,
|
||||
@ -108,7 +107,6 @@ export function RecordingView({
|
||||
filter,
|
||||
updateFilter,
|
||||
refreshData,
|
||||
onMotionSearch,
|
||||
}: RecordingViewProps) {
|
||||
const { t } = useTranslation(["views/events", "components/dialog"]);
|
||||
const { data: config } = useSWR<FrigateConfig>("config");
|
||||
@ -727,9 +725,6 @@ export function RecordingView({
|
||||
setCustomShareTimestamp(initialTimestamp);
|
||||
setShareTimestampOpen(true);
|
||||
}}
|
||||
onMotionSearchClick={
|
||||
onMotionSearch ? () => onMotionSearch(mainCamera) : undefined
|
||||
}
|
||||
onDebugReplayClick={
|
||||
isAdmin
|
||||
? () => {
|
||||
@ -812,9 +807,6 @@ export function RecordingView({
|
||||
}
|
||||
}}
|
||||
onShareTimestamp={onShareReviewLink}
|
||||
onMotionSearch={
|
||||
onMotionSearch ? () => onMotionSearch(mainCamera) : undefined
|
||||
}
|
||||
onUpdateFilter={updateFilter}
|
||||
setRange={setExportRange}
|
||||
setMode={setExportMode}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user