Compare commits

..
Author SHA1 Message Date
Josh Hawkins f0de4d3592 serialize recording frame extraction for review descriptions 2026-11-12 18:02:11 -06:00
Josh Hawkins 98ca637608 move genai review frame extraction off the embeddings loop
- Run frame gathering for finished review items on a thread so per-frame ffmpeg calls don't stall the embeddings maintainer
- Share one path between live and regenerated review descriptions
- Stop padding review start and end times in place, which widened the stored review item
- Lock timestamp expiry in EventsPerSecond since update() and eps() now run on different threads
2026-11-12 17:52:25 -06:00
b2f5315e09 Validate MQTT TLS options before startup (#24951)
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
* Validate MQTT TLS verification options before client startup

* Test MQTT configuration recovery with authentication available

* Cover both MQTT TLS verification values in safe-mode recovery

* Use a documentation address in MQTT regression tests

* Skip MQTT TLS validation when MQTT is disabled

* Simplify MQTT TLS validation regression coverage

---------

Co-authored-by: Peter Kay <dougle.peter.kay@googlemail.com>
2026-11-12 10:04:09 -06:00
Josh HawkinsandGitHub a655ddcb2e Pin debug replay to main recording stream (#24952)
* pin debug replay to one recording stream

Debug replay stream-copied the merged VOD playlist into one mp4. When main had gaps that sub filled, the clip switched streams mid-file, and a camera with an H.265 main and an H.264 sub produced an mp4 labeled H.264 that also held HEVC packets. The replay camera's decoder then failed and the watchdog killed ffmpeg. Replay now follows the clip download rule: it pins the playlist to main and only falls back to sub when the range has no main recordings.

* choose the replay stream with the vod coverage resolver

Stream selection counted a main row that only touched the range, while the pinned playlist served nothing for it, so replay failed instead of using the sub footage. Selection now uses resolve_coverage, the same resolver the pinned vod and export use.
2026-11-12 08:33:33 -07:00
Josh HawkinsandGitHub 806ee66562 Fix sub label filter for non-ascii names (#24953)
* fix sub label filter for non-ascii names

The sub_labels filter lowercased the requested name in Python but the stored value with SQLite's LOWER(), which only folds ASCII. Cyrillic and other non-ASCII names never matched, so filtering by a face name returned no events. Both sides are now lowered in SQLite, which keeps ASCII matching case-insensitive and non-ASCII matching exact.

* cover events/search in sub label filter test
2026-11-12 08:33:04 -07:00
Hosted WeblateandJosh Hawkins 42f5a7560b Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 21836c7444 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
e70eba2fb8 Translated using Weblate (Serbian (Latin script))
Currently translated at 28.7% (25 of 87 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 2.0% (1 of 49 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 21.8% (19 of 87 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 6.6% (4 of 60 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 97.9% (142 of 145 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 5.0% (3 of 60 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 44.1% (64 of 145 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (141 of 141 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 56.1% (791 of 1409 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 2.2% (2 of 87 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (68 of 68 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 5.2% (26 of 500 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (66 of 66 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 23.1% (326 of 1409 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 17.9% (252 of 1404 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (6 of 6 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (522 of 522 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (37 of 37 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (120 of 120 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 0.1% (2 of 1404 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 80.6% (421 of 522 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 52.2% (139 of 266 strings)

Translated using Weblate (Serbian (Latin script))

Currently translated at 5.0% (25 of 500 strings)

Co-authored-by: Branislav Maricic <b.n.maricic@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-icons/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-recording/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/sr_Latn/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-auth
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-icons
Translation: Frigate NVR/components-input
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-recording
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-11-12 08:23:49 -06:00
e0b5a6c965 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (66 of 66 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (775 of 775 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (522 of 522 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (282 of 282 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1409 of 1409 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (87 of 87 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (68 of 68 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (266 of 266 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 93.9% (728 of 775 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 97.5% (509 of 522 strings)

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 91.7% (711 of 775 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 94.2% (492 of 522 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (37 of 37 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (120 of 120 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 99.2% (264 of 266 strings)

Co-authored-by: GuoQing Liu <zhaisoul@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hans/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-11-12 08:23:49 -06:00
6733a757e8 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (46 of 46 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 96.8% (62 of 64 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 81.8% (54 of 66 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 88.4% (23 of 26 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 89.6% (695 of 775 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 90.4% (472 of 522 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (141 of 141 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 64.1% (181 of 282 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 89.8% (1261 of 1404 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 77.5% (100 of 129 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.8% (86 of 87 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.5% (67 of 68 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 59.4% (22 of 37 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 89.1% (107 of 120 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 90.2% (240 of 266 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (500 of 500 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 96.8% (62 of 64 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 88.4% (23 of 26 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 89.6% (695 of 775 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 90.4% (472 of 522 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (141 of 141 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 89.7% (1260 of 1404 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.8% (86 of 87 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 98.5% (67 of 68 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 90.2% (240 of 266 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: 蘭蘭露 <flandretw@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-input/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/zh_Hant/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Groups
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/components-input
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 94e700fbac Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 5f59f7c53c Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 0e2da4e24a Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins dade7a6b3e Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 329430760e Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 2fbb66e475 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 5019984a33 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
db58f9f5cc Translated using Weblate (French)
Currently translated at 27.2% (211 of 775 strings)

Translated using Weblate (French)

Currently translated at 61.8% (323 of 522 strings)

Translated using Weblate (French)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (French)

Currently translated at 100.0% (120 of 120 strings)

Translated using Weblate (French)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (French)

Currently translated at 100.0% (266 of 266 strings)

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: tiphergane <tiphergane@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins f83b8b57db Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 146d61017e Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 86c9b4bf74 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins ed1a515c1b Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins c013f6dcb8 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins bb1736bb4d Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
68988780cd Translated using Weblate (Hindi)
Currently translated at 29.0% (145 of 500 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Thomas Maslanky <terminallychill@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/hi/
Translation: Frigate NVR/audio
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins c148063e76 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 37ca717f63 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins b6d16fb6f3 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins e45b7de79b Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins a1f8f95497 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
cabbfd5d73 Translated using Weblate (Catalan)
Currently translated at 100.0% (1409 of 1409 strings)

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Eduardo Pastor Fernández <edugaga@users.noreply.hosted.weblate.org>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 20a3bd7ac5 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins e287833e8d Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins e150c5d1dd Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 60c4e37303 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
3ac35fe777 Translated using Weblate (Romanian)
Currently translated at 100.0% (1409 of 1409 strings)

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 8b0325aefb Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
c57b48e977 Translated using Weblate (Estonian)
Currently translated at 100.0% (266 of 266 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Priit Jõerüüt <jrthwlate@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/et/
Translation: Frigate NVR/common
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 74a2aac866 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 2af6dc34da Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Hosted WeblateandJosh Hawkins 9eb336a3d8 Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
bb0e2c0d5e Translated using Weblate (Turkish)
Currently translated at 100.0% (1409 of 1409 strings)

Update translation files

Updated by "Cleanup translation files" add-on in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Murat Odabasi <modabas@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/tr/
Translation: Frigate NVR/views-settings
2026-11-12 08:23:49 -06:00
Josh HawkinsandGitHub 06d1365056 Report ownership scan progress (#24940)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* report ownership scan progress

The ownership scan printed nothing until it finished, which took 30+ minutes on large recordings volumes and looked stuck. It now logs a running count of checked and mismatched entries every 30 seconds.

* check the scan clock on every entry
2026-11-11 08:52:58 -07:00
Josh HawkinsandGitHub 8a88cefc9a Autotracking tweaks (#24935)
* fix autotracking movement time prediction and recorded move times

Movement time prediction multiplied the fitted intercept by the saved zero-step calibration time instead of 1, so predictions didn't match the regression. In absolute zoom mode, a pan/tilt followed by a zoom recorded the zoom's timestamps against the pan/tilt distance, because _zoom_absolute resets the PTZ start and stop times. The pan/tilt times are now captured before the zoom runs.

* predict velocity and zoom size for objects approaching the autotracked camera

The velocity check required both box corners to move in the same direction, but the corners of an approaching object diverge, so its velocity was rejected and zeroed and the camera aimed where the object had been. That check is removed, and the existing magnitude, delta, and variance checks still reject noise. The zoom size prediction fit area against epoch time with no intercept, which returned roughly the mean area instead of a forecast. It now scales the target box by the growth the corner velocities predict over the movement time.

* keep predicted target box positive and add regression tests

A shrinking object's predicted size could reach zero, so the relative zoom ratio divided by zero and sent a NaN zoom to the camera. The predicted size is now floored at 1 px. Tests also cover the movement time intercept and the pan/tilt timestamps that absolute zoom overwrites.
2026-11-11 08:38:21 -07:00
Josh HawkinsandGitHub fba967c74b fix annotation offset seeking in tracking details (#24936)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
The VOD manifest used to find the keyframe-snapped clip start was built from the live annotation offset while the playing clip used the offset from when the event opened. Each offset step fetched a manifest for a different clip, so the seek mapping drifted: the video jumped forward about one GOP every second of offset and flickered while each new manifest loaded. The manifest now uses the same stable source offset as the playlist
2026-11-10 07:57:21 -06:00
Josh HawkinsandGitHub 33c55649a4 report custom go2rtc and ffmpeg builds in analytics (#24928)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
Users can swap in their own go2rtc binary at /config/go2rtc or point ffmpeg.path at a custom build, and either can explain stream behavior that looks like a Frigate regression. The install section now reports a custom_go2rtc boolean and an ffmpeg enum (default, bundled, custom) for the configured global path, so no version strings or path text leave the install.
2026-11-09 14:05:16 -07:00
Josh HawkinsandGitHub c88434dc29 Write migrated config atomically (#24926)
* write migrated config atomically

Config migration rewrote config.yml in place with open("w") after each step, so nginx's get_nginx_settings.py, which starts alongside frigate, could read an empty file, get None from yaml.load, and crash on config.get. Each step now writes to a temp file with the original mode and os.replace's it into place.

* keep config owner and handle file bind mounts in migration writes

os.replace failed with EBUSY when config.yml was a file bind mount, as in the documented --validate-config command, and the replaced file lost its original owner when Frigate ran as root. The write now copies in place on EBUSY and restores the original owner when permitted. Tests cover a failed dump leaving the original config and the bind-mount fallback.

* follow config symlinks in migration writes
2026-11-09 13:56:34 -07:00
Josh HawkinsandGitHub e7256d58a1 strip nginx user directive when not running as root (#24925)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
2026-11-09 12:41:19 -06:00
b2a7e992c9 Request FP16 TensorRT engine for ONNX detector with device Tensorrt (#24923)
* Request FP16 TensorRT engine for ONNX detector with device Tensorrt

get_ort_providers only enables trt_fp16_enable when requires_fp16 is
passed, and the ONNX detector never passed it, so device: Tensorrt
always built an FP32 engine. USE_FP16=False still disables FP16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Make TensorRT FP16 opt-in via USE_FP16 only

Drop the unused requires_fp16 parameter from get_ort_providers and decide
trt_fp16_enable from the USE_FP16 environment variable alone, defaulting
to false. Revert the ONNX detector change and document USE_FP16=true for
device: Tensorrt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Drop the TensorRT docs line

The stock image ships only the TensorRT runtime, not the builder
libraries needed to create engines, so the instructions were misleading.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:46:25 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11f4dd194c Update google-genai requirement (#24920)
Updates the requirements on [google-genai](https://github.com/googleapis/python-genai) to permit the latest version.

Updates `google-genai` to 3.0.0
- [Release notes](https://github.com/googleapis/python-genai/releases)
- [Changelog](https://github.com/googleapis/python-genai/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/python-genai/compare/v2.34.0...v3.0.0)

---
updated-dependencies:
- dependency-name: google-genai
  dependency-version: 3.0.0
  dependency-type: direct:production
  dependency-group: pydantic-genai
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-09 08:25:39 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0e7da1bee4 Bump the web-minor-patch group in /web with 5 updates (#24922)
Bumps the web-minor-patch group in /web with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [framer-motion](https://github.com/motiondivision/motion) | `14.9.1` | `14.10.0` |
| [i18next](https://github.com/i18next/i18next) | `26.7.1` | `26.7.2` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.69.0` | `1.70.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.1.12` | `17.1.13` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.64.1` | `1.65.0` |


Updates `framer-motion` from 14.9.1 to 14.10.0
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v14.9.1...v14.10.0)

Updates `i18next` from 26.7.1 to 26.7.2
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.7.1...v26.7.2)

Updates `lucide-react` from 1.69.0 to 1.70.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.70.0/packages/lucide-react)

Updates `react-i18next` from 17.1.12 to 17.1.13
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.1.12...v17.1.13)

Updates `@playwright/test` from 1.64.1 to 1.65.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.64.1...v1.65.0)

---
updated-dependencies:
- dependency-name: framer-motion
  dependency-version: 14.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: i18next
  dependency-version: 26.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: lucide-react
  dependency-version: 1.70.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: react-i18next
  dependency-version: 17.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: "@playwright/test"
  dependency-version: 1.65.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-09 08:25:23 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
14870f5237 Bump ruff in /docker/main in the pip-main-minor-patch group (#24921)
Bumps the pip-main-minor-patch group in /docker/main with 1 update: [ruff](https://github.com/astral-sh/ruff).


Updates `ruff` from 0.17.3 to 0.17.4
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.17.3...0.17.4)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.17.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: pip-main-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-09 08:25:17 -06:00
Josh HawkinsandGitHub ca851a9238 Miscellaneous Fixes (#24914)
* strip intel media driver on install

The manually built iHD_drv_video.so was installed with full debug info, making it 513 MB in the image instead of 41 MB stripped. Installing with install/strip removes about 240 MB from the compressed amd64 image.

* remove rjsf 6.11.0 dependabot hold

The hold covered the GenAI form refilling the saved model after a provider switch. That was a GenAIModelWidget bug that rjsf 6.11 exposed, and it was fixed with the 6.11.1 bump in #24901, so no rjsf version needs holding.

* strip intel media driver after install

install/strip skipped iHD_drv_video.so because media-driver installs it with install(PROGRAMS), not as a target. Strip it directly instead.

* bump dev docs version in header

* attest published images with github build provenance

Published images carried unsigned BuildKit provenance, so nothing tied an image digest to the commit it was built from. Each published image (the default multi-arch build and the six variants) now gets an actions/attest provenance attestation pushed to GHCR, verifiable with gh attestation verify. Release tags are skopeo copies with the same digest, so they're covered too. Storage records are off because they require an org-owned repo.
2026-11-09 06:44:48 -07:00
81cf7be89a Add on-demand Frigate+ API key validation (#24918)
* Add on-demand Frigate+ API key validation

The Frigate+ settings page reported the API key as validated whenever it
matched the expected format, without ever checking it with the server.

- Clarify the status text to say the key matches the expected format
- Add PlusApi.validate_key(), which requests a token from Frigate+ and
  reports the key as valid, invalid, unreachable, or not set; server
  errors and timeouts are not reported as an invalid key
- Add an admin-only POST /plus/validate endpoint and regenerate the API spec
- Add a "Validate with Frigate+" button that shows the result inline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde

* Validate Frigate+ key asynchronously and keep cached token on bad replies

- Use aiohttp for the validation request so the endpoint awaits it
  instead of blocking a worker thread
- Only replace the cached token when the reply has a string accessToken
  and numeric expires; otherwise report unreachable so an unexpected
  response can't break later Frigate+ requests
- Test against a local aiohttp server, covering timeouts, connection
  failures, non-JSON replies and unusable token shapes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde

* Honor proxy settings when validating the Frigate+ key

aiohttp ignores HTTP(S)_PROXY unless trust_env is set, so validation
reported Frigate+ as unreachable on installs that need a proxy while
other Frigate+ requests still worked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E4iDiZE2b3tVFVYyZrfZde

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 06:30:35 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Josh Hawkins
52d469ed20 Bump the rjsf group across 1 directory with 4 updates (#24901)
CI / AMD64 Build (push) Canceled after 0s
CI / AMD64 Smoke Test (push) Canceled after 0s
CI / ARM Build (push) Canceled after 0s
CI / Jetson Jetpack 6 (push) Canceled after 0s
CI / AMD64 Extra Build (push) Canceled after 0s
CI / ARM Extra Build (push) Canceled after 0s
CI / Synaptics Build (push) Canceled after 0s
CI / Assemble and push default build (push) Canceled after 0s
* Bump the rjsf group across 1 directory with 4 updates

Bumps the rjsf group with 4 updates in the /web directory: [@rjsf/core](https://github.com/rjsf-team/react-jsonschema-form), [@rjsf/shadcn](https://github.com/rjsf-team/react-jsonschema-form), [@rjsf/utils](https://github.com/rjsf-team/react-jsonschema-form) and [@rjsf/validator-ajv8](https://github.com/rjsf-team/react-jsonschema-form).


Updates `@rjsf/core` from 6.10.1 to 6.11.1
- [Release notes](https://github.com/rjsf-team/react-jsonschema-form/releases)
- [Changelog](https://github.com/rjsf-team/react-jsonschema-form/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rjsf-team/react-jsonschema-form/compare/v6.10.1...v6.11.1)

Updates `@rjsf/shadcn` from 6.10.1 to 6.11.1
- [Release notes](https://github.com/rjsf-team/react-jsonschema-form/releases)
- [Changelog](https://github.com/rjsf-team/react-jsonschema-form/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rjsf-team/react-jsonschema-form/compare/v6.10.1...v6.11.1)

Updates `@rjsf/utils` from 6.10.1 to 6.11.1
- [Release notes](https://github.com/rjsf-team/react-jsonschema-form/releases)
- [Changelog](https://github.com/rjsf-team/react-jsonschema-form/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rjsf-team/react-jsonschema-form/compare/v6.10.1...v6.11.1)

Updates `@rjsf/validator-ajv8` from 6.10.1 to 6.11.1
- [Release notes](https://github.com/rjsf-team/react-jsonschema-form/releases)
- [Changelog](https://github.com/rjsf-team/react-jsonschema-form/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rjsf-team/react-jsonschema-form/compare/v6.10.1...v6.11.1)

---
updated-dependencies:
- dependency-name: "@rjsf/core"
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rjsf
- dependency-name: "@rjsf/shadcn"
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rjsf
- dependency-name: "@rjsf/utils"
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rjsf
- dependency-name: "@rjsf/validator-ajv8"
  dependency-version: 6.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rjsf
...

Signed-off-by: dependabot[bot] <support@github.com>

* clear the genai model with undefined so a provider switch stays a pending change

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
2026-11-08 08:40:14 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a9105e12f1 Bump @hookform/resolvers from 6.0.2 to 7.0.3 in /web (#24909)
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 6.0.2 to 7.0.3.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v6.0.2...v7.0.3)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:27:37 -06:00
Josh HawkinsandGitHub d4ebe64fde Close config paths to command execution (#24890)
* check go2rtc stream sources on the internal port

auth() returned admin for internal port requests before the go2rtc src check ran, so anyone who could reach port 5000 could send an unknown src to /api/ws, and go2rtc created a stream from it and started its producer, including ffmpeg sources with #raw= arguments. The internal port now rejects any src that isn't an existing stream, the same as the external port, and still skips the per-camera role checks.

* reject go2rtc stream names that can inject transcode parameters

live.transcode.source names a go2rtc stream that is spliced into an ffmpeg:{name}#video=h264 source, and stream names were never validated, so a stream named with #raw= parameters, whitespace, quotes or a ${VAR} reference added its own ffmpeg arguments. New stream names with these characters are rejected through the API, transcode.source fails validation when its stream name has them, and startup skips the generated transcode stream.

* only export allowlisted environment_vars

apply_config_env_vars wrote every unprefixed environment_vars key into os.environ and exported every FRIGATE_ key, so a config could set LD_PRELOAD or LIGHTER_ANE_EP to load a library into Frigate or its children, or set FRIGATE_RUN_AS_ROOT to turn off the ffmpeg.path writable-tree guard. Only the documented names are now exported, each with a value check, and a name the container already sets is never overwritten. FRIGATE_ keys still feed substitution, but only FRIGATE_JWT_SECRET is exported. Anything else logs a warning naming the key.

* don't trust live stream names as existing go2rtc streams
2026-11-08 07:16:48 -07:00
Josh HawkinsandGitHub b55723146d Don't run config ffmpeg as root (#24891)
* don't run a config ffmpeg.path as root at boot

The frigate and go2rtc run scripts probed "$ffmpeg_path" -version as root before dropping privileges, and resolve_ffmpeg_path only refused runtime-user-writable paths when frigate itself was a root service. In the default mode uid 1000 could set ffmpeg.path to a /config directory, plant bin/ffmpeg there, and get root on the next restart, and under FRIGATE_ROOT_SERVICES=go2rtc a root go2rtc used that path as its ffmpeg bin. The probe now runs as the service user whenever the service drops privileges, and resolve_ffmpeg_path refuses writable paths for whichever service will run the binary, returning the resolved path so a symlink can't be retargeted after the check. go2rtc falls back to the bundled build's libavformat version if its user can't run the custom build.

* resolve the full ffmpeg path under granular root
2026-11-08 07:15:02 -07:00
Josh HawkinsandGitHub d9ce4b061b Replace nginx secure token module (#24893)
* remove nginx-secure-token-module from the nginx build

Kaltura deleted the nginx-secure-token-module repo, so the tarball download in build_nginx.sh returned a 404 and any build without a cached nginx layer failed. This drops the module and its secure_token directives from the vod location.

* use sub_filter to pass vod playlist args to segment URLs

Home Assistant signs vod playlist URLs with authSig and needs it on every init, segment, and index URL in the playlist. sub_filter appends the request args to the three URL endings vod writes, giving byte-identical playlists to the old secure_token output.
2026-11-08 07:14:34 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
62d775672b Bump @cycjimmy/jsmpeg-player from 6.1.8 to 7.0.0 in /web (#24908)
Bumps [@cycjimmy/jsmpeg-player](https://github.com/cycjimmy/jsmpeg-player) from 6.1.8 to 7.0.0.
- [Release notes](https://github.com/cycjimmy/jsmpeg-player/releases)
- [Changelog](https://github.com/cycjimmy/jsmpeg-player/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/cycjimmy/jsmpeg-player/compare/v6.1.8...v7.0.0)

---
updated-dependencies:
- dependency-name: "@cycjimmy/jsmpeg-player"
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:03:05 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d7c6e83ae3 Bump the web-minor-patch group in /web with 11 updates (#24907)
Bumps the web-minor-patch group in /web with 11 updates:

| Package | From | To |
| --- | --- | --- |
| [apexcharts](https://github.com/apexcharts/apexcharts.js) | `8.6.0` | `8.7.0` |
| [framer-motion](https://github.com/motiondivision/motion) | `14.9.0` | `14.9.1` |
| [i18next](https://github.com/i18next/i18next) | `26.5.12` | `26.7.1` |
| [i18next-http-backend](https://github.com/i18next/i18next-http-backend) | `4.0.7` | `4.0.10` |
| [konva](https://github.com/konvajs/konva) | `10.9.0` | `10.9.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.65.0` | `1.69.0` |
| [react-i18next](https://github.com/i18next/react-i18next) | `17.1.9` | `17.1.12` |
| [virtua](https://github.com/inokawa/virtua) | `0.54.0` | `0.54.2` |
| [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) | `6.1.2` | `6.2.1` |
| [globals](https://github.com/sindresorhus/globals) | `17.13.0` | `17.14.0` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `8.3.5` | `8.3.6` |


Updates `apexcharts` from 8.6.0 to 8.7.0
- [Release notes](https://github.com/apexcharts/apexcharts.js/releases)
- [Commits](https://github.com/apexcharts/apexcharts.js/compare/v8.6.0...v8.7.0)

Updates `framer-motion` from 14.9.0 to 14.9.1
- [Changelog](https://github.com/motiondivision/motion/blob/main/CHANGELOG.md)
- [Commits](https://github.com/motiondivision/motion/compare/v14.9.0...v14.9.1)

Updates `i18next` from 26.5.12 to 26.7.1
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.5.12...v26.7.1)

Updates `i18next-http-backend` from 4.0.7 to 4.0.10
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/compare/v4.0.7...v4.0.10)

Updates `konva` from 10.9.0 to 10.9.1
- [Release notes](https://github.com/konvajs/konva/releases)
- [Changelog](https://github.com/konvajs/konva/blob/master/CHANGELOG.md)
- [Commits](https://github.com/konvajs/konva/compare/10.9.0...10.9.1)

Updates `lucide-react` from 1.65.0 to 1.69.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.69.0/packages/lucide-react)

Updates `react-i18next` from 17.1.9 to 17.1.12
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.1.9...v17.1.12)

Updates `virtua` from 0.54.0 to 0.54.2
- [Release notes](https://github.com/inokawa/virtua/releases)
- [Commits](https://github.com/inokawa/virtua/compare/0.54.0...0.54.2)

Updates `@vitejs/plugin-react` from 6.1.2 to 6.2.1
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.2.1/packages/plugin-react)

Updates `globals` from 17.13.0 to 17.14.0
- [Release notes](https://github.com/sindresorhus/globals/releases)
- [Commits](https://github.com/sindresorhus/globals/compare/v17.13.0...v17.14.0)

Updates `vite` from 8.3.5 to 8.3.6
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.3.6/packages/vite)

---
updated-dependencies:
- dependency-name: apexcharts
  dependency-version: 8.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: framer-motion
  dependency-version: 14.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: i18next
  dependency-version: 26.7.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: i18next-http-backend
  dependency-version: 4.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: konva
  dependency-version: 10.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: lucide-react
  dependency-version: 1.69.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: react-i18next
  dependency-version: 17.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: virtua
  dependency-version: 0.54.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.2.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: globals
  dependency-version: 17.14.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: web-minor-patch
- dependency-name: vite
  dependency-version: 8.3.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: web-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:59 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
74ca38dfb9 Update openai requirement from ==3.41.* to ==3.46.* in /docker/main (#24905)
Updates the requirements on [openai](https://github.com/openai/openai-python) to permit the latest version.
- [Release notes](https://github.com/openai/openai-python/releases)
- [Changelog](https://github.com/openai/openai-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/openai/openai-python/compare/v3.41.0...v3.46.0)

---
updated-dependencies:
- dependency-name: openai
  dependency-version: 3.46.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:54 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c6b0f4de9f Update pyzmq requirement from ==27.2.* to ==27.3.* in /docker/main (#24904)
Updates the requirements on [pyzmq](https://github.com/zeromq/pyzmq) to permit the latest version.
- [Release notes](https://github.com/zeromq/pyzmq/releases)
- [Commits](https://github.com/zeromq/pyzmq/compare/v27.2.0...v27.3.0)

---
updated-dependencies:
- dependency-name: pyzmq
  dependency-version: 27.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:49 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
98079cbeca Bump python-multipart in /docker/main in the pip-main-minor-patch group (#24903)
Bumps the pip-main-minor-patch group in /docker/main with 1 update: [python-multipart](https://github.com/Kludex/python-multipart).


Updates `python-multipart` from 0.0.32 to 0.0.33
- [Release notes](https://github.com/Kludex/python-multipart/releases)
- [Changelog](https://github.com/Kludex/python-multipart/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Kludex/python-multipart/compare/0.0.32...0.0.33)

---
updated-dependencies:
- dependency-name: python-multipart
  dependency-version: 0.0.33
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: pip-main-minor-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:43 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
197e740be7 Update joserfc requirement in /docker/main in the crypto group (#24900)
Updates the requirements on [joserfc](https://github.com/authlib/joserfc) to permit the latest version.

Updates `joserfc` to 1.8.0
- [Release notes](https://github.com/authlib/joserfc/releases)
- [Changelog](https://github.com/authlib/joserfc/blob/main/docs/changelog.rst)
- [Commits](https://github.com/authlib/joserfc/compare/1.7.0...1.8.0)

---
updated-dependencies:
- dependency-name: joserfc
  dependency-version: 1.8.0
  dependency-type: direct:production
  dependency-group: crypto
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:38 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6716cd3d6f Update paho-mqtt requirement from ==2.1.* to ==2.2.* in /docker/main (#24906)
Updates the requirements on [paho-mqtt](https://github.com/eclipse-paho/paho.mqtt.python) to permit the latest version.
- [Release notes](https://github.com/eclipse-paho/paho.mqtt.python/releases)
- [Changelog](https://github.com/eclipse-paho/paho.mqtt.python/blob/master/ChangeLog.txt)
- [Commits](https://github.com/eclipse-paho/paho.mqtt.python/compare/v2.1.0...v2.2.0)

---
updated-dependencies:
- dependency-name: paho-mqtt
  dependency-version: 2.2.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:33 -06:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3eee34ad7f Bump the actions group across 2 directories with 1 update (#24902)
Bumps the actions group with 1 update in the / directory: [docker/login-action](https://github.com/docker/login-action).
Bumps the actions group with 1 update in the /.github/actions/setup directory: [docker/login-action](https://github.com/docker/login-action).


Updates `docker/login-action` from 4.7.0 to 4.8.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/81c6f93d4bc991e510b342a00b419b3bb4efc5c6...8aa5831fb8d48d8f76594c7ddde61ae58019f550)

Updates `docker/login-action` from 4.7.0 to 4.8.0
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/81c6f93d4bc991e510b342a00b419b3bb4efc5c6...8aa5831fb8d48d8f76594c7ddde61ae58019f550)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
- dependency-name: docker/login-action
  dependency-version: 4.8.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-08 08:02:28 -06:00
166 changed files with 9897 additions and 1729 deletions
+3 -1
View File
@@ -4,6 +4,8 @@ inputs:
GITHUB_TOKEN:
required: true
outputs:
image-repo:
value: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}
image-name:
value: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ steps.create-short-sha.outputs.SHORT_SHA }}
cache-name:
@@ -42,7 +44,7 @@ runs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to the Container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550
with:
registry: ghcr.io
username: ${{ github.actor }}
-3
View File
@@ -136,9 +136,6 @@ updates:
# Later versions need Node 22 and extract false keys
- dependency-name: "i18next-cli"
versions: [">1.5.11"]
# 6.11.0 refills the saved model in the GenAI settings form
- dependency-name: "@rjsf/*"
versions: ["6.11.0"]
- package-ecosystem: "npm"
directory: "/docs"
+87 -1
View File
@@ -423,6 +423,11 @@ jobs:
run: docker rm -f frigate || true
arm64_build:
runs-on: ubuntu-22.04-arm
permissions:
contents: read
packages: write
attestations: write
id-token: write
name: ARM Build
steps:
- name: Check out code
@@ -435,6 +440,7 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push arm64 standard build
id: standard-arm64
uses: docker/build-push-action@v7
with:
context: .
@@ -445,6 +451,13 @@ jobs:
tags: |
${{ steps.setup.outputs.image-name }}-standard-arm64
cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64
- name: Attest arm64 standard build
uses: actions/attest@v4
with:
subject-name: ${{ steps.setup.outputs.image-repo }}
subject-digest: ${{ steps.standard-arm64.outputs.digest }}
push-to-registry: true
create-storage-record: false
- name: Build and push RPi build
uses: docker/bake-action@v7
with:
@@ -458,6 +471,11 @@ jobs:
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-arm64,mode=max
jetson_jp6_build:
runs-on: ubuntu-22.04-arm
permissions:
contents: read
packages: write
attestations: write
id-token: write
name: Jetson Jetpack 6
steps:
- name: Check out code
@@ -470,6 +488,7 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push TensorRT (Jetson, Jetpack 6)
id: tensorrt-jp6
env:
ARCH: arm64
BASE_IMAGE: nvcr.io/nvidia/tensorrt:23.12-py3-igpu
@@ -485,8 +504,20 @@ jobs:
tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt-jp6
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-jp6,mode=max
- name: Attest TensorRT (Jetson, Jetpack 6)
uses: actions/attest@v4
with:
subject-name: ${{ steps.setup.outputs.image-repo }}
subject-digest: ${{ fromJSON(steps.tensorrt-jp6.outputs.metadata)['tensorrt']['containerimage.digest'] }}
push-to-registry: true
create-storage-record: false
amd64_extra_builds:
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
attestations: write
id-token: write
name: AMD64 Extra Build
needs:
- amd64_build
@@ -501,6 +532,7 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push TensorRT (x86 GPU)
id: tensorrt
env:
COMPUTE_LEVEL: "50 60 70 80 90"
uses: docker/bake-action@v7
@@ -513,7 +545,15 @@ jobs:
tensorrt.tags=${{ steps.setup.outputs.image-name }}-tensorrt
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-tensorrt,mode=max
- name: Attest TensorRT (x86 GPU)
uses: actions/attest@v4
with:
subject-name: ${{ steps.setup.outputs.image-repo }}
subject-digest: ${{ fromJSON(steps.tensorrt.outputs.metadata)['tensorrt']['containerimage.digest'] }}
push-to-registry: true
create-storage-record: false
- name: AMD/ROCm general build
id: rocm
env:
HSA_OVERRIDE: 0
uses: docker/bake-action@v7
@@ -526,8 +566,20 @@ jobs:
rocm.tags=${{ steps.setup.outputs.image-name }}-rocm
*.cache-to=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm,mode=max
*.cache-from=type=registry,ref=${{ steps.setup.outputs.cache-name }}-rocm
- name: Attest AMD/ROCm general build
uses: actions/attest@v4
with:
subject-name: ${{ steps.setup.outputs.image-repo }}
subject-digest: ${{ fromJSON(steps.rocm.outputs.metadata)['rocm']['containerimage.digest'] }}
push-to-registry: true
create-storage-record: false
arm64_extra_builds:
runs-on: ubuntu-22.04-arm
permissions:
contents: read
packages: write
attestations: write
id-token: write
name: ARM Extra Build
needs:
- arm64_build
@@ -542,6 +594,7 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Rockchip build
id: rk
uses: docker/bake-action@v7
with:
source: .
@@ -551,8 +604,20 @@ jobs:
set: |
rk.tags=${{ steps.setup.outputs.image-name }}-rk
*.cache-from=type=gha
- name: Attest Rockchip build
uses: actions/attest@v4
with:
subject-name: ${{ steps.setup.outputs.image-repo }}
subject-digest: ${{ fromJSON(steps.rk.outputs.metadata)['rk']['containerimage.digest'] }}
push-to-registry: true
create-storage-record: false
synaptics_build:
runs-on: ubuntu-22.04-arm
permissions:
contents: read
packages: write
attestations: write
id-token: write
name: Synaptics Build
needs:
- arm64_build
@@ -567,6 +632,7 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Synaptics build
id: synaptics
uses: docker/bake-action@v7
with:
source: .
@@ -576,10 +642,22 @@ jobs:
set: |
synaptics.tags=${{ steps.setup.outputs.image-name }}-synaptics
*.cache-from=type=gha
- name: Attest Synaptics build
uses: actions/attest@v4
with:
subject-name: ${{ steps.setup.outputs.image-repo }}
subject-digest: ${{ fromJSON(steps.synaptics.outputs.metadata)['synaptics']['containerimage.digest'] }}
push-to-registry: true
create-storage-record: false
# The majority of users running arm64 are rpi users, so the rpi
# build should be the primary arm64 image
assemble_default_build:
runs-on: ubuntu-22.04
permissions:
contents: read
packages: write
attestations: write
id-token: write
name: Assemble and push default build
needs:
- amd64_build
@@ -590,7 +668,7 @@ jobs:
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -598,8 +676,16 @@ jobs:
- name: Create short sha
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
- uses: int128/docker-manifest-create-action@v2
id: manifest
with:
tags: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}
sources: |
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-amd64
ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}:${{ env.SHORT_SHA }}-rpi
- name: Attest default build
uses: actions/attest@v4
with:
subject-name: ghcr.io/${{ steps.lowercaseRepo.outputs.lowercase }}
subject-digest: ${{ steps.manifest.outputs.digest }}
push-to-registry: true
create-storage-record: false
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6
uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550
with:
registry: ghcr.io
username: ${{ github.actor }}
+1
View File
@@ -46,3 +46,4 @@ make -C /tmp/media-driver/build -j"$(nproc)"
# Install driver to rootfs for COPY --from
make -C /tmp/media-driver/build install DESTDIR=/rootfs
strip --strip-unneeded /rootfs/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so
-7
View File
@@ -4,7 +4,6 @@ set -euxo pipefail
NGINX_VERSION="1.27.4"
VOD_MODULE_VERSION="v1.9.1"
SECURE_TOKEN_MODULE_VERSION="1.5"
SET_MISC_MODULE_VERSION="v0.33"
NGX_DEVEL_KIT_VERSION="v0.3.3"
@@ -52,11 +51,6 @@ patch -d /tmp/nginx-vod-module/ -p1 << 'EOF'
EOF
mkdir /tmp/nginx-secure-token-module
wget https://github.com/kaltura/nginx-secure-token-module/archive/refs/tags/${SECURE_TOKEN_MODULE_VERSION}.tar.gz
tar -zxf ${SECURE_TOKEN_MODULE_VERSION}.tar.gz -C /tmp/nginx-secure-token-module --strip-components=1
rm ${SECURE_TOKEN_MODULE_VERSION}.tar.gz
mkdir /tmp/ngx_devel_kit
wget https://github.com/vision5/ngx_devel_kit/archive/refs/tags/${NGX_DEVEL_KIT_VERSION}.tar.gz
tar -zxf ${NGX_DEVEL_KIT_VERSION}.tar.gz -C /tmp/ngx_devel_kit --strip-components=1
@@ -80,7 +74,6 @@ cd /tmp/nginx
--add-module=../ngx_devel_kit \
--add-module=../nginx-set-misc-module \
--add-module=../nginx-vod-module \
--add-module=../nginx-secure-token-module \
--with-cc-opt="-O3 -Wno-error=implicit-fallthrough"
make CC="ccache gcc" -j$(nproc) && make install
+1 -1
View File
@@ -1,4 +1,4 @@
ruff == 0.17.3
ruff == 0.17.4
# types
types-peewee == 4.0.*
+5 -5
View File
@@ -7,18 +7,18 @@ starlette-context == 0.5.*
fastapi[standard-no-fastapi-cloud-cli] == 0.162.*
uvicorn == 0.54.*
slowapi == 0.1.*
joserfc == 1.7.*
joserfc == 1.8.*
cryptography == 50.0.*
pathvalidate == 3.3.*
markupsafe == 3.0.*
python-multipart == 0.0.32
python-multipart == 0.0.33
# Classification Model Training
tensorflow == 2.19.* ; platform_machine == 'aarch64'
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
# General
mypy == 2.4.1
onvif-zeep-async == 4.3.*
paho-mqtt == 2.1.*
paho-mqtt == 2.2.*
pandas == 3.0.*
peewee == 4.5.*
peewee_migrate == 2.3.*
@@ -47,9 +47,9 @@ onnxruntime == 1.31.*
# Embeddings
transformers == 5.22.*
# Generative AI
google-genai == 2.34.*
google-genai == 3.0.*
ollama == 0.6.*
openai == 3.41.*
openai == 3.46.*
# push notifications
py-vapid == 1.9.6
pywebpush == 2.5.*
@@ -14,11 +14,17 @@ fi
# Tell S6-Overlay not to restart this service
s6-svc -O .
# Runs as root with root's HOME and LD_LIBRARY_PATH; -I skips user site-packages
# The helper runs as root (-I skips user site-packages). The probe runs as the
# service user when the service drops privileges; a root probe relies on
# resolve_ffmpeg_path refusing runtime-user-writable paths.
function set_libva_version() {
local ffmpeg_path
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py)
LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
local probe_as=()
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then
probe_as=(s6-setuidgid frigate)
fi
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py frigate)
LIBAVFORMAT_VERSION_MAJOR=$("${probe_as[@]}" "$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
export LIBAVFORMAT_VERSION_MAJOR
}
@@ -57,10 +57,19 @@ function get_ip_and_port_from_supervisor() {
export FRIGATE_GO2RTC_WEBRTC_CANDIDATE_INTERNAL="${ip_address}:${webrtc_port}"
}
# The probe runs as the service user when the service drops privileges; a root
# probe relies on resolve_ffmpeg_path refusing runtime-user-writable paths
function set_libva_version() {
local ffmpeg_path
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py)
LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
local probe_as=()
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then
probe_as=(s6-setuidgid go2rtc)
fi
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py go2rtc)
if ! LIBAVFORMAT_VERSION_MAJOR=$("${probe_as[@]}" "$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+"); then
echo "[WARN] Could not read the libavformat version from ${ffmpeg_path} as the go2rtc user; using the bundled build's version"
LIBAVFORMAT_VERSION_MAJOR=$("${probe_as[@]}" "/usr/lib/ffmpeg/${DEFAULT_FFMPEG_VERSION}/bin/ffmpeg" -version | grep -Po "libavformat\W+\K\d+")
fi
export LIBAVFORMAT_VERSION_MAJOR
}
@@ -80,6 +80,11 @@ cp -r /usr/local/nginx/conf/. /tmp/nginx/conf/
set_worker_processes
# A non-root master ignores `user` and warns on every start
if [[ "$runs_as_root" -eq 0 ]]; then
sed -i '/^user root;$/d' /tmp/nginx/conf/nginx.conf
fi
# TLS certs: user-mounted certs at /etc/letsencrypt/live/frigate (documented
# contract) always win; otherwise fall back to a self-signed cert persisted in
# /config/tls, which stays writable under a read-only root filesystem.
+14 -1
View File
@@ -105,6 +105,8 @@ if [[ -n "$sentinel" ]]; then
mismatch_expr+=(-a ! -path "$sentinel")
fi
exec 3>&1
for path in "$@"; do
# An absent root is an incomplete sweep, not a finished one: /media/frigate
# is not in the image, so a boot before the volume is mounted would
@@ -121,7 +123,18 @@ for path in "$@"; do
# stale mount. Tolerate it rather than aborting under errexit, but never
# read a failed scan as "nothing to do": that would record the sweep as
# complete without having looked.
if ! count=$(find "$path" -name lost+found -prune -o "${mismatch_expr[@]}" -printf '.' 2>/dev/null | wc -c); then
# The walk prints nothing until it ends, so report every 30s through fd 3
# (stdout) while the count itself comes back through the substitution.
if ! count=$(find "$path" -name lost+found -prune -o "(" "${mismatch_expr[@]}" ")" -printf 'm\n' -o -printf '.\n' 2>/dev/null \
| awk -v path="$path" '
BEGIN { next_report = systime() + 30 }
$0 == "m" { mismatched++ }
systime() >= next_report {
printf "[INFO] fix-ownership: still scanning %s, %d entries checked, %d need changes\n", path, NR, mismatched > "/dev/fd/3"
fflush("/dev/fd/3")
next_report = systime() + 30
}
END { print mismatched + 0 }'); then
swept_clean=0
echo "[WARN] fix-ownership: could not scan ${path}; will retry on next boot"
continue
@@ -25,4 +25,5 @@ except FileNotFoundError:
config: dict[str, Any] = {}
path = config.get("ffmpeg", {}).get("path", "default")
print(resolve_ffmpeg_path(path, "ffmpeg"))
service = sys.argv[1] if len(sys.argv) > 1 else "frigate"
print(resolve_ffmpeg_path(path, "ffmpeg", service))
@@ -40,16 +40,10 @@ try:
except FileNotFoundError:
config: dict[str, Any] = {}
# No validator runs here, so install environment_vars ourselves. FRIGATE_
# names only: anything else lands in os.environ, where the exec gate reads
# GO2RTC_ALLOW_ARBITRARY_EXEC.
# No validator runs here, so install environment_vars ourselves.
config_env_vars = config.get("environment_vars")
apply_config_env_vars(
{
key: value
for key, value in config_env_vars.items()
if str(key).startswith("FRIGATE_")
}
{str(key): value for key, value in config_env_vars.items()}
if isinstance(config_env_vars, dict)
else {}
)
@@ -103,7 +97,7 @@ if not is_valid_ffmpeg_path(path):
print(
"[ERROR] ffmpeg path contains spaces, quotes or shell characters, using the default ffmpeg instead."
)
ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg")
ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg", "go2rtc")
for key in apply_go2rtc_ffmpeg_config(go2rtc_config, ffmpeg_path):
print(
@@ -2,7 +2,7 @@
# file, all other path directives follow --prefix and must stay absolute.
daemon off;
# ignored by a non-root master; keeps workers root under FRIGATE_RUN_AS_ROOT
# keeps workers root under FRIGATE_RUN_AS_ROOT; the run script strips it otherwise
user root;
worker_processes auto;
@@ -143,8 +143,14 @@ http {
# version 6 (RFC 8216 section 7); the module default is 4
vod_hls_version 6;
secure_token $args;
secure_token_types application/vnd.apple.mpegurl;
# Pass the playlist's query args (e.g. Home Assistant's authSig) on
# to every URL in it. These match the only URL endings vod writes
# with fmp4 and no subtitles.
sub_filter_types application/vnd.apple.mpegurl;
sub_filter_once off;
sub_filter '.m3u8' '.m3u8$is_args$args';
sub_filter '.m4s' '.m4s$is_args$args';
sub_filter '.mp4"' '.mp4$is_args$args"';
include security_headers.conf;
add_header Cache-Control "no-store";
@@ -240,7 +240,7 @@ logger:
# Optional: set environment variables
environment_vars:
EXAMPLE_VAR: value
LIBVA_DRIVER_NAME: i965
# Optional: birdseye configuration
# NOTE: Can (enabled, mode) be overridden at the camera level
+12 -3
View File
@@ -65,7 +65,16 @@ go2rtc:
This section sets environment variables in the Frigate process for those unable to modify the environment of the container, like within Home Assistant OS. It's meant for process settings such as `LIBVA_DRIVER_NAME` or the TensorFlow thread counts below. Docker users should set environment variables in their `docker run` command (`-e LIBVA_DRIVER_NAME=i965`) or `docker-compose.yml` file (`environment:` section) instead. Values set here are stored in plain text in your config file, so credentials belong in `secrets.yaml`, Docker environment variables, or Docker secrets instead.
Names prefixed with `FRIGATE_` set here also take part in `{FRIGATE_VARIABLE_NAME}` substitution (see [below](#substitution-sources-and-precedence)), but `secrets.yaml` is the better home for them.
Besides `FRIGATE_` names, only these are accepted, and a variable already set in the container environment keeps its container value:
- `LIBVA_DRIVER_NAME` and `HSA_OVERRIDE_GFX_VERSION`
- `TF_INTRA_OP_PARALLELISM_THREADS`, `TF_INTER_OP_PARALLELISM_THREADS`, `TF_DATASET_THREAD_POOL_SIZE`, and `SHM_MAX_FRAMES`
- `USE_FP16`, `HF_HUB_OFFLINE`, and `TRANSFORMERS_OFFLINE`
- `HF_ENDPOINT`, `GITHUB_ENDPOINT`, `GITHUB_RAW_ENDPOINT`, and `TF_KERAS_MOBILENET_V2_WEIGHTS_URL` (an `http://` or `https://` URL)
Any other name is ignored with a warning and must be set in the container environment.
Names prefixed with `FRIGATE_` set here take part in `{FRIGATE_VARIABLE_NAME}` substitution (see [below](#substitution-sources-and-precedence)), but `secrets.yaml` is the better home for them. Of these, only `FRIGATE_JWT_SECRET` is also set in the process environment.
<ConfigTabs>
<TabItem value="ui">
@@ -398,9 +407,9 @@ To do this:
2. Update the `ffmpeg.path` in your Frigate config to `/config/custom-ffmpeg`.
3. Restart Frigate and the custom version will be used if the steps above were done correctly.
Both binaries have to be executable by Frigate's unprivileged runtime user, so `chmod 755` them after extracting. The startup ownership sweep runs only once, so anything you add to `/config` later keeps whatever ownership and mode you gave it.
Both binaries have to be executable by Frigate's unprivileged runtime user, so `chmod 755` them after extracting. go2rtc runs as its own user, so the build must be world-executable for go2rtc to use it (`ffmpeg:` sources, the birdseye restream and transcoded live streams). The startup ownership sweep runs only once, so anything you add to `/config` later keeps whatever ownership and mode you gave it.
There is one exception, and it only affects [`FRIGATE_ROOT_SERVICES`](/configuration/non_root#keeping-individual-services-root) listing `frigate`. That mode runs Frigate as root while still handing `/config` to the unprivileged runtime user, so anything running as that user could swap the binary and gain root. A build inside any of Frigate's writable volumes (`/config`, `/media/frigate`, the cache and shm dirs) is ignored there and the bundled one is used, with a warning in the log. Keep the build somewhere root-owned (any absolute `ffmpeg.path` works, so a read-only bind mount such as `/opt/custom-ffmpeg` is enough) if you need both. The default mode and `FRIGATE_RUN_AS_ROOT=true` are unaffected and behave exactly as they always have.
There is one exception, and it only affects [`FRIGATE_ROOT_SERVICES`](/configuration/non_root#keeping-individual-services-root) listing `frigate` or `go2rtc`. That mode runs the listed service as root while still handing `/config` to the unprivileged runtime user, so anything running as that user could swap the binary and gain root. A build inside any of Frigate's writable volumes (`/config`, `/media/frigate`, the cache and shm dirs) is ignored by the root service, which uses the bundled one and logs a warning. A service that is not listed keeps using the custom build. Keep the build somewhere root-owned (any absolute `ffmpeg.path` works, so a read-only bind mount such as `/opt/custom-ffmpeg` is enough) if you need both. The default mode and `FRIGATE_RUN_AS_ROOT=true` are unaffected and behave exactly as they always have.
### Custom go2rtc version
+2 -1
View File
@@ -71,13 +71,14 @@ Both the script and the boot sweep report progress, so you can tell a slow sweep
```
[INFO] fix-ownership: scanning /media/frigate for ownership mismatches; this may take a while on large filesystems
[INFO] fix-ownership: still scanning /media/frigate, 2481000 entries checked, 2390112 need changes
[WARN] fix-ownership: adjusting ownership of 4823941 entries under /media/frigate
[INFO] fix-ownership: /media/frigate 5% (241197/4823941 entries)
[INFO] fix-ownership: /media/frigate 10% (482394/4823941 entries)
[INFO] fix-ownership: finished /media/frigate in 12m 4s
```
The scan has no percentage because the total isn't known until it finishes. Watch the boot sweep with `docker logs -f frigate`.
The scan has no percentage because the total isn't known until it finishes, so it reports a running count every 30 seconds instead. Watch the boot sweep with `docker logs -f frigate`.
Once the volumes are aligned, start Frigate normally. A file at `/config/.permissions_version` records what was done, so later boots skip the sweep unless you change `PUID`/`PGID`.
+11 -10
View File
@@ -413,17 +413,18 @@ Avoid removing audio from only one of the two streams (for example with `-an`).
As a general rule, features that read recordings prefer the main stream and fall back to the sub stream for time ranges where the main recordings have expired. Analytics features use only the main stream.
| Feature | Stream used |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| Recording playback (History and Review) | Both (main preferred with sub fallback by default), or exactly one stream when a quality is selected manually |
| Tracking details and Explore clip playback | Main, falling back to sub where the main recordings have expired |
| Feature | Stream used |
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Recording playback (History and Review) | Both (main preferred with sub fallback by default), or exactly one stream when a quality is selected manually |
| Tracking details and Explore clip playback | Main, falling back to sub where the main recordings have expired |
| Exports | Both (main preferred with sub fallback by default), or exactly one stream when a quality is selected in the export dialog |
| Clip downloads | Main; sub is used when no main recordings remain in the range (streams are never mixed in one file) |
| Frames grabbed from a recording in History (download snapshot, submit frame to Frigate+) | Main preferred, sub fallback |
| Audio extraction (e.g., transcription) | Main preferred, sub fallback |
| Motion search | Main only |
| Review timeline motion data | Main only |
| Storage usage statistics | Both streams counted, and listed separately per camera |
| Clip downloads | Main; sub is used when no main recordings remain in the range (streams are never mixed in one file) |
| Debug replay | Main; sub is used when no main recordings remain in the range (streams are never mixed in one file) |
| Frames grabbed from a recording in History (download snapshot, submit frame to Frigate+) | Main preferred, sub fallback |
| Audio extraction (e.g., transcription) | Main preferred, sub fallback |
| Motion search | Main only |
| Review timeline motion data | Main only |
| Storage usage statistics | Both streams counted, and listed separately per camera |
This table covers only features that read recordings from disk. Tracked object snapshots and thumbnails (the images shown in Explore and sent with notifications, and the images submitted to Frigate+ from a tracked object) are captured live from the `detect` stream as the object is tracked, never from recordings, so sub stream recording does not affect them.
+1 -1
View File
@@ -4,7 +4,7 @@ import type { Config, PluginConfig } from "@docusaurus/types";
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
// Bump when a new stable release ships
const STABLE_VERSION = "0.18";
const STABLE_VERSION = "0.19";
const config: Config = {
title: "Frigate",
+23 -1
View File
@@ -683,6 +683,15 @@
"title": "FeaturesSection",
"type": "object"
},
"FfmpegSource": {
"enum": [
"default",
"bundled",
"custom"
],
"title": "FfmpegSource",
"type": "string"
},
"FpsBucket": {
"enum": [
"le_5",
@@ -1017,6 +1026,17 @@
"description": "CPU architecture",
"x-public": true
},
"custom_go2rtc": {
"description": "Whether an executable /config/go2rtc build exists",
"title": "Custom Go2Rtc",
"type": "boolean",
"x-public": true
},
"ffmpeg": {
"$ref": "#/$defs/FfmpegSource",
"description": "Configured global ffmpeg.path: the default build, another bundled version, or a custom path",
"x-public": true
},
"image_variant": {
"$ref": "#/$defs/ImageVariant",
"description": "Published image the install runs",
@@ -1054,7 +1074,9 @@
"install_type",
"arch",
"kernel",
"run_as_root"
"run_as_root",
"custom_go2rtc",
"ffmpeg"
],
"title": "InstallSection",
"type": "object"
+19
View File
@@ -3148,6 +3148,25 @@ paths:
- frigateUserAuth: []
x-required-role: any
description: '**Access:** Any authenticated user.'
/plus/validate:
post:
tags:
- App
summary: Validate Plus Key
description: |-
**Access:** Admin role required.
Validate the configured Frigate+ API key with the Frigate+ server.
operationId: validate_plus_key_plus_validate_post
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
security:
- frigateAdminAuth: []
x-required-role: admin
/recognized_license_plates:
get:
tags:
+22 -1
View File
@@ -6,7 +6,15 @@ import re
from frigate.analytics.collectors.common import closed
from frigate.analytics.context import ReportContext
from frigate.analytics.schema import Arch, ImageVariant, InstallSection, InstallType
from frigate.analytics.schema import (
Arch,
FfmpegSource,
ImageVariant,
InstallSection,
InstallType,
)
from frigate.const import CONFIG_DIR, DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS
from frigate.util.config import is_valid_ffmpeg_path
from frigate.version import VERSION
KERNEL_PATTERN = re.compile(r"^(\d{1,3})\.(\d{1,3})")
@@ -54,6 +62,17 @@ def kernel(release: str) -> str:
return f"{match.group(1)}.{match.group(2)}" if match else "unknown"
def ffmpeg_source(path: str) -> FfmpegSource:
"""Classify the configured ffmpeg.path, applying resolve_ffmpeg_path's alias fallbacks."""
if path.startswith("/") and is_valid_ffmpeg_path(path):
return FfmpegSource.custom
if path != DEFAULT_FFMPEG_VERSION and path in INCLUDED_FFMPEG_VERSIONS:
return FfmpegSource.bundled
return FfmpegSource.default
def collect(ctx: ReportContext) -> InstallSection:
"""Describe the Frigate version, image, install method, and platform."""
return InstallSection(
@@ -63,4 +82,6 @@ def collect(ctx: ReportContext) -> InstallSection:
arch=arch(platform.machine()),
kernel=kernel(platform.release()),
run_as_root=os.geteuid() == 0,
custom_go2rtc=os.access(os.path.join(CONFIG_DIR, "go2rtc"), os.X_OK),
ffmpeg=ffmpeg_source(ctx.config.ffmpeg.path),
)
+10
View File
@@ -47,6 +47,12 @@ class Arch(StrEnum):
other = "other"
class FfmpegSource(StrEnum):
default = "default"
bundled = "bundled"
custom = "custom"
class GpuVendor(StrEnum):
intel = "intel"
amd = "amd"
@@ -201,6 +207,10 @@ class InstallSection(AnalyticsModel):
"Host kernel as major.minor", pattern=r"^(\d{1,3}\.\d{1,3}|unknown)$"
)
run_as_root: bool = metric("Whether the main process runs as root")
custom_go2rtc: bool = metric("Whether an executable /config/go2rtc build exists")
ffmpeg: FfmpegSource = metric(
"Configured global ffmpeg.path: the default build, another bundled version, or a custom path"
)
class GpuInfo(AnalyticsModel):
+12 -1
View File
@@ -54,7 +54,7 @@ from frigate.jobs.media_sync import (
start_media_sync_job,
)
from frigate.models import Event, Timeline
from frigate.plus import load_plus_model_info
from frigate.plus import PlusKeyStatus, load_plus_model_info
from frigate.stats.prometheus import get_metrics, update_metrics
from frigate.types import JobStatusTypesEnum
from frigate.util.builtin import (
@@ -627,6 +627,7 @@ def _restricted_go2rtc_response(
Go2rtcStreamIssue.invalid_placeholder,
"Invalid variable placeholder in go2rtc stream",
),
(Go2rtcStreamIssue.invalid_name, "Invalid go2rtc stream name"),
):
names = [name for name, found in issues.items() if found == issue]
if names:
@@ -1674,6 +1675,16 @@ def plusModels(request: Request, filterByCurrentModelDetector: bool = False):
return JSONResponse(content=validModels)
@router.post("/plus/validate", dependencies=[Depends(require_role(["admin"]))])
async def validate_plus_key(request: Request):
"""Validate the configured Frigate+ API key with the Frigate+ server."""
status = await request.app.frigate_config.plus_api.validate_key()
return JSONResponse(
content={"success": status == PlusKeyStatus.valid, "status": status.value}
)
@router.get(
"/recognized_license_plates", dependencies=[Depends(allow_any_authenticated())]
)
+14 -1
View File
@@ -39,6 +39,7 @@ from frigate.config import AuthConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
from frigate.notices import raise_notice
from frigate.util.live_streams import is_transcode_stream_name
from frigate.util.origin import origin_allowed
logger = logging.getLogger(__name__)
@@ -739,6 +740,13 @@ def auth(request: Request):
int(request.headers.get("x-server-port", default=0))
== request.app.auth_internal_port
):
# go2rtc runs the producer an unknown src describes for any caller
deny_status = deny_response_for_go2rtc_stream(
request.headers.get("x-original-url"), None, request
)
if deny_status is not None:
return Response("", status_code=deny_status)
success_response.headers["remote-user"] = "anonymous"
success_response.headers["remote-role"] = "admin"
return success_response
@@ -1275,7 +1283,12 @@ def _is_existing_go2rtc_stream(request: Request, stream_name: str) -> bool:
"""
frigate_config = request.app.frigate_config
if _get_stream_owner_cameras(request, stream_name):
# live.streams values are free-form config, so only names Frigate controls
# skip the lookup
if any(
stream_name == camera or is_transcode_stream_name(camera, stream_name)
for camera in frigate_config.cameras
):
return True
config_streams = getattr(frigate_config.go2rtc, "streams", None) or {}
+14 -22
View File
@@ -199,18 +199,14 @@ def events(
sub_label_clauses.append(Event.sub_label.is_null())
for label in filtered_sub_labels:
lowered = label.lower()
sub_label_clauses.append(
fn.LOWER(Event.sub_label.cast("text")) == lowered
) # include exact matches (case-insensitive)
# SQLite LOWER() only folds ASCII, so lower both sides in SQL to
# keep non-ASCII names matching exactly
stored = fn.LOWER(Event.sub_label.cast("text"))
sub_label_clauses.append(stored == fn.LOWER(label))
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
sub_label_clauses.append(
fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*"
)
sub_label_clauses.append(
fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*"
)
# include this label when part of a list
sub_label_clauses.append(stored % fn.LOWER(f"*{label},*"))
sub_label_clauses.append(stored % fn.LOWER(f"*, {label}*"))
sub_label_clause = reduce(operator.or_, sub_label_clauses)
clauses.append(sub_label_clause)
@@ -633,18 +629,14 @@ def events_search(
sub_label_clauses.append(Event.sub_label.is_null())
for label in filtered_sub_labels:
lowered = label.lower()
sub_label_clauses.append(
fn.LOWER(Event.sub_label.cast("text")) == lowered
) # include exact matches (case-insensitive)
# SQLite LOWER() only folds ASCII, so lower both sides in SQL to
# keep non-ASCII names matching exactly
stored = fn.LOWER(Event.sub_label.cast("text"))
sub_label_clauses.append(stored == fn.LOWER(label))
# include this label when part of a list (LIKE is case-insensitive in sqlite for ASCII)
sub_label_clauses.append(
fn.LOWER(Event.sub_label.cast("text")) % f"*{lowered},*"
)
sub_label_clauses.append(
fn.LOWER(Event.sub_label.cast("text")) % f"*, {lowered}*"
)
# include this label when part of a list
sub_label_clauses.append(stored % fn.LOWER(f"*{label},*"))
sub_label_clauses.append(stored % fn.LOWER(f"*, {label}*"))
event_filters.append(reduce(operator.or_, sub_label_clauses))
+6
View File
@@ -39,6 +39,7 @@ from frigate.util.image import create_mask
from frigate.util.live_streams import (
default_transcode_source,
is_transcode_stream_name,
is_valid_go2rtc_stream_name,
transcode_stream_name,
transcode_streams,
)
@@ -314,6 +315,11 @@ def apply_live_transcode_streams(
f"Camera {camera_config.name} has transcoded streams enabled, but its source {transcode.source} is not a go2rtc stream."
)
if not is_valid_go2rtc_stream_name(transcode.source):
raise ValueError(
f"Camera {camera_config.name} transcodes go2rtc stream {transcode.source}, whose name contains '#', whitespace or quotes."
)
generated = transcode_streams(
camera_config.name,
transcode.source,
+10
View File
@@ -84,3 +84,13 @@ class MqttConfig(FrigateBaseModel):
if (self.user is None) != (self.password is None):
raise ValueError("Password must be provided with username.")
return self
@model_validator(mode="after")
def tls_insecure_requires_tls(self) -> Self:
"""Reject TLS verification settings without TLS when MQTT is enabled."""
if self.enabled and self.tls_insecure is not None and self.tls_ca_certs is None:
raise ValueError(
"tls_insecure requires tls_ca_certs. Remove tls_insecure for plaintext "
"MQTT, or set tls_ca_certs to a CA certificate path to enable TLS."
)
return self
@@ -68,6 +68,9 @@ class ReviewDescriptionProcessor(PostProcessorApi):
self.review_desc_dps = EventsPerSecond()
self.review_desc_dps.start()
# one review's ffmpeg extraction at a time, as on the maintainer loop
self._frame_extraction_lock = threading.Lock()
def calculate_frame_count(
self,
camera: str,
@@ -179,53 +182,72 @@ class ReviewDescriptionProcessor(PostProcessorApi):
):
return
image_source = camera_config.review.genai.image_source
frame_mode = camera_config.review.genai.frame_mode
threading.Thread(
target=self._describe_finished_review,
name=f"review_description_{id}",
daemon=True,
args=(
camera_config,
final_data,
camera_config.review.genai.image_source,
),
).start()
if image_source == ImageSourceEnum.recordings:
buffer_extension = get_recording_buffer_extension(
final_data["end_time"] - final_data["start_time"]
)
final_data["start_time"] -= buffer_extension
final_data["end_time"] += buffer_extension
def _describe_finished_review(
self,
camera_config: CameraConfig,
final_data: dict[str, Any],
image_source: ImageSourceEnum,
preview_fallback: bool = True,
named_objects: list[tuple[str, str]] | None = None,
) -> None:
"""Gather frames for a finished review item and start its description.
Runs off the maintainer loop since recording frames shell out to ffmpeg
once per frame.
"""
camera = final_data["camera"]
id = final_data["id"]
genai_config = camera_config.review.genai
frames: list[tuple[bytes, float]] = []
if image_source == ImageSourceEnum.recordings:
buffer_extension = get_recording_buffer_extension(
final_data["end_time"] - final_data["start_time"]
)
with self._frame_extraction_lock:
frames = self.get_recording_frames(
camera,
final_data["start_time"],
final_data["end_time"],
final_data["start_time"] - buffer_extension,
final_data["end_time"] + buffer_extension,
height=480, # Use 480p for good balance between quality and token usage
frame_mode=frame_mode,
frame_mode=genai_config.frame_mode,
)
if not frames:
# Fallback to preview frames if no recordings available
logger.warning(
f"No recording frames found for {camera}, falling back to preview frames"
)
frames = self.get_preview_frames_as_bytes(
camera,
final_data["start_time"],
final_data["end_time"],
final_data["thumb_path"],
id,
camera_config.review.genai.debug_save_thumbnails,
frame_mode,
)
elif camera_config.review.genai.debug_save_thumbnails:
if frames:
if genai_config.debug_save_thumbnails:
self.save_debug_recording_frames(id, frames)
elif not preview_fallback:
logger.error("No recording frames are available for review item %s", id)
return
else:
# Use preview frames
frames = self.get_preview_frames_as_bytes(
logger.warning(
"No recording frames found for %s, falling back to preview frames",
camera,
final_data["start_time"],
final_data["end_time"],
final_data["thumb_path"],
id,
camera_config.review.genai.debug_save_thumbnails,
frame_mode,
)
self.start_analysis(camera_config, final_data, frames)
if not frames:
frames = self.get_preview_frames_as_bytes(
camera,
final_data["start_time"],
final_data["end_time"],
final_data["thumb_path"],
id,
genai_config.debug_save_thumbnails,
genai_config.frame_mode,
)
self.start_analysis(camera_config, final_data, frames, named_objects)
def handle_request(self, topic: str, request_data: dict[str, Any]) -> str | None:
if topic == EmbeddingsRequestEnum.regenerate_review_description.value:
@@ -419,31 +441,12 @@ class ReviewDescriptionProcessor(PostProcessorApi):
logger.error("Review item %s has not ended yet", review_id)
return
buffer_extension = get_recording_buffer_extension(
final_data["end_time"] - final_data["start_time"]
)
frames = self.get_recording_frames(
str(review.camera),
final_data["start_time"] - buffer_extension,
final_data["end_time"] + buffer_extension,
height=480,
frame_mode=camera_config.review.genai.frame_mode,
)
if not frames:
logger.error(
"No recording frames are available for review item %s", review_id
)
return
if camera_config.review.genai.debug_save_thumbnails:
self.save_debug_recording_frames(review_id, frames)
self.start_analysis(
self._describe_finished_review(
camera_config,
final_data,
frames,
get_named_objects(
ImageSourceEnum.recordings,
preview_fallback=False,
named_objects=get_named_objects(
final_data["data"].get("detections") or [],
self.config.all_attributes,
),
+29 -29
View File
@@ -16,18 +16,21 @@ from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Optional, cast
from peewee import ModelSelect
from frigate.config import FrigateConfig
from frigate.config.camera.updater import CameraConfigUpdatePublisher
from frigate.const import REPLAY_CAMERA_PREFIX, REPLAY_DIR
from frigate.const import (
REPLAY_CAMERA_PREFIX,
REPLAY_DIR,
STREAM_TYPE_MAIN,
STREAM_TYPE_SUB,
)
from frigate.jobs.export import JobStatePublisher
from frigate.jobs.job import Job
from frigate.jobs.manager import job_is_running, set_current_job
from frigate.models import Export, Recordings
from frigate.models import Export
from frigate.types import JobStatusTypesEnum
from frigate.util.ffmpeg import run_ffmpeg_with_progress
from frigate.util.recording_coverage import recording_overlap
from frigate.util.recording_coverage import resolve_coverage
if TYPE_CHECKING:
from frigate.debug_replay import DebugReplayManager
@@ -94,24 +97,6 @@ class DebugReplayJob(Job):
}
def query_recordings(source_camera: str, start_ts: float, end_ts: float) -> ModelSelect:
"""Return the Recordings query for the time range.
Module-level so tests can patch it without instantiating a runner.
"""
query = (
Recordings.select(
Recordings.path,
Recordings.start_time,
Recordings.end_time,
)
.where(recording_overlap(start_ts, end_ts))
.where(Recordings.camera == source_camera)
.order_by(Recordings.start_time.asc())
)
return cast(ModelSelect, query)
class NoRecordingsError(ValueError):
"""Raised when no recordings exist in the requested time range."""
@@ -156,7 +141,8 @@ class RecordingDebugReplaySource(DebugReplaySource):
Feeds ffmpeg the internal VOD endpoint so segments with mismatched
SPS/PPS (e.g. across day/night transitions) stitch cleanly via HLS
discontinuities.
discontinuities. The playlist is pinned to one stream because the
stream-copied clip cannot change codec or resolution mid-file.
"""
def __init__(
@@ -170,6 +156,7 @@ class RecordingDebugReplaySource(DebugReplaySource):
self._start_ts = start_ts
self._end_ts = end_ts
self._internal_port = internal_port
self._stream_type = STREAM_TYPE_MAIN
@property
def source_camera(self) -> str:
@@ -187,15 +174,28 @@ class RecordingDebugReplaySource(DebugReplaySource):
if self._end_ts <= self._start_ts:
raise ValueError("End time must be after start time")
if not query_recordings(self._camera, self._start_ts, self._end_ts).count():
raise NoRecordingsError(
f"No recordings found for camera '{self._camera}' in the specified time range"
)
# the VOD resolver decides what the pinned playlist serves, so it
# also decides the stream; a row merely touching the range has no
# interval and must not win
intervals = resolve_coverage(self._camera, self._start_ts, self._end_ts)
if any(interval.main for interval in intervals):
self._stream_type = STREAM_TYPE_MAIN
return
if any(interval.sub for interval in intervals):
self._stream_type = STREAM_TYPE_SUB
return
raise NoRecordingsError(
f"No recordings found for camera '{self._camera}' in the specified time range"
)
def ffmpeg_input_args(self, working_dir: str) -> list[str]:
playlist_url = (
f"http://127.0.0.1:{self._internal_port}/vod/{self._camera}"
f"/start/{self._start_ts}/end/{self._end_ts}/index.m3u8"
f"/{self._stream_type}/start/{self._start_ts}/end/{self._end_ts}"
"/index.m3u8"
)
return [
"-protocol_whitelist",
+81 -1
View File
@@ -3,9 +3,11 @@ import json
import logging
import os
import re
from enum import Enum
from pathlib import Path
from typing import Any
from typing import Any, TypeGuard
import aiohttp
import cv2
import requests
from numpy import ndarray
@@ -17,6 +19,17 @@ from frigate.const import MODEL_CACHE_DIR, PLUS_API_HOST, PLUS_ENV_VAR
logger = logging.getLogger(__name__)
PLUS_VALIDATE_TIMEOUT = 10
class PlusKeyStatus(str, Enum):
"""Result of validating a Frigate+ API key with the server."""
valid = "valid"
invalid = "invalid"
unreachable = "unreachable"
not_set = "not_set"
def add_hailo_alias(model_info: dict[str, Any]) -> dict[str, Any]:
"""Name the hailo detector by its current key as well as its old one.
@@ -72,6 +85,19 @@ def get_jpg_bytes(image: ndarray, max_dim: int, quality: int) -> bytes:
return jpg_bytes if isinstance(jpg_bytes, bytes) else b""
def _is_usable_token(token_data: Any) -> TypeGuard[dict[str, Any]]:
"""Whether a Frigate+ token response has a usable token and expiry."""
if not isinstance(token_data, dict):
return False
expires = token_data.get("expires")
return (
isinstance(token_data.get("accessToken"), str)
and isinstance(expires, (int, float))
and not isinstance(expires, bool)
)
class PlusApi:
def __init__(self) -> None:
self.host = PLUS_API_HOST
@@ -153,6 +179,60 @@ class PlusApi:
def is_active(self) -> bool:
return self._is_active
async def validate_key(self) -> PlusKeyStatus:
"""Check the configured API key against the Frigate+ server.
The key format is checked at startup, but only the server can tell
whether the key is actually valid. This requests a fresh token so the
result reflects the key's current state rather than a cached token.
Returns:
The key's status as reported by the Frigate+ server
"""
if self.key is None:
return PlusKeyStatus.not_set
key_id, _, secret = self.key.partition(":")
# Use a single request with a timeout instead of the retrying session,
# since this is user initiated and should fail fast when unreachable.
# trust_env honors proxy settings, matching the other Frigate+ requests.
try:
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=PLUS_VALIDATE_TIMEOUT),
trust_env=True,
) as session:
async with session.get(
f"{self.host}/v1/auth/token",
auth=aiohttp.BasicAuth(key_id, secret),
) as r:
status = r.status
token_data = await r.json(content_type=None) if r.ok else None
except (aiohttp.ClientError, TimeoutError) as err:
logger.warning("Unable to reach Frigate+ to validate API key: %s", err)
return PlusKeyStatus.unreachable
except ValueError:
logger.warning("Invalid response validating Frigate+ API key")
return PlusKeyStatus.unreachable
if status in (401, 403):
return PlusKeyStatus.invalid
if status >= 400:
logger.warning(
"Unexpected response validating Frigate+ API key: %s", status
)
return PlusKeyStatus.unreachable
# Keep the cached token unless the reply is a usable one, so an
# unexpected response can't break later Frigate+ requests
if not _is_usable_token(token_data):
logger.warning("Invalid response validating Frigate+ API key")
return PlusKeyStatus.unreachable
self._token_data = token_data
return PlusKeyStatus.valid
def upload_image(self, image: ndarray, camera: str) -> str:
r = self._get("image/signed_urls")
if not r.ok:
+40 -51
View File
@@ -585,26 +585,36 @@ class PtzAutoTracker(threading.Thread):
def _predict_movement_time(self, camera, pan, tilt):
combined_movement = abs(pan) + abs(tilt)
input_data = np.array([self.intercept[camera], combined_movement])
input_data = np.array([1, combined_movement])
return np.dot(self.move_coefficients[camera], input_data)
def _predict_area_after_time(self, camera, time):
return np.dot(
self.tracked_object_metrics[camera]["area_coefficients"],
[self.tracked_object_history[camera][-1]["frame_time"] + time],
)
def _predict_target_box(self, camera, predicted_time):
target_box = self.tracked_object_metrics[camera]["target_box"]
tom = self.tracked_object_metrics[camera]
target_box = tom["target_box"]
if not predicted_time:
return target_box
frame_shape = self.config.cameras[camera].frame_shape
camera_config = self.config.cameras[camera]
left, top, right, bottom = self.tracked_object_history[camera][-1]["box"]
size = max(right - left, bottom - top)
return target_box + self._predict_area_after_time(camera, predicted_time) / (
frame_shape[0] * frame_shape[1]
if size <= 0:
return target_box
# the corner velocities diverge as the object approaches the camera
frames = camera_config.detect.fps * predicted_time
velocity = tom["velocity"]
predicted_size = max(
right - left + frames * (velocity[2] - velocity[0]),
bottom - top + frames * (velocity[3] - velocity[1]),
1,
)
# target_box is the squared size raised to the zoom factor
return target_box * (predicted_size / size) ** (
2 * camera_config.onvif.autotracking.zoom_factor
)
def _calculate_tracked_object_metrics(self, camera, obj):
@@ -660,26 +670,6 @@ class PtzAutoTracker(threading.Thread):
filtered_areas = remove_outliers(areas) if len(areas) > 3 else areas
# Filter entries that are not touching the frame edge
filtered_areas_not_touching_edge = [
entry
for entry in filtered_areas
if self._touching_frame_edges(camera, entry["box"]) == 0
]
# Calculate regression for area change predictions
if len(filtered_areas_not_touching_edge):
X = np.array(
[item["frame_time"] for item in filtered_areas_not_touching_edge]
)
y = np.array([item["area"] for item in filtered_areas_not_touching_edge])
tom["area_coefficients"] = np.linalg.lstsq(X.reshape(-1, 1), y, rcond=None)[
0
]
else:
tom["area_coefficients"] = np.array([0])
weights = np.arange(1, len(filtered_areas) + 1)
weighted_area = np.average(
[item["area"] for item in filtered_areas], weights=weights
@@ -744,6 +734,8 @@ class PtzAutoTracker(threading.Thread):
continue
else:
pan_tilt_times = None
if (
camera_config.onvif.autotracking.zooming
== ZoomingModeEnum.relative
@@ -755,17 +747,28 @@ class PtzAutoTracker(threading.Thread):
await self._wait_until_stopped(camera, metrics)
# _zoom_absolute resets the metrics timestamps
pan_tilt_times = (
metrics.start_time.value,
metrics.stop_time.value,
)
if zoom > 0 and metrics.zoom_level.value != zoom:
await self.onvif._zoom_absolute(camera, zoom, 1)
await self._wait_until_stopped(camera, metrics)
start_time, stop_time = pan_tilt_times or (
metrics.start_time.value,
metrics.stop_time.value,
)
if camera_config.onvif.autotracking.movement_weights:
logger.debug(
f"{camera}: Predicted movement time: {self._predict_movement_time(camera, pan, tilt)}"
)
logger.debug(
f"{camera}: Actual movement time: {metrics.stop_time.value - metrics.start_time.value}"
f"{camera}: Actual movement time: {stop_time - start_time}"
)
# save metrics for better estimate calculations
@@ -781,8 +784,8 @@ class PtzAutoTracker(threading.Thread):
{
"pan": pan,
"tilt": tilt,
"start_timestamp": metrics.start_time.value,
"end_timestamp": metrics.stop_time.value,
"start_timestamp": start_time,
"end_timestamp": stop_time,
}
)
@@ -845,7 +848,6 @@ class PtzAutoTracker(threading.Thread):
# Thresholds
x_mags_thresh = camera_width / camera_fps / 2
y_mags_thresh = camera_height / camera_fps / 2
dir_thresh = 0.93
delta_thresh = 20
var_thresh = 10
@@ -863,24 +865,12 @@ class PtzAutoTracker(threading.Thread):
stdev_list = np.std(velocities, axis=0)
high_variances = np.any(stdev_list > var_thresh)
# Check direction difference
# the corners of an object moving toward or away from the camera move
# in different directions, so the delta bounds how far they may differ
velocities = np.round(velocities)
invalid_dirs = False
if np.all(np.linalg.norm(velocities, axis=1)):
cosine_sim = np.dot(velocities[0], velocities[1]) / (
np.linalg.norm(velocities[0]) * np.linalg.norm(velocities[1])
)
dir_thresh = 0.6 if np.all(delta < delta_thresh / 2) else dir_thresh
invalid_dirs = cosine_sim < dir_thresh
# Combine
invalid = (
invalid_x_mags
or invalid_y_mags
or invalid_dirs
or invalid_delta
or high_variances
)
invalid = invalid_x_mags or invalid_y_mags or invalid_delta or high_variances
if invalid:
logger.debug(
@@ -891,7 +881,6 @@ class PtzAutoTracker(threading.Thread):
for var_name, is_invalid in [
("invalid_x_mags", invalid_x_mags),
("invalid_y_mags", invalid_y_mags),
("invalid_dirs", invalid_dirs),
("invalid_delta", invalid_delta),
("high_variances", high_variances),
]
@@ -5,6 +5,7 @@ import tempfile
import unittest
from unittest.mock import MagicMock, Mock, patch
import requests
import ruamel.yaml
from fastapi import Request
@@ -234,6 +235,72 @@ class TestAuthInternalPort(BaseTestHttp):
finally:
os.unlink(config_path)
@patch(
"frigate.api.auth.requests.get",
side_effect=requests.ConnectionError("go2rtc unavailable"),
)
def test_internal_port_rejects_unknown_go2rtc_src(self, _mock_get):
app = self._create_app()
with AuthTestClient(app) as client:
for url in (
"/live/mse/api/ws?src=exec:id",
"/live/webrtc/api/ws?src=ffmpeg:x%23raw=-f",
"/api/go2rtc/webrtc?src=front_door&src=unknown",
):
resp = client.get(
"/auth",
headers={"x-server-port": "5000", "x-original-url": url},
)
self.assertEqual(resp.status_code, 403, url)
def test_internal_port_allows_existing_go2rtc_stream(self):
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.get(
"/auth",
headers={
"x-server-port": "5000",
"x-original-url": "/live/mse/api/ws?src=front_door",
},
)
self.assertEqual(resp.status_code, 202)
self.assertEqual(resp.headers["remote-role"], "admin")
def test_internal_port_skips_go2rtc_check_for_other_paths(self):
app = self._create_app()
with AuthTestClient(app) as client:
for url in ("/api/stats", "/api/go2rtc/streams?src=unknown"):
resp = client.get(
"/auth",
headers={"x-server-port": "5000", "x-original-url": url},
)
self.assertEqual(resp.status_code, 202, url)
@patch(
"frigate.api.auth.requests.get",
side_effect=requests.ConnectionError("go2rtc unavailable"),
)
def test_live_stream_value_is_not_trusted_as_a_go2rtc_stream(self, _mock_get):
src = "ffmpeg:x#raw=-f"
self.minimal_config["cameras"]["front_door"]["live"] = {
"streams": {"Main": src}
}
app = self._create_app()
with AuthTestClient(app) as client:
resp = client.get(
"/auth",
headers={
"x-server-port": "5000",
"x-original-url": "/live/mse/api/ws?src=ffmpeg:x%23raw=-f",
},
)
self.assertEqual(resp.status_code, 403)
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -151,6 +151,19 @@ class TestConfigGo2rtcStreamRestrictions(BaseTestHttp):
assert resp.status_code == 200, resp.text
assert _EXISTING_SRC in self._read_config()
def test_set_rejects_stream_name_with_ffmpeg_params(self):
before = self._read_config()
resp = self._set_streams(
{"x#raw=-f#raw=mpegts#raw=/tmp/x": "rtsp://10.0.0.2:554/video"}
)
assert resp.status_code == 400
assert "Invalid go2rtc stream name" in resp.json()["message"]
assert self._read_config() == before
def test_set_allows_plain_stream_names(self):
resp = self._set_streams({"back-yard.main_2": "rtsp://10.0.0.2:554/video"})
assert resp.status_code == 200, resp.text
def test_set_allows_injected_source_when_override_set(self):
with patch("frigate.util.services._GO2RTC_ARBITRARY_EXEC_ENV", "true"):
resp = self._set_streams({"new_stream": [_INJECTED_SRC]})
+34
View File
@@ -485,6 +485,40 @@ class TestHttpApp(BaseTestHttp):
assert sub_labels
assert sub_labels == [sub_label]
def test_get_event_list_filter_sub_labels(self):
sub_labels = {
"1.cyrillic": "Кузнецова Ира",
"2.joined": "Ира, Bob",
"3.ascii": "Alice",
"4.none": None,
}
mock_embeddings = Mock()
mock_embeddings.search_thumbnail.return_value = [(id, 0.1) for id in sub_labels]
self.app.frigate_config.semantic_search.enabled = True
self.app.embeddings = mock_embeddings
with AuthTestClient(self.app) as client:
for id, sub_label in sub_labels.items():
super().insert_mock_event(id)
Event.update(sub_label=sub_label).where(Event.id == id).execute()
search_params = {"search_type": "similarity", "event_id": "1.cyrillic"}
for path, params in (("/events", {}), ("/events/search", search_params)):
def ids(query: str) -> set[str]:
events = client.get(
path, params={**params, "sub_labels": query}
).json()
return {event["id"] for event in events}
assert ids("Кузнецова Ира") == {"1.cyrillic"}
assert ids("Ира") == {"2.joined"}
assert ids("bob") == {"2.joined"}
assert ids("ALICE") == {"3.ascii"}
assert ids("None") == {"4.none"}
####################################################################################################################
################################### GET /metrics Endpoint #########################################################
####################################################################################################################
+51
View File
@@ -0,0 +1,51 @@
"""Tests for the Frigate+ API endpoints."""
import unittest
from unittest.mock import patch
from frigate.models import Event
from frigate.plus import PlusKeyStatus
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
class TestHttpPlusValidate(BaseTestHttp):
def setUp(self):
super().setUp([Event])
def _validate(self, status: PlusKeyStatus) -> dict:
app = self.create_app()
with patch.object(
app.frigate_config.plus_api, "validate_key", return_value=status
):
with AuthTestClient(app) as client:
response = client.post("/plus/validate")
self.assertEqual(response.status_code, 200)
return response.json()
def test_a_valid_key(self):
self.assertEqual(
self._validate(PlusKeyStatus.valid), {"success": True, "status": "valid"}
)
def test_an_invalid_key(self):
self.assertEqual(
self._validate(PlusKeyStatus.invalid),
{"success": False, "status": "invalid"},
)
def test_viewers_cannot_validate(self):
app = self.create_app()
with AuthTestClient(app) as client:
response = client.post(
"/plus/validate",
headers={"remote-user": "viewer", "remote-role": "viewer"},
)
self.assertEqual(response.status_code, 403)
if __name__ == "__main__":
unittest.main(verbosity=2)
@@ -12,6 +12,7 @@ from frigate.analytics.collectors.common import closed, histogram, rate
from frigate.analytics.schema import (
Arch,
DecodeFamily,
FfmpegSource,
GpuVendor,
HardwareKey,
ImageVariant,
@@ -56,6 +57,36 @@ class TestInstallCollector(unittest.TestCase):
self.assertEqual(section.arch, Arch.aarch64)
self.assertEqual(section.kernel, "6.8")
self.assertFalse(section.run_as_root)
self.assertEqual(section.ffmpeg, FfmpegSource.default)
def test_ffmpeg_source_follows_path_resolution(self):
with (
patch.object(install, "DEFAULT_FFMPEG_VERSION", "8.0"),
patch.object(install, "INCLUDED_FFMPEG_VERSIONS", ["8.0", "7.0"]),
):
self.assertEqual(install.ffmpeg_source("default"), FfmpegSource.default)
self.assertEqual(install.ffmpeg_source("8.0"), FfmpegSource.default)
self.assertEqual(install.ffmpeg_source("4.4"), FfmpegSource.default)
self.assertEqual(install.ffmpeg_source("7.0"), FfmpegSource.bundled)
self.assertEqual(
install.ffmpeg_source("/config/custom-ffmpeg"), FfmpegSource.custom
)
self.assertEqual(
install.ffmpeg_source("/config/a b; rm"), FfmpegSource.default
)
def test_custom_go2rtc_needs_an_executable_binary(self):
with tempfile.TemporaryDirectory() as config_dir:
binary = os.path.join(config_dir, "go2rtc")
with patch.object(install, "CONFIG_DIR", config_dir):
self.assertFalse(install.collect(make_context()).custom_go2rtc)
open(binary, "w").close()
self.assertFalse(install.collect(make_context()).custom_go2rtc)
os.chmod(binary, 0o755)
self.assertTrue(install.collect(make_context()).custom_go2rtc)
def test_image_variant_is_dev_when_unset_and_other_when_unknown(self):
self.assertEqual(install.image_variant(None), ImageVariant.dev)
+2
View File
@@ -25,6 +25,8 @@ INSTALL = InstallSection(
arch="x86_64",
kernel="6.8",
run_as_root=False,
custom_go2rtc=False,
ffmpeg="default",
)
+37
View File
@@ -1,5 +1,6 @@
"""Tests for migrating detectors and model into the models list."""
import errno
import logging
import os
import tempfile
@@ -240,6 +241,42 @@ class TestMigrateConfigFile(unittest.TestCase):
with open(self.config_file) as f:
return YAML().load(f)
def test_a_failed_write_leaves_the_original_config(self):
original = "mqtt:\n enabled: false\ncameras: {}\n"
with open(self.config_file, "w") as f:
f.write(original)
with (
patch.object(YAML, "dump", side_effect=RuntimeError),
self.assertRaises(RuntimeError),
):
migrate_frigate_config(self.config_file)
with open(self.config_file) as f:
self.assertEqual(f.read(), original)
def test_a_file_bind_mount_is_written_in_place(self):
with patch(
"frigate.util.config.os.replace",
side_effect=OSError(errno.EBUSY, "Device or resource busy"),
):
migrated = self._migrate("mqtt:\n enabled: false\ncameras: {}\n")
self.assertEqual(migrated["version"], CURRENT_CONFIG_VERSION)
self.assertFalse(os.path.exists(f"{self.config_file}.tmp"))
def test_a_symlinked_config_migrates_its_target(self):
target = os.path.join(self.temp_dir.name, "frigate.yml")
with open(target, "w") as f:
f.write("mqtt:\n enabled: false\ncameras: {}\n")
os.symlink(target, self.config_file)
migrate_frigate_config(self.config_file)
self.assertTrue(os.path.islink(self.config_file))
with open(target) as f:
self.assertEqual(YAML().load(f)["version"], CURRENT_CONFIG_VERSION)
def test_migrates_a_config_already_stamped_with_the_current_version(self):
# 0.19 is unreleased, so a dev config can be current and still use
# the pre-models keys
+41 -25
View File
@@ -18,6 +18,7 @@ from frigate.jobs.debug_replay import (
from frigate.jobs.export import JobStatePublisher
from frigate.jobs.manager import _completed_jobs, _current_jobs
from frigate.types import JobStatusTypesEnum
from frigate.util.recording_coverage import CoverageInterval
def _reset_job_manager() -> None:
@@ -77,6 +78,31 @@ class TestDebugReplayJob(unittest.TestCase):
self.assertEqual(results["end_ts"], 200.0)
class TestRecordingDebugReplaySource(unittest.TestCase):
def _source_url(self, intervals: list[CoverageInterval]) -> str:
source = RecordingDebugReplaySource(
source_camera="front", start_ts=100.0, end_ts=200.0, internal_port=5000
)
with patch(
"frigate.jobs.debug_replay.resolve_coverage", return_value=intervals
):
source.validate()
return source.ffmpeg_input_args("/tmp")[-1]
def test_pins_main_when_main_has_recordings(self) -> None:
url = self._source_url(
[
CoverageInterval(100.0, 150.0, None, MagicMock()),
CoverageInterval(150.0, 200.0, MagicMock(), MagicMock()),
]
)
self.assertIn("/vod/front/main/start/100.0/end/200.0/", url)
def test_falls_back_to_sub_without_main_recordings(self) -> None:
url = self._source_url([CoverageInterval(100.0, 200.0, None, MagicMock())])
self.assertIn("/vod/front/sub/start/100.0/end/200.0/", url)
class TestStartDebugReplayJob(unittest.TestCase):
def setUp(self) -> None:
_reset_job_manager()
@@ -87,9 +113,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
self.publisher = MagicMock()
self.recordings_qs = MagicMock()
self.recordings_qs.count.return_value = 1
self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
self.coverage = [CoverageInterval(100.0, 200.0, MagicMock(), None)]
def tearDown(self) -> None:
runner = get_active_runner()
@@ -127,9 +151,7 @@ class TestStartDebugReplayJob(unittest.TestCase):
)
def test_rejects_when_no_recordings(self) -> None:
empty_qs = MagicMock()
empty_qs.count.return_value = 0
with patch("frigate.jobs.debug_replay.query_recordings", return_value=empty_qs):
with patch("frigate.jobs.debug_replay.resolve_coverage", return_value=[]):
with self.assertRaises(NoRecordingsError):
start_debug_replay_job(
source=RecordingDebugReplaySource(
@@ -152,8 +174,8 @@ class TestStartDebugReplayJob(unittest.TestCase):
with (
patch(
"frigate.jobs.debug_replay.query_recordings",
return_value=self.recordings_qs,
"frigate.jobs.debug_replay.resolve_coverage",
return_value=self.coverage,
),
patch(
"frigate.jobs.debug_replay.run_ffmpeg_with_progress",
@@ -192,8 +214,8 @@ class TestStartDebugReplayJob(unittest.TestCase):
with (
patch(
"frigate.jobs.debug_replay.query_recordings",
return_value=self.recordings_qs,
"frigate.jobs.debug_replay.resolve_coverage",
return_value=self.coverage,
),
patch(
"frigate.jobs.debug_replay.run_ffmpeg_with_progress",
@@ -242,9 +264,7 @@ class TestRunnerHappyPath(unittest.TestCase):
self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
self.publisher = MagicMock()
self.recordings_qs = MagicMock()
self.recordings_qs.count.return_value = 1
self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
self.coverage = [CoverageInterval(100.0, 200.0, MagicMock(), None)]
def tearDown(self) -> None:
runner = get_active_runner()
@@ -272,8 +292,8 @@ class TestRunnerHappyPath(unittest.TestCase):
with (
patch(
"frigate.jobs.debug_replay.query_recordings",
return_value=self.recordings_qs,
"frigate.jobs.debug_replay.resolve_coverage",
return_value=self.coverage,
),
patch(
"frigate.jobs.debug_replay.run_ffmpeg_with_progress",
@@ -325,9 +345,7 @@ class TestRunnerFailurePath(unittest.TestCase):
self.frigate_config.cameras = {"front": MagicMock()}
self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
self.publisher = MagicMock()
self.recordings_qs = MagicMock()
self.recordings_qs.count.return_value = 1
self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
self.coverage = [CoverageInterval(100.0, 200.0, MagicMock(), None)]
def tearDown(self) -> None:
runner = get_active_runner()
@@ -350,8 +368,8 @@ class TestRunnerFailurePath(unittest.TestCase):
with (
patch(
"frigate.jobs.debug_replay.query_recordings",
return_value=self.recordings_qs,
"frigate.jobs.debug_replay.resolve_coverage",
return_value=self.coverage,
),
patch(
"frigate.jobs.debug_replay.run_ffmpeg_with_progress",
@@ -399,9 +417,7 @@ class TestRunnerCancellation(unittest.TestCase):
self.frigate_config.cameras = {"front": MagicMock()}
self.frigate_config.ffmpeg.ffmpeg_path = "/bin/true"
self.publisher = MagicMock()
self.recordings_qs = MagicMock()
self.recordings_qs.count.return_value = 1
self.recordings_qs.__iter__.return_value = iter([MagicMock(path="/tmp/r1.mp4")])
self.coverage = [CoverageInterval(100.0, 200.0, MagicMock(), None)]
def tearDown(self) -> None:
runner = get_active_runner()
@@ -431,8 +447,8 @@ class TestRunnerCancellation(unittest.TestCase):
with (
patch(
"frigate.jobs.debug_replay.query_recordings",
return_value=self.recordings_qs,
"frigate.jobs.debug_replay.resolve_coverage",
return_value=self.coverage,
),
patch(
"frigate.jobs.debug_replay.run_ffmpeg_with_progress",
+81 -15
View File
@@ -194,15 +194,16 @@ class TestEnvVars(unittest.TestCase):
def setUp(self):
self._original_env_vars = dict(FRIGATE_ENV_VARS)
self._original_environ = os.environ.copy()
names_patch = patch.object(env, "_CONTAINER_ENV_NAMES", frozenset())
names_patch.start()
self.addCleanup(names_patch.stop)
def tearDown(self):
FRIGATE_ENV_VARS.clear()
FRIGATE_ENV_VARS.update(self._original_env_vars)
env._CONFIG_ENV_VARS.clear()
# Clean up any env vars we set
for key in list(os.environ.keys()):
if key not in self._original_environ:
del os.environ[key]
os.environ.clear()
os.environ.update(self._original_environ)
def _make_context(self, install: bool):
"""Create a mock ValidationInfo with the given install flag."""
@@ -215,10 +216,10 @@ class TestEnvVars(unittest.TestCase):
return mock
def test_install_sets_os_environ(self):
"""validate_env_vars with install=True sets os.environ."""
"""validate_env_vars with install=True sets allowed names in os.environ."""
ctx = self._make_context(install=True)
validate_env_vars({"MY_CUSTOM_VAR": "value123"}, ctx)
self.assertEqual(os.environ.get("MY_CUSTOM_VAR"), "value123")
validate_env_vars({"SHM_MAX_FRAMES": "75"}, ctx)
self.assertEqual(os.environ.get("SHM_MAX_FRAMES"), "75")
def test_install_updates_frigate_env_vars(self):
"""validate_env_vars with install=True updates FRIGATE_ENV_VARS for FRIGATE_ keys."""
@@ -229,9 +230,9 @@ class TestEnvVars(unittest.TestCase):
def test_install_skips_non_frigate_in_env_vars_dict(self):
"""Non-FRIGATE_ keys are set in os.environ but not in FRIGATE_ENV_VARS."""
ctx = self._make_context(install=True)
validate_env_vars({"OTHER_VAR": "value"}, ctx)
self.assertEqual(os.environ.get("OTHER_VAR"), "value")
self.assertNotIn("OTHER_VAR", FRIGATE_ENV_VARS)
validate_env_vars({"USE_FP16": "False"}, ctx)
self.assertEqual(os.environ.get("USE_FP16"), "False")
self.assertNotIn("USE_FP16", FRIGATE_ENV_VARS)
def test_no_install_does_not_set(self):
"""validate_env_vars without install=True does not modify state."""
@@ -258,10 +259,10 @@ class TestEnvVars(unittest.TestCase):
ctx = self._make_context(install=True)
with patch("frigate.util.services.os.path.isfile", return_value=False):
for key in ("GO2RTC_ALLOW_ARBITRARY_EXEC", "go2rtc_allow_arbitrary_exec"):
validate_env_vars({key: "true", "OTHER_VAR": "value"}, ctx)
validate_env_vars({key: "true", "USE_FP16": "False"}, ctx)
self.assertNotIn(key, os.environ)
self.assertFalse(is_go2rtc_arbitrary_exec_allowed())
self.assertEqual(os.environ.get("OTHER_VAR"), "value")
self.assertEqual(os.environ.get("USE_FP16"), "False")
def test_install_keeps_container_go2rtc_exec_override(self):
"""An override set in the container environment is left untouched."""
@@ -307,17 +308,82 @@ class TestVariableSources(unittest.TestCase):
self.assertEqual(env.FRIGATE_ENV_VARS["FRIGATE_CAM_PASS"], "hunter2")
def test_config_env_vars_do_not_become_container_env(self):
"""environment_vars writes os.environ but must not gain env precedence."""
"""environment_vars FRIGATE_ keys stay out of os.environ and env precedence."""
env.apply_config_env_vars({"FRIGATE_CAM_PASS": "from_config"})
self.assertEqual(os.environ["FRIGATE_CAM_PASS"], "from_config")
self.assertNotIn("FRIGATE_CAM_PASS", os.environ)
self.assertNotIn("FRIGATE_CAM_PASS", env._CONTAINER_ENV)
def test_non_frigate_config_env_vars_only_set_os_environ(self):
"""Unprefixed environment_vars keys reach os.environ but not substitution."""
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "i965")
self.assertNotIn("LIBVA_DRIVER_NAME", env.FRIGATE_ENV_VARS)
def test_rejected_names_warn_and_are_not_exported(self):
"""Names off the allowlist log a warning without the value."""
for key in ("MY_CUSTOM_VAR", "LD_PRELOAD", "LIGHTER_ANE_EP", "PATH"):
with self.subTest(key=key):
original = os.environ.get(key)
with self.assertLogs("frigate.util.env", level="WARNING") as logs:
env.apply_config_env_vars({key: "/config/evil.so"})
self.assertEqual(os.environ.get(key), original)
self.assertEqual(len(logs.output), 1)
self.assertIn(key, logs.output[0])
self.assertIn("container environment", logs.output[0])
self.assertNotIn("evil", logs.output[0])
def test_allowed_name_with_bad_value_is_rejected(self):
"""LIBVA_DRIVER_NAME cannot carry a path."""
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
with self.assertLogs("frigate.util.env", level="WARNING"):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "../x"})
self.assertNotEqual(os.environ.get("LIBVA_DRIVER_NAME"), "../x")
def test_unsupported_values_are_rejected_without_crashing(self):
"""Non-ASCII digits and NUL bytes never reach os.environ."""
os.environ.pop("SHM_MAX_FRAMES", None)
os.environ.pop("HF_ENDPOINT", None)
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
with self.assertLogs("frigate.util.env", level="WARNING") as logs:
env.apply_config_env_vars(
{"SHM_MAX_FRAMES": "\u0663\u0664", "HF_ENDPOINT": "https://x\x00"}
)
self.assertEqual(len(logs.output), 2)
self.assertNotIn("SHM_MAX_FRAMES", os.environ)
self.assertNotIn("HF_ENDPOINT", os.environ)
def test_container_env_wins_over_allowed_name(self):
"""A name the container set is left as the container set it."""
os.environ["LIBVA_DRIVER_NAME"] = "iHD"
with patch.object(
env, "_CONTAINER_ENV_NAMES", frozenset({"LIBVA_DRIVER_NAME"})
):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "iHD")
def test_second_apply_is_not_blocked_by_first(self):
"""A safe-mode reload can change a value the first load exported."""
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "i965"})
env.apply_config_env_vars({"LIBVA_DRIVER_NAME": "radeonsi"})
self.assertEqual(os.environ["LIBVA_DRIVER_NAME"], "radeonsi")
def test_run_as_root_is_substitutable_but_not_exported(self):
"""FRIGATE_RUN_AS_ROOT from the config never reaches os.environ."""
os.environ.pop("FRIGATE_RUN_AS_ROOT", None)
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"FRIGATE_RUN_AS_ROOT": "true"})
self.assertNotIn("FRIGATE_RUN_AS_ROOT", os.environ)
self.assertEqual(env.substitute_frigate_vars("{FRIGATE_RUN_AS_ROOT}"), "true")
def test_jwt_secret_is_exported(self):
"""Auth reads FRIGATE_JWT_SECRET from os.environ."""
os.environ.pop("FRIGATE_JWT_SECRET", None)
with patch.object(env, "_CONTAINER_ENV_NAMES", frozenset()):
env.apply_config_env_vars({"FRIGATE_JWT_SECRET": "s3cret"})
self.assertEqual(os.environ["FRIGATE_JWT_SECRET"], "s3cret")
def test_collision_warns_once_naming_the_winner(self):
"""A key from two sources logs one warning naming the source that won."""
with patch.dict(env._CONTAINER_ENV, {"FRIGATE_MQTT_HOST": "from_env"}):
+125 -6
View File
@@ -1,13 +1,15 @@
"""Tests for custom ffmpeg path resolution and the root-mode guard."""
import os
import tempfile
import unittest
from unittest.mock import patch
from frigate.const import DEFAULT_FFMPEG_VERSION
from frigate.util.config import (
_warn_ignored_ffmpeg_path,
frigate_service_is_granular_root,
resolve_ffmpeg_path,
service_is_granular_root,
)
BUNDLED = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
@@ -21,12 +23,20 @@ class TestConfigFfmpegRootGuard(unittest.TestCase):
# the warning is memoized so it fires once per path, not per camera
_warn_ignored_ffmpeg_path.cache_clear()
def _resolve(self, path: str, *, euid: int, env: dict, binary: str = "ffmpeg"):
def _resolve(
self,
path: str,
*,
euid: int,
env: dict,
binary: str = "ffmpeg",
service: str = "frigate",
):
with (
patch("os.geteuid", return_value=euid),
patch.dict("os.environ", env, clear=True),
):
return resolve_ffmpeg_path(path, binary)
return resolve_ffmpeg_path(path, binary, service)
def test_custom_path_used_when_service_is_unprivileged(self) -> None:
self.assertEqual(
@@ -65,6 +75,59 @@ class TestConfigFfmpegRootGuard(unittest.TestCase):
f"{CUSTOM}/bin/ffmpeg",
)
def test_custom_path_ignored_for_go2rtc_when_go2rtc_is_a_root_service(
self,
) -> None:
self.assertEqual(
self._resolve(
CUSTOM,
euid=0,
env={"FRIGATE_ROOT_SERVICES": "go2rtc"},
service="go2rtc",
),
BUNDLED,
)
def test_go2rtc_keeps_custom_path_when_only_frigate_is_root(self) -> None:
self.assertEqual(
self._resolve(
CUSTOM,
euid=0,
env={"FRIGATE_ROOT_SERVICES": "frigate"},
service="go2rtc",
),
f"{CUSTOM}/bin/ffmpeg",
)
def test_go2rtc_custom_path_used_in_default_drop_mode(self) -> None:
# the run scripts resolve as root but probe as the service user
self.assertEqual(
self._resolve(CUSTOM, euid=0, env={}, service="go2rtc"),
f"{CUSTOM}/bin/ffmpeg",
)
def test_escape_hatch_keeps_go2rtc_custom_path(self) -> None:
self.assertEqual(
self._resolve(
CUSTOM,
euid=0,
env={"FRIGATE_RUN_AS_ROOT": "true", "FRIGATE_ROOT_SERVICES": "go2rtc"},
service="go2rtc",
),
f"{CUSTOM}/bin/ffmpeg",
)
def test_go2rtc_root_owned_path_is_left_alone(self) -> None:
self.assertEqual(
self._resolve(
"/opt/custom-ffmpeg",
euid=0,
env={"FRIGATE_ROOT_SERVICES": "go2rtc"},
service="go2rtc",
),
"/opt/custom-ffmpeg/bin/ffmpeg",
)
def test_root_services_has_no_effect_under_docker_user(self) -> None:
# docker's own user: means the service never had root to keep, which is
# also the case for get_ffmpeg_path.py in a --user container
@@ -115,15 +178,65 @@ class TestConfigFfmpegRootGuard(unittest.TestCase):
)
class TestFrigateServiceIsGranularRoot(unittest.TestCase):
class TestConfigFfmpegRootGuardSymlinks(unittest.TestCase):
"""A root service gets the resolved path, so a link cannot be retargeted."""
def setUp(self) -> None:
_warn_ignored_ffmpeg_path.cache_clear()
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
root = os.path.realpath(tmp.name)
self.writable = os.path.join(root, "config")
self.root_owned = os.path.join(root, "opt", "ffmpeg")
os.makedirs(self.writable)
os.makedirs(self.root_owned)
self.link = os.path.join(self.writable, "link")
dirs = patch("frigate.util.config.RUNTIME_USER_WRITABLE_DIRS", (self.writable,))
dirs.start()
self.addCleanup(dirs.stop)
def _resolve(self, env: dict, service: str = "frigate") -> str:
with (
patch("os.geteuid", return_value=0),
patch.dict("os.environ", env, clear=True),
):
return resolve_ffmpeg_path(self.link, "ffmpeg", service)
def test_link_to_a_root_owned_tree_resolves_to_its_target(self) -> None:
os.symlink(self.root_owned, self.link)
for service in ("frigate", "go2rtc"):
self.assertEqual(
self._resolve({"FRIGATE_ROOT_SERVICES": service}, service),
f"{self.root_owned}/bin/ffmpeg",
)
def test_link_to_a_writable_tree_is_refused(self) -> None:
evil = os.path.join(self.writable, "evil")
os.makedirs(evil)
os.symlink(evil, self.link)
self.assertEqual(self._resolve({"FRIGATE_ROOT_SERVICES": "frigate"}), BUNDLED)
def test_bin_linked_into_a_writable_tree_is_refused(self) -> None:
evil = os.path.join(self.writable, "evil")
os.makedirs(evil)
os.symlink(evil, os.path.join(self.root_owned, "bin"))
os.symlink(self.root_owned, self.link)
self.assertEqual(self._resolve({"FRIGATE_ROOT_SERVICES": "frigate"}), BUNDLED)
def test_unprivileged_service_keeps_the_path_as_written(self) -> None:
os.symlink(self.root_owned, self.link)
self.assertEqual(self._resolve({}), f"{self.link}/bin/ffmpeg")
class TestServiceIsGranularRoot(unittest.TestCase):
"""Root via FRIGATE_ROOT_SERVICES only, never via the escape hatch."""
def _check(self, *, euid: int, env: dict) -> bool:
def _check(self, *, euid: int, env: dict, service: str = "frigate") -> bool:
with (
patch("os.geteuid", return_value=euid),
patch.dict("os.environ", env, clear=True),
):
return frigate_service_is_granular_root()
return service_is_granular_root(service)
def test_false_without_any_root_signal(self) -> None:
self.assertFalse(self._check(euid=0, env={}))
@@ -146,6 +259,12 @@ class TestFrigateServiceIsGranularRoot(unittest.TestCase):
self._check(euid=0, env={"FRIGATE_ROOT_SERVICES": "go2rtc,nginx"})
)
def test_matches_the_named_service(self) -> None:
env = {"FRIGATE_ROOT_SERVICES": "go2rtc"}
self.assertTrue(self._check(euid=0, env=env, service="go2rtc"))
self.assertFalse(self._check(euid=0, env=env, service="frigate"))
self.assertFalse(self._check(euid=1000, env=env, service="go2rtc"))
def test_substring_of_a_service_name_does_not_match(self) -> None:
self.assertFalse(self._check(euid=0, env={"FRIGATE_ROOT_SERVICES": "frigatee"}))
+58
View File
@@ -5,6 +5,7 @@ per-camera gating those processors apply once they exist.
"""
import sys
import threading
import unittest
from unittest.mock import MagicMock, patch
@@ -19,6 +20,8 @@ for mod in _MOCK_MODULES:
if mod not in sys.modules:
sys.modules[mod] = MagicMock()
from frigate.config.camera.review import ImageSourceEnum # noqa: E402
# imported from the maintainer to avoid tripping the circular import between
# the maintainer and the processor modules
from frigate.embeddings.maintainer import ( # noqa: E402
@@ -211,3 +214,58 @@ class TestObjectDescriptionCameraGating(unittest.TestCase):
mock_create_thumbnail.assert_called_once()
self.assertEqual(len(processor.tracked_events["1234.5-abcdef"]), 1)
class TestReviewDescriptionOffLoop(unittest.TestCase):
"""Frame extraction for a finished review must not block process_data."""
def test_recording_frames_extracted_off_the_caller_thread(self):
camera = MagicMock()
camera.review.genai.enabled = True
camera.review.genai.alerts = True
camera.review.genai.image_source = ImageSourceEnum.recordings
camera.review.genai.debug_save_thumbnails = False
config = MagicMock()
config.cameras = {"front": camera}
processor = ReviewDescriptionProcessor(
config, MagicMock(), MagicMock(), MagicMock()
)
release = threading.Event()
extracted = threading.Event()
analyzed = threading.Event()
def slow_frames(*args, **kwargs):
release.wait(5)
extracted.set()
return [(b"jpg", 1.0)]
with (
patch.object(processor, "get_recording_frames", side_effect=slow_frames),
patch.object(
processor, "start_analysis", side_effect=lambda *a, **k: analyzed.set()
) as start_analysis,
):
processor.process_data(
{
"type": "end",
"after": {
"id": "1234.5-abcdef",
"camera": "front",
"severity": "alert",
"start_time": 100.0,
"end_time": 110.0,
},
},
PostProcessDataEnum.review,
)
self.assertFalse(extracted.is_set())
release.set()
self.assertTrue(extracted.wait(5))
self.assertTrue(analyzed.wait(5))
start_analysis.assert_called_once()
final_data = start_analysis.call_args.args[1]
self.assertEqual(final_data["start_time"], 100.0)
self.assertEqual(final_data["end_time"], 110.0)
+6 -1
View File
@@ -23,7 +23,12 @@ _CONFIG = {
"dual_user": ["front_door", "back_door"],
}
},
"go2rtc": {"streams": {"doorbell_audio": ["rtsp://10.0.0.4:554/audio"]}},
"go2rtc": {
"streams": {
"doorbell_audio": ["rtsp://10.0.0.4:554/audio"],
"front_door_sub": ["rtsp://10.0.0.1:554/sub"],
}
},
"cameras": {
"front_door": {
"ffmpeg": {
+35
View File
@@ -12,6 +12,7 @@ from frigate.util.live_streams import (
default_transcode_source,
generated_transcode_streams,
is_transcode_stream_name,
is_valid_go2rtc_stream_name,
measure_stream_bitrate,
raw_transcode_streams,
sync_transcode_streams,
@@ -37,6 +38,13 @@ class TestTranscodeNaming(unittest.TestCase):
"#raw=-b:v#raw=500k#raw=-maxrate#raw=500k#raw=-bufsize#raw=1000k",
)
def test_valid_go2rtc_stream_names(self):
for name in ("front", "front_sub", "back-yard.main_2", "Haustür"):
self.assertTrue(is_valid_go2rtc_stream_name(name), name)
for name in ("x#raw=-f", "front door", "front\tdoor", "a'b", 'a"b', "a${X}"):
self.assertFalse(is_valid_go2rtc_stream_name(name), name)
def test_default_source_skips_transcoded_streams(self):
streams = {"Low": "front_transcode_360p", "Main": "front_main"}
self.assertEqual(default_transcode_source("front", streams), "front_main")
@@ -121,6 +129,22 @@ class TestRawTranscodeStreams(unittest.TestCase):
self.assertEqual(list(raw_transcode_streams(config)), ["front_transcode_360p"])
def test_unsafe_source_name_is_skipped(self):
config = {
"cameras": {
"front": {
"live": {
"transcode": {"enabled": True, "source": "x#raw=-f#raw=null"}
}
}
}
}
self.assertEqual(raw_transcode_streams(config), {})
config["cameras"]["front"]["live"]["transcode"]["source"] = "front_main"
self.assertEqual(len(raw_transcode_streams(config)), 3)
def test_disabled_or_missing_generates_nothing(self):
self.assertEqual(raw_transcode_streams({}), {})
self.assertEqual(
@@ -286,6 +310,17 @@ class TestLiveTranscodeConfig(unittest.TestCase):
with self.assertRaises(ValidationError):
self.streams({"transcode": {"enabled": True, "source": "missing"}})
def test_source_name_with_ffmpeg_params_fails(self):
name = "x#raw=-f#raw=null"
with self.assertRaisesRegex(ValidationError, "contains '#'"):
self.streams(
{"transcode": {"enabled": True, "source": name}},
go2rtc={
"front": ["rtsp://10.0.0.1:554/main"],
name: ["rtsp://10.0.0.1:554/main"],
},
)
def test_duplicate_heights_fail(self):
with self.assertRaises(ValidationError):
self.streams(
+24
View File
@@ -0,0 +1,24 @@
"""Tests for MQTT TLS configuration validation."""
import unittest
from contextlib import nullcontext
from pydantic import ValidationError
from frigate.config import MqttConfig
class TestMqttTls(unittest.TestCase):
def test_tls_insecure_requires_tls_only_when_enabled(self) -> None:
"""Require TLS for either explicit verification flag only if MQTT is enabled."""
for enabled in (True, False):
for insecure in (True, False):
expected = (
self.assertRaisesRegex(
ValidationError, "tls_insecure requires tls_ca_certs"
)
if enabled
else nullcontext()
)
with self.subTest(enabled=enabled, tls_insecure=insecure), expected:
MqttConfig(enabled=enabled, tls_insecure=insecure)
+138 -1
View File
@@ -1,12 +1,17 @@
"""Tests for Frigate+ model metadata."""
import asyncio
import json
import os
import tempfile
import unittest
from unittest.mock import MagicMock, patch
from frigate.plus import PlusApi, load_plus_model_info
import aiohttp
from aiohttp import web
from aiohttp.test_utils import TestServer
from frigate.plus import PlusApi, PlusKeyStatus, load_plus_model_info
class TestHailoAlias(unittest.TestCase):
@@ -75,5 +80,137 @@ class TestLoadPlusModelInfo(unittest.TestCase):
self.assertIsNone(load_plus_model_info("bad"))
class TestValidateKey(unittest.IsolatedAsyncioTestCase):
"""The key format is checked locally, but only the server knows if it is valid."""
CACHED_TOKEN = {"accessToken": "cached", "expires": 2000000000}
async def _serve(self, handler) -> str:
"""Serve the token endpoint locally and return its base url."""
app = web.Application()
app.router.add_get("/v1/auth/token", handler)
server = TestServer(app)
await server.start_server()
self.addAsyncCleanup(server.close)
return str(server.make_url("")).rstrip("/")
def _api(self, host: str, key: str | None = "id:secret") -> PlusApi:
api = PlusApi.__new__(PlusApi)
api.host = host
api.key = key
api._token_data = dict(self.CACHED_TOKEN)
return api
async def _validate(self, handler) -> tuple[PlusKeyStatus, PlusApi]:
api = self._api(await self._serve(handler))
return await api.validate_key(), api
def _replies(self, status: int = 200, body: object = None, text: str | None = None):
async def handler(request: web.Request) -> web.Response:
self.auth = request.headers.get("Authorization")
if text is not None:
return web.Response(status=status, text=text)
return web.json_response(body, status=status)
return handler
async def test_an_accepted_key_is_valid(self):
token = {"accessToken": "fresh", "expires": 2000000500.5}
status, api = await self._validate(self._replies(body=token))
self.assertEqual(status, PlusKeyStatus.valid)
self.assertEqual(api._token_data, token)
self.assertEqual(self.auth, aiohttp.BasicAuth("id", "secret").encode())
async def test_a_rejected_key_is_invalid(self):
for code in (401, 403):
status, api = await self._validate(self._replies(code, {"error": "no"}))
self.assertEqual(status, PlusKeyStatus.invalid)
self.assertEqual(api._token_data, self.CACHED_TOKEN)
async def test_a_server_error_is_not_reported_as_invalid(self):
status, _ = await self._validate(self._replies(500, {"error": "down"}))
self.assertEqual(status, PlusKeyStatus.unreachable)
async def test_unusable_replies_keep_the_cached_token(self):
# a successful reply without a usable token must not replace one that
# later Frigate+ requests depend on
for body in (
None,
[],
{},
{"accessToken": "t"},
{"expires": 2000000000},
{"accessToken": 1, "expires": 2000000000},
{"accessToken": "t", "expires": "soon"},
{"accessToken": "t", "expires": True},
):
with self.subTest(body=body):
status, api = await self._validate(self._replies(body=body))
self.assertEqual(status, PlusKeyStatus.unreachable)
self.assertEqual(api._token_data, self.CACHED_TOKEN)
async def test_a_non_json_reply_keeps_the_cached_token(self):
status, api = await self._validate(self._replies(text="<html>"))
self.assertEqual(status, PlusKeyStatus.unreachable)
self.assertEqual(api._token_data, self.CACHED_TOKEN)
async def test_a_connection_failure_is_unreachable(self):
# serve and stop, so the port refuses connections
server = TestServer(web.Application())
await server.start_server()
host = str(server.make_url("")).rstrip("/")
await server.close()
api = self._api(host)
self.assertEqual(await api.validate_key(), PlusKeyStatus.unreachable)
async def test_a_slow_server_times_out_as_unreachable(self):
async def slow(request: web.Request) -> web.Response:
await asyncio.sleep(5)
return web.json_response(self.CACHED_TOKEN)
with patch("frigate.plus.PLUS_VALIDATE_TIMEOUT", 0.1):
status, _ = await self._validate(slow)
self.assertEqual(status, PlusKeyStatus.unreachable)
async def test_the_request_goes_through_a_configured_proxy(self):
# installs that reach Frigate+ through a proxy must validate the same way
token = {"accessToken": "proxied", "expires": 2000000000}
proxied = []
async def proxy(request: web.Request) -> web.Response:
proxied.append(str(request.url))
return web.json_response(token)
proxy_url = await self._serve(proxy)
api = self._api("http://plus.invalid")
with patch.dict(
os.environ, {"HTTP_PROXY": proxy_url, "NO_PROXY": ""}, clear=False
):
status = await api.validate_key()
self.assertEqual(status, PlusKeyStatus.valid)
self.assertEqual(proxied, ["http://plus.invalid/v1/auth/token"])
async def test_no_key_is_not_sent_to_the_server(self):
requests_seen = []
async def handler(request: web.Request) -> web.Response:
requests_seen.append(request)
return web.json_response(self.CACHED_TOKEN)
api = self._api(await self._serve(handler), key=None)
self.assertEqual(await api.validate_key(), PlusKeyStatus.not_set)
self.assertEqual(requests_seen, [])
if __name__ == "__main__":
unittest.main(verbosity=2)
+117 -2
View File
@@ -11,11 +11,14 @@ KeyError on the autotracker thread or silently keep the wrong state:
running its motion estimator for a camera that could not autotrack.
"""
import asyncio
import unittest
from unittest.mock import MagicMock
from unittest.mock import AsyncMock, MagicMock
import numpy as np
from frigate.camera import PTZMetrics
from frigate.config import FrigateConfig
from frigate.config import FrigateConfig, ZoomingModeEnum
from frigate.config.camera.updater import CameraConfigUpdateEnum
from frigate.ptz.autotrack import PtzAutoTracker, calculate_max_target_box
@@ -123,6 +126,118 @@ class TestAutotrackerDisable(unittest.TestCase):
self.assertIs(payload, autotracking)
class TestAutotrackerPrediction(unittest.TestCase):
def test_movement_time_uses_the_fitted_intercept_unchanged(self) -> None:
tracker = _make_tracker()
tracker.intercept = {CAMERA: 0.5}
tracker.move_coefficients = {CAMERA: [0.3, 0.8]}
self.assertAlmostEqual(tracker._predict_movement_time(CAMERA, 0.25, 0.25), 0.7)
def test_shrinking_box_keeps_a_positive_target_box(self) -> None:
# 60 px shrinking 6 px/frame reaches zero after 2 s at 5 fps
target_box = self._target_box_for((0, 0, 60, 60), [3, 3, -3, -3], 2.0)
self.assertGreater(target_box, 0)
self.assertTrue(np.isfinite(target_box))
def test_approaching_object_keeps_its_velocity(self) -> None:
# the box grows, so the corners move in opposite directions
tracker = _make_tracker()
obj = MagicMock()
obj.obj_data = {"estimate_velocity": np.array([[-3.0, -4.0], [3.0, 1.0]])}
valid, velocity = tracker._get_valid_velocity(CAMERA, obj)
self.assertTrue(valid)
self.assertEqual(list(velocity), [-3, -4, 3, 1])
def test_corners_that_disagree_too_much_are_rejected(self) -> None:
tracker = _make_tracker()
obj = MagicMock()
obj.obj_data = {"estimate_velocity": np.array([[-15.0, 0.0], [15.0, 0.0]])}
valid, velocity = tracker._get_valid_velocity(CAMERA, obj)
self.assertFalse(valid)
self.assertFalse(np.any(velocity))
def _target_box(self, velocity: list[float], predicted_time: float) -> float:
return self._target_box_for((900, 400, 1000, 600), velocity, predicted_time)
def _target_box_for(
self,
box: tuple[int, int, int, int],
velocity: list[float],
predicted_time: float,
) -> float:
tracker = _make_tracker()
tracker.tracked_object_history = {CAMERA: [{"box": box}]}
tracker.tracked_object_metrics = {
CAMERA: {"target_box": 0.2, "velocity": np.array(velocity)}
}
return tracker._predict_target_box(CAMERA, predicted_time)
def test_target_box_grows_with_an_approaching_object(self) -> None:
# the 200px height gains 4px per frame at 5 fps for 1 second
zoom_factor = 0.3
self.assertAlmostEqual(
self._target_box([-1, -2, 1, 2], 1.0),
0.2 * (220 / 200) ** (2 * zoom_factor),
)
def test_target_box_is_unchanged_without_growth_or_time(self) -> None:
self.assertAlmostEqual(self._target_box([5, 0, 5, 0], 1.0), 0.2)
self.assertEqual(self._target_box([-1, -2, 1, 2], 0), 0.2)
class TestAutotrackerMoveQueue(unittest.IsolatedAsyncioTestCase):
async def test_absolute_zoom_does_not_overwrite_the_pan_tilt_sample(self) -> None:
tracker = _make_tracker()
autotracking = tracker.config.cameras[CAMERA].onvif.autotracking
autotracking.zooming = ZoomingModeEnum.absolute
autotracking.calibrate_on_startup = True
metrics = tracker.ptz_metrics[CAMERA]
tracker.stop_event = MagicMock()
tracker.stop_event.is_set.side_effect = [False, True]
tracker.move_queues = {CAMERA: asyncio.Queue()}
tracker.move_queue_locks = {CAMERA: asyncio.Lock()}
tracker.intercept = {CAMERA: 0.5}
tracker.move_metrics = {CAMERA: []}
tracker._wait_until_stopped = AsyncMock()
tracker._calculate_move_coefficients = MagicMock()
async def move_relative(*_args) -> None:
metrics.start_time.value = 100.0
metrics.stop_time.value = 102.0
async def zoom_absolute(*_args) -> None:
metrics.start_time.value = 103.0
metrics.stop_time.value = 0
tracker.onvif._move_relative = AsyncMock(side_effect=move_relative)
tracker.onvif._zoom_absolute = AsyncMock(side_effect=zoom_absolute)
tracker.move_queues[CAMERA].put_nowait((50.0, 0.5, 0.5, 0.5))
await tracker._process_move_queue(CAMERA)
tracker.onvif._zoom_absolute.assert_awaited_once()
self.assertEqual(
tracker.move_metrics[CAMERA],
[
{
"pan": 0.5,
"tilt": 0.5,
"start_timestamp": 100.0,
"end_timestamp": 102.0,
}
],
)
class TestMaxTargetBox(unittest.TestCase):
def test_follows_zoom_factor(self) -> None:
self.assertAlmostEqual(calculate_max_target_box(0.5), 0.6**2)
+30
View File
@@ -321,6 +321,36 @@ class TestLighterANE(unittest.TestCase):
ane.assert_not_called()
class TestTensorRTProvider(unittest.TestCase):
def _trt_options(self, env):
with (
patch.object(
model_util.ort,
"get_available_providers",
return_value=["TensorrtExecutionProvider", "CPUExecutionProvider"],
),
patch.object(model_util.os, "makedirs"),
patch.dict(os.environ, env, clear=False),
):
if "USE_FP16" not in env:
os.environ.pop("USE_FP16", None)
providers, options = model_util.get_ort_providers(device="Tensorrt")
return options[providers.index("TensorrtExecutionProvider")]
def test_fp16_is_disabled_by_default(self):
self.assertFalse(self._trt_options({})["trt_fp16_enable"])
def test_use_fp16_enables_fp16(self):
for value in ("true", "True", "TRUE"):
with self.subTest(value=value):
options = self._trt_options({"USE_FP16": value})
self.assertTrue(options["trt_fp16_enable"])
def test_use_fp16_false_keeps_fp16_disabled(self):
self.assertFalse(self._trt_options({"USE_FP16": "False"})["trt_fp16_enable"])
class TestMIGraphX(unittest.TestCase):
"""AMD's MIGraphX plugin provider, selected through its devices in the ROCm image."""
+7 -2
View File
@@ -9,6 +9,7 @@ import queue
import re
import shlex
import struct
import threading
import time
import urllib.parse
from collections import deque
@@ -42,6 +43,7 @@ class EventsPerSecond:
self._max_events = max_events
self._last_n_seconds = last_n_seconds
self._timestamps: deque[float] = deque(maxlen=max_events)
self._lock = threading.Lock()
def start(self) -> None:
self._start = time.monotonic()
@@ -71,8 +73,11 @@ class EventsPerSecond:
# remove aged out timestamps
def expire_timestamps(self, now: float) -> None:
threshold = now - self._last_n_seconds
while self._timestamps and self._timestamps[0] < threshold:
self._timestamps.popleft()
# update() and eps() can run on different threads
with self._lock:
while self._timestamps and self._timestamps[0] < threshold:
self._timestamps.popleft()
class InferenceSpeed:
+57 -33
View File
@@ -1,6 +1,8 @@
"""configuration utils."""
import asyncio
import contextlib
import errno
import ipaddress
import logging
import os
@@ -46,14 +48,14 @@ DROPPED_DETECTOR_OPTIONS = {
}
# Trees the unprivileged runtime user can write. A root frigate service must
# not execute a binary from any of them; a compromised uid-1000 process could
# Trees the unprivileged runtime user can write. A root service must not
# execute a binary from any of them; a compromised uid-1000 process could
# plant one and be root after the next restart.
RUNTIME_USER_WRITABLE_DIRS = (CONFIG_DIR, BASE_DIR, CACHE_DIR, "/dev/shm", "/tmp")
def frigate_service_is_granular_root() -> bool:
"""Report whether FRIGATE_ROOT_SERVICES runs frigate as root.
def service_is_granular_root(service: str) -> bool:
"""Report whether FRIGATE_ROOT_SERVICES runs the given service as root.
The escape hatch is excluded: it never sweeps /config and leaves no
unprivileged service running, so custom binaries stay as safe as they
@@ -66,7 +68,7 @@ def frigate_service_is_granular_root() -> bool:
return False
entries = os.environ.get("FRIGATE_ROOT_SERVICES", "").split(",")
return any("".join(entry.split()) == "frigate" for entry in entries)
return any("".join(entry.split()) == service for entry in entries)
def is_runtime_user_writable(path: str) -> bool:
@@ -79,11 +81,12 @@ def is_runtime_user_writable(path: str) -> bool:
@cache
def _warn_ignored_ffmpeg_path(path: str) -> None:
def _warn_ignored_ffmpeg_path(path: str, service: str) -> None:
"""Warn once per path; resolution runs per camera and per binary."""
logger.warning(
"Ignoring ffmpeg.path %s because FRIGATE_ROOT_SERVICES runs frigate as root and that location is writable by the unprivileged user; using the bundled build",
"Ignoring ffmpeg.path %s because FRIGATE_ROOT_SERVICES runs %s as root and that location is writable by the unprivileged user; using the bundled build",
path,
service,
)
@@ -97,16 +100,19 @@ def is_valid_ffmpeg_path(path: str) -> bool:
return not path.startswith("/") or _CUSTOM_FFMPEG_PATH.fullmatch(path) is not None
def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str:
def resolve_ffmpeg_path(
path: str, binary: str = "ffmpeg", service: str = "frigate"
) -> str:
"""Resolve an ffmpeg version alias or custom path to a binary path.
A bare version alias that is no longer bundled (for example one that was
dropped when the default version changed) falls back to the default
bundled version so existing configs keep working across an upgrade or a
revert. Custom install paths (anything absolute) are used as-is, unless
they fail `is_valid_ffmpeg_path` (which falls back to the default), or
they are in a runtime-user-writable tree while FRIGATE_ROOT_SERVICES
makes frigate root; see RUNTIME_USER_WRITABLE_DIRS.
revert. Custom install paths (anything absolute) are used as-is. While
FRIGATE_ROOT_SERVICES makes the service that execs it root, a custom path
is resolved to its real location and refused inside a runtime-user-writable
tree; see RUNTIME_USER_WRITABLE_DIRS. Paths that fail
`is_valid_ffmpeg_path` also fall back to the default.
"""
if (
path == "default"
@@ -116,11 +122,16 @@ def resolve_ffmpeg_path(path: str, binary: str = "ffmpeg") -> str:
version = DEFAULT_FFMPEG_VERSION
elif path in INCLUDED_FFMPEG_VERSIONS:
version = path
elif not service_is_granular_root(service):
return f"{path}/bin/{binary}"
else:
if not (frigate_service_is_granular_root() and is_runtime_user_writable(path)):
return f"{path}/bin/{binary}"
# return the resolved form so a link in the path cannot be retargeted
# after the check
real = os.path.realpath(f"{path}/bin/{binary}")
if is_valid_ffmpeg_path(real) and not is_runtime_user_writable(real):
return real
_warn_ignored_ffmpeg_path(path)
_warn_ignored_ffmpeg_path(path, service)
version = DEFAULT_FFMPEG_VERSION
return f"/usr/lib/ffmpeg/{version}/bin/{binary}"
@@ -203,6 +214,28 @@ def find_config_file() -> str:
return config_path
def _write_config(config_file: str, yaml: YAML, config: dict[str, Any]) -> None:
config_file = os.path.realpath(config_file)
st = os.stat(config_file)
tmp_file = f"{config_file}.tmp"
with open(tmp_file, "w") as f:
yaml.dump(config, f)
os.chmod(tmp_file, st.st_mode)
with contextlib.suppress(PermissionError):
os.chown(tmp_file, st.st_uid, st.st_gid)
try:
os.replace(tmp_file, config_file)
except OSError as e:
if e.errno != errno.EBUSY:
raise
# a file bind mount can't be renamed over, so write it in place
shutil.copyfile(tmp_file, config_file)
os.remove(tmp_file)
def migrate_frigate_config(config_file: str):
"""handle migrating the frigate config."""
logger.info("Checking if frigate config needs migration...")
@@ -250,8 +283,7 @@ def migrate_frigate_config(config_file: str):
if previous_version < "0.14":
logger.info(f"Migrating frigate config from {previous_version} to 0.14...")
new_config = migrate_014(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.14"
logger.info("Migrating export file names...")
@@ -268,56 +300,48 @@ def migrate_frigate_config(config_file: str):
if previous_version < "0.15-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.15-0...")
new_config = migrate_015_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.15-0"
if previous_version < "0.15-1":
logger.info(f"Migrating frigate config from {previous_version} to 0.15-1...")
new_config = migrate_015_1(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.15-1"
if previous_version < "0.16-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.16-0...")
new_config = migrate_016_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.16-0"
if previous_version < "0.17-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.17-0...")
new_config = migrate_017_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.17-0"
if previous_version < "0.18-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.18-0...")
new_config = migrate_018_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.18-0"
if previous_version < "0.19-0":
logger.info(f"Migrating frigate config from {previous_version} to 0.19-0...")
new_config = migrate_019_0(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
previous_version = "0.19-0"
if needs_models:
logger.info("Migrating frigate detectors and model to models...")
new_config = migrate_models(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
if needs_detector_rename:
logger.info("Migrating renamed frigate detectors...")
new_config = rename_hailo_detector(new_config)
with open(config_file, "w") as f:
yaml.dump(new_config, f)
_write_config(config_file, yaml, new_config)
logger.info("Finished frigate config migration...")
+59 -15
View File
@@ -26,6 +26,8 @@ _SECRETS_FILE: dict[str, str] = {}
_CONTAINER_ENV: dict[str, str] = {
k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")
}
# Empty image defaults, like ROCm's HSA_OVERRIDE_GFX_VERSION, don't count as set.
_CONTAINER_ENV_NAMES = frozenset(k for k, v in os.environ.items() if v)
_CREDENTIALS_DIR: dict[str, str] = {}
_SOURCES: tuple[tuple[str, dict[str, str]], ...] = (
@@ -165,38 +167,80 @@ def reload_sources(warn: bool = True) -> None:
_rebuild(warn)
# Security overrides that must come from the container environment, which is
# controlled by the operator, rather than from the config, which admins can edit
_PROTECTED_ENV_VARS = frozenset({"GO2RTC_ALLOW_ARBITRARY_EXEC"})
_INT = re.compile(r"\d+", re.ASCII)
_WORD = re.compile(r"\w+", re.ASCII)
_URL = re.compile(r"https?://\S+")
# The only unprefixed names the config may export, with the values each accepts.
# Anything else, such as LD_PRELOAD, can load code into Frigate or its children.
_ALLOWED_ENV_VARS: dict[str, re.Pattern[str]] = {
"LIBVA_DRIVER_NAME": re.compile(r"[A-Za-z0-9_]+"),
"TF_INTRA_OP_PARALLELISM_THREADS": _INT,
"TF_INTER_OP_PARALLELISM_THREADS": _INT,
"TF_DATASET_THREAD_POOL_SIZE": _INT,
"SHM_MAX_FRAMES": _INT,
"USE_FP16": _WORD,
"HF_HUB_OFFLINE": _WORD,
"TRANSFORMERS_OFFLINE": _WORD,
"HSA_OVERRIDE_GFX_VERSION": re.compile(r"[0-9.]+"),
"HF_ENDPOINT": _URL,
"GITHUB_ENDPOINT": _URL,
"GITHUB_RAW_ENDPOINT": _URL,
"TF_KERAS_MOBILENET_V2_WEIGHTS_URL": _URL,
}
# Auth reads it from os.environ; every other FRIGATE_ name only feeds substitution.
_EXPORTED_FRIGATE_VARS = frozenset({"FRIGATE_JWT_SECRET"})
def apply_config_env_vars(values: Mapping[str, object]) -> None:
"""Install the environment_vars block as the lowest priority source.
Unprefixed keys only set os.environ. Protected security overrides are
skipped.
FRIGATE_ keys feed substitution. Allowlisted unprefixed keys with valid
values are exported to os.environ. A name the container already set is
never overwritten.
"""
exports: dict[str, str] = {}
for key, value in values.items():
if key.upper() in _PROTECTED_ENV_VARS:
resolved = str(value)
if key.startswith("FRIGATE_"):
_CONFIG_ENV_VARS[key] = resolved
continue
pattern = _ALLOWED_ENV_VARS.get(key)
if pattern is None:
logger.warning(
"Ignoring %s in environment_vars, it must be set in the container environment",
key,
)
continue
resolved = str(value)
if not pattern.fullmatch(resolved):
logger.warning("Ignoring %s in environment_vars, unsupported value", key)
continue
if key.startswith("FRIGATE_"):
_CONFIG_ENV_VARS[key] = resolved
else:
os.environ[key] = resolved
exports[key] = resolved
_rebuild()
# Export the winning value; auth reads FRIGATE_JWT_SECRET from os.environ.
for key in values:
if key.startswith("FRIGATE_"):
os.environ[key] = FRIGATE_ENV_VARS[key]
for key in _EXPORTED_FRIGATE_VARS & values.keys():
exports[key] = FRIGATE_ENV_VARS[key]
for key, value in exports.items():
if key in _CONTAINER_ENV_NAMES:
logger.info(
"Ignoring %s in environment_vars, the container environment sets it",
key,
)
continue
try:
os.environ[key] = value
except ValueError:
logger.warning("Ignoring %s in environment_vars, unsupported value", key)
reload_sources(warn=False)
+13 -1
View File
@@ -31,6 +31,16 @@ def is_transcode_stream_name(camera: str, name: str) -> bool:
return re.fullmatch(rf"{re.escape(camera)}_transcode_\d+p", name) is not None
def is_valid_go2rtc_stream_name(name: str) -> bool:
"""Return whether a stream name is safe to splice into a go2rtc source.
go2rtc splits sources on `#` and ffmpeg commands on whitespace, and expands
`${VAR}` at load, so a name with any of these, or with quotes, could add its
own parameters.
"""
return re.search(r"[\s#$'\"]", name) is None
def transcode_stream_source(source: str, height: int, bitrate: int) -> str:
"""Build the go2rtc ffmpeg source that transcodes a stream to H.264.
@@ -101,7 +111,9 @@ def raw_transcode_streams(config: dict[str, Any]) -> dict[str, str]:
camera, live_streams or {camera: camera}
)
if source is None:
# an unsafe name fails Frigate's validation; skipping it here keeps
# it out of go2rtc
if source is None or not is_valid_go2rtc_stream_name(str(source)):
continue
qualities = transcode.get("qualities")
+2 -3
View File
@@ -318,7 +318,6 @@ def post_process_yolox(
def get_ort_providers(
force_cpu: bool = False,
device: str | None = "AUTO",
requires_fp16: bool = False,
) -> tuple[list[str], list[dict[str, Any]]]:
if force_cpu:
return (
@@ -357,8 +356,8 @@ def get_ort_providers(
options.append(
{
"device_id": device_id,
"trt_fp16_enable": requires_fp16
and os.environ.get("USE_FP16", "True") != "False",
"trt_fp16_enable": os.environ.get("USE_FP16", "False").lower()
== "true",
"trt_timing_cache_enable": True,
"trt_engine_cache_enable": True,
"trt_timing_cache_path": os.path.join(
+2 -2
View File
@@ -32,8 +32,8 @@ from pathlib import Path
from frigate.const import MODEL_CACHE_DIR
from frigate.util.config import (
frigate_service_is_granular_root,
is_runtime_user_writable,
service_is_granular_root,
)
from frigate.util.file import FileLock
@@ -163,7 +163,7 @@ def _usable_reason() -> str | None:
base = user_base()
if frigate_service_is_granular_root() and is_runtime_user_writable(str(base)):
if service_is_granular_root("frigate") and is_runtime_user_writable(str(base)):
return (
f"{base} is writable by the unprivileged user while "
"FRIGATE_ROOT_SERVICES runs frigate as root"
+7
View File
@@ -31,6 +31,7 @@ from frigate.const import (
)
from frigate.util.builtin import clean_camera_user_pass, escape_special_characters
from frigate.util.env import UnknownVariableError, substitute_frigate_vars
from frigate.util.live_streams import is_valid_go2rtc_stream_name
logger = logging.getLogger(__name__)
@@ -1177,6 +1178,7 @@ class Go2rtcStreamIssue(str, Enum):
restricted = "restricted"
unknown_variable = "unknown_variable"
invalid_placeholder = "invalid_placeholder"
invalid_name = "invalid_name"
def find_restricted_go2rtc_stream_changes(
@@ -1188,12 +1190,17 @@ def find_restricted_go2rtc_stream_changes(
in the saved config for the same stream is left alone, so streams the
operator configured on disk keep working; any other source must resolve
its {FRIGATE_*} placeholders and pass `is_restricted_go2rtc_api_source`.
New stream names must pass `is_valid_go2rtc_stream_name`.
"""
old_streams = old_streams if isinstance(old_streams, dict) else {}
new_streams = new_streams if isinstance(new_streams, dict) else {}
issues: dict[str, Go2rtcStreamIssue] = {}
for name, stream in new_streams.items():
if name not in old_streams and not is_valid_go2rtc_stream_name(str(name)):
issues[name] = Go2rtcStreamIssue.invalid_name
continue
existing = _go2rtc_stream_sources(old_streams.get(name))
for source in _go2rtc_stream_sources(stream):
+199 -181
View File
@@ -9,8 +9,8 @@
"version": "0.0.0",
"hasInstallScript": true,
"dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.8",
"@hookform/resolvers": "^6.0.2",
"@cycjimmy/jsmpeg-player": "^7.0.0",
"@hookform/resolvers": "^7.0.3",
"@radix-ui/react-alert-dialog": "^1.2.1",
"@radix-ui/react-aspect-ratio": "^1.2.0",
"@radix-ui/react-checkbox": "^1.4.1",
@@ -33,11 +33,11 @@
"@radix-ui/react-toggle": "^1.2.1",
"@radix-ui/react-toggle-group": "^1.2.1",
"@radix-ui/react-tooltip": "^1.4.1",
"@rjsf/core": "^6.10.1",
"@rjsf/shadcn": "^6.10.1",
"@rjsf/core": "^6.11.1",
"@rjsf/shadcn": "^6.11.1",
"@rjsf/utils": "^6.10.1",
"@rjsf/validator-ajv8": "^6.10.1",
"apexcharts": "^8.6.0",
"@rjsf/validator-ajv8": "^6.11.1",
"apexcharts": "^8.7.0",
"axios": "^1.21.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -45,15 +45,15 @@
"copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
"framer-motion": "^14.9.0",
"framer-motion": "^14.10.0",
"hls.js": "^1.7.4",
"i18next": "^26.5.12",
"i18next-http-backend": "^4.0.7",
"i18next": "^26.7.2",
"i18next-http-backend": "^4.0.10",
"idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0",
"konva": "^10.9.0",
"konva": "^10.9.1",
"lodash": "^4.18.1",
"lucide-react": "^1.65.0",
"lucide-react": "^1.70.0",
"monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
@@ -65,7 +65,7 @@
"react-dropzone": "^20.1.4",
"react-grid-layout": "^2.3.0",
"react-hook-form": "^7.91.0",
"react-i18next": "^17.1.9",
"react-i18next": "^17.1.13",
"react-icons": "^5.7.0",
"react-konva": "^19.3.2",
"react-markdown": "^10.1.0",
@@ -81,19 +81,19 @@
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0",
"vaul": "^1.1.2",
"virtua": "^0.54.0",
"virtua": "^0.54.2",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.64.1",
"@playwright/test": "^1.65.0",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.27",
"@types/node": "^26.6.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"@vitejs/plugin-react": "^6.2.1",
"autoprefixer": "^10.6.2",
"esbuild": "^0.28.2",
"eslint": "^10.14.0",
@@ -101,7 +101,7 @@
"eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.13.0",
"globals": "^17.14.0",
"i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0",
"patch-package": "^8.0.1",
@@ -111,7 +111,7 @@
"tailwindcss": "^3.4.19",
"typescript": "^6.0.3",
"typescript-eslint": "^8.72.1",
"vite": "^8.3.5"
"vite": "^8.3.6"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -428,9 +428,9 @@
}
},
"node_modules/@cycjimmy/jsmpeg-player": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-6.1.8.tgz",
"integrity": "sha512-ccG7JvidQWWPvmmpu6Ob6TUnbyaz6WaTHl9V/5BD4vBVndxnxchLmVU3rpJguB2lWEoEAhFPBIU3GoeXtc3RaQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@cycjimmy/jsmpeg-player/-/jsmpeg-player-7.0.0.tgz",
"integrity": "sha512-aPKrOKaon5ncMSN5ec2O6yqiIotPQr0KsSchIC4pnTnebA30bq4D3hKk4g4JynZzSIqBRWhd2TnyZh1vvTXxEA==",
"license": "MIT"
},
"node_modules/@date-fns/tz": {
@@ -1059,15 +1059,14 @@
"license": "MIT"
},
"node_modules/@hookform/resolvers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-6.0.2.tgz",
"integrity": "sha512-e2dqUOlaGuirKVOd/iZWk/Yg5zeaGw7t0hSc3LzxL3o7eZGwZkTyEbBL8H52m0wRKEsNLWoMGOiXGHbPHTXlEQ==",
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-7.0.3.tgz",
"integrity": "sha512-beGY1JY6K5cyWlswD4BVf7UTb5MxSjKkjljrRyLPT3KIQzsTU0jOibBKqtdcKqIZYQUUyURed7roLWZtSdAtiQ==",
"license": "MIT",
"dependencies": {
"@standard-schema/utils": "^0.3.0"
},
"peerDependencies": {
"@sinclair/typebox": ">=0.25.24",
"@standard-schema/spec": "^1.0.0",
"@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0",
"ajv": "^8.12.0",
@@ -1087,15 +1086,13 @@
"react-hook-form": "^7.55.0",
"superstruct": ">=0.12.0",
"typanion": "^3.3.2",
"typebox": "^1.0.0",
"valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc",
"vest": ">=6.0.0",
"yup": "^1.0.0",
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"@sinclair/typebox": {
"optional": true
},
"@standard-schema/spec": {
"optional": true
},
@@ -1150,6 +1147,9 @@
"typanion": {
"optional": true
},
"typebox": {
"optional": true
},
"valibot": {
"optional": true
},
@@ -1691,9 +1691,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.155.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz",
"integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==",
"version": "0.156.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz",
"integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==",
"dev": true,
"license": "MIT",
"funding": {
@@ -1714,13 +1714,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.64.1.tgz",
"integrity": "sha512-IaYGml/nSOiZWROfPtuIL7neuu9qDOZ3TNvNljUIrhDMxiGTgPjzj/g9p9RxpS2tSepPDRY+UwAaEeiKtUvIAg==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.65.0.tgz",
"integrity": "sha512-iGURLGuKDLakSEl7Pgfv8h7f+XLT+V861Wfhz0zlEl5nyFuKUopdkFBPKqTllTUp06aOwPuH8iuUEb3yp3MpbQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.64.1"
"playwright": "1.65.0"
},
"bin": {
"playwright": "cli.js"
@@ -2903,47 +2903,47 @@
"license": "MIT"
},
"node_modules/@rjsf/core": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.10.1.tgz",
"integrity": "sha512-LHLbXYEq1+c2CpynBlf5JBlUhPNxOkVV+txf1BvamgJSt4QOisXFw5Uv+mBdLRi4BI+FfT0ofMVOgGWnuwG7gw==",
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@rjsf/core/-/core-6.11.1.tgz",
"integrity": "sha512-2kH39lspUUQf552l+vSTj9vnwdv1gSOyDKqSDmFuTo16+92zbJ381knrQpk3KKowjp/sbxu4qPiBptgXH6i4UQ==",
"license": "Apache-2.0",
"dependencies": {
"markdown-to-jsx": "^9.8.2"
"markdown-to-jsx": "^9.10.3"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@rjsf/utils": "^6.10.1",
"@rjsf/utils": "^6.11.1",
"react": ">=18"
}
},
"node_modules/@rjsf/shadcn": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.10.1.tgz",
"integrity": "sha512-URVLkYcf3V8hNo1ZvcwR9zdMx+2z2k5IhDpfmQqPqWrMvY469BL+Wgyp+P7PEF01JhlQA3GOB6EwfKTMTkpoAw==",
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@rjsf/shadcn/-/shadcn-6.11.1.tgz",
"integrity": "sha512-W00s4kLx5f4kerYtsE/kLLABqh3eg4Iih1D9llxKp0FDVnW6BEuJmKP8G9VRVphMdMQVkrZNeNBjlOE9Bc7ZDw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-radio-group": "^1.4.7",
"@radix-ui/react-separator": "^1.1.15",
"@radix-ui/react-slider": "^1.4.7",
"@radix-ui/react-slot": "^1.3.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^1.16.0",
"tailwind-merge": "^3.6.0"
"lucide-react": "^1.49.0",
"tailwind-merge": "^3.7.0"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@rjsf/core": "^6.10.1",
"@rjsf/utils": "^6.10.1",
"@rjsf/core": "^6.11.1",
"@rjsf/utils": "^6.11.1",
"react": ">=18"
}
},
@@ -2958,14 +2958,14 @@
}
},
"node_modules/@rjsf/utils": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.10.1.tgz",
"integrity": "sha512-dlbQUCoTl/+/TenyYs1zAKXf9sLTebtY+6pB+bUFsaN4xkdAtR8YBLwmpa1N5yNiRzf3fbxveISNuZmDrZE/yQ==",
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-6.11.1.tgz",
"integrity": "sha512-rHQ4jNkhws3YUjvM/6RyW78l0mVfnt6w2j3/giUcS5F0cendCLYykFuSnE2fQxBwmknv4ANMOugwXOgcIbfvnA==",
"license": "Apache-2.0",
"dependencies": {
"@x0k/json-schema-merge": "^1.0.6",
"fast-equals": "^6.0.0",
"fast-uri": "^4.1.4",
"@x0k/json-schema-merge": "^1.1.0",
"fast-equals": "^6.0.4",
"fast-uri": "^4.2.1",
"jsonpointer": "^5.0.1",
"react-is": "^18.3.1"
},
@@ -2977,9 +2977,9 @@
}
},
"node_modules/@rjsf/utils/node_modules/fast-equals": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.0.tgz",
"integrity": "sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.2.2.tgz",
"integrity": "sha512-n+XqoarWCgcar0O3om3DeNYx6xTuLWGTUF1bRZ9iuO00tsPepzP1L4V7rqtxxZ4PEya34UCESReimFvxmEVwIA==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -3002,9 +3002,9 @@
"license": "BSD-3-Clause"
},
"node_modules/@rjsf/validator-ajv8": {
"version": "6.10.1",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.10.1.tgz",
"integrity": "sha512-Jyim45UKd39SgWn4ryaydTigZ0dddNEeqZUfTlLgsy2v23uMDLyeKjNCItC09dy00dtKzwM3Q4WA0FtQ5IjOgA==",
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-6.11.1.tgz",
"integrity": "sha512-LV7ibrep/kaRzkwzhLAmJY9SHOIZuX9fQO+0VCfm0FOGXneGU9xQEJvHft7R84uqtDTTQZqRq40HcQjqY0rsyw==",
"license": "Apache-2.0",
"dependencies": {
"ajv": "^8.20.0",
@@ -3014,13 +3014,13 @@
"node": ">=20"
},
"peerDependencies": {
"@rjsf/utils": "^6.10.1"
"@rjsf/utils": "^6.11.1"
}
},
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
"integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz",
"integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==",
"cpu": [
"arm"
],
@@ -3035,9 +3035,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz",
"integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz",
"integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==",
"cpu": [
"arm64"
],
@@ -3052,9 +3052,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz",
"integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz",
"integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==",
"cpu": [
"arm64"
],
@@ -3069,9 +3069,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz",
"integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz",
"integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==",
"cpu": [
"x64"
],
@@ -3086,9 +3086,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz",
"integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz",
"integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==",
"cpu": [
"x64"
],
@@ -3103,9 +3103,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz",
"integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz",
"integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==",
"cpu": [
"arm"
],
@@ -3120,13 +3120,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz",
"integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz",
"integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3137,13 +3140,16 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz",
"integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz",
"integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==",
"cpu": [
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3154,13 +3160,16 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz",
"integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz",
"integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==",
"cpu": [
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3171,13 +3180,16 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz",
"integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz",
"integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==",
"cpu": [
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3188,13 +3200,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz",
"integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz",
"integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3205,13 +3220,16 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz",
"integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz",
"integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==",
"cpu": [
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -3222,9 +3240,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz",
"integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz",
"integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==",
"cpu": [
"arm64"
],
@@ -3239,9 +3257,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz",
"integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz",
"integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==",
"cpu": [
"arm64"
],
@@ -3256,9 +3274,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz",
"integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz",
"integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==",
"cpu": [
"x64"
],
@@ -3893,9 +3911,9 @@
"license": "ISC"
},
"node_modules/@vitejs/plugin-react": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.2.tgz",
"integrity": "sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==",
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.2.1.tgz",
"integrity": "sha512-9KyWCij3r0+qLG3Q62vBmv0VyevmYibAtxaCIVK9o0k6CXgaajZRIe8tpfchQcdZArJ5b/7OrIz4W3b2uxVRDA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3905,9 +3923,9 @@
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0 || ^0.3.0",
"babel-plugin-react-compiler": "^1.0.0",
"oxc-transform-react": "^0.152.0",
"oxc-transform-react": ">=0.152.0 <0.157.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
@@ -4063,9 +4081,9 @@
"license": "SEE LICENSE IN LICENSE.md"
},
"node_modules/apexcharts": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.6.0.tgz",
"integrity": "sha512-5524MLoDei2n8EcVMK+u3eOZTWrTpT1BIPRWu4AK8kkJdkODWs1hp3IEax8pMz78sSNAtsD4Tp7tr0s0SW/npQ==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-8.7.0.tgz",
"integrity": "sha512-HHpMlM4QvLXFLL6hyn1iivF+3IyoCrBEXYcV9elJuT+Dc4At9P5+RAaSYA1onOrcXg59+VTAsnyUDdm4GRhUfA==",
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"apex-commons": "^0.8.3"
@@ -5578,12 +5596,12 @@
}
},
"node_modules/framer-motion": {
"version": "14.9.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.9.0.tgz",
"integrity": "sha512-g1hZu8WoTwgsoVAaqEEcefhvR03cKGtkmrKaRrWpXhnM3LGR8jl1u2qyJ1PMVlO7LKQHE8TTRQYUlMmnrY4L+g==",
"version": "14.10.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-14.10.0.tgz",
"integrity": "sha512-e8MjIet+IJKohYxBj8TAk9Mg5cD0HiOX87qde1YTPL67gI2ao074O6yBtQnIcWCk+bTX7XX4litepufLiYPrLg==",
"license": "MIT",
"dependencies": {
"motion-dom": "14.9.0",
"motion-dom": "14.10.0",
"motion-utils": "14.7.0",
"tslib": "^2.4.0"
},
@@ -5786,9 +5804,9 @@
}
},
"node_modules/globals": {
"version": "17.13.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
"integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
"version": "17.14.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-17.14.0.tgz",
"integrity": "sha512-UtdkTVyAJePQv0J2M9Xe4L0ruNSDV314XiIJsADI2Zf40bcdAonE/D/OhAU8AT99oij9mhLByb+NvA3QWZYgCg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -5991,9 +6009,9 @@
}
},
"node_modules/i18next": {
"version": "26.5.12",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.5.12.tgz",
"integrity": "sha512-vTt8tc3y5axoU7wwTYg8vjpILLyP33a5GyGJAVI7FDRx1XueYxZVIkcVeStoftrVnAM/o28hKvK0sslucDI9aQ==",
"version": "26.7.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.7.2.tgz",
"integrity": "sha512-RsOY3BeaCDdkk52NeJSFAXELQ2rQXBO/tOub6L8ue2ICPfR/FRDWDOfEDg4xMPd1SqO5goVYQ6i72qWEUB/PPw==",
"funding": [
{
"type": "individual",
@@ -6106,9 +6124,9 @@
}
},
"node_modules/i18next-http-backend": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.7.tgz",
"integrity": "sha512-SQy3OPy66YzumNJ43boHDBtzzMISnoPhQ0Zqjetk+luW74Tok8Ez9ujJJbJHKSHkG5sjDGldHznjuPtSxBwvuw==",
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-4.0.10.tgz",
"integrity": "sha512-CfdQuwIVijbrS/fmiTAQYcYyje4nMbH9aIjKYhhm5bgVmWKWLOM1F7qrA1o63Jl1JxQcRjdtWhKlvDzyqV9aPg==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -6585,9 +6603,9 @@
}
},
"node_modules/konva": {
"version": "10.9.0",
"resolved": "https://registry.npmjs.org/konva/-/konva-10.9.0.tgz",
"integrity": "sha512-3NOOlcbimFDp4+/DfXGTBkfn3cn3VP1BESPySQ6ayCzgMA9JSMGJNNxeIKw5jpHL6SAl6gHsrv10DUEjNRL0Qg==",
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/konva/-/konva-10.9.1.tgz",
"integrity": "sha512-m49x+GatSxEUSvqahYhxpTjgKbtrLtw4P4o4B4iux953ZFpceaK52Ea+CFsAsd7W4I0Y5u6mMoRUsY/eltNZmw==",
"funding": [
{
"type": "patreon",
@@ -6978,9 +6996,9 @@
}
},
"node_modules/lucide-react": {
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.65.0.tgz",
"integrity": "sha512-roSdtzudEY39i2hn5LLjA8toYfhF2z4S10WSqZF4ubXpHlOmNTvaxMxZLdJfQDi6pEifQWX+cRmYihKSsvAxfA==",
"version": "1.70.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.70.0.tgz",
"integrity": "sha512-gCTPPN1MlQUBtUgdsUv+0CAUj+X8hifRUiRBw4E5v7lvDIOIpx0EZezdGQMSimS8pKA6esvullJub4GYr0mCHA==",
"license": "ISC",
"peerDependencies": {
"@types/react": "*",
@@ -7003,9 +7021,9 @@
}
},
"node_modules/markdown-to-jsx": {
"version": "9.10.2",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.10.2.tgz",
"integrity": "sha512-iR9GadlIox0q1uXnpqdxpF02Vb1WDmZ/QIXWjBR5htzjUEEhyIWbM65LuVKavdwJaVo4q95C/F2OOyNjWe91ig==",
"version": "9.10.5",
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-9.10.5.tgz",
"integrity": "sha512-m/OFdkxlmWY13o3R0Yy6STooQRzJ0Q5IqqDqQeHOb7pAA2LNgLplEI+jhDyMdxpdr1pceFH6ykrBWGD6oRvW/A==",
"license": "MIT",
"engines": {
"node": ">= 18"
@@ -8055,9 +8073,9 @@
}
},
"node_modules/motion-dom": {
"version": "14.9.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.9.0.tgz",
"integrity": "sha512-wGjKbNDMHd75aLStkxLCKZ+T8/wusZHklmNLK1Ak68UrdBt2X8I4yweYR/0WonxGnBsw6oloDTIV8pN+UTvzzA==",
"version": "14.10.0",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-14.10.0.tgz",
"integrity": "sha512-k0LwJZ1R5xCvqrScszO4ywuZP52lRcT3QO0IUovgczom9ZHb0BaRLOX6M2O40q7TrSLvzX/fIHHmDm8TUMt3fg==",
"license": "MIT",
"dependencies": {
"motion-utils": "14.7.0"
@@ -8519,13 +8537,13 @@
}
},
"node_modules/playwright": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.64.1.tgz",
"integrity": "sha512-HCrVhHr5QQuwYoswEUGTb7gGp522yflbv/ST8nF4sFWGuLjQCt17Q/EubpGGSNKYn1tsTCPZxqHhHCfVURu7UA==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.65.0.tgz",
"integrity": "sha512-F3AMWcR44qMBt4kq+6Kb6mX7bjGXshktKFOPULd7reED6ZJWevjbNRqysP7q+AATE9kdPEZBibex1BYsvpVoeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.64.1"
"playwright-core": "1.65.0"
},
"bin": {
"playwright": "cli.js"
@@ -8535,9 +8553,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.64.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.64.1.tgz",
"integrity": "sha512-BC4s8y6SEVA6Q1rSxZTto8hI1KUI3xXlHGZgvGqP4IqPdI/a6FX9H29IDNz7vKo9l21b6LfYosScPL6M3xxyDQ==",
"version": "1.65.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.65.0.tgz",
"integrity": "sha512-vNSKgH6YqtN7cdT/L334zo0a+YwkrpicyrHJlWWPIm/Q+Nn2SWInil01oT0mbu96U4GgoRtCYQD9CzJQgju+lA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -9059,9 +9077,9 @@
}
},
"node_modules/react-i18next": {
"version": "17.1.9",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.9.tgz",
"integrity": "sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==",
"version": "17.1.13",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.1.13.tgz",
"integrity": "sha512-ZxhOSlyKSKPtbUNPLwYMmE3w3bhoc0jaZJZ6bc7wGOT6cLbrbs//6m01HxCLtFdSjHvFpwFwoPhxxzIlHSs9cw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.7",
@@ -9477,13 +9495,13 @@
}
},
"node_modules/rolldown": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz",
"integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==",
"version": "1.2.16",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz",
"integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.155.0",
"@oxc-project/types": "=0.156.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -9493,21 +9511,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm-eabi": "1.2.15",
"@rolldown/binding-android-arm64": "1.2.15",
"@rolldown/binding-darwin-arm64": "1.2.15",
"@rolldown/binding-darwin-x64": "1.2.15",
"@rolldown/binding-freebsd-x64": "1.2.15",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.15",
"@rolldown/binding-linux-arm64-gnu": "1.2.15",
"@rolldown/binding-linux-arm64-musl": "1.2.15",
"@rolldown/binding-linux-ppc64-gnu": "1.2.15",
"@rolldown/binding-linux-s390x-gnu": "1.2.15",
"@rolldown/binding-linux-x64-gnu": "1.2.15",
"@rolldown/binding-linux-x64-musl": "1.2.15",
"@rolldown/binding-openharmony-arm64": "1.2.15",
"@rolldown/binding-win32-arm64-msvc": "1.2.15",
"@rolldown/binding-win32-x64-msvc": "1.2.15"
"@rolldown/binding-android-arm-eabi": "1.2.16",
"@rolldown/binding-android-arm64": "1.2.16",
"@rolldown/binding-darwin-arm64": "1.2.16",
"@rolldown/binding-darwin-x64": "1.2.16",
"@rolldown/binding-freebsd-x64": "1.2.16",
"@rolldown/binding-linux-arm-gnueabihf": "1.2.16",
"@rolldown/binding-linux-arm64-gnu": "1.2.16",
"@rolldown/binding-linux-arm64-musl": "1.2.16",
"@rolldown/binding-linux-ppc64-gnu": "1.2.16",
"@rolldown/binding-linux-s390x-gnu": "1.2.16",
"@rolldown/binding-linux-x64-gnu": "1.2.16",
"@rolldown/binding-linux-x64-musl": "1.2.16",
"@rolldown/binding-openharmony-arm64": "1.2.16",
"@rolldown/binding-win32-arm64-msvc": "1.2.16",
"@rolldown/binding-win32-x64-msvc": "1.2.16"
}
},
"node_modules/run-async": {
@@ -10364,9 +10382,9 @@
}
},
"node_modules/virtua": {
"version": "0.54.0",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.0.tgz",
"integrity": "sha512-8nGGrDhX61MDVeqgyIaWrJqhRm9z2Nqa1WDqt0tk1m+ZWrvLesMP0iL0vL6xNsLCF2jE2ohwY+PqufdbVmRgEw==",
"version": "0.54.2",
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.54.2.tgz",
"integrity": "sha512-lYCAxyXCGkhXVvuKv8a+LklZquy0E9dIObYcOUyo6nP4VSvLeiIsiz94mA3ZKYuM5egkke5RIi2nXi5OIATc7A==",
"license": "MIT",
"peerDependencies": {
"@angular/common": ">=20.0.0",
@@ -10402,16 +10420,16 @@
}
},
"node_modules/vite": {
"version": "8.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz",
"integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==",
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.3.6.tgz",
"integrity": "sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.33.0",
"picomatch": "^4.0.7",
"postcss": "^8.5.30",
"rolldown": "~1.2.14",
"postcss": "^8.5.32",
"rolldown": "~1.2.16",
"tinyglobby": "^0.2.17"
},
"bin": {
+17 -17
View File
@@ -21,8 +21,8 @@
"i18n:status": "i18next-cli status"
},
"dependencies": {
"@cycjimmy/jsmpeg-player": "^6.1.8",
"@hookform/resolvers": "^6.0.2",
"@cycjimmy/jsmpeg-player": "^7.0.0",
"@hookform/resolvers": "^7.0.3",
"@radix-ui/react-alert-dialog": "^1.2.1",
"@radix-ui/react-aspect-ratio": "^1.2.0",
"@radix-ui/react-checkbox": "^1.4.1",
@@ -45,11 +45,11 @@
"@radix-ui/react-toggle": "^1.2.1",
"@radix-ui/react-toggle-group": "^1.2.1",
"@radix-ui/react-tooltip": "^1.4.1",
"@rjsf/core": "^6.10.1",
"@rjsf/shadcn": "^6.10.1",
"@rjsf/core": "^6.11.1",
"@rjsf/shadcn": "^6.11.1",
"@rjsf/utils": "^6.10.1",
"@rjsf/validator-ajv8": "^6.10.1",
"apexcharts": "^8.6.0",
"@rjsf/validator-ajv8": "^6.11.1",
"apexcharts": "^8.7.0",
"axios": "^1.21.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -57,15 +57,15 @@
"copy-to-clipboard": "^4.0.2",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
"framer-motion": "^14.9.0",
"framer-motion": "^14.10.0",
"hls.js": "^1.7.4",
"i18next": "^26.5.12",
"i18next-http-backend": "^4.0.7",
"i18next": "^26.7.2",
"i18next-http-backend": "^4.0.10",
"idb-keyval": "^6.3.0",
"js-yaml": "^5.5.0",
"konva": "^10.9.0",
"konva": "^10.9.1",
"lodash": "^4.18.1",
"lucide-react": "^1.65.0",
"lucide-react": "^1.70.0",
"monaco-yaml": "^5.5.1",
"next-themes": "^0.4.6",
"nosleep.js": "^0.12.0",
@@ -77,7 +77,7 @@
"react-dropzone": "^20.1.4",
"react-grid-layout": "^2.3.0",
"react-hook-form": "^7.91.0",
"react-i18next": "^17.1.9",
"react-i18next": "^17.1.13",
"react-icons": "^5.7.0",
"react-konva": "^19.3.2",
"react-markdown": "^10.1.0",
@@ -93,19 +93,19 @@
"tailwindcss-animate": "^1.0.7",
"use-long-press": "^3.3.0",
"vaul": "^1.1.2",
"virtua": "^0.54.0",
"virtua": "^0.54.2",
"vite-plugin-monaco-editor": "^1.1.0",
"zod": "^4.6.7"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.64.1",
"@playwright/test": "^1.65.0",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.17.27",
"@types/node": "^26.6.6",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"@vitejs/plugin-react": "^6.2.1",
"autoprefixer": "^10.6.2",
"esbuild": "^0.28.2",
"eslint": "^10.14.0",
@@ -113,7 +113,7 @@
"eslint-plugin-prettier": "^5.5.7",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^17.13.0",
"globals": "^17.14.0",
"i18next-cli": "^1.5.11",
"monaco-editor": "^0.53.0",
"patch-package": "^8.0.1",
@@ -123,7 +123,7 @@
"tailwindcss": "^3.4.19",
"typescript": "^6.0.3",
"typescript-eslint": "^8.72.1",
"vite": "^8.3.5"
"vite": "^8.3.6"
},
"overrides": {
"@radix-ui/react-compose-refs": "1.1.2",
@@ -1289,8 +1289,6 @@
},
"apiKey": {
"title": "Ключ API Frigate+",
"validated": "Ключ API Frigate+ выяўлены і правераны",
"notValidated": "Ключ API Frigate+ не выяўлены або не правераны",
"desc": "Ключ API Frigate+ уключае інтэграцыю з сэрвісам Frigate+.",
"plusLink": "Падрабязней пра Frigate+"
},
@@ -1086,8 +1086,6 @@
},
"apiKey": {
"title": "Frigate+ API ključ",
"validated": "Frigate+ API ključ je detektovan i validiran",
"notValidated": "Frigate+ API ključ nije detektovan ili nije validiran",
"desc": "Frigate+ API ključ omogućava integraciju sa uslugom Frigate+.",
"plusLink": "Pročitajte više o Frigate+"
},
+8 -3
View File
@@ -714,9 +714,14 @@
"apiKey": {
"plusLink": "Llegeix més sobre Frigate+",
"title": "Clau API de Frigate+",
"validated": "La clau API de Frigate+ ha estat detectada i validada",
"notValidated": "La clau API de Frigate+ no ha estat detectada o no ha estat validada",
"desc": "La clau API de Frigate+ habilita la integració amb el servei Frigate+."
"desc": "La clau API de Frigate+ habilita la integració amb el servei Frigate+.",
"formatValid": "S'ha detectat la clau API Frigate+ i coincideix amb el format esperat",
"formatInvalid": "La clau API Frigate+ no es detecta o no coincideix amb el format esperat",
"validate": "Valida amb Frigate+",
"validating": "S'està validant…",
"serverValid": "Frigate+ ha acceptat aquesta clau API",
"serverInvalid": "Frigate+ ha rebutjat aquesta clau API",
"serverUnreachable": "No s'ha pogut arribar a Frigate+ per validar aquesta clau API"
},
"unsavedChanges": "Canvis dels paràmetres de Frigate+ sense desar",
"title": "Paràmetres de Frigate+",
@@ -58,9 +58,7 @@
"cleanCopyWarning": "Některé kamery mají snímky zakázané"
},
"apiKey": {
"notValidated": "API klíč Frigate+ nebyl rozpoznán nebo nebyl ověřen",
"plusLink": "Přečtěte si více o Frigate+",
"validated": "API klíč Frigate+ byl rozpoznán a ověřen",
"desc": "API klíč Frigate+ umožňuje integraci se službou Frigate+.",
"title": "Frigate+ API Klíč"
},
@@ -639,8 +639,6 @@
"apiKey": {
"title": "Frigate+ API Key",
"desc": "Der Frigate+ API Key aktiviert die Integration des Frigate+ Dienstes.",
"validated": "Frigate+ API Key erkannt und validiert",
"notValidated": "Frigate+ API Key nicht erkannt und validiert",
"plusLink": "Lese mehr zu Frigate+"
},
"snapshotConfig": {
+7 -2
View File
@@ -1285,8 +1285,13 @@
},
"apiKey": {
"title": "Frigate+ API Key",
"validated": "Frigate+ API key is detected and validated",
"notValidated": "Frigate+ API key is not detected or not validated",
"formatValid": "Frigate+ API key is detected and matches the expected format",
"formatInvalid": "Frigate+ API key is not detected or does not match the expected format",
"validate": "Validate with Frigate+",
"validating": "Validating…",
"serverValid": "Frigate+ accepted this API key",
"serverInvalid": "Frigate+ rejected this API key",
"serverUnreachable": "Unable to reach Frigate+ to validate this API key",
"desc": "The Frigate+ API key enables integration with the Frigate+ service.",
"plusLink": "Read more about Frigate+"
},
+1 -3
View File
@@ -621,10 +621,8 @@
"title": "Configuración de Frigate+",
"apiKey": {
"title": "Clave API de Frigate+",
"notValidated": "La clave API de Frigate+ no ha sido detectada o no ha sido validada",
"plusLink": "Lee más sobre Frigate+",
"desc": "La clave API de Frigate+ permite la integración con el servicio Frigate+.",
"validated": "La clave API de Frigate+ ha sido detectada y validada"
"desc": "La clave API de Frigate+ permite la integración con el servicio Frigate+."
},
"snapshotConfig": {
"title": "Configuración de instantáneas",
+2 -1
View File
@@ -76,7 +76,8 @@
"24hour": "dd. MMM yyyy, HH:mm"
},
"formattedTimestampMonthDay": "dd. MMM",
"never": "Mitte kunagi"
"never": "Mitte kunagi",
"custom": "Kohanadatud…"
},
"menu": {
"user": {
+1 -3
View File
@@ -1143,11 +1143,9 @@
},
"frigatePlus": {
"apiKey": {
"notValidated": "کلید API ‏Frigate+ شناسایی نشده یا معتبرسازی نشده است",
"desc": "کلید API ‏Frigate+ امکان یکپارچه‌سازی با سرویس Frigate+ را فراهم می‌کند.",
"plusLink": "دربارهٔ Frigate+ بیشتر بخوانید",
"title": "کلید API فرigate+",
"validated": "کلید API فرigate+ شناسایی و تأیید شد"
"title": "کلید API فرigate+"
},
"snapshotConfig": {
"title": "پیکربندی عکس فوری",
+2 -1
View File
@@ -82,7 +82,8 @@
"inProgress": "En cours",
"invalidStartTime": "Heure de début invalide",
"invalidEndTime": "Heure de fin invalide",
"never": "Jamais"
"never": "Jamais",
"custom": "Personnalisé…"
},
"button": {
"apply": "Appliquer",
+2 -2
View File
@@ -7,11 +7,11 @@
"label": "Supprimer le groupe de caméras",
"confirm": {
"title": "Confirmez la suppression",
"desc": "Êtes-vous sûr de vouloir supprimer le groupe de caméras <em>{{name}}</em> ?"
"desc": "Êtes-vous sûr de vouloir supprimer le groupe de caméras <em>{{name}}</em>?"
}
},
"name": {
"placeholder": "Saisissez un nom.",
"placeholder": "Saisissez un nom…",
"label": "Nom",
"errorMessage": {
"mustLeastCharacters": "Le nom du groupe de caméras doit comporter au moins 2 caractères.",
+5 -2
View File
@@ -87,7 +87,8 @@
"placeholder": "Sélectionner un dossier",
"newCaseOption": "Créer un nouveau cas",
"newCaseNamePlaceholder": "Nouveau nom de cas",
"newCaseDescriptionPlaceholder": "Description de cas"
"newCaseDescriptionPlaceholder": "Description de cas",
"nameRequired": "Nom du dossier requis"
},
"queueing": "Mise en file d'attente de l'export...",
"tabs": {
@@ -116,7 +117,9 @@
"selectWithActivity": "Caméras avec des objets suivis",
"selectGroup": "Sélectionner un groupe",
"noMatchingCameras": "Aucune caméra ne correspond à votre recherche",
"selectedCount": "{{selected}} / {{total}} sélectionnées"
"selectedCount": "{{selected}} / {{total}} sélectionnées",
"noCamerasSelected": "Sélectionner au moins une caméra",
"caseNameDefault": "Export Caméra multiple - {{date}}"
},
"multi": {
"title_one": "Export {{count}} revue",
+2 -2
View File
@@ -21,7 +21,7 @@
"title": "Toutes les dates",
"short": "Dates"
},
"selectPreset": "Sélectionnez un préréglage."
"selectPreset": "Sélectionnez un préréglage…"
},
"more": "Plus de filtres",
"reset": {
@@ -105,7 +105,7 @@
"allLogs": "Tous les journaux"
},
"recognizedLicensePlates": {
"placeholder": "Tapez pour rechercher des plaques d'immatriculation.",
"placeholder": "Tapez pour rechercher des plaques d'immatriculation…",
"noLicensePlatesFound": "Aucune plaque d'immatriculation trouvée",
"loading": "Chargement des plaques d'immatriculation reconnues…",
"title": "Plaques d'immatriculation reconnues",
+66 -7
View File
@@ -48,7 +48,7 @@
},
"labelmap": {
"label": "Customisation Audio",
"description": "Remplacements ou entrées de remappage à fusionner avec la table standard des libellés audio."
"description": "Remplacements ou réassignations à fusionner aux étiquettes audio standard."
}
},
"audio_transcription": {
@@ -78,8 +78,8 @@
"description": "Emplacement numérique contrôlant l'ordre de la caméra dans la disposition en vue à vol d'oiseau."
},
"modes": {
"label": "Types d'activité",
"description": "Types d’activité qui incluent des caméras dans Birdseye."
"label": "Types d'événements",
"description": "Types d'activité qui incluent les caméras dans Birdseye."
}
},
"detect": {
@@ -143,7 +143,7 @@
},
"scene": {
"label": "Détecter la scène",
"description": "L'environnement filmé par cette caméra, utilisé pour déterminer lequel des modèles configurés doit être exécuté sur celle-ci. Les caméras laissées sur « default » utilisent le modèle configuré avec une scène définie sur « default »."
"description": "L'environnement filmé par cette caméra, utilisé pour choisir le modèle configuré à exécuter. Les caméras laissées sur « default » exécutent le modèle configuré avec la scène « default »."
}
},
"face_recognition": {
@@ -240,6 +240,30 @@
"quality": {
"label": "Qualité de la diffusion en direct",
"description": "Qualité de l'encodage pour le flux jsmpeg (1 étant la plus élevée, 31 la plus faible)."
},
"transcode": {
"label": "Flux transcodés",
"description": "Flux en direct transcodé en qualité inférieur à la demande de go2rtc.",
"enabled": {
"label": "Activé les flux transcodés",
"description": "ajouter un flux en direct de qualité inférieur que go2rtc transcode en temps réel pendant que quelqu'un regarde."
},
"source": {
"label": "Flux source",
"description": "flux à décoder par go2rtc. Par défaut le premier flux en direct."
},
"qualities": {
"label": "Qualités",
"description": "Un flux transcodé est ajouté par qualité.",
"height": {
"label": "Taille",
"description": "Taille de sortie en pixels; le forma respect le ratio de la source."
},
"bitrate": {
"label": "Bitrate",
"description": "Cible et bitrate maximum de la vidéo en kbps."
}
}
}
},
"lpr": {
@@ -517,6 +541,41 @@
"enabled_in_config": {
"label": "État original de l'enregistrement",
"description": "Indique si l'enregistrement était activé dans la configuration statique originale."
},
"sub": {
"label": "Enregistrement flux secondaire",
"description": "Paramètres pour enregistrer un second flux, de qualité moindre.",
"enabled": {
"label": "Activer l'enregistrement des flux secondaires",
"description": "Activer l'enregistrement d'un flux secondaire, de qualité moindre pour une qualité adaptative et une rétention étendue."
},
"continuous": {
"label": "Rétention continue des flux secondaires",
"description": "Nombre de jour de rétention des enregistrements des flux secondaire sans prise en compte d'objet traqué ou de mouvement.",
"days": {
"label": "Jour de rétention",
"description": "Combien de jour conserver les enregistrements."
}
},
"motion": {
"label": "Rétention des flux secondaires liés au mouvement",
"description": "Nombre de jours de conservation des enregistrements des flux secondaires déclenchés par un mouvement.",
"days": {
"label": "Jours de rétention",
"description": "Combien de jours conserver les enregistrements."
}
},
"alerts": {
"label": "Alerte rétention flux secondaires",
"description": "Paramètres de conservation pour les flux secondaires liés aux alertes.",
"days": {
"label": "Jours de rétention",
"description": "Nombre de jour de conservation des enregistrements lié à la détection d'événements."
},
"mode": {
"label": "Mode de rétention"
}
}
}
},
"review": {
@@ -534,7 +593,7 @@
},
"required_zones": {
"label": "Zones requises",
"description": "Zones qu'un objet doit entrer pour être considéré comme une alerte. (Laisser vide afin d'autoriser n'importe quelle zone.)"
"description": "Zones dans lesquelles un objet doit entrer pour être considéré comme une alarme; laisser vide pour définir l'entièreté de la zone"
},
"enabled_in_config": {
"label": "État des alertes originales",
@@ -544,13 +603,13 @@
"label": "Temps de coupure des alertes",
"description": "Délai en secondes après l'absence d'activité déclenchant une alerte avant de la couper."
},
"description": "Paramètres pour lesquels les objets suivis génèrent des alertes et comment ces alertes sont conservées."
"description": "Paramètres définissant quels objets suivis génèrent des alertes et comment ces alertes sont conservées."
},
"detections": {
"label": "Configuration des détections",
"description": "Paramètres pour lesquels les objets suivis génèrent des détections (hors alerte) et comment les détections sont conservées.",
"enabled": {
"label": "Activer les détections",
"label": "Activation des détections",
"description": "Activer ou désactiver les événements de détection pour cette caméra."
},
"labels": {
+68 -7
View File
@@ -88,8 +88,8 @@
"label": "Qualité d'encodage"
},
"modes": {
"label": "Types d'activité",
"description": "Types d’activité qui incluent des caméras dans Birdseye."
"label": "Types d'événements",
"description": "Types d'activité qui incluent les caméras dans Birdseye."
}
},
"detect": {
@@ -98,7 +98,7 @@
},
"scene": {
"label": "Détecter la scène",
"description": "L'environnement filmé par cette caméra, utilisé pour déterminer lequel des modèles configurés doit être exécuté sur celle-ci. Les caméras laissées sur « default » utilisent le modèle configuré avec une scène définie sur « default »."
"description": "L'environnement filmé par cette caméra, utilisé pour choisir le modèle configuré à exécuter. Les caméras laissées sur « default » exécutent le modèle configuré avec la scène « default »."
}
},
"motion": {
@@ -222,7 +222,7 @@
},
"labelmap": {
"label": "Customisation Audio",
"description": "Remplacements ou entrées de remappage à fusionner avec la table standard des libellés audio."
"description": "Remplacements ou réassignations à fusionner aux étiquettes audio standard."
}
},
"ffmpeg": {
@@ -341,6 +341,41 @@
"enabled_in_config": {
"label": "État original de l'enregistrement",
"description": "Indique si l'enregistrement était activé dans la configuration statique originale."
},
"sub": {
"label": "Enregistrement flux secondaire",
"description": "Paramètres pour enregistrer un second flux, de qualité moindre.",
"enabled": {
"label": "Activer l'enregistrement des flux secondaires",
"description": "Activer l'enregistrement d'un flux secondaire, de qualité moindre pour une qualité adaptative et une rétention étendue."
},
"continuous": {
"label": "Rétention continue des flux secondaires",
"description": "Nombre de jour de rétention des enregistrements des flux secondaire sans prise en compte d'objet traqué ou de mouvement.",
"days": {
"label": "Jour de rétention",
"description": "Combien de jour conserver les enregistrements."
}
},
"motion": {
"label": "Rétention des flux secondaires liés au mouvement",
"description": "Nombre de jours de conservation des enregistrements des flux secondaires déclenchés par un mouvement.",
"days": {
"label": "Jours de rétention",
"description": "Combien de jours conserver les enregistrements."
}
},
"alerts": {
"label": "Alerte rétention flux secondaires",
"description": "Paramètres de conservation pour les flux secondaires liés aux alertes.",
"days": {
"label": "Jours de rétention",
"description": "Nombre de jour de conservation des enregistrements lié à la détection d'événements."
},
"mode": {
"label": "Mode de rétention"
}
}
}
},
"review": {
@@ -356,7 +391,7 @@
},
"required_zones": {
"label": "Zones requises",
"description": "Zones qu'un objet doit entrer pour être considéré comme une alerte. (Laisser vide afin d'autoriser n'importe quelle zone.)"
"description": "Zones dans lesquelles un objet doit entrer pour être considéré comme une alarme; laisser vide pour définir l'entièreté de la zone"
},
"enabled_in_config": {
"label": "État des alertes originales",
@@ -366,13 +401,13 @@
"label": "Temps de coupure des alertes",
"description": "Délai en secondes après l'absence d'activité déclenchant une alerte avant de la couper."
},
"description": "Paramètres pour lesquels les objets suivis génèrent des alertes et comment ces alertes sont conservées."
"description": "Paramètres définissant quels objets suivis génèrent des alertes et comment ces alertes sont conservées."
},
"detections": {
"label": "Configuration des détections",
"description": "Paramètres pour lesquels les objets suivis génèrent des détections (hors alerte) et comment les détections sont conservées.",
"enabled": {
"label": "Activer les détections"
"label": "Activation des détections"
},
"labels": {
"label": "Étiquettes de détection",
@@ -415,5 +450,31 @@
"description": "Une liste de préoccupations ou de notes supplémentaires que la GenAI devrait prendre en compte lors de l'évaluation de l'activité sur cette caméra."
}
}
},
"live": {
"transcode": {
"label": "Flux transcodés",
"description": "Flux en direct transcodé en qualité inférieur à la demande de go2rtc.",
"enabled": {
"label": "Activé les flux transcodés",
"description": "ajouter un flux en direct de qualité inférieur que go2rtc transcode en temps réel pendant que quelqu'un regarde."
},
"source": {
"label": "Flux source",
"description": "flux à décoder par go2rtc. Par défaut le premier flux en direct."
},
"qualities": {
"label": "Qualités",
"description": "Un flux transcodé est ajouté par qualité.",
"height": {
"label": "Taille",
"description": "Taille de sortie en pixels; le forma respect le ratio de la source."
},
"bitrate": {
"label": "Bitrate",
"description": "Cible et bitrate maximum de la vidéo en kbps."
}
}
}
}
}
@@ -209,9 +209,7 @@
},
"frigatePlus": {
"apiKey": {
"notValidated": "La clé API Frigate+ n'est pas détectée ou n'est pas validée.",
"title": "Clé API Frigate+",
"validated": "La clé API Frigate+ est détectée et validée",
"desc": "La clé API Frigate+ permet l'intégration avec le service Frigate+.",
"plusLink": "En savoir plus sur Frigate+"
},
@@ -573,8 +573,6 @@
"title": "הגדרות +Frigate",
"apiKey": {
"title": "Frigate+ API מפתח",
"validated": "מפתח ה-API של Frigate+ זוהה ואושר",
"notValidated": "מפתח ה-API של Frigate+ לא זוהה או לא אומת",
"desc": "מפתח ה-API של Frigate+ מאפשר אינטגרציה עם שירות Frigate+.",
"plusLink": "קרא עוד על Frigate+"
},
+3 -1
View File
@@ -141,5 +141,7 @@
"aircraft": "विमान",
"bicycle": "साइकिल",
"bellow": "गर्जना करना",
"motorcycle": "मोटरसाइकिल"
"motorcycle": "मोटरसाइकिल",
"snicker": "खिलखिलाना",
"choir": "गायक-वृन्द"
}
@@ -897,8 +897,6 @@
"title": "Frigate+ postavke",
"apiKey": {
"title": "Frigate+ API ključ",
"validated": "Frigate+ API ključ je detektiran i validiran",
"notValidated": "Frigate+ API ključ nije detektiran ili nije validiran",
"desc": "Frigate+ API ključ omogućuje integraciju s Frigate+ uslugom.",
"plusLink": "Pročitajte više o Frigate+"
},
@@ -246,8 +246,6 @@
"apiKey": {
"plusLink": "Tudjon meg többet a Frigate+-ról",
"title": "Frigate+ API kulcs",
"validated": "Frigate+ API kulcs észlelve és ellenőrizve",
"notValidated": "Frigate+ API kulcs nem található vagy nem ellenőrzött",
"desc": "A Frigate+ API kulcs lehetővé teszi az integrációt a Frigate+ szolgáltatással."
},
"snapshotConfig": {
@@ -1310,8 +1310,6 @@
},
"apiKey": {
"title": "Kunci API Frigate+",
"validated": "Kunci API Frigate+ terdeteksi dan tervalidasi",
"notValidated": "Kunci API Frigate+ tidak terdeteksi atau tidak tervalidasi",
"desc": "Kunci API Frigate+ memungkinkan integrasi dengan layanan Frigate+.",
"plusLink": "Baca lebih lanjut tentang Frigate+"
},
@@ -29,8 +29,6 @@
},
"apiKey": {
"title": "Chiave API Frigate+",
"validated": "La chiave API Frigate+ è stata rilevata e convalidata",
"notValidated": "La chiave API Frigate+ non è stata rilevata o non è stata convalidata",
"desc": "La chiave API Frigate+ consente l'integrazione con il servizio Frigate+.",
"plusLink": "Scopri di più su Frigate+"
},
@@ -734,8 +734,6 @@
"title": "Frigate+ 設定",
"apiKey": {
"title": "Frigate+ API キー",
"validated": "Frigate+ API キーが検出され、検証されました",
"notValidated": "Frigate+ API キーが検出されないか、検証されていません",
"desc": "Frigate+ API キーは Frigate+ サービスとの統合を有効にします。",
"plusLink": "Frigate+ の詳細を読む"
},
@@ -1193,8 +1193,6 @@
},
"apiKey": {
"title": "Frigate+ API Key",
"validated": "Frigate+ API key ត្រូវបានរកឃើញ និងផ្ទៀងផ្ទាត់",
"notValidated": "Frigate+ API key មិនត្រូវបានរកឃើញ ឬមិនបានផ្ទៀងផ្ទាត់",
"desc": "Frigate+ API key អនុញ្ញាតឱ្យមានការរួមបញ្ចូលជាមួយសេវាកម្ម Frigate+។",
"plusLink": "អានបន្ថែមអំពី Frigate+"
},
@@ -1373,8 +1373,6 @@
},
"apiKey": {
"title": "Frigate+ API 키",
"validated": "Frigate+ API 키가 확인 및 검증되었습니다",
"notValidated": "Frigate+ API 키가 감지되지 않았거나 유효하지 않습니다",
"desc": "Frigate+ API 키를 통해 Frigate+ 서비스와의 연동을 활성화합니다.",
"plusLink": "Frigate+ 자세히 알아보기"
},
@@ -657,8 +657,6 @@
"title": "Frigate+ Nustatymai",
"apiKey": {
"title": "Frigate+ API raktas",
"validated": "Frigate+ API raktas aptiktas ir patvirtintas",
"notValidated": "Frigate+ API raktas neaptiktas ir nepatvirtintas",
"desc": "Frigate+ API raktas įgalina integraciją su Frigate+ paslauga.",
"plusLink": "Skaityti daugiau apie Frigate+"
},
@@ -616,9 +616,7 @@
},
"frigatePlus": {
"apiKey": {
"notValidated": "Frigate+ API-nøkkel er ikke detektert eller validert",
"title": "Frigate+ API-nøkkel",
"validated": "Frigate+ API-nøkkel er detektert og validert",
"desc": "Frigate+ API-nøkkelen muliggjør integrasjon med Frigate+ tjenesten.",
"plusLink": "Les mer om Frigate+"
},
+1 -3
View File
@@ -618,9 +618,7 @@
"apiKey": {
"title": "Frigate+ API-sleutel",
"plusLink": "Lees meer over Frigate+",
"validated": "Frigate+ API-sleutel is gedetecteerd en gevalideerd",
"desc": "Met de Frigate+ API-sleutel is integratie met de Frigate+ service mogelijk.",
"notValidated": "Frigate+ API-sleutel wordt niet gedetecteerd of niet gevalideerd"
"desc": "Met de Frigate+ API-sleutel is integratie met de Frigate+ service mogelijk."
},
"snapshotConfig": {
"title": "Snapshot-configuratie",
@@ -621,9 +621,7 @@
"title": "Ustawienia Frigate+",
"apiKey": {
"title": "Klucz API Frigate+",
"validated": "Klucz API Frigate+ został wykryty i zweryfikowany",
"plusLink": "Dowiedz się więcej o Frigate+",
"notValidated": "Klucz API Frigate+ nie został wykryty lub nie został zweryfikowany",
"desc": "Klucz API Frigate+ umożliwia integrację z usługą Frigate+."
},
"snapshotConfig": {
@@ -719,8 +719,6 @@
"title": "Configurações do Frigate+",
"apiKey": {
"title": "Chave de API do Frigate+",
"validated": "A chave de API do Frigate+ detectada e validada",
"notValidated": "Chave de API do Frigate+ não detectada ou não validada",
"desc": "A chave de API do Frigate+ habilita a integração com o serviço do Frigate+.",
"plusLink": "Leia mais sobre o Frigate+"
},
@@ -173,8 +173,6 @@
},
"title": "Configurações Frigate+",
"apiKey": {
"validated": "A chave da API do Frigate+ foi detectada e validada",
"notValidated": "A chave da API do Frigate+ não foi detectada ou não foi validada",
"desc": "A chave de API do Frigate+ permite a integração com o serviço Frigate+.",
"plusLink": "Saiba mais sobre o Frigate+",
"title": "Chave de API do Frigate+"
+7 -2
View File
@@ -715,9 +715,14 @@
"apiKey": {
"plusLink": "Citește mai multe despre Frigate+",
"desc": "Cheia API Frigate+ permite integrarea cu serviciul Frigate+.",
"validated": "Cheia API Frigate+ a fost detectată și validată",
"title": "Cheie API Frigate+",
"notValidated": "Cheia API Frigate+ nu este detectată sau validată"
"formatValid": "Cheia API Frigate+ este detectată și corespunde formatului așteptat",
"formatInvalid": "Cheia API Frigate+ nu este detectată sau nu corespunde formatului așteptat",
"validate": "Validează cu Frigate+",
"validating": "Se validează…",
"serverValid": "Frigate+ a acceptat această cheie API",
"serverInvalid": "Frigate+ a respins această cheie API",
"serverUnreachable": "Nu s-a putut contacta Frigate+ pentru a valida această cheie API"
},
"snapshotConfig": {
"title": "Configurare snapshot-uri",
@@ -631,8 +631,6 @@
"title": "Настройки Frigate+",
"apiKey": {
"title": "Ключ API Frigate+",
"validated": "Ключ API Frigate+ найден и проверен",
"notValidated": "Ключ API Frigate+ не найден или не проверен",
"desc": "Ключ API Frigate+ включает интеграцию с сервисом Frigate+.",
"plusLink": "Подробнее про Frigate+"
},
@@ -883,8 +883,6 @@
"title": "Nastavenie Frigate+",
"apiKey": {
"title": "Frigate + API kľúč",
"validated": "Frigate + API kľúč je detekovaný a overený",
"notValidated": "Frigate + API kľúč nie je detekovaný alebo nie je overený",
"desc": "Frigate+ API kľúč umožňuje integráciu s Frigate+ služby.",
"plusLink": "Prečítajte si viac o Frigate+"
},
@@ -1142,8 +1142,6 @@
},
"apiKey": {
"title": "Frigate+ API ključ",
"validated": "API ključ je zaznan in potrjen",
"notValidated": "API ključ ni zaznan ali ni veljaven",
"desc": "API ključ omogoča integracijo s storitvijo Frigate+.",
"plusLink": "Preberi več o Frigate+"
},
+368 -1
View File
@@ -1 +1,368 @@
{}
{
"time": {
"untilForTime": "Do {{time}}",
"untilForRestart": "Dok se Frigate ne ponovo pokrene.",
"untilRestart": "Do ponovnog pokretanja",
"never": "Nikada",
"ago": "Pre {{timeAgo}}",
"justNow": "Upravo sada",
"today": "Danas",
"yesterday": "Juče",
"last7": "Poslednjih 7 dana",
"last14": "Poslednjih 14 dana",
"last30": "Poslednjih 30 dana",
"thisWeek": "Ove nedelje",
"lastWeek": "Prošle nedelje",
"thisMonth": "Ovog meseca",
"lastMonth": "Prošlog meseca",
"5minutes": "5 minuta",
"10minutes": "10 minuta",
"30minutes": "30 minuta",
"1hour": "1 sat",
"12hours": "12 sati",
"24hours": "24 sata",
"custom": "Prilagođeno...",
"pm": "popodne",
"am": "pre podne",
"yr": "{{time}} god.",
"year_one": "{{time}} godina",
"year_few": "{{time}} godine",
"year_other": "{{time}} godina",
"mo": "{{time}} mes.",
"month_one": "{{time}} mesec",
"month_few": "{{time}} meseca",
"month_other": "{{time}} meseci",
"d": "{{time}} d.",
"day_one": "{{time}} dan",
"day_few": "{{time}} dana",
"day_other": "{{time}} dana",
"h": "{{time}} č.",
"hour_one": "{{time}} sat",
"hour_few": "{{time}} sata",
"hour_other": "{{time}} sati",
"m": "{{time}} min.",
"minute_one": "{{time}} minut",
"minute_few": "{{time}} minuta",
"minute_other": "{{time}} minuta",
"s": "{{time}} s",
"second_one": "{{time}} sekunda",
"second_few": "{{time}} sekunde",
"second_other": "{{time}} sekundi",
"formattedTimestamp": {
"12hour": "d. MMM, h:mm:ss aaa",
"24hour": "d. MMM, HH:mm:ss"
},
"formattedTimestamp2": {
"12hour": "dd.MM. h:mm:ssa",
"24hour": "d. MMM, HH:mm:ss"
},
"formattedTimestampHourMinute": {
"12hour": "h:mm aaa",
"24hour": "HH:mm"
},
"formattedTimestampHourMinuteSecond": {
"12hour": "h:mm:ss aaa",
"24hour": "HH:mm:ss"
},
"formattedTimestampMonthDayHourMinute": {
"12hour": "d. MMM, h:mm aaa",
"24hour": "d. MMM, HH:mm"
},
"formattedTimestampMonthDayYear": {
"12hour": "d. MMM yyyy.",
"24hour": "d. MMM yyyy."
},
"formattedTimestampMonthDayYearHourMinute": {
"12hour": "d. MMM yyyy, h:mm aaa",
"24hour": "d. MMM yyyy, HH:mm"
},
"formattedTimestampMonthDay": "d. MMM",
"formattedTimestampFilename": {
"12hour": "dd-MM-yy-h-mm-ss-a",
"24hour": "dd-MM-yy-HH-mm-ss"
},
"inProgress": "U toku",
"invalidStartTime": "Nevažeće vreme početka",
"invalidEndTime": "Nevažeće vreme završetka"
},
"commandMenu": {
"action": {
"toggleTheme": "Prebaci na svetli ili tamni režim"
},
"camera": {
"review": "Pregled",
"live": "Uživo"
},
"section": {
"actions": "Akcije",
"settings": "Podešavanja",
"cameraGroups": "Grupe kamera",
"cameras": "Kamere",
"pages": "Stranice",
"recent": "Nedavno"
},
"hint": "{{key}} ili / za otvaranje, Enter za pokretanje",
"empty": "Nema rezultata",
"placeholder": "Pretražite stranice, kamere, podešavanja i akcije",
"description": "Pretražite stranice, kamere, podešavanja i akcije",
"title": "Komandni meni"
},
"credentialField": {
"savedPlaceholder": "Sačuvano — ostavite prazno da zadržite trenutno stanje"
},
"validation_errors": "Greške pri validaciji",
"unit": {
"speed": {
"kph": "km/h",
"mph": "mph"
},
"length": {
"feet": "stopa",
"meters": "metri"
},
"data": {
"kbps": "kB/s",
"mbps": "MB/s",
"gbps": "GB/s",
"kbph": "kB/sat",
"mbph": "MB/sat",
"gbph": "GB/sat"
}
},
"label": {
"back": "Idi nazad",
"hide": "Sakrij {{item}}",
"show": "Prikaži {{item}}",
"ID": "ID",
"none": "Ništa",
"all": "Sve",
"other": "Ostalo"
},
"list": {
"two": "{{0}} i {{1}}",
"many": "{{items}} i {{last}}",
"separatorWithSpace": ", "
},
"field": {
"optional": "Opciono",
"internalID": "Interni ID koji Frigate koristi u konfiguraciji i bazi podataka"
},
"button": {
"add": "Dodaj",
"apply": "Primeni",
"applying": "Primenjuje se…",
"reset": "Resetuj",
"undo": "Opozovi",
"done": "Završeno",
"enabled": "Omogućeno",
"enable": "Omogući",
"disabled": "Onemogućeno",
"disable": "Onemogući",
"save": "Sačuvaj",
"saving": "Čuvanje…",
"cancel": "Otkaži",
"close": "Zatvori",
"expand": "Proširi",
"collapse": "Skupi",
"clear": "Očisti",
"copy": "Kopiraj",
"copiedToClipboard": "Kopirano u privremenu memoriju",
"back": "Nazad",
"history": "Istorija",
"fullscreen": "Ceo ekran",
"exitFullscreen": "Izađi iz celog ekrana",
"pictureInPicture": "Slika u slici",
"twoWayTalk": "Dvosmerna komunikacija",
"cameraAudio": "Zvuk kamere",
"on": "Uključeno",
"off": "Isključeno",
"edit": "Izmeni",
"copyCoordinates": "Kopiraj koordinate",
"delete": "Obriši",
"yes": "Da",
"no": "Ne",
"download": "Preuzmi",
"info": "Informacije",
"suspended": "Suspendovano",
"unsuspended": "Aktiviraj ponovo",
"play": "Pusti",
"unselect": "Poništi izbor",
"export": "Izvezi",
"deleteNow": "Obriši odmah",
"next": "Sledeće",
"continue": "Nastavi",
"modified": "Izmenjeno",
"overridden": "Nadjačano",
"resetToGlobal": "Vrati na globalna podešavanja",
"resetToDefault": "Vrati na podrazumevano",
"saveAll": "Sačuvaj sve",
"savingAll": "Čuvanje svega…",
"undoAll": "Opozovi sve",
"retry": "Pokušaj ponovo",
"reload": "Osveži"
},
"menu": {
"system": "Sistem",
"systemMetrics": "Stanje i metrike",
"configuration": "Konfiguracija",
"systemLogs": "Dnevnik",
"profiles": "Profili",
"settings": "Podešavanja",
"configurationEditor": "Editor konfiguracije",
"languages": "Jezici",
"language": {
"en": "Engleski (English)",
"es": "Španski (Español)",
"zhCN": "Pojednostavljeni kineski (简体中文)",
"zhHant": "Tradicionalni kineski (繁體中文)",
"hi": "Hindi (हिन्दी)",
"fr": "Francuski (Français)",
"ar": "Arapski (العربية)",
"pt": "Portugalski (Português)",
"ptBR": "Brazilski portugalski (Português brasileiro)",
"ru": "Ruski (Русский)",
"de": "Nemački (Deutsch)",
"ja": "Japanski (日本語)",
"tr": "Turski (Türkçe)",
"it": "Italijanski (Italiano)",
"nl": "Holandski (Nederlands)",
"sv": "Švedski (Svenska)",
"cs": "Češki (Čeština)",
"nb": "Norveški bukmol (Norsk Bokmål)",
"ko": "Korejski (한국어)",
"vi": "Vijetnamski (Tiếng Việt)",
"fa": "Persijski (فارسی)",
"pl": "Poljski (Polski)",
"uk": "Ukrajinski (Українська)",
"be": "Beloruski (Беларуская)",
"he": "Hebrejski (עברית)",
"el": "Grčki (Ελληνικά)",
"ro": "Rumunski (Română)",
"hu": "Mađarski (Magyar)",
"fi": "Finski (Suomi)",
"da": "Danski (Dansk)",
"sk": "Slovački (Slovenčina)",
"yue": "Kantonski (粵語)",
"th": "Tajlandski (ไทย)",
"ca": "Katalonski (Català)",
"hr": "Hrvatski (Hrvatski)",
"bs": "Bosanski (Bosanski)",
"sr": "Srpski (Српски)",
"sl": "Slovenački (Slovenščina)",
"lt": "Litvanski (Lietuvių)",
"bg": "Bugarski (Български)",
"gl": "Galicijski (Galego)",
"id": "Indonežanski (Bahasa Indonesia)",
"ur": "Urdu (اردو)",
"withSystem": {
"label": "Koristi sistemska podešavanja za jezik"
}
},
"appearance": "Izgled",
"darkMode": {
"label": "Tamni režim",
"light": "Svetli",
"dark": "Tamni",
"withSystem": {
"label": "Koristi sistemska podešavanja za svetli ili tamni režim"
}
},
"withSystem": "Sistem",
"theme": {
"label": "Tema",
"blue": "Plava",
"green": "Zelena",
"nord": "Nord",
"red": "Crvena",
"highcontrast": "Visok kontrast",
"default": "Podrazumevano"
},
"help": "Pomoć",
"documentation": {
"title": "Dokumentacija",
"label": "Frigate dokumentacija"
},
"restart": "Ponovo pokreni Frigate",
"live": {
"title": "Uživo",
"allCameras": "Sve kamere",
"cameras": {
"title": "Kamere",
"count_one": "{{count}} kamera",
"count_few": "{{count}} kamere",
"count_other": "{{count}} kamera"
}
},
"review": "Pregled",
"explore": "Istraži",
"export": "Izvezi",
"actions": "Akcije",
"uiPlayground": "Korisničko okruženje za testiranje",
"features": "Funkcije",
"faceLibrary": "Biblioteka lica",
"classification": "Klasifikacija",
"chat": "Ćaskanje",
"user": {
"title": "Korisnik",
"account": "Nalog",
"current": "Trenutni korisnik: {{user}}",
"anonymous": "anoniman",
"logout": "Odjavi se",
"setPassword": "Postavi lozinku"
}
},
"toast": {
"copyUrlToClipboard": "URL je kopiran u privremenu memoriju.",
"save": {
"title": "Sačuvaj",
"error": {
"title": "Neuspešno čuvanje promena u konfiguraciji: {{errorMessage}}",
"noMessage": "Neuspešno čuvanje promena u konfiguraciji"
},
"success": "Promene u konfiguraciji su uspešno sačuvane."
}
},
"role": {
"title": "Uloga",
"admin": "Administrator",
"viewer": "Posmatrač",
"desc": "Administratori imaju potpun pristup svim funkcijama u Frigate korisničkom interfejsu. Gledaci su ograničeni na gledanje kamera, pregled stavki i istorijskih snimaka u korisničkom interfejsu."
},
"pagination": {
"label": "straničenje",
"previous": {
"title": "Prethodna",
"label": "Idi na prethodnu stranicu"
},
"next": {
"title": "Sledeća",
"label": "Idi na sledeću stranicu"
},
"more": "Više stranica"
},
"accessDenied": {
"documentTitle": "Pristup odbijen - Frigate",
"title": "Pristup odbijen",
"desc": "Nemate dozvolu za pregled ove stranice."
},
"notFound": {
"documentTitle": "Nije pronađeno - Frigate",
"title": "404",
"desc": "Stranica nije pronađena"
},
"error": {
"title": "Ova stranica je prestala sa radom",
"desc": "Došlo je do neočekivane greške na stranici. Osvežavanje obično rešava problem. Ako se greška ponavlja, kopirajte detalje i priložite ih diskusiji na GitHub-u.",
"staleTitle": "Potrebno je osvežavanje",
"staleDesc": "Deo aplikacije nije mogao da se učita, što obično znači da je Frigate ažuriran dok je ova kartica bila otvorena. Osvežite stranicu da biste prešli na novu verziju.",
"partial": "Deo aplikacije je prestao sa radom.",
"copyDetails": "Kopiraj detalje",
"copyFailed": "Neuspešno kopiranje detalja u privremenu memoriju"
},
"selectItem": "Izaberi {{item}}",
"readTheDocumentation": "Pročitaj dokumentaciju",
"information": {
"pixels": "{{area}} px"
},
"no_items": "Nema elemenata"
}
@@ -1 +1,90 @@
{}
{
"group": {
"label": "Grupe kamera",
"add": "Dodaj grupu kamera",
"showAll": "Prikaži sve grupe kamera",
"showLess": "Prikaži manje",
"edit": "Izmeni grupu kamera",
"editGroups": "Izmeni grupe kamera",
"delete": {
"label": "Obriši grupu kamera",
"confirm": {
"title": "Potvrdite brisanje",
"desc": "Da li ste sigurni da želite da obrišete grupu kamera <em>{{name}}</em>?"
}
},
"name": {
"label": "Naziv",
"placeholder": "Unesite naziv…",
"errorMessage": {
"mustLeastCharacters": "Naziv grupe kamera mora imati najmanje 2 karaktera.",
"exists": "Naziv grupe kamera već postoji.",
"nameMustNotPeriod": "Naziv grupe kamera ne sme da sadrži tačku.",
"invalid": "Nevažeći naziv grupe kamera."
}
},
"cameras": {
"label": "Kamere",
"desc": "Izaberite kamere za ovu grupu."
},
"icon": "Ikona",
"success": "Grupa kamera ({{name}}) je sačuvana.",
"camera": {
"birdseye": "Birdseye",
"setting": {
"label": "Podešavanja strimovanja kamere",
"title": "Podešavanja strimovanja za kameru {{cameraName}}",
"desc": "Promenite opcije strimovanja uživo za kontrolnu tablu ove grupe kamera. <em>Ova podešavanja su specifična za uređaj/veb-pregledač.</em>",
"audioIsAvailable": "Zvuk je dostupan za ovaj strim",
"audioIsUnavailable": "Zvuk je nedostupan za ovaj strim",
"audio": {
"tips": {
"title": "Zvuk mora biti poslat sa vaše kamere i konfigurisan u go2rtc-u za ovaj strim."
}
},
"stream": "Strim",
"placeholder": "Izaberite strim",
"streamMethod": {
"label": "Metod strimovanja",
"placeholder": "Izaberite metod strimovanja",
"method": {
"noStreaming": {
"label": "Bez strimovanja",
"desc": "Slike sa kamere će se osvežavati samo jednom u minuti i neće biti strimovanja uživo."
},
"smartStreaming": {
"label": "Pametno strimovanje (preporučeno)",
"desc": "Pametno strimovanje će osvežavati sliku sa vaše kamere jednom u minuti kada nema uočljivih aktivnosti, kako bi se uštedeo protok podataka i resursi. Kada se detektuje aktivnost, prikaz se neprimetno prebacuje na strim uživo."
},
"continuousStreaming": {
"label": "Neprekidno strimovanje",
"desc": {
"title": "Slika sa kamere će uvek biti strim uživo kada je vidljiva na kontrolnoj tabli, čak i ako se ne detektuje nikakva aktivnost.",
"warning": "Neprekidno strimovanje može izazvati visoku potrošnju protoka podataka i probleme sa performansama. Koristite oprezno."
}
}
}
},
"compatibilityMode": {
"label": "Režim kompatibilnosti",
"desc": "Omogućite ovu opciju samo ako strim uživo sa vaše kamere prikazuje izobličenja boja (artefakte) i ima dijagonalnu liniju na desnoj strani slike."
}
}
}
},
"debug": {
"options": {
"label": "Podešavanja",
"title": "Opcije",
"showOptions": "Prikaži opcije",
"hideOptions": "Sakrij opcije"
},
"boundingBox": "Uokvireni prostor",
"timestamp": "Vremenska oznaka",
"zones": "Zone",
"mask": "Maska",
"motion": "Pokret",
"regions": "Regioni",
"paths": "Putanje"
}
}
@@ -1 +1,222 @@
{}
{
"restart": {
"title": "Da li ste sigurni da želite da ponovo pokrenete Frigate?",
"description": "Ovo će nakratko zaustaviti Frigate dok se ponovo pokreće.",
"button": "Ponovo pokreni",
"restarting": {
"title": "Frigate se ponovo pokreće",
"content": "Ova stranica će se ponovo učitati za {{countdown}} sekundi.",
"button": "Osveži odmah"
}
},
"explore": {
"plus": {
"submitToPlus": {
"label": "Pošalji na Frigate+",
"desc": "Objekti na lokacijama koje želite da izbegnete nisu lažno pozitivni rezultati. Njihovo slanje kao lažno pozitivnih zbuniće model."
},
"review": {
"question": {
"label": "Potvrdite ovu oznaku za Frigate Plu",
"ask_a": "Da li je ovaj objekat <code>{{label}}</code>?",
"ask_an": "Da li je ovaj objekat <code>{{label}}</code>?",
"ask_full": "Da li je ovaj objekat <code>{{untranslatedLabel}}</code> ({{translatedLabel}})?"
},
"state": {
"submitted": "Poslato"
},
"toast": {
"error": "Slanje na Frigate+ nije uspelo. Proverite mrežnu vezu i pokušajte ponovo."
}
}
},
"video": {
"viewInHistory": "Prikaži u istoriji"
}
},
"export": {
"name": {
"placeholder": "Imenujte izvezeni snimak"
},
"stream": {
"label": "Kvalitet",
"auto": "Automatski",
"main": "Izvorni",
"sub": "Nizak",
"autoDesc": "Koristi glavni strim, a u slučaju njegove nedostupnosti prebacuje se na sporedni strim nižeg kvaliteta.",
"mainDesc": "Izvozi isključivo glavni strim izvornog kvaliteta. Vremenski periodi u kojima glavni strim nije bio dostupan biće izostavljeni.",
"subDesc": "Izvozi isključivo sporedni strim nižeg kvaliteta. Vremenski periodi u kojima sporedni strim nije bio dostupan biće izostavljeni."
},
"case": {
"newCaseOption": "Kreiraj novi slučaj",
"newCaseNamePlaceholder": "Naziv novog slučaja",
"newCaseDescriptionPlaceholder": "Opis slučaja",
"label": "Slučaj",
"placeholder": "Izaberite slučaj",
"nameRequired": "Naziv slučaja je obavezan"
},
"select": "Izaberi",
"export": "Izvezi",
"queueing": "Dodavanje izvoza u red...",
"selectOrExport": "Izaberi ili izvezi",
"tabs": {
"export": "Jedna kamera",
"multiCamera": "Više kamera"
},
"multiCamera": {
"timeRange": "Vremenski opseg",
"selectFromTimeline": "Izaberite sa vremenske linije",
"cameraSelection": "Kamere",
"checkingActivity": "Provera aktivnosti kamera...",
"cameraSelectionHelp": "Kamere sa praćenim objektima u ovom vremenskom rasponu su unapred izabrane",
"searchOrSelectGroup": "Pretražite ili izaberite grupu kamera...",
"selectAll": "Izaberi sve kamere",
"clearSelection": "Poništi izbor",
"selectWithActivity": "Kamere sa praćenim objektima",
"selectGroup": "Izaberi grupu",
"noMatchingCameras": "Nema kamera koje odgovaraju pretrazi",
"selectedCount": "Izabrano: {{selected}} od {{total}}",
"noCameras": "Nema dostupnih kamera",
"noCamerasSelected": "Izaberite najmanje jednu kameru",
"detectionCount_one": "1 praćeni objekat",
"detectionCount_few": "{{count}} praćena objekata",
"detectionCount_other": "{{count}} praćenih objekata",
"nameLabel": "Naziv izvoza",
"caseNameDefault": "Izvoz sa više kamera - {{date}}",
"namePlaceholder": "Opcioni osnovni naziv za ove izvoze",
"queueingButton": "Dodavanje izvoza u red...",
"exportButton_one": "Izvezi 1 kameru",
"exportButton_few": "Izvezi {{count}} kamere",
"exportButton_other": "Izvezi {{count}} kamera"
},
"time": {
"fromTimeline": "Izaberite sa vremenske linije",
"lastHour_one": "Poslednji sat",
"lastHour_few": "Poslednjih {{count}} sata",
"lastHour_other": "Poslednjih {{count}} sati",
"custom": "Prilagođeno",
"start": {
"title": "Vreme početka",
"label": "Izaberite vreme početka"
},
"end": {
"title": "Vreme završetka",
"label": "Izaberite vreme završetka"
}
},
"multi": {
"title_one": "Izvezi 1 pregled",
"title_few": "Izvezi {{count}} pregleda",
"title_other": "Izvezi {{count}} pregleda",
"description": "Izvezite svaki izabrani pregled. Svi izvezeni snimci biće grupisani pod jednim slučajem.",
"descriptionNoCase": "Izvezite svaki izabrani pregled.",
"caseNamePlaceholder": "Izvoz pregleda - {{date}}",
"exportButton_one": "Izvezi 1 pregled",
"exportButton_few": "Izvezi {{count}} pregleda",
"exportButton_other": "Izvezi {{count}} pregleda",
"exportingButton": "Izvoženje...",
"toast": {
"started_one": "Pokrenut 1 izvoz.",
"started_few": "Pokrenuto {{count}} izvoza.",
"started_other": "okrenuto {{count}} izvoza.",
"partial": "Pokrenuto je {{successful}} od {{total}} izvoza. Neuspešno: {{failedItems}}",
"failed": "Pokretanje {{total}} izvoza nije uspelo. Neuspešno: {{failedItems}}"
}
},
"toast": {
"success": "Izvoz je uspešno pokrenut. Pogledajte datoteku na stranici sa izvezenim snimcima.",
"queued": "Izvoz je dodat u red. Pratite napredak na stranici sa izvezenim snimcima.",
"view": "Prikaži",
"batchSuccess_one": "Pokrenut 1 izvoz. Otvaranje slučaja...",
"batchSuccess_few": "Pokrenuto {{count}} izvoza. Otvaranje slučaja...",
"batchSuccess_other": "Pokrenuto {{count}} izvoza. Otvaranje slučaja...",
"batchPartial": "Pokrenuto je {{successful}} od {{total}} izvoza. Neuspešne kamere: {{failedCameras}}",
"batchFailed": "Pokretanje {{total}} izvoza nije uspelo. Neuspešne kamere: {{failedCameras}}",
"batchQueuedSuccess_one": "Dodat 1 izvoz u red.",
"batchQueuedSuccess_few": "Dodato {{count}} izvoza u red.",
"batchQueuedSuccess_other": "Dodato {{count}} izvoza u red.",
"batchQueuedPartial": "Dodato je {{successful}} od {{total}} izvoza u red. Neuspešne kamere: {{failedCameras}}",
"batchQueueFailed": "Dodavanje {{total}} izvoza u red nije uspelo. Neuspešne kamere: {{failedCameras}}",
"error": {
"failed": "Dodavanje izvoza u red nije uspelo: {{error}}",
"endTimeMustAfterStartTime": "Vreme završetka mora biti nakon vremena početka",
"noValidTimeSelected": "Nije izabran važeći vremenski opseg"
}
},
"fromTimeline": {
"saveExport": "Sačuvaj izvezeni snimak",
"queueingExport": "Dodavanje izvoza u red...",
"previewExport": "Pregledaj izvezeni snimak",
"useThisRange": "Koristi ovaj opseg"
}
},
"streaming": {
"label": "Strim",
"restreaming": {
"disabled": "Ponovno strimovanje nije omogućeno za ovu kameru.",
"desc": {
"title": "Podesite go2rtc za dodatne opcije prikaza uživo i zvuk za ovu kameru."
}
},
"showStats": {
"label": "Prikaži statistiku strima",
"desc": "Omogućite ovu opciju da biste prikazali statistiku strima kao sloj preko video-izvora kamere."
},
"debugView": "Prikaz za otklanjanje grešaka"
},
"search": {
"saveSearch": {
"label": "Sačuvaj pretragu",
"desc": "Unesite naziv za ovu sačuvanu pretragu.",
"placeholder": "Unesite naziv za svoju pretragu",
"overwrite": "{{searchName}} već postoji. Čuvanje će prepisati postojeću vrednost.",
"success": "Pretraga ({{searchName}}) je sačuvana.",
"button": {
"save": {
"label": "Sačuvaj ovu pretragu"
}
}
}
},
"recording": {
"shareTimestamp": {
"label": "Deli vremensku oznaku",
"title": "Deli vremensku oznaku",
"description": "Podelite URL sa vremenskom oznakom trenutne pozicije plejera ili izaberite prilagođenu vremensku oznaku. Napomena: ovo nije javni URL za deljenje i dostupan je samo korisnicima koji imaju pristup Frigate-u i ovoj kameri.",
"custom": "Prilagođena vremenska oznaka",
"button": "URL za deljenje vremenske oznake",
"shareTitle": "Frigate pregled vremenske oznake: {{camera}}"
},
"confirmDelete": {
"title": "Potvrdite brisanje",
"desc": {
"selected": "Da li ste sigurni da želite da obrišete sav snimljeni video povezan sa ovom stavkom pregleda?<br /><br />Držite taster <em>Shift</em> da biste ubuduće zaobišli ovaj prozor."
},
"toast": {
"success": "Video-snimci povezani sa izabranim stavkama pregleda su uspešno obrisani.",
"error": "Neuspešno brisanje: {{error}}"
}
},
"button": {
"export": "Izvezi",
"markAsReviewed": "Označi kao pregledano",
"markAsUnreviewed": "Označi kao nepregledano",
"deleteNow": "Obriši odmah",
"generateDescription": "Generiši opis"
},
"genaiDescription": {
"toast": {
"success": "Zatražen je GenAI opis za ovu stavku pregleda.",
"error": "Slanje zahteva za opis nije uspelo: {{error}}"
}
}
},
"imagePicker": {
"selectImage": "Izaberite sličicu praćenog objekta",
"unknownLabel": "Sačuvana slika pokretača",
"search": {
"placeholder": "Pretražite po oznaci ili podoznaci..."
},
"noImages": "Nema pronađenih sličica za ovu kameru"
}
}
@@ -1 +1,140 @@
{}
{
"filter": "Filtriraj",
"classes": {
"label": "Klase",
"all": {
"title": "Sve klase"
},
"count_one": "{{count}} klasa",
"count_other": "{{count}} klase"
},
"labels": {
"label": "Oznake",
"all": {
"title": "Sve oznake",
"short": "Oznake"
},
"count_one": "{{count}} oznaka",
"count_other": "{{count}} oznake"
},
"zones": {
"label": "Zone",
"all": {
"title": "Sve zone",
"short": "Zone"
}
},
"dates": {
"selectPreset": "Izaberite unapred podešenu poziciju…",
"all": {
"title": "Svi datumi",
"short": "Datumi"
}
},
"more": "Više filtera",
"reset": {
"label": "Vrati filtere na podrazumevane vrednosti"
},
"timeRange": "Vremenski raspon",
"subLabels": {
"label": "Podoznake",
"all": "Sve podoznake"
},
"attributes": {
"label": "Atributi klasifikacije",
"all": "Svi atributi"
},
"score": "Rezultat",
"estimatedSpeed": "Procenjena brzina ({{unit}})",
"features": {
"label": "Funkcije",
"hasSnapshot": "Ima snimak",
"hasVideoClip": "Ima video-snimak",
"submittedToFrigatePlus": {
"label": "Poslato na Frigate+",
"tips": "Morate prvo filtrirati praćene objekte koji imaju snimak.<br /><br />Praćeni objekti bez snimka ne mogu se poslati na Frigate+."
}
},
"sort": {
"label": "Sortiranje",
"dateAsc": "Datum (Rastuće)",
"dateDesc": "Datum (Opadajuće)",
"scoreAsc": "Rezultat objekta (Rastuće)",
"scoreDesc": "Rezultat objekta (Opadajuće)",
"speedAsc": "Procenjena brzina (Rastuće)",
"speedDesc": "Procenjena brzina (Opadajuće)",
"relevance": "Relevantnost"
},
"cameras": {
"label": "Filter kamera",
"all": {
"title": "Sve kamere",
"short": "Kamere"
}
},
"review": {
"showReviewed": "Prikaži pregledano"
},
"motion": {
"showMotionOnly": "Prikaži samo kretanje"
},
"explore": {
"settings": {
"title": "Podešavanja",
"defaultView": {
"title": "Podrazumevani prikaz",
"desc": "Kada nijedan filter nije izabran, prikaži sažetak najnovijih praćenih objekata po oznaci ili prikaži nefiltriranu mrežu.",
"summary": "Sažetak",
"unfilteredGrid": "Nefiltrirana mreža"
},
"gridColumns": {
"title": "Kolone u mreži",
"desc": "Izaberite broj kolona u prikazu mreže."
},
"searchSource": {
"label": "Izvor pretrage",
"desc": "Izaberite da li želite da pretražujete sličice (umanjene prikaze) ili opise vaših praćenih objekata.",
"options": {
"thumbnailImage": "Sličica",
"description": "Opis"
}
}
},
"date": {
"selectDateBy": {
"label": "Izaberite datum za filtriranje"
}
}
},
"logSettings": {
"label": "Filtriraj nivo dnevnika",
"filterBySeverity": "Filtriraj dnevnik prema ozbiljnosti",
"loading": {
"title": "Učitavanje",
"desc": "Kada se panel dnevnika pomeri do samog dna, novi zapisi se automatski strimuju čim se dodaju."
},
"disableLogStreaming": "Onemogući strimovanje dnevnika",
"allLogs": "Ceo dnevnik"
},
"trackedObjectDelete": {
"title": "Potvrdite brisanje",
"desc": "Brisanjem ovih {{objectLength}} praćenih objekata uklanja se snimak, svi sačuvani vektorski prikazi i svi povezani unosi o životnom ciklusu objekta. Snimljeni materijal ovih praćenih objekata u prikazu istorije <em>NEĆE</em> biti obrisan.<br /><br />Da li ste sigurni da želite da nastavite?<br /><br />Držite taster <em>Shift</em> da biste ubuduće zaobišli ovaj prozor.",
"toast": {
"success": "Praćeni objekti su uspešno obrisani.",
"error": "Neuspešno brisanje praćenih objekata: {{errorMessage}}"
}
},
"zoneMask": {
"filterBy": "Filtriraj prema maski zone"
},
"recognizedLicensePlates": {
"title": "Prepoznate registarske tablice",
"loadFailed": "Učitavanje prepoznatih registarskih tablica nije uspelo.",
"loading": "Učitavanje prepoznatih registarskih tablica…",
"placeholder": "Unesite tekst za pretragu tablica…",
"noLicensePlatesFound": "Nema pronađenih registarskih tablica.",
"selectPlatesFromList": "Izaberite jednu ili više tablica sa liste.",
"selectAll": "Izaberi sve",
"clearAll": "Poništi sve"
}
}
@@ -1 +1,61 @@
{}
{
"noRecordingsFoundForThisTime": "Nema pronađenih snimaka za ovaj vremenski period",
"noPreviewFound": "Pregled nije pronađen",
"noPreviewFoundFor": "Pregled nije pronađen za kameru {{cameraName}}",
"submitFrigatePlus": {
"title": "Pošalji ovaj kadar na Frigate+?",
"submit": "Pošalji",
"previewError": "Učitavanje predpregleda snimka nije uspelo. Snimak možda nije dostupan u ovom trenutku."
},
"livePlayerRequiredIOSVersion": "Za ovaj tip strima uživo potreban je iOS 17.1 ili noviji.",
"streamOffline": {
"title": "Strim je van mreže",
"desc": "Nijedan kadar nije primljen na strimu za detekciju <code>detect</code> kamere {{cameraName}}, proverite dnevnik grešaka"
},
"cameraOff": "Kamera je isključena",
"quality": {
"auto": "Automatski",
"autoLow": "Reprodukcija u niskom kvalitetu (ograničen protok podataka)",
"autoLowCodec": "Reprodukcija u niskom kvalitetu (izvorni kvalitet nije podržan u ovom veb-pregledaču)",
"autoLowSaveData": "Reprodukcija u niskom kvalitetu (ušteda podataka)",
"main": "Izvorni",
"sub": "Nizak",
"notSupportedBrowser": "Nije podržano u ovom veb-pregledaču",
"label": "Kvalitet",
"noAudio": "Nema zvuka",
"audioRate": "{{rate}} kHz audio",
"audioCodecRate": "{{codec}} {{rate}} kHz",
"noRecordings": "Nema snimaka u ovom vremenskom opsegu"
},
"stats": {
"streamType": {
"title": "Tip strima:",
"short": "Tip"
},
"bandwidth": {
"title": "Propusni opseg:",
"short": "Propusni opseg"
},
"totalFrames": "Ukupno kadrova:",
"droppedFrames": {
"title": "Odbačeni kadrovi:",
"short": {
"title": "Odbačeno",
"value": "{{droppedFrames}} kadrova"
}
},
"decodedFrames": "Dekodirani kadrovi:",
"droppedFrameRate": "Brzina odbacivanja kadrova:",
"stream": "Strim:",
"streamAuto": "{{stream}} (automatski)"
},
"toast": {
"success": {
"submittedFrigatePlus": "Kadar je uspešno poslat na Frigate+"
},
"error": {
"submitFrigatePlusFailed": "Slanje kadra na Frigate+ nije uspelo",
"playRecordingsFailed": "Reprodukcija snimaka nije uspela (greška {{code}}): {{message}}"
}
}
}
@@ -1 +1,40 @@
{}
{
"minimum": "Mora biti najmanje {{limit}}",
"maximum": "Mora biti najviše {{limit}}",
"exclusiveMinimum": "Mora biti veće od {{limit}}",
"exclusiveMaximum": "Mora biti manje od {{limit}}",
"minLength": "Mora imati najmanje {{limit}} karakter(a)",
"maxLength": "Mora imati najviše {{limit}} karakter(a)",
"minItems": "Mora imati najmanje {{limit}} stavki",
"maxItems": "Mora imati najviše {{limit}} stavki",
"pattern": "Nevažeći format",
"required": "Ovo polje je obavezno",
"type": "Nevažeći tip vrednosti",
"enum": "Mora biti jedna od dozvoljenih vrednosti",
"const": "Vrednost se ne poklapa sa očekivanom konstantom",
"uniqueItems": "Sve stavke moraju biti jedinstvene",
"format": "Nevažeći format",
"additionalProperties": "Nepoznato svojstvo nije dozvoljeno",
"oneOf": "Mora se podudarati sa tačno jednom od dozvoljenih šema",
"anyOf": "Mora se podudarati sa najmanje jednom od dozvoljenih šema",
"proxy": {
"header_map": {
"roleHeaderRequired": "Zaglavlje uloge je obavezno kada su konfigurisana mapiranja uloga."
}
},
"ffmpeg": {
"inputs": {
"rolesUnique": "Svaka uloga može biti dodeljena samo jednom ulaznom strimu.",
"detectRequired": "Najmanje jednom ulaznom strimu mora biti dodeljena uloga „detect“.",
"hwaccelDetectOnly": "Samo ulazni strim sa ulogom „detect“ može definisati argumente za hardversko ubrzanje."
}
},
"detect": {
"dimensionMustBeEven": "Mora biti paran broj."
},
"models": {
"defaultRequired": "Jedan model mora koristiti podrazumevanu scenu. Bez nje, svaka kamera koja ne izabere scenu neće imati rezervni model na koji može da se osloni.",
"sceneDuplicate": "Svaki model mora koristiti različitu scenu.",
"sameModel": "Modeli za scenu {{scene}} i scenu {{other}} koriste istu datoteku modela. Pokretanje jednog modela pod više različitih scena samo deli posao detekcije na zasebne detektore, što je sporije nego pokretanje jednog modela sa svim hardverskim resursima navedenim pod njim."
}
}

Some files were not shown because too many files have changed in this diff Show More