Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub 7bd75348aa Bump js-yaml from 4.1.1 to 4.3.0 in /web
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 13:34:56 +00:00
Blake BlackshearandClaude Opus 4.8 ea131e1663 Merge remote-tracking branch 'origin/master' into dev
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
Resolve conflicts in the export pipeline where dev's job-queue refactor
met master's chapter-metadata and security work.

- Unify chapter support under ChaptersEnum (none / recording_segments /
  review_items); the realtime stream-copy export selects the per-segment
  or per-review-item builder by the camera's configured mode. Thread
  chapters through ExportRecordingsBody -> _build_export_job -> ExportJob
  -> RecordingExporter.
- Keep master's creation_time/comment export metadata and fix a
  video_path duplication the textual merge introduced in the preview
  command.
- Move the chapters request field to ExportRecordingsBody (the single
  export endpoint) where it is actually honored.

Restore security fixes the automatic merge would have reverted:
- frigate/util/services.py: restore the #23493 rename to the public
  is_go2rtc_arbitrary_exec_allowed so create_config.py's dynamic-source
  exec guard imports and runs (the merge otherwise left a broken import).
- Preserve the export image-path ".." traversal check inside
  _sanitize_existing_image, applied to single/custom/batch exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 15:04:01 +02:00
Josh HawkinsandGitHub e2ce0c82ff show Frigate+ submission failures in the UI instead of showing a false success (#23579)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
2026-06-27 16:38:24 -06:00
Josh HawkinsandGitHub 3d4dd3ac4b allow non-admin users to send PTZ commands for cameras they have access to (#23578)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
2026-06-27 15:55:39 -06:00
Josh HawkinsandGitHub 1ec511b66c Docs tweaks (#23572)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* small tweaks

* add link to docs from detect.fps field message
2026-06-26 15:11:51 -06:00
Nicolas MowenandGitHub accbab7afc Rebuild object docs (#23570)
* Rebuild object docs

* Tweak styling and fix mixing tabs

* Fix warning

* Cleanup styling
2026-06-26 06:48:42 -06:00
Josh HawkinsandGitHub cbf6d032cb Add optional docs link to config field messages (#23569)
CI / AMD64 Build (push) Waiting to run
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
CI / Assemble and push default build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
* add optional docs link to config field messages

* docs tweaks

* add field messages for model dimensions
2026-06-25 17:25:59 -06:00
Josh HawkinsandGitHub 933a7f1a3f resolve the leaked Query default so media Cache-Control max-age is always a valid int (#23553)
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
CI / AMD64 Build (push) Has been cancelled
2026-06-24 07:57:46 -05:00
Josh HawkinsandGitHub 4e5e8e3c59 Offload preview encoding and Plus upload off the API event loop (#23552)
* offload preview ffmpeg encoding to a thread to avoid blocking the api event loop

* offload Frigate+ recording snapshot upload to a thread to avoid blocking the api event loop
2026-06-24 07:17:23 -05:00
ec3fb00494 perf(track): use sum()/len() instead of np.mean in average_boxes (#23521)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* perf(track): avoid numpy reductions on tiny box lists in position smoothing

update_position runs per tracked object per frame. While a position has
fewer than 10 samples it calls np.percentile four times, and average_boxes
(per stationary object per frame) calls np.mean four times - all on lists of
at most 10 ints, where numpy's per-call dispatch/validation overhead
dominates the actual work.

Replace them with pure-Python equivalents:
- average_boxes: sum()/len() instead of np.mean (bit-identical output)
- interpolated_percentile(): linear-interpolated percentile matching
  numpy.percentile (including its lerp branch at frac>=0.5) for the small
  lists used here, in place of np.percentile

Measured in the release image (numpy 1.26.4) on a 10-element list:
np.percentile 18735 ns -> 191 ns/call (98x); np.mean-based average_boxes
7480 ns -> 591 ns (12.7x); ~74 us saved per object-frame in update_position.
A live py-spy --gil profile of a camera process_frames worker showed
np.percentile (update_position) and np.mean (average_boxes) among the top
Frigate-owned on-CPU frames.

Output is unchanged: added tests assert both helpers are bit-identical to
numpy over randomized small inputs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Drop interpolated_percentile, keep only average_boxes

Per review: reimplementing np.percentile hurts readability and risks
divergence from numpy (e.g. numpy 2.x). Revert update_position to
np.percentile and remove the helper; keep only the average_boxes change
(sum()/len() instead of np.mean), which stays bit-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:47:04 -06:00
081d6f95ef perf(track): avoid per-frame allocations and list lookups in tracker (#23523)
Two small per-object-per-frame improvements in the tracker hot path
(match_and_update), both bit-identical:

- get_stationary_threshold returned a freshly constructed StationaryThresholds
  (a dataclass plus a list) on every call for any label not in the three
  known lists - i.e. for common labels like person/dog. The default thresholds
  are constant and never mutated, so return a shared module-level singleton,
  as the other three cases already do.
- untracked_object_boxes membership used `box not in [list of boxes]` (O(n));
  build a set of box tuples for O(1) membership. Boxes are hashable as tuples
  and output is unchanged.

get_stationary_threshold appeared in a live py-spy --gil profile of a camera
process_frames worker. Adds tests for the threshold lookups.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:24:05 -06:00
a2b46f5d84 perf(util): cut redundant work in per-frame detection consolidation (#23522)
video/detect.py runs these for every frame:

- get_cluster_candidates: used_boxes was a list with `in` membership tests
  inside the nested loop (O(n) per check). It is only ever membership-tested,
  so switching it to a set (O(1)) leaves output unchanged.
- get_consolidated_object_detections: area(current_box) was recomputed on
  every inner-loop iteration though it is loop-invariant; hoist it to one
  call per outer detection.

Both are bit-identical (verified against the previous implementations over
randomized inputs). Measured in the release image, get_cluster_candidates on
a frame of 30 detection boxes: 59.2 us -> 42.1 us (1.4x); the gain scales
with the number of boxes per frame.

Adds a partition-invariant test (every box index lands in exactly one
cluster).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 15:23:18 -06:00
f065cc8642 fix unbounded recordings_info growth for cameras with no cache segments (#23528)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
A record-enabled camera whose record stream produces no cache segments
never appears in grouped_recordings, so the per-camera prune in
RecordingMaintainer.move_files() never runs for it. Its
object_recordings_info and audio_recordings_info buffers then grow
without bound until the recording process is OOM-killed (discussion
#23451).

Run a prune every move_files() cycle for cameras absent from
grouped_recordings, dropping entries older than the longest a segment
could still wait in cache before being matched
(MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2). Cameras present in
grouped_recordings are left untouched and keep their existing prune.

Add a regression test asserting that an absent camera's stale entries
are dropped (recent ones kept) while a present camera's entries are
left intact.

Co-authored-by: John Pescatore <johnpescatore@claude.internal.johnpescatore.com>
2026-06-22 14:33:56 -06:00
Josh HawkinsandGitHub 9ce80e7266 Improve storage docs (#23542)
* improve storage docs

* clarify

* tweak language

* move section
2026-06-22 15:27:24 -05:00
mayerwinandGitHub bb5056a68a docs: correct face_recognition min_area default to 750 (#23535) 2026-06-22 06:38:52 -06:00
d982b3a782 perf(util): use monotonic clock and bounded deque in EventsPerSecond (#23520)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* perf(util): use monotonic clock and bounded deque in EventsPerSecond

EventsPerSecond is updated on every captured frame, every detection and
every processed frame across all cameras and detectors. The previous
implementation derived timestamps from datetime.now().timestamp() (wall
clock), so an NTP or manual clock adjustment could skew the rolling-window
expiry; it also stored timestamps in a list and expired them with
del self._timestamps[0] (O(n) per removal) plus a periodic slice-copy to
cap growth.

Switch to time.monotonic() for the interval math (correct by construction
and immune to wall-clock jumps) and a collections.deque(maxlen=...) so
expiry is O(1) (popleft) and retention is bounded automatically. This
mirrors the deque-based expiry already used in video/ffmpeg.py and
watchdog.py. Observable output is unchanged.

Adds frigate/test/test_builtin.py covering rate calculation, window
expiry and the memory bound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: drop test_timestamps_are_memory_bounded

It only asserted that deque(maxlen=) caps length, which is stdlib behavior
rather than something this change needs to verify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:38:41 -06:00
Josh HawkinsandGitHub d036061e3f cache the preview_frames directory listing so concurrent per-camera frame requests share one scan instead of each re-listing the whole directory (#23526)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
2026-06-20 14:56:05 -05:00
Josh HawkinsandGitHub 5003ab895c add camera search, select-all/clear, and group selection to the multi-camera export dialog (#23516)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Extra Build (push) Blocked by required conditions
2026-06-19 15:50:19 -06:00
Josh HawkinsandGitHub 652ea2454f Miscellaneous fixes (#23513)
* display zone names consistently using friendly_name or raw id without transformation

* enforce camera-level access on go2rtc live stream websocket endpoints
2026-06-19 10:10:22 -06:00
Josh HawkinsandGitHub 37ea6b46b5 small docs tweaks (#23506) 2026-06-18 12:44:04 -06:00
Josh HawkinsandGitHub 8203e39b7f add go2rtc settings section to the save all flow (#23501)
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
CI / AMD64 Build (push) Has been cancelled
2026-06-17 08:10:23 -06:00
Josh HawkinsandGitHub 282e70d4bf Add go2rtc stream selection to camera configuration (#23496)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* add go2rtc stream selection to camera ffmpeg config

* i18n

* add config-schema.json to generated e2e mock data

* e2e test

* docs

* fix test
2026-06-16 16:12:39 -06:00
Josh HawkinsandGitHub a7df17cc61 update ffmpeg navpath title (#23494) 2026-06-16 09:59:25 -06:00
Nicolas MowenandGitHub b3ce4486b9 Catch edge cases in security protections (#23493)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* Fix go2rtc nested key dict

* Don't allow path traversal
2026-06-16 08:07:12 -06:00
Josh HawkinsandGitHub c79ca9838f UI tweaks (#23492)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* slightly darken bg-card

* change menu label

* move snapshot retain out of advanced fields

* add new ui options for collapsibles

* backend title and description

* remove unused snapshot retention field

* update reference config

* remove further references to snapshots retain.mode
2026-06-16 08:56:52 -05:00
Josh HawkinsandGitHub e84a89ef3e fix camera audio availability detection on mobile live grid (#23488)
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Assemble and push default build (push) Blocked by required conditions
2026-06-15 07:26:19 -06:00
Josh HawkinsandGitHub ba29e141da Docs tweaks (#23487)
* docs tweaks

* tweak

* title tweak
2026-06-15 07:03:37 -06:00
Nicolas MowenandGitHub 32e433cafc Allow GenAI providers to be initialized lazily (#23482)
CI / AMD64 Build (push) Waiting to run
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
CI / Assemble and push default build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
* allow GenAI providers to be initialized even if they failed on previous attempts

* mypy
2026-06-14 11:40:33 -05:00
Josh HawkinsandGitHub bc816926a5 Replace export ffmpeg argument blocklist with a structural allowlist (#23478)
CI / AMD64 Build (push) Waiting to run
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
CI / Assemble and push default build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
* use allowlist for custom export ffmpeg args

* reject brackets in export filtergraph validation instead of stripping link labels
2026-06-13 16:43:22 -06:00
Josh HawkinsandGitHub b79ad9871a generate the API docs OpenAPI spec from the app with per-endpoint auth requirements (#23476) 2026-06-13 16:04:22 -06:00
Josh HawkinsandGitHub 8be7a97fa6 guard norfair distance against non-finite and zero-area boxes that could crash autotracking cameras (#23475) 2026-06-13 16:23:30 -05:00
Nicolas MowenandGitHub d7ad3ba699 Fix chat tool calling and prompt breaking (#23457)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* Implement tool call history keeping

* Refactor to match single message implementation

* Simplify data representation

* Cleanup chat page rendering

* Include system message to not break cache

* Formatting

* Update tests and update .gitignore
2026-06-12 07:48:43 -05:00
Josh HawkinsandGitHub e6601d50a6 Add recording keyframe analysis to camera probe dialog (#23453)
CI / AMD64 Build (push) Waiting to run
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 / Assemble and push default build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
* backend: endpoint and util funcs

* tests

* frontend and i18n

* update openapi spec

* add tip to docs
2026-06-11 14:16:41 -06:00
Josh HawkinsandGitHub efe585a920 Miscellaneous fixes (#23445)
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
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
* keep global camera config subscribers broad when only one camera exists at startup

* update glossary
2026-06-11 05:36:30 -06:00
Nicolas MowenandGitHub 06e3d0ac5d Chapter tweaks (#23440)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* Add camera metadata and fix preview chapters

* Add config option for chapters
2026-06-09 09:07:42 -06:00
Josh HawkinsandGitHub f3a352ef3f Miscellaneous fixes (#23413)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
* update e2e mock data to remove deprecated fields

* remove scream audio label

scream was never mapped to anything in frigate's custom labelmap, yell is used everywhere

* document common audio labels

* deprecate ffmpeg 5

* language tweak

* add field message to recommend presets instead of manual hwaccel args

* add guidance to docs on choosing a detect fps
2026-06-08 09:14:16 -06:00
ad968efd3e Added translation using Weblate (Zuni)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Added translation using Weblate (Zuni)

Co-authored-by: Firas <firas.amm@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2026-06-06 22:25:39 -05:00
3fe91e20d0 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (1171 of 1171 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 100.0% (53 of 53 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: OverTheHillsAndFarAway <prosjektx@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nb_NO/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
bd52a1cc48 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1272 of 1272 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (809 of 809 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (807 of 807 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1268 of 1268 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 94.6% (1196 of 1263 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1195 of 1195 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (239 of 239 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% (100 of 100 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1186 of 1186 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1183 of 1183 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1181 of 1181 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Chinese (Simplified Han script))

Currently translated at 100.0% (1176 of 1176 strings)

Co-authored-by: GuoQing Liu <842607283@qq.com>
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-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-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/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-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
cb40343be7 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 99.2% (803 of 809 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (239 of 239 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 99.5% (237 of 238 strings)

Translated using Weblate (Chinese (Traditional Han script))

Currently translated at 100.0% (26 of 26 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: KelvinKueh <kelvin.kueh@gmail.com>
Co-authored-by: Yu Chun Huang <yujun@bo2.tw>
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-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/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
2026-06-06 22:25:39 -05:00
b912a62e0b Translated using Weblate (Uzbek)
Currently translated at 0.3% (2 of 501 strings)

Co-authored-by: Hamza Foziljonov <hamza.uztranslator@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/uz/
Translation: Frigate NVR/audio
2026-06-06 22:25:39 -05:00
fcfab8ef14 Translated using Weblate (Khmer (Central))
Currently translated at 0.9% (5 of 501 strings)

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Added translation using Weblate (Khmer (Central))

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: reanyouda <mr.reanyouda@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/km/
Translation: Frigate NVR/audio
2026-06-06 22:25:39 -05:00
e0d0b2a345 Translated using Weblate (Persian)
Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Persian)

Currently translated at 45.4% (368 of 809 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Persian)

Currently translated at 50.0% (639 of 1276 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Persian)

Currently translated at 98.7% (236 of 239 strings)

Translated using Weblate (Persian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Persian)

Currently translated at 17.4% (15 of 86 strings)

Co-authored-by: Amir reza Irani ali poor <amir1376irani@yahoo.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/fa/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fa/
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
b72f5a986e Translated using Weblate (Swedish)
Currently translated at 92.0% (46 of 50 strings)

Translated using Weblate (Swedish)

Currently translated at 94.0% (94 of 100 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Swedish)

Currently translated at 50.7% (647 of 1276 strings)

Translated using Weblate (Swedish)

Currently translated at 54.4% (55 of 101 strings)

Translated using Weblate (Swedish)

Currently translated at 77.7% (136 of 175 strings)

Translated using Weblate (Swedish)

Currently translated at 54.4% (55 of 101 strings)

Translated using Weblate (Swedish)

Currently translated at 50.7% (647 of 1276 strings)

Translated using Weblate (Swedish)

Currently translated at 90.0% (54 of 60 strings)

Translated using Weblate (Swedish)

Currently translated at 93.0% (120 of 129 strings)

Translated using Weblate (Swedish)

Currently translated at 92.8% (222 of 239 strings)

Translated using Weblate (Swedish)

Currently translated at 94.4% (137 of 145 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Swedish)

Currently translated at 94.0% (94 of 100 strings)

Translated using Weblate (Swedish)

Currently translated at 90.0% (54 of 60 strings)

Translated using Weblate (Swedish)

Currently translated at 77.7% (136 of 175 strings)

Translated using Weblate (Swedish)

Currently translated at 50.7% (647 of 1276 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Swedish)

Currently translated at 94.4% (137 of 145 strings)

Translated using Weblate (Swedish)

Currently translated at 93.0% (120 of 129 strings)

Translated using Weblate (Swedish)

Currently translated at 92.8% (222 of 239 strings)

Translated using Weblate (Swedish)

Currently translated at 91.2% (218 of 239 strings)

Co-authored-by: Douglas Stier <douglas.stier@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Mona Lisa <monalisa@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/sv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/sv/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-filter
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-search
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-06-06 22:25:39 -05:00
63b1506dd6 Translated using Weblate (French)
Currently translated at 5.4% (44 of 809 strings)

Translated using Weblate (French)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (French)

Currently translated at 67.0% (850 of 1268 strings)

Translated using Weblate (French)

Currently translated at 85.1% (86 of 101 strings)

Translated using Weblate (French)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (French)

Currently translated at 82.1% (83 of 101 strings)

Co-authored-by: Gloup <emeric.denis@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: LeBuzzy <bwinster2@outlook.com>
Co-authored-by: Lorent Felix <comloren@gmail.com>
Co-authored-by: Thomas <arpelboxes@yahoo.fr>
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/config-global/fr/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/fr/
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
90a18852ef Translated using Weblate (Spanish)
Currently translated at 100.0% (1276 of 1276 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1272 of 1272 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (809 of 809 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (807 of 807 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1268 of 1268 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Spanish)

Currently translated at 99.2% (1253 of 1263 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1186 of 1186 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1183 of 1183 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1181 of 1181 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (1176 of 1176 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Saninn Salas Diaz <saninnsalas@gmail.com>
Co-authored-by: ThatStella7922 <stella@thatstel.la>
Co-authored-by: jjavin <javiernovoa@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-configeditor/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/es/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/es/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-configeditor
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-exports
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
b1c133bfd1 Translated using Weblate (Dutch)
Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Dutch)

Currently translated at 77.2% (78 of 101 strings)

Translated using Weblate (Dutch)

Currently translated at 97.0% (232 of 239 strings)

Translated using Weblate (Dutch)

Currently translated at 83.9% (397 of 473 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Dutch)

Currently translated at 97.9% (794 of 811 strings)

Translated using Weblate (Dutch)

Currently translated at 93.7% (224 of 239 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Dutch)

Currently translated at 92.8% (221 of 238 strings)

Translated using Weblate (Dutch)

Currently translated at 98.0% (1148 of 1171 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Hosted Weblate user 151476 <marijndekker3@gmail.com>
Co-authored-by: Hosted Weblate user 151476 <micel@users.noreply.hosted.weblate.org>
Co-authored-by: bb61523 <brambini@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/nl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/nl/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-facelibrary
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
68e74c32e3 Translated using Weblate (Indonesian)
Currently translated at 100.0% (1276 of 1276 strings)

Translated using Weblate (Indonesian)

Currently translated at 5.0% (24 of 473 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Indonesian)

Currently translated at 1.8% (15 of 811 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Indonesian)

Currently translated at 86.6% (52 of 60 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Indonesian)

Currently translated at 59.3% (38 of 64 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (1176 of 1176 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Indonesian)

Currently translated at 30.7% (39 of 127 strings)

Co-authored-by: Arif Budiman <arifpedia@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Joseph K <o.joseph.k@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/id/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/id/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/objects
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-06-06 22:25:39 -05:00
9ced2c25ee Translated using Weblate (Arabic)
Currently translated at 28.3% (142 of 501 strings)

Translated using Weblate (Arabic)

Currently translated at 18.8% (24 of 127 strings)

Co-authored-by: Firas <firas.amm@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ar/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ar/
Translation: Frigate NVR/audio
Translation: Frigate NVR/objects
2026-06-06 22:25:39 -05:00
b76457e0af Translated using Weblate (Italian)
Currently translated at 55.3% (448 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 55.2% (447 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 75.3% (358 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 74.7% (355 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 54.8% (444 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 50.7% (241 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 41.7% (338 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (1276 of 1276 strings)

Translated using Weblate (Italian)

Currently translated at 34.1% (276 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 37.0% (176 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (1272 of 1272 strings)

Translated using Weblate (Italian)

Currently translated at 26.5% (126 of 475 strings)

Translated using Weblate (Italian)

Currently translated at 28.4% (230 of 809 strings)

Translated using Weblate (Italian)

Currently translated at 94.6% (1204 of 1272 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Italian)

Currently translated at 26.4% (125 of 473 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (1195 of 1195 strings)

Translated using Weblate (Italian)

Currently translated at 28.3% (230 of 811 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Italian)

Currently translated at 26.2% (124 of 473 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Italian)

Currently translated at 28.2% (229 of 811 strings)

Translated using Weblate (Italian)

Currently translated at 28.1% (228 of 811 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (1183 of 1183 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Italian)

Currently translated at 26.0% (123 of 473 strings)

Co-authored-by: Filippo-riccardo Franzin (filippo franzin) <filric01@gmail.com>
Co-authored-by: Frank_ai <cyberpez.ai@gmail.com>
Co-authored-by: Gringo <ita.translations@tiscali.it>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/it/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/it/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-events
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
4626d91fbb Translated using Weblate (Polish)
Currently translated at 24.0% (114 of 475 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Polish)

Currently translated at 97.0% (232 of 239 strings)

Translated using Weblate (Polish)

Currently translated at 63.3% (64 of 101 strings)

Translated using Weblate (Polish)

Currently translated at 4.8% (39 of 809 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Polish)

Currently translated at 96.6% (231 of 239 strings)

Translated using Weblate (Polish)

Currently translated at 62.3% (63 of 101 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Polish)

Currently translated at 94.1% (224 of 238 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (501 of 501 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Piotr Łoboda <loboda4450@gmail.com>
Co-authored-by: Tomasz Słuszniak <tomasz.sluszniak@gmail.com>
Co-authored-by: magnumek <m4gnumek@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/pl/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/pl/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
2026-06-06 22:25:39 -05:00
77474ccfea Translated using Weblate (Czech)
Currently translated at 84.8% (123 of 145 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Milan K. <kostler.milan@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/cs/
Translation: Frigate NVR/views-explore
2026-06-06 22:25:39 -05:00
8be5b9d8d0 Translated using Weblate (Catalan)
Currently translated at 100.0% (1276 of 1276 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1272 of 1272 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (809 of 809 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1268 of 1268 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (807 of 807 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (45 of 45 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1195 of 1195 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1186 of 1186 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1183 of 1183 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1181 of 1181 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (1176 of 1176 strings)

Co-authored-by: Eduardo Pastor Fernández <123eduardoneko123@gmail.com>
Co-authored-by: Gerard Ricart Castells <gerard.ricart@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ca/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ca/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-replay
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-06-06 22:25:39 -05:00
ffed173d5a Translated using Weblate (Japanese)
Currently translated at 100.0% (1268 of 1268 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (807 of 807 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Japanese)

Currently translated at 93.9% (1186 of 1263 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (10 of 10 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (501 of 501 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (59 of 59 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (129 of 129 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (1186 of 1186 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (127 of 127 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (64 of 64 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (86 of 86 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (25 of 25 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (74 of 74 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (45 of 45 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: alpha <etc@alpha-line.org>
Co-authored-by: yhi264 <yhiraki@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-auth/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-filter/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-groups/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-events/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-exports/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ja/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ja/
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-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
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-settings
Translation: Frigate NVR/views-system
2026-06-06 22:25:39 -05:00
f70c142892 Translated using Weblate (Ukrainian)
Currently translated at 93.0% (120 of 129 strings)

Translated using Weblate (Ukrainian)

Currently translated at 77.7% (136 of 175 strings)

Translated using Weblate (Ukrainian)

Currently translated at 54.9% (649 of 1181 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Ukrainian)

Currently translated at 92.2% (119 of 129 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.1% (25 of 26 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Ukrainian)

Currently translated at 54.7% (644 of 1176 strings)

Translated using Weblate (Ukrainian)

Currently translated at 90.0% (91 of 101 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: ivabil <ivanbilych@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/uk/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/uk/
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-06-06 22:25:39 -05:00
7c3b7f3c12 Translated using Weblate (Bulgarian)
Currently translated at 71.2% (357 of 501 strings)

Translated using Weblate (Bulgarian)

Currently translated at 20.4% (26 of 127 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: dirty <dirty@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/bg/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/bg/
Translation: Frigate NVR/audio
Translation: Frigate NVR/objects
2026-06-06 22:25:39 -05:00
0756889d0e Translated using Weblate (Romanian)
Currently translated at 100.0% (1276 of 1276 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (809 of 809 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1272 of 1272 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (175 of 175 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (101 of 101 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (475 of 475 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (62 of 62 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1268 of 1268 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (807 of 807 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1263 of 1263 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1186 of 1186 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1183 of 1183 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (53 of 53 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (1176 of 1176 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (238 of 238 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: lukasig <lukasig@hotmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/ro/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/ro/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-explore
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-settings
Translation: Frigate NVR/views-system
2026-06-06 22:25:39 -05:00
bb1f8757e6 Translated using Weblate (Estonian)
Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Estonian)

Currently translated at 35.0% (21 of 60 strings)

Translated using Weblate (Estonian)

Currently translated at 2.5% (12 of 473 strings)

Translated using Weblate (Estonian)

Currently translated at 13.9% (18 of 129 strings)

Translated using Weblate (Estonian)

Currently translated at 41.3% (60 of 145 strings)

Translated using Weblate (Estonian)

Currently translated at 18.5% (234 of 1263 strings)

Translated using Weblate (Estonian)

Currently translated at 1.8% (15 of 811 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (49 of 49 strings)

Translated using Weblate (Estonian)

Currently translated at 14.8% (8 of 54 strings)

Translated using Weblate (Estonian)

Currently translated at 11.4% (20 of 175 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Estonian)

Currently translated at 26.6% (12 of 45 strings)

Translated using Weblate (Estonian)

Currently translated at 3.3% (2 of 59 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (Estonian)

Currently translated at 1.6% (8 of 473 strings)

Translated using Weblate (Estonian)

Currently translated at 0.3% (3 of 811 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/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-classificationmodel/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-explore/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-facelibrary/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-replay/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-search/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/et/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-system/et/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-classificationmodel
Translation: Frigate NVR/views-explore
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-06-06 22:25:39 -05:00
38bc0397a6 Translated using Weblate (German)
Currently translated at 100.0% (807 of 807 strings)

Translated using Weblate (German)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (German)

Currently translated at 100.0% (1268 of 1268 strings)

Translated using Weblate (German)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (German)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (German)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (German)

Currently translated at 95.6% (1208 of 1263 strings)

Translated using Weblate (German)

Currently translated at 100.0% (100 of 100 strings)

Translated using Weblate (German)

Currently translated at 100.0% (238 of 238 strings)

Translated using Weblate (German)

Currently translated at 100.0% (811 of 811 strings)

Translated using Weblate (German)

Currently translated at 100.0% (23 of 23 strings)

Translated using Weblate (German)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (German)

Currently translated at 100.0% (473 of 473 strings)

Translated using Weblate (German)

Currently translated at 99.5% (1178 of 1183 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Sebastian Sie <sebastian.neuplanitz@googlemail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-cameras/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-global/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/config-validation/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-chat/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-live/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-motionsearch/de/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/de/
Translation: Frigate NVR/Config - Cameras
Translation: Frigate NVR/Config - Global
Translation: Frigate NVR/Config - Validation
Translation: Frigate NVR/common
Translation: Frigate NVR/components-player
Translation: Frigate NVR/views-chat
Translation: Frigate NVR/views-live
Translation: Frigate NVR/views-motionSearch
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
1674058b85 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.5% (238 of 239 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 80.1% (81 of 101 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (499 of 501 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.3% (234 of 238 strings)

Co-authored-by: AmilcarNetto <amilcar.netto@gmail.com>
Co-authored-by: Geraldo Fensterseifer Júnior <gerafenster@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/pt_BR/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-dialog
2026-06-06 22:25:39 -05:00
9b9bde9491 Added translation using Weblate (Telugu)
Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Added translation using Weblate (Telugu)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Pavann Kumar Kade <pavannkumar.kade@gmail.com>
2026-06-06 22:25:39 -05:00
fb3c72359f Translated using Weblate (Latvian)
Currently translated at 94.5% (226 of 239 strings)

Translated using Weblate (Latvian)

Currently translated at 24.7% (124 of 501 strings)

Translated using Weblate (Latvian)

Currently translated at 100.0% (26 of 26 strings)

Translated using Weblate (Latvian)

Currently translated at 22.0% (28 of 127 strings)

Translated using Weblate (Latvian)

Currently translated at 100.0% (50 of 50 strings)

Translated using Weblate (Latvian)

Currently translated at 6.6% (85 of 1276 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Jānis Sanders <sanders.janis@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/audio/lv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/common/lv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-camera/lv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-player/lv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/objects/lv/
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/views-settings/lv/
Translation: Frigate NVR/audio
Translation: Frigate NVR/common
Translation: Frigate NVR/components-camera
Translation: Frigate NVR/components-player
Translation: Frigate NVR/objects
Translation: Frigate NVR/views-settings
2026-06-06 22:25:39 -05:00
ec7d0c8f7b Translated using Weblate (Turkish)
Currently translated at 88.1% (89 of 101 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Turhan Munis <turhan.munis@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/frigate-nvr/components-dialog/tr/
Translation: Frigate NVR/components-dialog
2026-06-06 22:25:39 -05:00
b7cdc1c614 Docs updates (#23407)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
* refactor go2rtc docs

* clarify go2rtc language in live

* add export docs

* Move around config items to reflect reference config is now for advanced users

* Remove outdated ipv6 section

* Fix broken links

* live usage docs

* review usage docs

* history usage

* explore usage

* add usage sidebar and move related text to usage sections

* update links

* update live

* move exports to usage

* fix anchors

* Make starts of usage pages consistent

* refactor network config

* Adjustments for review

* Add AI details to history page

* describe alerts vs detections in review usage

* simplify

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-06-04 17:07:12 -06:00
Josh HawkinsandGitHub d594e9d9a6 Add script to investigate accuracy of classification training set (#23324)
* add classification testing script

* add caveat
2026-06-04 15:19:45 -06:00
Josh HawkinsandGitHub 8343a96746 Update reference config (#23404)
* update reference config to include missing fields

* tweak
2026-06-04 14:24:52 -06:00
Josh HawkinsandGitHub a4f077b128 Miscellaneous fixes (#23394)
* serialize OpenVINO inference per process to prevent concurrent-inference segfault

* clean up

* add max scaling meta to login page

* add more detect section field messages

* fix icon layout in settings field messages

* tweak edit icon color
2026-06-04 12:48:58 -06:00
Josh HawkinsandGitHub b751025339 Mobile UI/UX improvements (#23402)
CI / Synaptics Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Assemble and push default build (push) Blocked by required conditions
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Build (push) Waiting to run
* increase camera group icon size on mobile

add an animated slider when there is not enough space for all defined camera groups

* change desktop and mobile edit camera groups icon to pencil and add desktop tooltip

* apply safe area insets to mobile layout in PWA mode using viewport-fit=cover

* adaptively size bottom bar nav targets to 48px when they fit, else compact

icon size now targets the standardized 48×48px mobile touch target (Material Design 3 / Android 48dp bottom-nav minimum)
2026-06-04 09:56:11 -06:00
Josh HawkinsandGitHub 7e83d5de90 add snapshot download to History player (#23395)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
2026-06-03 16:17:04 -06:00
Nicolas MowenandGitHub a08e2d7529 Upgrade ffmpeg to 8 by default (#23393)
* Upgrade to ffmpeg 8

* Remove workaround

* Cleanup ffmpeg version resolution

* Include older 7.0 for testing purposes

* include
2026-06-03 12:28:28 -05:00
Nicolas MowenandGitHub 3f0ebb3577 Add ability to hide cameras from review UI (#23387)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* Add field to control if cameras show in review

* i18n

* Add config to UI
2026-06-02 16:11:42 -05:00
c25a522fcc docs: fix spelling mistakes in documentation (#23380)
* docs: fix spelling mistakes in documentation

* docs: fix typos and revert incorrect dfine to define rename

* docs: fix typo in installation.md

---------

Co-authored-by: TheInfamousToTo <TheInfamousToTo@users.noreply.github.com>
2026-06-02 05:49:42 -06:00
Josh HawkinsandGitHub db9e64c598 replace motion activity resample apply/agg lambdas with vectorized max() and first() (#23383)
CI / AMD64 Build (push) Waiting to run
CI / Assemble and push default build (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
CI / Synaptics Build (push) Blocked by required conditions
2026-06-01 15:51:43 -06:00
Josh HawkinsandGitHub 570e21340a Miscellaneous fixes (#23373)
* republish MQTT switch states when a profile is activated or deactivated

* fix object mask default name when created from Explore tracking details

* tweak annotation offset max in UI

* optimize recordings/unavailable gap detection and drop empty motion activity buckets

* add tests
2026-06-01 13:55:52 -06:00
Josh HawkinsandGitHub 8073174c20 Refactor motion search (#23378)
* refactor motion search

* cleanup dead code and tests

* tweaks

* fix multi-day seeking

* start playback a few seconds before the change so the motion is in view
2026-06-01 12:08:46 -05:00
Josh HawkinsandGitHub 47a06c8b30 Tweaks (#23367)
CI / AMD64 Build (push) Waiting to run
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
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* add ptz presets and default role widgets

* language tweaks

* fix width in triggers view

* tweak iOS PWA message in notifications settings

* deprecate ui.date_style and ui.time_style

these have been unused since date/time formatting has been pushed to i18n

* add config migrator to remove date_style and time_style

* remove date_style and time_style from reference config

* fix camera list scrolling in state classification wizard on mobile
2026-05-31 15:09:10 -06:00
Josh HawkinsandGitHub ae60197cb0 Support onvif PasswordText cameras in the add camera wizard (#23365)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run
* try both onvif WS-Security password encodings when probing in the add camera wizard

* update onvif docs

* add tests
2026-05-31 08:20:09 -06:00
Josh HawkinsandGitHub 407817a3b1 Motion search fixes (#23359)
* improve error parsing and increase skip default

* improve motion search  layout to match tracking details

* implement draw and move mode on mobile

* update motion search docs

* language tweaks

* improve tips

* note actions menu
2026-05-31 07:51:32 -06:00
Nicolas MowenandGitHub 28e3e1ec74 Add ability to control chapters set on MP4 Export (#23310)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
2026-05-25 13:06:16 -05:00
Josh HawkinsandGitHub fa07109a85 filter motion review by allowed cameras (#23294)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
2026-05-23 06:47:32 -06:00
Josh HawkinsandGitHub 910059281f update mask docs for more clarity (#23282) 2026-05-21 14:00:46 -06:00
Josh HawkinsandGitHub ef44c18c07 Docs update (#23280)
* stationary car detection troubleshooting tips

* tweak
2026-05-21 09:04:41 -05:00
Josh HawkinsandGitHub 06b059c36a fix admin response cache leak to non-admin users via nginx proxy_cache (#23261)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
2026-05-20 07:29:37 -05:00
Nicolas MowenandGitHub 26d31300e6 Add metadata for creation time to recording segments and exports (#23239)
CI / AMD64 Build (push) Has been cancelled
CI / ARM Build (push) Has been cancelled
CI / Jetson Jetpack 6 (push) Has been cancelled
CI / AMD64 Extra Build (push) Has been cancelled
CI / ARM Extra Build (push) Has been cancelled
CI / Assemble and push default build (push) Has been cancelled
CI / Synaptics Build (push) Has been cancelled
2026-05-18 10:58:10 -05:00
0013555528 Fixes (#23235)
* use stable empty object reference for swr metadata default

* version bump

* Refactor get_min_region_size for dimension normalization

Refactor get_min_region_size to normalize dimensions for smaller models and ensure minimum region size is 320 for larger models.

* reject restricted go2rtc stream sources when added via api

* add env var check function

* fix typing

---------

Co-authored-by: Nicolas Mowen <nickmowen213@gmail.com>
2026-05-18 10:32:39 -05:00
495 changed files with 24856 additions and 5991 deletions
+9
View File
@@ -162,6 +162,7 @@ mpegts
mqtt
mse
msenc
muxing
namedtuples
nbytes
nchw
@@ -197,6 +198,8 @@ OWASP
paddleocr
paho
passwordless
PCMA
PCMU
popleft
posthog
postprocess
@@ -222,7 +225,9 @@ radeontop
rawvideo
rcond
RDONLY
realmonitor
rebranded
recvonly
referer
reindex
Reolink
@@ -239,8 +244,11 @@ rocminfo
rootfs
rtmp
RTSP
rtsps
rtspx
ruamel
scroller
sendonly
setproctitle
setpts
shms
@@ -251,6 +259,7 @@ SNDMORE
socs
sqliteq
sqlitevecq
Srtp
ssdlite
statm
stimeout
+2
View File
@@ -125,5 +125,7 @@ jobs:
run: devcontainer up --workspace-folder .
- name: Run mypy in devcontainer
run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m mypy --config-file frigate/mypy.ini frigate"
- name: Check API spec is up to date
run: devcontainer exec --workspace-folder . bash -lc "python3 generate_api_auth_spec.py --check"
- name: Run unit tests in devcontainer
run: devcontainer exec --workspace-folder . bash -lc "python3 -u -m unittest"
+10
View File
@@ -235,6 +235,14 @@ ruff check frigate/
# Type check
python3 -u -m mypy --config-file frigate/mypy.ini frigate
# Regenerate the OpenAPI spec after adding, changing, or removing an API
# endpoint or its auth dependency — outputs docs/static/frigate-api.yaml,
# annotated with each endpoint's auth requirement (admin / any / camera /
# public). NEVER edit that file by hand. CI runs the --check variant and fails
# if it is out of date. (from repo root)
python3 generate_api_auth_spec.py
python3 generate_api_auth_spec.py --check
```
### Frontend (from web/ directory)
@@ -316,6 +324,8 @@ async def get_events(request: Request, limit: int = 100):
# Implementation
```
After adding, changing, or removing an endpoint (or its auth dependency), regenerate the OpenAPI spec with `python3 generate_api_auth_spec.py` so `docs/static/frigate-api.yaml` stays in sync and the endpoint's auth requirement is documented. CI enforces this via the `--check` variant; never edit that file by hand.
### Configuration Access
```python
+2 -2
View File
@@ -265,8 +265,8 @@ ENV PATH="/usr/local/go2rtc/bin:/usr/local/tempio/bin:/usr/local/nginx/sbin:${PA
RUN --mount=type=bind,source=docker/main/install_deps.sh,target=/deps/install_deps.sh \
/deps/install_deps.sh
ENV DEFAULT_FFMPEG_VERSION="7.0"
ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:5.0"
ENV DEFAULT_FFMPEG_VERSION="8.0"
ENV INCLUDED_FFMPEG_VERSIONS="${DEFAULT_FFMPEG_VERSION}:7.0:5.0"
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
&& sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \
+10 -2
View File
@@ -52,9 +52,13 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe
rm -rf ffmpeg.tar.xz
mkdir -p /usr/lib/ffmpeg/7.0
wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-03-19-13-03/ffmpeg-n7.1.3-43-g5a1f107b4c-linux64-gpl-7.1.tar.xz"
wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz"
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe
rm -rf ffmpeg.tar.xz
mkdir -p /usr/lib/ffmpeg/8.0
wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linux64-gpl-8.1.tar.xz"
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe
rm -rf ffmpeg.tar.xz
fi
# ffmpeg -> arm64
@@ -64,9 +68,13 @@ if [[ "${TARGETARCH}" == "arm64" ]]; then
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe
rm -f ffmpeg.tar.xz
mkdir -p /usr/lib/ffmpeg/7.0
wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-03-19-13-03/ffmpeg-n7.1.3-43-g5a1f107b4c-linuxarm64-gpl-7.1.tar.xz"
wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz"
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe
rm -f ffmpeg.tar.xz
mkdir -p /usr/lib/ffmpeg/8.0
wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linuxarm64-gpl-8.1.tar.xz"
tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe
rm -f ffmpeg.tar.xz
fi
# arch specific packages
@@ -5,11 +5,7 @@ from typing import Any
from ruamel.yaml import YAML
sys.path.insert(0, "/opt/frigate")
from frigate.const import (
DEFAULT_FFMPEG_VERSION,
INCLUDED_FFMPEG_VERSIONS,
)
from frigate.util.config import find_config_file
from frigate.util.config import find_config_file, resolve_ffmpeg_path
sys.path.remove("/opt/frigate")
@@ -29,9 +25,4 @@ except FileNotFoundError:
config: dict[str, Any] = {}
path = config.get("ffmpeg", {}).get("path", "default")
if path == "default":
print(f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg")
elif path in INCLUDED_FFMPEG_VERSIONS:
print(f"/usr/lib/ffmpeg/{path}/bin/ffmpeg")
else:
print(f"{path}/bin/ffmpeg")
print(resolve_ffmpeg_path(path, "ffmpeg"))
@@ -11,18 +11,28 @@ sys.path.insert(0, "/opt/frigate")
from frigate.config.env import substitute_frigate_vars
from frigate.const import (
BIRDSEYE_PIPE,
DEFAULT_FFMPEG_VERSION,
INCLUDED_FFMPEG_VERSIONS,
LIBAVFORMAT_VERSION_MAJOR,
)
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
from frigate.util.config import find_config_file
from frigate.util.services import is_restricted_go2rtc_source
from frigate.util.config import find_config_file, resolve_ffmpeg_path
from frigate.util.services import (
is_go2rtc_arbitrary_exec_allowed,
is_restricted_go2rtc_source,
)
sys.path.remove("/opt/frigate")
yaml = YAML()
FRIGATE_ENV_VARS = {k: v for k, v in os.environ.items() if k.startswith("FRIGATE_")}
# read docker secret files as env vars too
if os.path.isdir("/run/secrets"):
for secret_file in os.listdir("/run/secrets"):
if secret_file.startswith("FRIGATE_"):
FRIGATE_ENV_VARS[secret_file] = (
Path(os.path.join("/run/secrets", secret_file)).read_text().strip()
)
config_file = find_config_file()
try:
@@ -81,12 +91,7 @@ if go2rtc_config.get("rtsp", {}).get("password") is not None:
# ensure ffmpeg path is set correctly
path = config.get("ffmpeg", {}).get("path", "default")
if path == "default":
ffmpeg_path = f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
elif path in INCLUDED_FFMPEG_VERSIONS:
ffmpeg_path = f"/usr/lib/ffmpeg/{path}/bin/ffmpeg"
else:
ffmpeg_path = f"{path}/bin/ffmpeg"
ffmpeg_path = resolve_ffmpeg_path(path, "ffmpeg")
if go2rtc_config.get("ffmpeg") is None:
go2rtc_config["ffmpeg"] = {"bin": ffmpeg_path}
@@ -107,7 +112,7 @@ for name in list(go2rtc_config.get("streams", {})):
if isinstance(stream, str):
try:
formatted_stream = substitute_frigate_vars(stream)
formatted_stream = stream.format(**FRIGATE_ENV_VARS)
if is_restricted_go2rtc_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' uses a restricted source (echo/expr/exec) which is disabled by default for security. "
@@ -126,7 +131,7 @@ for name in list(go2rtc_config.get("streams", {})):
filtered_streams = []
for i, stream_item in enumerate(stream):
try:
formatted_stream = substitute_frigate_vars(stream_item)
formatted_stream = stream_item.format(**FRIGATE_ENV_VARS)
if is_restricted_go2rtc_source(formatted_stream):
print(
f"[ERROR] Stream '{name}' item {i + 1} uses a restricted source (echo/expr/exec) which is disabled by default for security. "
@@ -150,6 +155,20 @@ for name in list(go2rtc_config.get("streams", {})):
)
del go2rtc_config["streams"][name]
elif isinstance(stream, dict):
# The map form ({"url": ...}) lets go2rtc resolve the source
# recursively, so it is effectively a dynamic way to generate the URL
# for a stream. That can only be backed by an exec source, so it cannot
# be allowed unless arbitrary exec is explicitly enabled. When it is
# enabled, leave the map untouched for go2rtc to resolve.
if not is_go2rtc_arbitrary_exec_allowed():
print(
f"[ERROR] Stream '{name}' uses a dynamic source format which is disabled by default for security. "
f"Set GO2RTC_ALLOW_ARBITRARY_EXEC=true to enable arbitrary exec sources."
)
del go2rtc_config["streams"][name]
continue
# add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False):
birdseye: dict[str, Any] = config.get("birdseye")
+349
View File
@@ -0,0 +1,349 @@
{
"edgeTPU": {
"title": "EdgeTPU",
"models": [
{
"key": "mobiledet",
"label": "Mobiledet",
"recommended": true,
"download": "A TensorFlow Lite model is provided in the container at `/edgetpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`.",
"yaml": "detectors:\n coral:\n type: edgetpu\n device: usb"
},
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": false,
"download": "[Download the model](https://github.com/dbro/frigate-detector-edgetpu-yolo9/releases/download/v1.0/yolov9-s-relu6-best_320_int8_edgetpu.tflite), bind mount the file into the container, and provide the path with `model.path`. Note that the linked model requires a 17-label [labelmap file](https://raw.githubusercontent.com/dbro/frigate-detector-edgetpu-yolo9/refs/heads/main/labels-coco17.txt) that includes only 17 COCO classes.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **EdgeTPU** from the detector type dropdown and click **Add**, then set device to `usb`. Then on the same page, in the **Custom Model** tab, configure the model settings:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize of the model) |\n| **Object detection model input height** | `320` (should match the imgsize of the model) |\n| **Custom object detector model path** | `/config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite` |\n| **Label map for custom object detector** | `/config/labels-coco17.txt` |",
"yaml": "detectors:\n coral:\n type: edgetpu\n device: usb\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize of the model, typically 320\n height: 320 # <--- should match the imgsize of the model, typically 320\n path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite\n labelmap_path: /config/labels-coco17.txt"
}
]
},
"hailo8l": {
"title": "Hailo-8/Hailo-8L",
"models": [
{
"key": "yolo",
"label": "YOLO",
"recommended": true,
"download": "If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup based on the detected hardware. Once cached under `/config/model_cache/hailo`, the model works fully offline.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Model Input Pixel Color Format** | `rgb` |\n| **Model Input D Type** | `int` |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |\n\nThe detector automatically selects the default model based on your hardware. Optionally, specify a local model path or URL to override.",
"yaml": "detectors:\n hailo:\n type: hailo8l\n device: PCIe\n\nmodel:\n width: 320\n height: 320\n input_tensor: nhwc\n input_pixel_format: rgb\n input_dtype: int\n model_type: yolo-generic\n labelmap_path: /labelmap/coco-80.txt\n\n # The detector automatically selects the default model based on your hardware:\n # - For Hailo-8 hardware: YOLOv6n (default: yolov6n.hef)\n # - For Hailo-8L hardware: YOLOv6n (default: yolov6n.hef)\n #\n # Optionally, you can specify a local model path to override the default.\n # If a local path is provided and the file exists, it will be used instead of downloading.\n # Example:\n # path: /config/model_cache/hailo/yolov6n.hef\n #\n # You can also override using a custom URL:\n # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8/yolov6n.hef\n # just make sure to give it the write configuration based on the model"
},
{
"key": "ssd",
"label": "SSD MobileNet v1",
"recommended": false,
"download": "For SSD-based models, provide either a model path or URL to your compiled SSD model. The integration will first check the local path before downloading if necessary. The model file is cached under `/config/model_cache/hailo`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **Hailo-8/Hailo-8L** from the detector type dropdown and click **Add**, then set device to `PCIe`. Then on the same page, in the **Custom Model** tab, configure the model settings:\n\n| Field | Value |\n| --------------------------------------- | ------ |\n| **Object detection model input width** | `300` |\n| **Object detection model input height** | `300` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Model Input Pixel Color Format** | `rgb` |\n| **Object Detection Model Type** | `ssd` |\n\nSpecify the local model path or URL for SSD MobileNet v1.",
"yaml": "detectors:\n hailo:\n type: hailo8l\n device: PCIe\n\nmodel:\n width: 300\n height: 300\n input_tensor: nhwc\n input_pixel_format: rgb\n model_type: ssd\n # Specify the local model path (if available) or URL for SSD MobileNet v1.\n # Example with a local path:\n # path: /config/model_cache/h8l_cache/ssd_mobilenet_v1.hef\n #\n # Or override using a custom URL:\n # path: https://hailo-model-zoo.s3.eu-west-2.amazonaws.com/ModelZoo/Compiled/v2.14.0/hailo8l/ssd_mobilenet_v1.hef"
}
]
},
"openvino": {
"title": "OpenVINO",
"models": [
{
"key": "ssd",
"label": "SSDLite MobileNet v2",
"recommended": true,
"download": "An OpenVINO model is provided in the container at `/openvino-model/ssdlite_mobilenet_v2.xml` and is used by this detector type by default. The model comes from Intel's Open Model Zoo [SSDLite MobileNet V2](https://github.com/openvinotoolkit/open_model_zoo/tree/master/models/public/ssdlite_mobilenet_v2) and is converted to an FP16 precision IR model.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------ |\n| **Object detection model input width** | `300` |\n| **Object detection model input height** | `300` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Custom object detector model path** | `/openvino-model/ssdlite_mobilenet_v2.xml` |\n| **Label map for custom object detector** | `/openvino-model/coco_91cl_bkgr.txt` |",
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU # Or NPU\n\nmodel:\n width: 300\n height: 300\n input_tensor: nhwc\n input_pixel_format: bgr\n path: /openvino-model/ssdlite_mobilenet_v2.xml\n labelmap_path: /openvino-model/coco_91cl_bkgr.txt"
},
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": false,
"download": "YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).\n\n```sh\ndocker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /yolov9\nADD https://github.com/WongKinYiu/yolov9.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript\nARG MODEL_SIZE\nARG IMG_SIZE\nADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt\nRUN sed -i \"s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g\" models/experimental.py\nRUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx\nFROM scratch\nARG MODEL_SIZE\nARG IMG_SIZE\nCOPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU # or NPU\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolo-legacy",
"label": "YOLO (v3, v4, v7)",
"recommended": false,
"download": "To export as ONNX:\n\n```sh\ngit clone https://github.com/NateMeyer/tensorrt_demos\ncd tensorrt_demos/yolo\n./download_yolo.sh\npython3 yolo_to_onnx.py -m yolov7-320\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU` (or `NPU`). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU # or NPU\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolonas",
"label": "YOLO-NAS",
"recommended": false,
"download": "You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).\n\n:::warning\n\nThe pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html\n\n:::\n\nThe input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` (should match whatever was set in notebook) |\n| **Object detection model input height** | `320` (should match whatever was set in notebook) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Custom object detector model path** | `/config/yolo_nas_s.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU\n\nmodel:\n model_type: yolonas\n width: 320 # <--- should match whatever was set in notebook\n height: 320 # <--- should match whatever was set in notebook\n input_tensor: nchw\n input_pixel_format: bgr\n path: /config/yolo_nas_s.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolox",
"label": "YOLOX",
"recommended": false,
"download": "YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ------------------------------------- | -------------------------------- |\n| **Object Detection Model Type** | `yolox` |\n| **Custom object detector model path** | path to your YOLOX ONNX model |",
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU\n\nmodel:\n model_type: yolox\n path: /config/model_cache/yolox.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "rfdetr",
"label": "RF-DETR",
"recommended": false,
"download": "RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'\nFROM python:3.12 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /rfdetr\nRUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript\nARG MODEL_SIZE\nRUN python3 -c \"from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)\"\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `GPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| --------------------------------------- | --------------------------------- |\n| **Object Detection Model Type** | `rfdetr` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` |",
"yaml": "detectors:\n ov:\n type: openvino\n device: GPU\n\nmodel:\n model_type: rfdetr\n width: 320\n height: 320\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/rfdetr.onnx"
},
{
"key": "dfine",
"label": "D-FINE / DEIMv2",
"recommended": false,
"download": "#### D-FINE\n\nD-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /dfine\nRUN git clone https://github.com/Peterande/D-FINE.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx onnxruntime onnxsim onnxscript\n# Create output directory and download checkpoint\nRUN mkdir -p output\nARG MODEL_SIZE\nRUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth\n# Modify line 58 of export_onnx.py to change batch size to 1\nRUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx\nEOF\n```\n\n#### DEIMv2\n\n[DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families:\n\n- **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n`\n- **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x`\n\nSet `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`).\n\n```sh\ndocker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF'\nFROM python:3.11-slim AS build\nRUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /deimv2\nRUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git .\n# Install CPU-only PyTorch first to avoid pulling CUDA variant\nRUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu\nRUN uv pip install --no-cache --system -r requirements.txt\nRUN uv pip install --no-cache --system onnx safetensors huggingface_hub\nRUN mkdir -p output\nARG BACKBONE\nARG MODEL_SIZE\n# Download from Hugging Face and convert safetensors to pth\nRUN python3 -c \"\\\nfrom huggingface_hub import hf_hub_download; \\\nfrom safetensors.torch import load_file; \\\nimport torch; \\\nbackbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \\\nsize = '${MODEL_SIZE}'.upper(); \\\nst = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \\\ntorch.save({'model': st}, 'output/deimv2.pth')\"\nRUN sed -i \"s/data = torch.rand(2/data = torch.rand(1/\" tools/deployment/export_onnx.py\n# HuggingFace safetensors omits frozen constants that the model constructor initializes\nRUN sed -i \"s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/\" tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth\nFROM scratch\nARG BACKBONE\nARG MODEL_SIZE\nCOPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **OpenVINO** from the detector type dropdown and click **Add**, then set device to `CPU`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------------- |\n| **Object Detection Model Type** | `dfine` |\n| **Object detection model input width** | `640` |\n| **Object detection model input height** | `640` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/dfine-s.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n ov:\n type: openvino\n device: CPU\n\nmodel:\n model_type: dfine\n width: 640\n height: 640\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/dfine-s.onnx\n labelmap_path: /labelmap/coco-80.txt"
}
]
},
"appleSilicon": {
"title": "Apple Silicon",
"models": [
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": true,
"download": "YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).\n\n```sh\ndocker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /yolov9\nADD https://github.com/WongKinYiu/yolov9.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript\nARG MODEL_SIZE\nARG IMG_SIZE\nADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt\nRUN sed -i \"s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g\" models/experimental.py\nRUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx\nFROM scratch\nARG MODEL_SIZE\nARG IMG_SIZE\nCOPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n apple-silicon:\n type: zmq\n endpoint: tcp://host.docker.internal:5555\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolo-legacy",
"label": "YOLO (v3, v4, v7)",
"recommended": false,
"download": "To export as ONNX:\n\n```sh\ngit clone https://github.com/NateMeyer/tensorrt_demos\ncd tensorrt_demos/yolo\n./download_yolo.sh\npython3 yolo_to_onnx.py -m yolov7-320\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ZMQ IPC** from the detector type dropdown and click **Add**, then set the endpoint to `tcp://host.docker.internal:5555`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n apple-silicon:\n type: zmq\n endpoint: tcp://host.docker.internal:5555\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
}
]
},
"onnx": {
"title": "ONNX",
"models": [
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": true,
"download": "YOLOv9 model can be exported as ONNX using the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=t` and `IMG_SIZE=320` in the first line to the [model size](https://github.com/WongKinYiu/yolov9#performance) you would like to convert (available model sizes are `t`, `s`, `m`, `c`, and `e`, common image sizes are `320` and `640`).\n\n```sh\ndocker build . --build-arg MODEL_SIZE=t --build-arg IMG_SIZE=320 --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y cmake libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /yolov9\nADD https://github.com/WongKinYiu/yolov9.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx==1.18.0 onnxruntime onnx-simplifier==0.4.* onnxscript\nARG MODEL_SIZE\nARG IMG_SIZE\nADD https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-${MODEL_SIZE}-converted.pt yolov9-${MODEL_SIZE}.pt\nRUN sed -i \"s/ckpt = torch.load(attempt_download(w), map_location='cpu')/ckpt = torch.load(attempt_download(w), map_location='cpu', weights_only=False)/g\" models/experimental.py\nRUN python3 export.py --weights ./yolov9-${MODEL_SIZE}.pt --imgsz ${IMG_SIZE} --simplify --include onnx\nFROM scratch\nARG MODEL_SIZE\nARG IMG_SIZE\nCOPY --from=build /yolov9/yolov9-${MODEL_SIZE}.onnx /yolov9-${MODEL_SIZE}-${IMG_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "rfdetr",
"label": "RF-DETR",
"recommended": false,
"download": "RF-DETR can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=Nano` in the first line to `Nano`, `Small`, or `Medium` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=Nano --rm --output . -f- <<'EOF'\nFROM python:3.12 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /bin/\nWORKDIR /rfdetr\nRUN uv pip install --system rfdetr[onnxexport] torch==2.8.0 onnx==1.19.1 transformers==4.57.6 onnxscript\nARG MODEL_SIZE\nRUN python3 -c \"from rfdetr import RFDETR${MODEL_SIZE}; x = RFDETR${MODEL_SIZE}(resolution=320); x.export(simplify=True)\"\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /rfdetr/output/inference_model.onnx /rfdetr-${MODEL_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| --------------------------------------- | --------------------------------- |\n| **Object Detection Model Type** | `rfdetr` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/rfdetr.onnx` |",
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: rfdetr\n width: 320\n height: 320\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/rfdetr.onnx"
},
{
"key": "yolonas",
"label": "YOLO-NAS",
"recommended": false,
"download": "You can build and download a compatible model with pre-trained weights using [this notebook](https://github.com/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb) which can be run directly in [Google Colab](https://colab.research.google.com/github/blakeblackshear/frigate/blob/dev/notebooks/YOLO_NAS_Pretrained_Export.ipynb).\n\n:::warning\n\nThe pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html\n\n:::\n\nThe input image size in this notebook is set to 320x320. This results in lower CPU usage and faster inference times without impacting performance in most cases due to the way Frigate crops video frames to areas of interest before running detection. The notebook and config can be updated to 640x640 if desired.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` (should match whatever was set in notebook) |\n| **Object detection model input height** | `320` (should match whatever was set in notebook) |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Custom object detector model path** | `/config/yolo_nas_s.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolonas\n width: 320 # <--- should match whatever was set in notebook\n height: 320 # <--- should match whatever was set in notebook\n input_pixel_format: bgr\n input_tensor: nchw\n path: /config/yolo_nas_s.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolox",
"label": "YOLOX",
"recommended": false,
"download": "YOLOx models can be downloaded [from the YOLOx repo](https://github.com/Megvii-BaseDetection/YOLOX/tree/main/demo/ONNXRuntime).",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolox` |\n| **Object detection model input width** | `416` (should match the imgsize set during model export) |\n| **Object detection model input height** | `416` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float_denorm` |\n| **Custom object detector model path** | `/config/model_cache/yolox_tiny.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolox\n width: 416 # <--- should match the imgsize set during model export\n height: 416 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float_denorm\n path: /config/model_cache/yolox_tiny.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "dfine",
"label": "D-FINE / DEIMv2",
"recommended": false,
"download": "#### Downloading D-FINE Model\n\nD-FINE can be exported as ONNX by running the command below. You can copy and paste the whole thing to your terminal and execute, altering `MODEL_SIZE=s` in the first line to `s`, `m`, or `l` size.\n\n```sh\ndocker build . --build-arg MODEL_SIZE=s --output . -f- <<'EOF'\nFROM python:3.11 AS build\nRUN apt-get update && apt-get install --no-install-recommends -y libgl1 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /dfine\nRUN git clone https://github.com/Peterande/D-FINE.git .\nRUN uv pip install --system -r requirements.txt\nRUN uv pip install --system onnx onnxruntime onnxsim onnxscript\n# Create output directory and download checkpoint\nRUN mkdir -p output\nARG MODEL_SIZE\nRUN wget https://github.com/Peterande/storage/releases/download/dfinev1.0/dfine_${MODEL_SIZE}_obj2coco.pth -O output/dfine_${MODEL_SIZE}_obj2coco.pth\n# Modify line 58 of export_onnx.py to change batch size to 1\nRUN sed -i '58s/data = torch.rand(.*)/data = torch.rand(1, 3, 640, 640)/' tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/dfine/objects365/dfine_hgnetv2_${MODEL_SIZE}_obj2coco.yml -r output/dfine_${MODEL_SIZE}_obj2coco.pth\nFROM scratch\nARG MODEL_SIZE\nCOPY --from=build /dfine/output/dfine_${MODEL_SIZE}_obj2coco.onnx /dfine-${MODEL_SIZE}.onnx\nEOF\n```\n\n#### Downloading DEIMv2 Model\n\n[DEIMv2](https://github.com/Intellindust-AI-Lab/DEIMv2) can be exported as ONNX by running the command below. Pretrained weights are available on Hugging Face for two backbone families:\n\n- **HGNetv2** (smaller/faster): `atto`, `femto`, `pico`, `n`\n- **DINOv3** (larger/more accurate): `s`, `m`, `l`, `x`\n\nSet `BACKBONE` and `MODEL_SIZE` in the first line to match your desired variant. Hugging Face model names use uppercase (e.g. `HGNetv2_N`, `DINOv3_S`), while config files use lowercase (e.g. `hgnetv2_n`, `dinov3_s`).\n\n```sh\ndocker build . --rm --build-arg BACKBONE=hgnetv2 --build-arg MODEL_SIZE=n --output . -f- <<'EOF'\nFROM python:3.11-slim AS build\nRUN apt-get update && apt-get install --no-install-recommends -y git libgl1 libglib2.0-0 && rm -rf /var/lib/apt/lists/*\nCOPY --from=ghcr.io/astral-sh/uv:0.8.0 /uv /bin/\nWORKDIR /deimv2\nRUN git clone https://github.com/Intellindust-AI-Lab/DEIMv2.git .\n# Install CPU-only PyTorch first to avoid pulling CUDA variant\nRUN uv pip install --no-cache --system torch torchvision --index-url https://download.pytorch.org/whl/cpu\nRUN uv pip install --no-cache --system -r requirements.txt\nRUN uv pip install --no-cache --system onnx safetensors huggingface_hub\nRUN mkdir -p output\nARG BACKBONE\nARG MODEL_SIZE\n# Download from Hugging Face and convert safetensors to pth\nRUN python3 -c \"\\\nfrom huggingface_hub import hf_hub_download; \\\nfrom safetensors.torch import load_file; \\\nimport torch; \\\nbackbone = '${BACKBONE}'.replace('hgnetv2','HGNetv2').replace('dinov3','DINOv3'); \\\nsize = '${MODEL_SIZE}'.upper(); \\\nst = load_file(hf_hub_download('Intellindust/DEIMv2_' + backbone + '_' + size + '_COCO', 'model.safetensors')); \\\ntorch.save({'model': st}, 'output/deimv2.pth')\"\nRUN sed -i \"s/data = torch.rand(2/data = torch.rand(1/\" tools/deployment/export_onnx.py\n# HuggingFace safetensors omits frozen constants that the model constructor initializes\nRUN sed -i \"s/cfg.model.load_state_dict(state)/cfg.model.load_state_dict(state, strict=False)/\" tools/deployment/export_onnx.py\nRUN python3 tools/deployment/export_onnx.py -c configs/deimv2/deimv2_${BACKBONE}_${MODEL_SIZE}_coco.yml -r output/deimv2.pth\nFROM scratch\nARG BACKBONE\nARG MODEL_SIZE\nCOPY --from=build /deimv2/output/deimv2.onnx /deimv2_${BACKBONE}_${MODEL_SIZE}.onnx\nEOF\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------- |\n| **Object Detection Model Type** | `dfine` |\n| **Object detection model input width** | `640` |\n| **Object detection model input height** | `640` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/dfine_m_obj2coco.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: dfine\n width: 640\n height: 640\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/dfine_m_obj2coco.onnx\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolo-legacy",
"label": "YOLO (v3, v4, v7)",
"recommended": false,
"download": "To export as ONNX:\n\n```sh\ngit clone https://github.com/NateMeyer/tensorrt_demos\ncd tensorrt_demos/yolo\n./download_yolo.sh\npython3 yolo_to_onnx.py -m yolov7-320\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **ONNX** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (should match the imgsize set during model export) |\n| **Object detection model input height** | `320` (should match the imgsize set during model export) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Custom object detector model path** | `/config/model_cache/yolo.onnx` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n onnx:\n type: onnx\n\nmodel:\n model_type: yolo-generic\n width: 320 # <--- should match the imgsize set during model export\n height: 320 # <--- should match the imgsize set during model export\n input_tensor: nchw\n input_dtype: float\n path: /config/model_cache/yolo.onnx\n labelmap_path: /labelmap/coco-80.txt"
}
]
},
"cpu": {
"title": "CPU",
"models": [
{
"key": "ssd",
"label": "MobileNet v2",
"recommended": true,
"download": "A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and is used by this detector type by default. To provide your own model, bind mount the file into the container and provide the path with `model.path`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **CPU** from the detector type dropdown and click **Add**. Configure the number of threads and click **Add** again to add additional CPU detectors as needed (one per camera is recommended).\n\n| Field | Value |\n| ----------------- | ----- |\n| **Detector type** | `cpu` |\n| **Num threads** | `3` |",
"yaml": "detectors:\n cpu1:\n type: cpu\n num_threads: 3"
}
]
},
"deepstack": {
"title": "DeepStack / CodeProject.AI",
"models": [
{
"key": "yolo",
"label": "YOLO",
"recommended": true,
"download": "This detector runs object detection over the network against a CodeProject.AI or DeepStack server, so no model is downloaded into Frigate itself. Visit the [CodeProject.AI official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to download and install the AI server on your preferred device (e.g. Raspberry Pi, Nvidia Jetson, or other compatible hardware) before configuring the detector.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeepStack** from the detector type dropdown and click **Add**. Set the API URL to point to your CodeProject.AI server (e.g., `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection`).\n\n| Field | Value |\n| ------------- | ---------------------------------------------------------------------- |\n| **API URL** | `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection` |\n| **API Timeout** | `0.1` (seconds) |",
"yaml": "detectors:\n deepstack:\n api_url: http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection\n type: deepstack\n api_timeout: 0.1 # seconds"
}
]
},
"memryx": {
"title": "MemryX",
"models": [
{
"key": "yolonas",
"label": "YOLO-NAS",
"recommended": true,
"download": "The [YOLO-NAS](https://github.com/Deci-AI/super-gradients/blob/master/YOLONAS.md) model included in this detector is downloaded automatically and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage).\n\n**Note:** The default model for the MemryX detector is YOLO-NAS 320x320.\n\nThe input size for **YOLO-NAS** can be set to either **320x320** (default) or **640x640**.\n\n- The default size of **320x320** is optimized for lower CPU usage and faster inference times.\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` (can be set to `640` for higher resolution) |\n| **Object detection model input height** | `320` (can be set to `640` for higher resolution) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: yolonas\n width: 320 # (Can be set to 640 for higher resolution)\n height: 320 # (Can be set to 640 for higher resolution)\n input_tensor: nchw\n input_dtype: float\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/yolonas.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 yolonas.dfp (a file ending with .dfp)\n # \u2514\u2500\u2500 yolonas_post.onnx (optional; only if the model includes a cropped post-processing network)"
},
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": false,
"download": "The YOLOv9s model included in this detector is downloaded from [the original GitHub](https://github.com/WongKinYiu/yolov9) and compiled to DFP with [mx_nc](https://developer.memryx.com/2p1/tools/neural_compiler.html#usage).\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------- |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` (can be set to `640` for higher resolution) |\n| **Object detection model input height** | `320` (can be set to `640` for higher resolution) |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: yolo-generic\n width: 320 # (Can be set to 640 for higher resolution)\n height: 320 # (Can be set to 640 for higher resolution)\n input_tensor: nchw\n input_dtype: float\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/yolov9.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 yolov9.dfp (a file ending with .dfp)"
},
{
"key": "yolox",
"label": "YOLOX",
"recommended": false,
"download": "The model is sourced from the [OpenCV Model Zoo](https://github.com/opencv/opencv_zoo) and precompiled to DFP.\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Object Detection Model Type** | `yolox` |\n| **Object detection model input width** | `640` |\n| **Object detection model input height** | `640` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float_denorm` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: yolox\n width: 640\n height: 640\n input_tensor: nchw\n input_dtype: float_denorm\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/yolox.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 yolox.dfp (a file ending with .dfp)"
},
{
"key": "ssd",
"label": "SSDLite MobileNet v2",
"recommended": false,
"download": "The model is sourced from the [OpenMMLab Model Zoo](https://mmdeploy-oss.openmmlab.com/model/mmdet-det/ssdlite-e8679f.onnx) and has been converted to DFP.\n\nMemryX `.dfp` models are automatically downloaded at runtime, if enabled, to the container at `/memryx_models/model_folder/`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **MemryX** from the detector type dropdown and click **Add**, then set device to `PCIe:0`. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Object Detection Model Type** | `ssd` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input D Type** | `float` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n memx0:\n type: memryx\n device: PCIe:0\n\nmodel:\n model_type: ssd\n width: 320\n height: 320\n input_tensor: nchw\n input_dtype: float\n labelmap_path: /labelmap/coco-80.txt\n # Optional: The model is normally fetched through the runtime, so 'path' can be omitted unless you want to use a custom or local model.\n # path: /config/ssdlite_mobilenet.zip\n # The .zip file must contain:\n # \u251c\u2500\u2500 ssdlite_mobilenet.dfp (a file ending with .dfp)\n # \u2514\u2500\u2500 ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network)"
}
]
},
"tensorrt": {
"title": "TensorRT",
"models": [
{
"key": "yolo-legacy",
"label": "YOLO (v3, v4, v7)",
"recommended": true,
"download": "The model used for TensorRT must be preprocessed on the same hardware platform that it will run on, so Frigate generates the `.trt` model file on-device at startup. Processed models are stored in the `/config/model_cache` folder.\n\nBy default no models are generated. Set the `YOLO_MODELS` environment variable in Docker to one or more comma-separated model names (from the available `yolov3`/`yolov4`/`yolov7` models) and each one will be generated on startup if the corresponding `{model}.trt` file is not already present in `model_cache` (delete it to force regeneration). On Jetson devices with DLAs (Xavier or Orin), append `-dla` to a model name to generate a DLA model. If your GPU does not support FP16 operations, pass `USE_FP16=False` to disable it.\n\nAn example `docker-compose.yml` fragment that converts the `yolov7-320` and `yolov7x-640` models:\n\n```yml\nfrigate:\n environment:\n - YOLO_MODELS=yolov7-320,yolov7x-640\n - USE_FP16=false\n```",
"ui": "Navigate to **Settings > System > Detectors and model** and select **TensorRT** from the detector type dropdown and click **Add**, then set the device to `0` (the default GPU index). Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ------------------------------------------------------------ |\n| **Custom object detector model path** | `/config/model_cache/tensorrt/yolov7-320.trt` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |\n| **Model Input Tensor Shape** | `nchw` |\n| **Model Input Pixel Color Format** | `rgb` |\n| **Object detection model input width** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |\n| **Object detection model input height** | `320` (MUST match the chosen model, e.g., yolov7-320 -> 320) |",
"yaml": "detectors:\n tensorrt:\n type: tensorrt\n device: 0 #This is the default, select the first GPU\n\nmodel:\n path: /config/model_cache/tensorrt/yolov7-320.trt\n labelmap_path: /labelmap/coco-80.txt\n input_tensor: nchw\n input_pixel_format: rgb\n width: 320 # MUST match the chosen model i.e yolov7-320 -> 320, yolov4-416 -> 416\n height: 320 # MUST match the chosen model i.e yolov7-320 -> 320 yolov4-416 -> 416"
}
]
},
"synaptics": {
"title": "Synaptics",
"models": [
{
"key": "ssd",
"label": "SSD MobileNet",
"recommended": true,
"download": "A synap model is provided in the container at `/mobilenet.synap` and is used by this detector type by default. The model comes from the [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80).",
"ui": "Navigate to **Settings > System > Detectors and model** and select **Synaptics** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------- |\n| **Custom object detector model path** | `/synaptics/mobilenet.synap` |\n| **Object detection model input width** | `224` |\n| **Object detection model input height** | `224` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors: # required\n synap_npu: # required\n type: synaptics # required\n\nmodel: # required\n path: /synaptics/mobilenet.synap # required\n width: 224 # required\n height: 224 # required\n input_tensor: nhwc # default value (optional. If you change the model, it is required)\n labelmap_path: /labelmap/coco-80.txt # required"
}
]
},
"rknn": {
"title": "RKNN",
"models": [
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": true,
"download": "If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.\n\nYou can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.",
"ui": "Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | -------------------------------------------------- |\n| **Custom object detector model path** | `frigate-fp16-yolov9-t` (or other yolov9 variants) |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "model: # required\n # name of model (will be automatically downloaded) or path to your own .rknn model file\n # possible values are:\n # - frigate-fp16-yolov9-t\n # - frigate-fp16-yolov9-s\n # - frigate-fp16-yolov9-m\n # - frigate-fp16-yolov9-c\n # - frigate-fp16-yolov9-e\n # your yolo_model.rknn\n path: frigate-fp16-yolov9-t\n model_type: yolo-generic\n width: 320\n height: 320\n input_tensor: nhwc\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolonas",
"label": "YOLO-NAS",
"recommended": false,
"download": "If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.\n\nYou can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.\n\n**Note:** The pre-trained YOLO-NAS weights from DeciAI are subject to their license and can't be used commercially. For more information, see: https://docs.deci.ai/super-gradients/latest/LICENSE.YOLONAS.html",
"ui": "Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------------------------------------------------------- |\n| **Custom object detector model path** | `deci-fp16-yolonas_s` (or `deci-fp16-yolonas_m`, `deci-fp16-yolonas_l`) |\n| **Object Detection Model Type** | `yolonas` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "model: # required\n # name of model (will be automatically downloaded) or path to your own .rknn model file\n # possible values are:\n # - deci-fp16-yolonas_s\n # - deci-fp16-yolonas_m\n # - deci-fp16-yolonas_l\n # your yolonas_model.rknn\n path: deci-fp16-yolonas_s\n model_type: yolonas\n width: 320\n height: 320\n input_pixel_format: bgr\n input_tensor: nhwc\n labelmap_path: /labelmap/coco-80.txt"
},
{
"key": "yolox",
"label": "YOLOx",
"recommended": false,
"download": "If no custom model is provided, the RKNN detector downloads a default model from GitHub on first startup. Once cached, the model works fully offline. All models are automatically downloaded and stored in the folder `config/model_cache/rknn_cache`. After upgrading Frigate, you should remove older models to free up space.\n\nYou can also provide your own `.rknn` model. You should not save your own models in the `rknn_cache` folder, store them directly in the `model_cache` folder or another subfolder. To convert a model to `.rknn` format see the `rknn-toolkit2` (requires a x86 machine). Note, that there is only post-processing for the supported models.",
"ui": "Navigate to **Settings > System > Detectors and model** and, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------------------------- |\n| **Custom object detector model path** | `rock-i8-yolox_nano` (or other yolox variants) |\n| **Object Detection Model Type** | `yolox` |\n| **Object detection model input width** | `416` |\n| **Object detection model input height** | `416` |\n| **Model Input Tensor Shape** | `nhwc` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "model: # required\n # name of model (will be automatically downloaded) or path to your own .rknn model file\n # possible values are:\n # - rock-i8-yolox_nano\n # - rock-i8-yolox_tiny\n # - rock-fp16-yolox_nano\n # - rock-fp16-yolox_tiny\n # your yolox_model.rknn\n path: rock-i8-yolox_nano\n model_type: yolox\n width: 416\n height: 416\n input_tensor: nhwc\n labelmap_path: /labelmap/coco-80.txt"
}
]
},
"axengine": {
"title": "AXEngine",
"models": [
{
"key": "yolov9",
"label": "YOLOv9",
"recommended": true,
"download": "A yolov9 axmodel is provided in the container at `/axmodels` and is used by this detector type by default. The AXEngine detector downloads its default model from HuggingFace on first startup; once cached, the model works fully offline.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **AXEngine NPU** from the detector type dropdown and click **Add**. Then on the same page, in the **Custom Model** tab, configure:\n\n| Field | Value |\n| ---------------------------------------- | ----------------------- |\n| **Custom object detector model path** | `frigate-yolov9-tiny` |\n| **Object Detection Model Type** | `yolo-generic` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Model Input D Type** | `int` |\n| **Model Input Pixel Color Format** | `bgr` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n axengine:\n type: axengine\n\nmodel:\n path: frigate-yolov9-tiny\n model_type: yolo-generic\n width: 320\n height: 320\n input_dtype: int\n input_pixel_format: bgr\n labelmap_path: /labelmap/coco-80.txt"
}
]
},
"degirumAiServer": {
"title": "DeGirum AI Server",
"models": [
{
"key": "ai-server-inference",
"label": "AI Server Inference",
"recommended": true,
"download": "Launch a DeGirum AI server as a Docker container, then point the detector at it. Add this to your `docker-compose.yml`:\n\n```yaml\ndegirum_detector:\n container_name: degirum\n image: degirum/aiserver:latest\n privileged: true\n ports:\n - \"8778:8778\"\n```\n\nSet `location` to the server's service name, container name, or `host:port`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.\n\n| Field | Value |\n| --- | --- |\n| **Location** | `degirum` |\n| **Zoo** | `degirum/public` |\n| **Token** | your AI Hub token (optional for the public zoo) |\n",
"yaml": "degirum_detector:\n type: degirum\n location: degirum\n zoo: degirum/public\n token: dg_example_token\n"
}
]
},
"degirumLocal": {
"title": "DeGirum Local",
"models": [
{
"key": "local-inference",
"label": "Local Inference",
"recommended": true,
"download": "Run hardware directly inside the Frigate container with `@local`, removing the AI server hop. The matching device runtime (e.g. the Hailo runtime) must be installed in the container; confirm it with `degirum sys-info`.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.\n\n| Field | Value |\n| --- | --- |\n| **Location** | `@local` |\n| **Zoo** | `degirum/public` |\n| **Token** | your AI Hub token (optional for the public zoo) |\n",
"yaml": "degirum_detector:\n type: degirum\n location: @local\n zoo: degirum/public\n token: dg_example_token\n"
}
]
},
"degirumCloud": {
"title": "DeGirum AI Hub Cloud",
"models": [
{
"key": "ai-hub-cloud-inference",
"label": "AI Hub Cloud Inference",
"recommended": true,
"download": "Run inferences on DeGirum's [AI Hub](https://hub.degirum.com) cloud with `@cloud`. Sign up, create an access token, and set it as `token`. Network latency may require lowering your detection fps.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **DeGirum** from the detector type dropdown and click **Add**.\n\n| Field | Value |\n| --- | --- |\n| **Location** | `@cloud` |\n| **Zoo** | `degirum/public` |\n| **Token** | your AI Hub token (optional for the public zoo) |\n",
"yaml": "degirum_detector:\n type: degirum\n location: @cloud\n zoo: degirum/public\n token: dg_example_token\n"
}
]
}
}
@@ -147,6 +147,13 @@ auth:
# NOTE: changing this value will not automatically update password hashes, you
# will need to change each user password for it to apply
hash_iterations: 600000
# Optional: Map roles to the list of cameras each role can access (default: none)
# NOTE: An empty list grants the role access to all cameras. Roles defined here can be
# referenced by proxy header role mapping or assigned to native users.
roles:
my_custom_role:
- front_door
- back_yard
# Optional: model modifications
# NOTE: The default values are for the EdgeTPU detector.
@@ -166,6 +173,9 @@ model:
# Required: Object detection model input tensor format
# Valid values are nhwc or nchw (default: shown below)
input_tensor: nhwc
# Optional: Data type of the model input tensor
# Valid values are float, float_denorm, or int (default: shown below)
input_dtype: int
# Required: Object detection model type, currently only used with the OpenVINO detector
# Valid values are ssd, yolox, yolonas (default: shown below)
model_type: ssd
@@ -196,11 +206,12 @@ audio:
# - 500 - medium sensitivity
# - 1000 - low sensitivity
min_volume: 500
# Optional: Number of threads to use for audio detection (default: shown below)
num_threads: 2
# Optional: Types of audio to listen for (default: shown below)
listen:
- bark
- fire_alarm
- scream
- speech
- yell
# Optional: Filters to configure detection.
@@ -257,7 +268,7 @@ birdseye:
# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets
ffmpeg:
# Optional: ffmpeg binary path (default: shown below)
# can also be set to `7.0` or `5.0` to specify one of the included versions
# can also be set to `8.0` or `5.0` to specify one of the included versions
# or can be set to any path that holds `bin/ffmpeg` & `bin/ffprobe`
path: "default"
# Optional: global ffmpeg args (default: shown below)
@@ -469,6 +480,8 @@ review:
- Animals in the garden
# Optional: Preferred response language (default: English)
preferred_language: English
# Optional: Save thumbnails sent to the GenAI provider for review/debugging purposes (default: shown below)
debug_save_thumbnails: False
# Optional: Motion configuration
# NOTE: Can be overridden at the camera level
@@ -500,6 +513,8 @@ motion:
# - 30 - medium sensitivity
# - 50 - low sensitivity
contour_area: 10
# Optional: Alpha blending factor used in frame differencing for motion calculation (default: shown below)
delta_alpha: 0.2
# Optional: Alpha value passed to cv2.accumulateWeighted when averaging frames to determine the background (default: shown below)
# Higher values mean the current frame impacts the average a lot, and a new object will be averaged into the background faster.
# Low values will cause things like moving shadows to be detected as motion for longer.
@@ -572,6 +587,8 @@ record:
timelapse_args: "-vf setpts=0.04*PTS -r 30"
# Optional: Global hardware acceleration settings for timelapse exports. (default: inherit)
hwaccel_args: auto
# Optional: Maximum number of export jobs to process at the same time (default: shown below)
max_concurrent: 3
# Optional: Recording Preview Settings
preview:
# Optional: Quality of recording preview (default: shown below).
@@ -714,28 +731,42 @@ lpr:
enhancement: 0
# Optional: Save plate images to /media/frigate/clips/lpr for debugging purposes (default: shown below)
debug_save_plates: False
# Optional: List of regex replacement rules to normalize detected plates (default: shown below)
replace_rules: {}
# Optional: List of regex replacement rules to normalize detected plates before matching (default: none)
replace_rules:
# Required: regex pattern to match in the detected plate
- pattern: "O"
# Required: string to replace the matched pattern with
replacement: "0"
# Optional: Configuration for AI / LLM provider
# Optional: Configuration for AI / LLM providers
# WARNING: Depending on the provider, this will send thumbnails over the internet
# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at
# the camera level to enhance privacy for indoor cameras.
# NOTE: genai is a map of named providers. Each key is a name you choose for the provider,
# and each role (chat, descriptions, embeddings) may be assigned to exactly one provider.
genai:
# Required: Provider must be one of ollama, gemini, or openai
provider: ollama
# Required if provider is ollama. May also be used for an OpenAI API compatible backend with the openai provider.
base_url: http://localhost::11434
# Required if gemini or openai
api_key: "{FRIGATE_GENAI_API_KEY}"
# Required: The model to use with the provider.
model: gemini-1.5-flash
# Optional additional args to pass to the GenAI Provider (default: None)
provider_options:
keep_alive: -1
# Optional: Options to pass during inference calls (default: {})
runtime_options:
temperature: 0.7
# Required: name of the provider (chosen by you, used to reference it elsewhere)
my_provider:
# Required: Provider must be one of ollama, openai, azure_openai, gemini, or llamacpp
provider: ollama
# Required if provider is ollama. May also be used for an OpenAI API compatible backend with the openai provider.
base_url: http://localhost::11434
# Required if gemini or openai
api_key: "{FRIGATE_GENAI_API_KEY}"
# Required: The model to use with the provider.
model: gemini-1.5-flash
# Optional: Roles this provider handles (default: shown below)
# Each role (chat, descriptions, embeddings) must be assigned to exactly one provider.
roles:
- chat
- descriptions
- embeddings
# Optional additional args to pass to the GenAI Provider (default: None)
provider_options:
keep_alive: -1
# Optional: Options to pass during inference calls (default: {})
runtime_options:
temperature: 0.7
# Optional: Configuration for audio transcription
# NOTE: only the enabled option can be overridden at the camera level
@@ -908,6 +939,9 @@ cameras:
inertia: 3
# Optional: Number of seconds that an object must loiter to be considered in the zone (default: shown below)
loitering_time: 0
# Optional: Minimum speed required for an object to be considered present in the zone (default: none)
# In real-world units if distances are set. Used for speed-based zone triggers.
speed_threshold: 2.5
# Optional: List of objects that can trigger this zone (default: all tracked objects)
objects:
- person
@@ -945,6 +979,9 @@ cameras:
order: 0
# Optional: Whether or not to show the camera in the Frigate UI (default: shown below)
dashboard: True
# Optional: Whether this camera is visible in review (the review page and its camera
# filter, motion review, and the history view) (default: shown below)
review: True
# Optional: connect to ONVIF camera
# to enable PTZ controls.
@@ -1083,22 +1120,6 @@ ui:
# Optional: Set the time format used.
# Options are browser, 12hour, or 24hour (default: shown below)
time_format: browser
# Optional: Set the date style for a specified length.
# Options are: full, long, medium, short
# Examples:
# short: 2/11/23
# medium: Feb 11, 2023
# full: Saturday, February 11, 2023
# (default: shown below).
date_style: short
# Optional: Set the time style for a specified length.
# Options are: full, long, medium, short
# Examples:
# short: 8:14 PM
# medium: 8:15:22 PM
# full: 8:15:22 PM Mountain Standard Time
# (default: shown below).
time_style: medium
# Optional: Set the unit system to either "imperial" or "metric" (default: metric)
# Used in the UI and in MQTT topics
unit_system: metric
@@ -1,7 +1,6 @@
---
id: advanced
title: Advanced Options
sidebar_label: Advanced Options
id: system
title: System
---
import ConfigTabs from "@site/src/components/ConfigTabs";
@@ -202,7 +201,7 @@ model:
:::warning
If the labelmap is customized then the labels used for alerts will need to be adjusted as well. See [alert labels](../configuration/review.md#restricting-alerts-to-specific-labels) for more info.
If the labelmap is customized then the labels used for alerts will need to be adjusted as well. See [alert labels](../review.md#restricting-alerts-to-specific-labels) for more info.
:::
@@ -234,26 +233,16 @@ Some labels have special handling and modifications can disable functionality.
## Network Configuration
Changes to Frigate's internal network configuration can be made by bind mounting nginx.conf into the container. For example:
```yaml
services:
frigate:
container_name: frigate
...
volumes:
...
- /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf
```
Frigate exposes a few networking options. IPv6 and the listen ports are set in the `networking` configuration (or from the Settings UI); more advanced changes require [customizing the bundled Nginx configuration](#customizing-the-nginx-configuration).
### Enabling IPv6
IPv6 is disabled by default. Enable it in the Frigate configuration.
By default Frigate listens on IPv4 only. To also listen on IPv6 — on port `5000`, and on `8971` when TLS is configured — enable it in the `networking` configuration.
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > Networking" /> and expand **IPv6 configuration**, then enable **Enable IPv6**.
Navigate to <NavPath path="Settings > System > Networking" /> and enable **IPv6**.
</TabItem>
<TabItem value="yaml">
@@ -261,7 +250,7 @@ Navigate to <NavPath path="Settings > System > Networking" /> and expand **IPv6
```yaml
networking:
ipv6:
enabled: True
enabled: true
```
</TabItem>
@@ -300,6 +289,20 @@ This setting is for advanced users. For the majority of use cases it's recommend
:::
### Customizing the Nginx configuration
More advanced changes to Frigate's internal network configuration can be made by bind mounting your own `nginx.conf` into the container. For example:
```yaml
services:
frigate:
container_name: frigate
...
volumes:
...
- /path/to/your/nginx.conf:/usr/local/nginx/conf/nginx.conf
```
## Base path
By default, Frigate runs at the root path (`/`). However some setups require to run Frigate under a custom path prefix (e.g. `/frigate`), especially when Frigate is located behind a reverse proxy that requires path-based routing.
+66 -3
View File
@@ -54,7 +54,7 @@ The ffmpeg process for capturing audio will be a separate connection to the came
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Camera configuration > FFmpeg" /> and add an input with the `audio` role pointing to a stream that includes audio.
Navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" /> and add an input with the `audio` role pointing to a stream that includes audio.
</TabItem>
<TabItem value="yaml">
@@ -88,7 +88,7 @@ Volume is considered motion for recordings, this means when the `record -> retai
### Configuring Audio Events
The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `scream`, `speech`, and `yell` are enabled but these can be customized.
The included audio model has over [500 different types](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) of audio that can be detected, many of which are not practical. By default `bark`, `fire_alarm`, `speech`, and `yell` are enabled but these can be customized.
<ConfigTabs>
<TabItem value="ui">
@@ -107,7 +107,6 @@ audio:
listen:
- bark
- fire_alarm
- scream
- speech
- yell
```
@@ -115,6 +114,70 @@ audio:
</TabItem>
</ConfigTabs>
### Common Audio Labels
The labelmap includes hundreds of sound types. The labels below are the ones most users may find practical, grouped by what they're typically used for. Use the exact label string from the left column in your `listen` config, or search for the label in the Frigate UI directly.
Some labels cover several related sounds: `yell` is triggered by shouting, yelling, children shouting, and screaming; `crying` covers baby cries, sobbing, and whimpering; and `speech` covers ordinary talking and conversation.
**Safety and security**
| Label | Detects |
| ---------------- | ---------------------------------- |
| `yell` | Shouting, yelling, screaming |
| `fire_alarm` | Fire and smoke alarm sirens |
| `smoke_detector` | Smoke detector beeps |
| `alarm` | General alarm sounds |
| `car_alarm` | Car alarms |
| `siren` | Emergency vehicle and civil sirens |
| `glass` | Glass clinking |
| `shatter` | Breaking glass |
| `breaking` | Something breaking |
| `gunshot` | Gunshots |
| `explosion` | Explosions |
**People and activity**
| Label | Detects |
| ----------- | ------------------------ |
| `speech` | Talking and conversation |
| `laughter` | Laughing |
| `crying` | Baby crying and sobbing |
| `cough` | Coughing |
| `footsteps` | Footsteps and walking |
| `knock` | Knocking on a door |
| `doorbell` | Doorbell |
| `ding-dong` | Doorbell chime |
**Pets and animals**
| Label | Detects |
| ---------- | ---------------- |
| `bark` | Dog barking |
| `dog` | Other dog sounds |
| `howl` | Howling |
| `growling` | Growling |
| `meow` | Cat meowing |
| `cat` | Other cat sounds |
| `hiss` | Hissing |
**Vehicles and driveway**
| Label | Detects |
| ----------------- | -------------------- |
| `car` | Passing cars |
| `honk` | Car horns |
| `truck` | Trucks |
| `reversing_beeps` | Vehicle backup beeps |
| `motorcycle` | Motorcycles |
| `engine_starting` | Engines starting |
:::tip
Frequently-heard labels like `speech` can generate a lot of events, and each event could save a snapshot and recording based on your configuration, so start with a focused set — the defaults (`bark`, `fire_alarm`, `speech`, `yell`) plus a few of the safety labels above cover most needs — and expand from there. See the [full audio labelmap](https://github.com/blakeblackshear/frigate/blob/dev/audio-labelmap.txt) or the Frigate UI for every available type.
:::
### Audio Transcription
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service — automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
+1 -1
View File
@@ -167,7 +167,7 @@ A fast [detector](object_detectors.md) is recommended. CPU detectors will not pe
A full-frame zone in `required_zones` is not recommended, especially if you've calibrated your camera and there are `movement_weights` defined in the configuration file. Frigate will continue to autotrack an object that has entered one of the `required_zones`, even if it moves outside of that zone.
Some users have found it helpful to adjust the zone `inertia` value. See the [configuration reference](index.md).
Some users have found it helpful to adjust the zone `inertia` value. See the [configuration reference](advanced/reference.md).
## Zooming
+20 -14
View File
@@ -6,10 +6,16 @@ import NavPath from "@site/src/components/NavPath";
In addition to Frigate's Live camera dashboard, Birdseye allows a portable heads-up view of your cameras to see what is going on around your property / space without having to watch all cameras that may have nothing happening. Birdseye allows specific modes that intelligently show and disappear based on what you care about.
Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the "+" icon on the Live page, and choose "Birdseye" as one of the cameras.
Birdseye can be viewed by adding the "Birdseye" camera to a Camera Group in the Web UI. Add a Camera Group by pressing the pencil icon in the sidebar on the Live page, and choose "Birdseye" as one of the cameras.
Birdseye can also be used in Home Assistant dashboards, cast to media devices, etc.
:::note
Each camera tile in Birdseye is composed from the frames of the stream assigned the `detect` role, so a camera's image quality in Birdseye matches its detect stream resolution rather than a higher-resolution recording stream. If a camera looks low quality in Birdseye, increasing the detect width and height (or assigning the `detect` role to a higher-resolution stream) is what affects it. See [setting up camera inputs](./cameras.md#setting-up-camera-inputs) for how roles are assigned.
:::
## Birdseye Behavior
### Birdseye Modes
@@ -35,10 +41,10 @@ To include a camera in Birdseye view only for specific circumstances, or exclude
**Per-camera overrides:** Navigate to <NavPath path="Settings > Camera configuration > Birdseye" /> to override the mode or disable Birdseye for a specific camera.
| Field | Description |
|-------|-------------|
| **Enable Birdseye** | Whether this camera appears in Birdseye view |
| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` |
| Field | Description |
| ------------------- | ------------------------------------------------------------- |
| **Enable Birdseye** | Whether this camera appears in Birdseye view |
| **Tracking mode** | When to show the camera: `continuous`, `motion`, or `objects` |
</TabItem>
<TabItem value="yaml">
@@ -72,8 +78,8 @@ By default birdseye shows all cameras that have had the configured activity in t
Navigate to <NavPath path="Settings > System > Birdseye" />.
| Field | Description |
|-------|-------------|
| Field | Description |
| ------------------------ | --------------------------------------------------------------------------- |
| **Inactivity threshold** | Seconds of inactivity before a camera is hidden from Birdseye (default: 30) |
</TabItem>
@@ -100,9 +106,9 @@ The resolution and aspect ratio of birdseye can be configured. Resolution will i
Navigate to <NavPath path="Settings > System > Birdseye" />.
| Field | Description |
|-------|-------------|
| **Width** | Birdseye output width in pixels (default: 1280) |
| Field | Description |
| ---------- | ----------------------------------------------- |
| **Width** | Birdseye output width in pixels (default: 1280) |
| **Height** | Birdseye output height in pixels (default: 720) |
</TabItem>
@@ -161,8 +167,8 @@ It is possible to limit the number of cameras shown on birdseye at one time. Whe
Navigate to <NavPath path="Settings > System > Birdseye" />.
| Field | Description |
|-------|-------------|
| Field | Description |
| ------------------------ | ----------------------------------------------------------------------------------- |
| **Layout > Max cameras** | Maximum number of cameras shown at once (e.g., `1` for only the most active camera) |
</TabItem>
@@ -187,8 +193,8 @@ By default birdseye tries to fit 2 cameras in each row and then double in size u
Navigate to <NavPath path="Settings > System > Birdseye" />.
| Field | Description |
|-------|-------------|
| Field | Description |
| --------------------------- | -------------------------------------------------------- |
| **Layout > Scaling factor** | Camera scaling factor between 1.0 and 5.0 (default: 2.0) |
</TabItem>
+9 -2
View File
@@ -24,12 +24,14 @@ Each role can only be assigned to one input per camera. The options for roles ar
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
| Field | Description |
| ----------------- | ------------------------------------------------------------------- |
| **Camera inputs** | List of input stream definitions (paths and roles) for this camera. |
For each input you can choose its source: select **Restream (go2rtc)** to pick an existing [go2rtc stream](restream.md) from a dropdown (Frigate uses the `rtsp://127.0.0.1:8554/<stream>` path and `preset-rtsp-restream` input args for that input automatically), or **Manual input path** to type the stream URL directly.
Navigate to <NavPath path="Settings > Camera configuration > Object detection" />.
| Field | Description |
@@ -143,6 +145,11 @@ If your ONVIF camera does not require authentication credentials, you may still
:::
If a camera connects but fails to authenticate, two optional fields can help:
- `tls_insecure`: Skips TLS certificate verification and sends the ONVIF password as plaintext (`PasswordText`) instead of a hashed digest (`PasswordDigest`). Some cameras reject the digest token and only accept plaintext. This weakens connection security, so only enable it on a trusted local network.
- `ignore_time_mismatch`: ONVIF authentication tokens include a timestamp, and a camera will reject the token if its clock differs too much from Frigate's. Enabling this makes Frigate compensate for the time offset so authentication can still succeed. Running NTP on both the camera and the Frigate host is the recommended fix; only use this in a "safe" environment, as it slightly weakens token validation.
If your camera has multiple ONVIF profiles, you can specify which one to use for PTZ control with the `profile` option, matched by token or name. When not set, Frigate selects the first profile with a valid PTZ configuration. Check the Frigate debug logs (`frigate.ptz.onvif: debug`) to see available profile names and tokens for your camera.
An ONVIF-capable camera that supports relative movement within the field of view (FOV) can also be configured to automatically track moving objects and keep them in the center of the frame. For autotracking setup, see the [autotracking](autotracking.md) docs.
@@ -174,7 +181,7 @@ The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.or
| Hikvision DS-2DE3A404IWG-E/W | ✅ | ✅ | |
| Reolink | ✅ | ❌ | |
| Speco O8P32X | ✅ | ❌ | |
| Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatable. |
| Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatible. |
| Tapo | ✅ | ❌ | Many models supported, ONVIF Service Port: 2020 |
| Uniview IPC672LR-AX4DUPK | ✅ | ❌ | Firmware says FOV relative movement is supported, but camera doesn't actually move when sending ONVIF commands |
| Uniview IPC6612SR-X33-VG | ✅ | ✅ | Leave `calibrate_on_startup` as `False`. A user has reported that zooming with `absolute` is working. |
@@ -1,5 +1,5 @@
---
id: index
id: config
title: Frigate Configuration
---
@@ -9,11 +9,54 @@ import NavPath from "@site/src/components/NavPath";
Frigate can be configured through the **Settings UI** or by editing the YAML configuration file directly. The Settings UI is the recommended approach — it provides validation and a guided experience for all configuration options.
It is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md).
## Using the Settings UI
The Settings UI groups every configuration option into sections that are listed in the left-hand menu. Each section presents a guided form with validation, so you don't need to remember the structure of the YAML or look up option names by hand.
### Global vs. camera-level configuration
Settings are organized into two scopes:
- **Global configuration** — values under <NavPath path="Settings > Global configuration" /> apply to every camera by default. This is where you set the baseline behavior for object detection, recording, snapshots, motion, and so on.
- **Camera configuration** — values under <NavPath path="Settings > Camera configuration" /> apply to a single camera. Use the camera selector button at the top of these pages to choose which camera you are editing.
When a camera-level section is left untouched, the camera simply inherits the global values. Changing a value on a camera page **overrides** the global value for that camera only — the global setting and every other camera are unaffected. This mirrors how the YAML works, where a value set under `cameras.<name>` takes precedence over the same value set at the top level.
To undo an override and go back to inheriting from the parent scope, use the reset button at the bottom of the section:
- On a camera section, the button is labeled **Reset to Global** and restores the camera to the global value.
- On a global section, the button is labeled **Reset to Default** and restores Frigate's built-in default.
Resetting asks for confirmation and cannot be undone once applied.
### Saving changes and the Save All button
Edits are not applied until you save them. As soon as you change a value, the UI tracks it as a pending change:
- The edited section shows a **Modified** badge, and the changed fields are highlighted.
- A **You have unsaved changes** notice appears above the section's **Save** and **Undo** buttons. **Save** commits just that section; **Undo** discards its pending edits.
Because pending changes can span multiple sections — and multiple cameras — the header provides a **Save All** button that writes every pending change at once. Next to it, **Review pending changes** opens a summary that lists each pending edit with its scope (Global or a specific camera), the affected field, and the new value, so you can confirm exactly what will be written before committing. **Undo All** discards every pending change across all sections.
### Restart-required indicators
Most settings take effect immediately, but some require Frigate to restart before they apply. Fields that require a restart are marked with a small restart icon and a **Restart required** tooltip next to the field label.
When you save a change that touches one of these fields, Frigate confirms the save and reminds you that a restart is needed (for example, _"Settings saved successfully. Restart Frigate to apply your changes."_). The notification includes a one-click **Restart Frigate** action so you can apply the change right away, or you can continue editing and restart later.
### The colored dots in the camera configuration menu
When you are working under <NavPath path="Settings > Camera configuration" />, small colored dots can appear next to a section's name in the menu. They give you an at-a-glance summary of that section's state for the selected camera:
- **Blue dot** — this section **overrides the global configuration**. One or more values in the section have been set specifically for this camera and differ from the global defaults.
- **Profile-colored dot** — when you are viewing a [camera profile](./profiles.md), a dot in that profile's assigned color indicates the section is **overridden by that profile**. Each profile is given its own distinct color so you can tell at a glance which sections it changes.
- **Amber dot** — this section has **unsaved changes**. It appears alongside the **Modified** badge whenever you have pending edits in the section that haven't been saved yet.
Hover over any dot to see a tooltip describing what it means. Open a section to see exactly which fields are overridden — the section header indicates how many fields differ from the global (or base) configuration.
## Configuration File Location
For users who prefer to edit the YAML configuration file directly:
For users who prefer to edit the YAML configuration file directly, it is recommended to start with a minimal configuration and add to it as described in [the getting started guide](../guides/getting_started.md).
- **Home Assistant App:** `/addon_configs/<addon_directory>/config.yml` — see [directory list](#accessing-app-config-dir)
- **All other installations:** Map to `/config/config.yml` inside the container
@@ -57,7 +100,7 @@ VS Code supports JSON schemas for automatically validating configuration files.
## Environment Variable Substitution
Frigate supports the use of environment variables starting with `FRIGATE_` **only** where specifically indicated in the [reference config](./reference.md). For example, the following values can be replaced at runtime by using environment variables:
Frigate supports the use of environment variables starting with `FRIGATE_` **only** where specifically indicated in the [reference config](./advanced/reference.md). For example, the following values can be replaced at runtime by using environment variables:
```yaml
mqtt:
@@ -92,7 +135,7 @@ genai:
## Common configuration examples
Here are some common starter configuration examples. These can be configured through the Settings UI or via YAML. Refer to the [reference config](./reference.md) for detailed information about all config values.
Here are some common starter configuration examples. These can be configured through the Settings UI or via YAML. Refer to the [reference config](./advanced/reference.md) for detailed information about all config values.
### Raspberry Pi Home Assistant App with USB Coral
+2 -2
View File
@@ -86,7 +86,7 @@ Navigate to <NavPath path="Settings > Enrichments > Face recognition" />.
- **Detection threshold**: Face detection confidence score required before recognition runs. This field only applies to the standalone face detection model; `min_score` should be used to filter for models that have face detection built in.
- Default: `0.7`
- **Minimum face area**: Minimum size (in pixels) a face must be before recognition runs. Depending on the resolution of your camera's `detect` stream, you can increase this value to ignore small or distant faces.
- Default: `500` pixels
- Default: `750` pixels
</TabItem>
<TabItem value="yaml">
@@ -95,7 +95,7 @@ Navigate to <NavPath path="Settings > Enrichments > Face recognition" />.
face_recognition:
enabled: true
detection_threshold: 0.7
min_area: 500
min_area: 750
```
</TabItem>
+1 -1
View File
@@ -33,7 +33,7 @@ Select the appropriate hwaccel preset for your hardware.
<TabItem value="ui">
1. Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to the appropriate preset for your hardware.
2. To override for a specific camera, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" /> and set **Hardware acceleration arguments** for that camera.
2. To override for a specific camera, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" /> and set **Hardware acceleration arguments** for that camera.
</TabItem>
<TabItem value="yaml">
+70
View File
@@ -0,0 +1,70 @@
---
id: go2rtc
title: go2rtc
---
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
Frigate uses the bundled go2rtc to power a number of key features:
- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio
- Live stream support for cameras in Home Assistant Integration
- RTSP relay for use with other consumers to reduce the number of connections to your camera streams
:::tip[Most users no longer need to configure go2rtc by hand]
The **camera setup wizard** is the recommended way to add cameras. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />, and the wizard probes your camera and writes its configuration for you — including the go2rtc restream and the live stream mapping — so go2rtc is set up automatically.
This guide is mainly useful if you are **upgrading from an older version and have existing cameras that don't yet use go2rtc**, or if you want to fine-tune a stream by hand (for example, to transcode a codec your browser can't play). The [go2rtc troubleshooting guide](/troubleshooting/go2rtc) applies regardless of how your cameras were added.
:::
## Adding a go2rtc stream manually
If you added your cameras with the wizard, go2rtc is already configured — you can skip straight to [troubleshooting](/troubleshooting/go2rtc). The steps below are for upgrading users with existing cameras that aren't using go2rtc yet, or for anyone who prefers to configure a stream by hand.
Configure go2rtc to connect to your camera by adding the stream you want to use for live view. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#module-streams), not just rtsp.
:::tip
For the best experience, set the stream name under `go2rtc` to match the name of your camera so that Frigate will automatically map it and be able to use better live view options for the camera.
See [the live view docs](/configuration/live#setting-streams-for-live-ui) for more information.
:::
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > go2rtc Streams" /> and click **Add stream**. Give the stream a name (use the camera's name so Frigate can auto-map it - for example, if your camera's name is `back`, use `back` as the go2rtc stream name), then paste the camera's stream URL into the **Source** field. Save the section.
</TabItem>
<TabItem value="yaml">
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
```
</TabItem>
</ConfigTabs>
After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream.
### Next steps
1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
## Troubleshooting
If your stream won't play, has no audio, uses excessive CPU, or otherwise misbehaves, see the dedicated [go2rtc troubleshooting guide](/troubleshooting/go2rtc). It walks through how to isolate where the problem is and covers the most common issues — unsupported codecs, H.265/HEVC, audio, WebRTC and two-way talk, hardware-accelerated transcoding with FFmpeg 8, and camera-specific quirks.
## Homekit Configuration
To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
@@ -72,7 +72,7 @@ Frigate can utilize most Intel integrated GPUs and Arc GPUs to accelerate video
:::note
The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced.md#environment_vars).
The default driver is `iHD`. You may need to change the driver to `i965` by adding the following environment variable `LIBVA_DRIVER_NAME=i965` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars).
See [The Intel Docs](https://www.intel.com/content/www/us/en/support/articles/000005505/processors.html) to figure out what generation your CPU is.
@@ -85,7 +85,7 @@ VAAPI supports automatic profile selection so it will work automatically with bo
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -105,7 +105,7 @@ ffmpeg:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Intel QuickSync (H.264)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -123,7 +123,7 @@ ffmpeg:
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Intel QuickSync (H.265)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -169,7 +169,7 @@ Frigate can utilize modern AMD integrated GPUs and AMD GPUs to accelerate video
### Configuring Radeon Driver
You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA App users](advanced.md#environment_vars).
You need to change the driver to `radeonsi` by adding the following environment variable `LIBVA_DRIVER_NAME=radeonsi` to your docker-compose file or [in the `config.yml` for HA App users](advanced/system.md#environment_vars).
### Via VAAPI
@@ -178,7 +178,7 @@ VAAPI supports automatic profile selection so it will work automatically with bo
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `VAAPI (Intel/AMD GPU)`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -193,7 +193,7 @@ ffmpeg:
## NVIDIA GPUs
While older GPUs may work, it is recommended to use modern, supported GPUs. NVIDIA provides a [matrix of supported GPUs and features](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). If your card is on the list and supports CUVID/NVDEC, it will most likely work with Frigate for decoding. However, you must also use [a driver version that will work with FFmpeg](https://github.com/FFmpeg/nv-codec-headers/blob/master/README). Older driver versions may be missing symbols and fail to work, and older cards are not supported by newer driver versions. The only way around this is to [provide your own FFmpeg](/configuration/advanced#custom-ffmpeg-build) that will work with your driver version, but this is unsupported and may not work well if at all.
While older GPUs may work, it is recommended to use modern, supported GPUs. NVIDIA provides a [matrix of supported GPUs and features](https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new). If your card is on the list and supports CUVID/NVDEC, it will most likely work with Frigate for decoding. However, you must also use [a driver version that will work with FFmpeg](https://github.com/FFmpeg/nv-codec-headers/blob/master/README). Older driver versions may be missing symbols and fail to work, and older cards are not supported by newer driver versions. The only way around this is to [provide your own FFmpeg](/configuration/advanced/system#custom-ffmpeg-build) that will work with your driver version, but this is unsupported and may not work well if at all.
A more complete list of cards and their compatible drivers is available in the [driver release readme](https://download.nvidia.com/XFree86/Linux-x86_64/525.85.05/README/supportedchips.html).
@@ -237,7 +237,7 @@ Using `preset-nvidia` ffmpeg will automatically select the necessary profile for
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `NVIDIA GPU`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -300,7 +300,7 @@ If you are using the HA App, you may need to use the full access variant and tur
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Raspberry Pi (H.264)` (for H.264 streams) or `Raspberry Pi (H.265)` (for H.265/HEVC streams). For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -420,7 +420,7 @@ For example, for H264 video, you'll select `preset-jetson-h264`.
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `NVIDIA Jetson (H.264)` (or `NVIDIA Jetson (H.265)` for HEVC streams). For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -452,7 +452,7 @@ Set the FFmpeg hwaccel preset to enable hardware video processing.
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and set **Hardware acceleration arguments** to `Rockchip RKMPP`. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -519,7 +519,7 @@ Set the FFmpeg hwaccel args to enable hardware video processing.
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />.
Navigate to <NavPath path="Settings > Global configuration > FFmpeg" /> and configure the hardware acceleration args and input args manually for Synaptics hardware. For per-camera overrides, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />.
</TabItem>
<TabItem value="yaml">
@@ -363,7 +363,7 @@ An example configuration for a dedicated LPR camera using a `license_plate`-dete
Navigate to <NavPath path="Settings > Enrichments > License plate recognition" /> and set **Enable LPR** to on. Set **Device** to `CPU` (can also be `GPU` if available).
Navigate to <NavPath path="Settings > Camera configuration > FFmpeg" /> and add your camera streams.
Navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" /> and add your camera streams.
Navigate to <NavPath path="Settings > Camera configuration > Object detection" />.
@@ -475,7 +475,7 @@ Navigate to <NavPath path="Settings > Camera configuration > License plate recog
| **Enable LPR** | Set to on |
| **Enhancement level** | Set to `3` (optional — enhances the image before trying to recognize characters) |
Navigate to <NavPath path="Settings > Camera configuration > FFmpeg" /> and add your camera streams.
Navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" /> and add your camera streams.
Navigate to <NavPath path="Settings > Camera configuration > Object detection" />.
@@ -671,7 +671,7 @@ lpr:
3. Ensure your plates are being _detected_.
If you are using a Frigate+ or `license_plate` detecting model:
- Watch the debug view (Settings --> Debug) to ensure that `license_plate` is being detected.
- Watch the [Debug view](/usage/live#the-single-camera-view) to ensure that `license_plate` is being detected.
- View MQTT messages for `frigate/events` to verify detected plates.
- You may need to adjust your `min_score` and/or `threshold` for the `license_plate` object if your plates are not being detected.
+4 -2
View File
@@ -11,7 +11,7 @@ Frigate intelligently displays your camera streams on the Live view dashboard. B
### Live View technologies
Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be configured as shown in the [step by step guide](/guides/configuring_go2rtc).
Frigate intelligently uses three different streaming technologies to display your camera streams on the dashboard and the single camera view, switching between available modes based on network bandwidth, player errors, or required features like two-way talk. The highest quality and fluency of the Live view requires the bundled `go2rtc` to be [configured](/configuration/go2rtc).
The jsmpeg live view will use more browser and client GPU resources. Using go2rtc is highly recommended and will provide a superior experience.
@@ -371,7 +371,7 @@ When your browser runs into problems playing back your camera streams, it will l
- Verify your camera's H.264/AAC settings (see [Frigate's camera settings recommendations](#camera-settings-recommendations)).
- Check go2rtc configuration for transcoding (e.g., audio to AAC/OPUS).
- Test with a different stream via the UI dropdown (if `live -> streams` is configured).
- For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see (WebRTC Extra Configuration)(#webrtc-extra-configuration)).
- For WebRTC-specific issues, ensure port 8555 is forwarded and candidates are set (see [WebRTC Extra Configuration](#webrtc-extra-configuration)).
- If your cameras are streaming at a high resolution, your browser may be struggling to load all of the streams before the buffering timeout occurs. Frigate prioritizes showing a true live view as quickly as possible. If the fallback occurs often, change your live view settings to use a lower bandwidth substream.
3. **It doesn't seem like my cameras are streaming on the Live dashboard. Why?**
@@ -432,3 +432,5 @@ When your browser runs into problems playing back your camera streams, it will l
roles:
- detect
```
The same applies to your `record` stream: if its aspect ratio differs from your `detect` stream, your recordings will appear in a different shape than the live view. For consistent framing across live view and recordings, use the same aspect ratio for all of a camera's streams (the resolution can still differ).
+23 -1
View File
@@ -7,6 +7,8 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
Frigate has two kinds of masks: motion masks and object filter masks. Both are narrow tools for fine-tuning, **not for hiding an area from Frigate**. Masks should be used sparingly; in most cases where users reach for one, a [zone](zones.md) with `required_zones` is the right tool instead. See [Which tool do I need?](#which-tool-do-i-need) and [Common mistakes](#common-mistakes) below if you're new to Frigate's mask behavior.
## Motion masks
Motion masks are used to prevent unwanted types of motion from triggering detection. Try watching the Debug feed (Settings --> Debug) with `Motion Boxes` enabled to see what may be regularly detected as motion. For example, you want to mask out your timestamp, the sky, rooftops, etc. Keep in mind that this mask only prevents motion from being detected and does not prevent objects from being detected if object detection was started due to motion in unmasked areas. Motion is also used during object tracking to refine the object detection area in the next frame. _Over-masking will make it more difficult for objects to be tracked._
@@ -21,7 +23,16 @@ Object filter masks can be used to filter out stubborn false positives in fixed
![object mask](/img/bottom-center-mask.jpg)
## Creating masks
## Which tool do I need?
| What you're trying to do | Recommended tool | How it works |
| ------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Don't get alerts or recordings for activity in an area (e.g., the sidewalk in front of your house) | A [zone](zones.md) combined with `review.alerts.required_zones` (and/or `review.detections.required_zones`) | Frigate keeps detecting and tracking activity in the area, but a review item is only created once the bottom-center of an object's bounding box enters a required zone. |
| Stop a stubborn false positive at a specific fixed spot (e.g., a tree base that keeps being detected as a person) | An **object filter mask** for that object type | Any detection of that object type whose bounding-box bottom-center lands inside the mask is treated as a false positive and discarded. |
| Ignore motion in an area that obviously isn't an object of interest (e.g., the camera timestamp, sky, flags, treetops swaying) | A **motion mask** | Motion inside the mask is ignored when deciding whether to run object detection. Objects can still be detected in a motion masked area if motion elsewhere in the frame triggers detection. |
| Stop tracking an object type altogether on this camera (e.g., you never care about cats) | Remove the object from the camera's [`objects.track`](objects.md) list | Frigate skips this object type entirely on this camera, regardless of where it appears. |
## Using the mask creator
<ConfigTabs>
<TabItem value="ui">
@@ -124,3 +135,14 @@ This is what `required_zones` are for. You should define a zone (remember this i
> Maybe my specific situation just warrants this. I've just been having a hard time understanding the relevance of this information - it seems to be that it's exactly what would be expected when "masking out" an area of ANY image.
That may be the case for you. Frigate will definitely work harder tracking people on the sidewalk to make sure it doesn't miss anyone who steps foot on your stoop. The trade off with the way you have it now is slower recognition of objects and potential misses. That may be acceptable based on your needs. Also, if your resolution is low enough on the detect stream, your regions may already be so big that they grab the entire object anyway.
## Common mistakes
**"I added a motion mask to ignore my driveway/sidewalk."**
A motion mask doesn't hide an area from Frigate. Objects can still be detected and tracked inside a masked area. The mask only stops motion _in that area_ from triggering object detection. If you want activity on the sidewalk to never produce a review item, define a [zone](zones.md) over the area you DO care about (your stoop, your driveway) and add it to `review.alerts.required_zones`. Frigate will still see people on the sidewalk, but it won't create an alert until they cross into the zone.
**"I added an object filter mask because I don't care about cars in my yard."**
Object filter masks are for stubborn false positives at fixed locations, not for filtering whole areas or whole object types. If you only want alerts when a car enters the driveway, use a [zone](zones.md) with `required_zones`. If you don't care about a whole object type on this camera, remove it from [`objects.track`](objects.md).
**"I masked everything except a thin strip on my stoop."**
Heavy masking hurts tracking. Frigate uses motion near a tracked object's previous bounding box to decide where to look in the next frame; with most of the frame masked, an object walking from an unmasked area into a masked one effectively disappears and gets picked up as a "new" object when it reappears. For example: someone walks down your sidewalk, stops under a tree (masked area) to tie their shoe, then continues. Frigate sees that as two separate people and can create two separate review items. Because Frigate needs several consecutive frames above the confidence threshold to commit to a detection, each re-appearance can also delay or miss alerts. Use `required_zones` for "only alert me about this spot" and leave the surrounding area unmasked so tracking stays intact.
+2
View File
@@ -59,6 +59,8 @@ Metrics are available at `/api/metrics` by default. No additional Frigate config
- `frigate_storage_used_bytes{storage=""}` - Storage used bytes
- `frigate_storage_mount_type{mount_type="", storage=""}` - Storage mount type info
These gauges report the operating system's figures for the whole filesystem (the same numbers as `df`), not Frigate's own recording footprint. For how this differs from the recordings usage shown in the UI, see [Understanding storage usage](/configuration/record#understanding-storage-usage).
### Service Metrics
- `frigate_service_uptime_seconds` - Uptime in seconds
+1 -1
View File
@@ -200,4 +200,4 @@ When the skip threshold is exceeded, **no motion is reported** for that frame, m
## Reviewing Detected Motion
To review what the detector picked up — or to search past recordings for motion in a specific region — see [Reviewing Motion](review.md#reviewing-motion) on the Review page.
To review what the detector picked up — or to search past recordings for motion in a specific region — see [Reviewing Motion](/usage/review#reviewing-motion) on the Review page.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -158,4 +158,4 @@ Models for both CPU and EdgeTPU (Coral) are bundled in the image. You can use yo
- EdgeTPU Model: `/edgetpu_model.tflite`
- Labels: `/labelmap.txt`
You also need to update the [model config](advanced.md#model) if they differ from the defaults.
You also need to update the [model config](advanced/system.md#model) if they differ from the defaults.
+66 -4
View File
@@ -11,6 +11,12 @@ Recordings can be enabled and are stored at `/media/frigate/recordings`. The fol
New recording segments are written from the camera stream to cache, they are only moved to disk if they match the setup recording retention policy.
:::tip
To keep a specific clip beyond your retention window, [export](/usage/exports) it rather than increasing retention for the whole camera. Exports are saved separately and are never removed by retention.
:::
H265 recordings can be viewed in Chrome 108+, Edge and Safari only. All other browsers require recordings to be encoded with H264.
## Common recording configurations
@@ -193,10 +199,6 @@ Because recording segments are written in 10 second chunks, pre-capture timing d
Pre and post capture footage is included in the **recording timeline**, visible in the History view. Note that pre/post capture settings only affect which recording segments are **retained on disk** — they do not change the start and end points shown in the UI. The History view will still center on the review item's actual time range, but you can scrub backward and forward through the retained pre/post capture footage on the timeline. The Explore view shows object-specific clips that are trimmed to when the tracked object was actually visible, so pre/post capture time will not be reflected there.
## Will Frigate delete old recordings if my storage runs out?
If there is less than an hour left of storage, the oldest hour of recordings will be deleted and a message will be printed in the Frigate logs. This emergency cleanup deletes the oldest recordings first regardless of retention settings to reclaim space as quickly as possible.
## Configuring Recording Retention
Frigate supports both continuous and tracked object based recordings with separate retention modes and retention periods.
@@ -349,3 +351,63 @@ Setting `verbose: true` writes a detailed report of every orphaned file and data
This operation uses considerable CPU resources and includes a safety threshold that aborts if more than 50% of files would be deleted. Only run when necessary. If you set `force: true` the safety threshold will be bypassed; do not use `force` unless you are certain the deletions are intended.
:::
## Understanding storage usage
The storage usage Frigate reports will not exactly match what the operating system reports with `df` or `du`. This is expected, not a bug. The sections below explain how Frigate derives its storage figures and why they differ from the disk's own accounting.
### How Frigate measures recording usage
The **Recordings** value on the Storage Metrics page (<NavPath path="System > Storage" />) — and the per-camera **Camera Storage** breakdown — is the sum of the recording segment sizes Frigate has written, taken from Frigate's database. It is **not** computed by a scan of the disk. Frigate tracks usage this way by design: repeatedly walking the entire drive to total its size would keep hard drives spun up and add unnecessary I/O.
The disk **total** shown beside it, and the free-space figure Frigate uses to decide when to delete recordings, instead come from the operating system's report for the whole filesystem mounted at `/media/frigate`. As a result, the **Unused** value on the page is _total disk capacity minus Frigate's recordings_ — not the drive's real free space, which will be lower whenever anything else is stored on the disk.
### What counts toward usage — and why it won't match `df`
Only **recording segments** (`/media/frigate/recordings`) are included in the recordings storage total. Plenty of other things consume real disk space but are **not** part of that number:
- **Snapshots and thumbnails** (`/media/frigate/clips`) — see [Snapshots](/configuration/snapshots). These are retained independently of recordings.
- **Preview videos** and **review thumbnails** (also under `/media/frigate/clips`).
- **Exports** (`/media/frigate/exports`) — exports are never removed by retention.
- **The database, downloaded detection models, and face / license plate training images** (stored under `/config`).
- **Debug images from enrichments** (`/media/frigate/clips`) — when enabled, License Plate Recognition's `debug_save_plates` and GenAI's `debug_save_thumbnails` save plate crops and request images for troubleshooting.
These files are the usual explanation for an "other" or seemingly unaccounted bucket of space — it is real, it is Frigate's, and it simply isn't part of the _recordings_ total. They are also why comparing the **Recordings** figure to `df -h` always shows a gap: `df` additionally counts any non-Frigate data on the disk, filesystem overhead and reserved blocks (ext4 reserves ~5% for root by default, so a disk can read "full" before recordings approach the total), and recently deleted recordings whose space has not yet been reclaimed.
:::tip
The Storage page is not intended to be a system-wide disk monitor — it shows how much space _Frigate's recordings_ use. To see true disk usage, use `df -h` (free space) and `du -sh` (per-directory usage) on the host.
:::
### Free space and the `/media/frigate` mount
Frigate reports the capacity and free space of whatever filesystem is actually mounted at `/media/frigate` **inside the container**. If an external drive or network share isn't truly mounted there — a missing `/etc/fstab` entry, a share that was offline when the container started, or a host that doesn't pass the path through — the container falls back to the host's OS disk, and Frigate will correctly report that smaller disk instead of the drive you intended.
If the reported capacity doesn't match your drive, the mount is the place to look, not Frigate. Verify what is actually mounted from inside the container:
```bash
docker exec -it frigate df -h /media/frigate
docker exec -it frigate mount | grep media
```
See the [storage mount layout](/frigate/installation#storage) for how the volumes are expected to be configured.
### The `/tmp/cache` area is separate
Recording segments are first written to `/tmp/cache` — a small, in-memory (`tmpfs`) area — before being checked and moved to `/media/frigate/recordings`. Because it is separate and small, `/tmp/cache` can fill up and produce `No space left on device` errors even when the recordings disk has plenty of room — they are different storage areas. See [Recordings troubleshooting](/troubleshooting/recordings) for diagnosing cache and slow-storage issues.
### When the metrics don't match what's on disk
Because usage is tracked in the database, deleting recording files directly on disk — or files left behind after an upgrade — will not update the reported usage, and can even push it above 100%. Frigate is unaware of files it didn't record and won't count or remove them automatically. Use [Syncing Media Files With Disk](#syncing-media-files-with-disk) to reconcile the database with what is actually on disk.
## Will Frigate delete old recordings if my storage runs out?
Yes. Frigate continuously checks the **free space of the disk** holding `/media/frigate/recordings`. This is different from adding up the size of every recording: free space is a single number the operating system already tracks, so Frigate can ask for it instantly without reading through your files or spinning up the disk — which is exactly why it relies on this check rather than scanning the drive. When less than roughly one hour of recording space remains — estimated from the current recording bitrate, **not** a fixed percentage — Frigate deletes the oldest recordings to reclaim space and logs a message. This emergency cleanup removes the oldest recordings first **regardless of retention settings**.
Two consequences follow from this being based on whole-disk free space:
- Because the check uses the disk's real free space, **anything** filling the drive — including non-Frigate files — can trigger deletion of your oldest recordings.
- Cleanup can run while a meaningful percentage of the disk is still free (for example, with high bitrates or many cameras), because the threshold is "less than ~1 hour of recording headroom," not "X% full."
Frequent emergency cleanups usually mean your configured retention exceeds what the disk can hold. Reduce your retention days so the normal retention cleanup keeps up and the emergency path rarely triggers.
+2 -2
View File
@@ -61,7 +61,7 @@ Configure the go2rtc stream and point the camera inputs at the local restream.
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > go2rtc streams" /> and add stream entries for each camera. Then navigate to <NavPath path="Settings > Camera configuration > FFmpeg" /> for each camera and set the input paths to use the local restream URL (`rtsp://127.0.0.1:8554/<camera_name>`).
Navigate to <NavPath path="Settings > System > go2rtc streams" /> and add stream entries for each camera. Then navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" /> for each camera. For each input, choose **Restream (go2rtc)** and pick the matching stream from the dropdown — Frigate uses the local restream URL (`rtsp://127.0.0.1:8554/<camera_name>`) and the `preset-rtsp-restream` input args for that input automatically. (Choose **Manual input path** instead to type a URL directly.)
</TabItem>
<TabItem value="yaml">
@@ -111,7 +111,7 @@ Two connections are made to the camera. One for the sub stream, one for the rest
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > System > go2rtc streams" /> and add stream entries for each camera and its sub stream. Then navigate to <NavPath path="Settings > Camera configuration > FFmpeg" /> for each camera and configure separate inputs for the main and sub streams using the local restream URLs.
Navigate to <NavPath path="Settings > System > go2rtc streams" /> and add stream entries for each camera and its sub stream. Then navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" /> for each camera and add separate inputs for the main and sub streams. Set each input's source to **Restream (go2rtc)** and pick the matching stream from the dropdown — Frigate uses the local restream URL and the `preset-rtsp-restream` input args for that input automatically.
</TabItem>
<TabItem value="yaml">
+1 -37
View File
@@ -133,40 +133,4 @@ Because zones don't apply to audio, audio labels will always be marked as a dete
## Reviewing Motion
The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above — see [Tuning Motion Detection](motion_detection.md) for how the underlying motion detector is configured.
### Motion Previews
The Motion Previews pane shows preview clips for periods of significant motion that did not produce a tracked object. It is useful for spotting things that motion detection picked up but object detection did not, which can help validate tuning or catch missed objects.
On the <NavPath path="Review > Motion" /> page, click the kebab menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out.
The pane provides a few controls:
- **Speed** — speeds up or slows down all of the preview clips at once.
- **Dim** — controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility.
- **Filter** — opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway.
Clicking a preview clip seeks the recording player to that timestamp so you can review the full footage.
### Motion Search
Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked).
To start a search, click the kebab menu on a camera in the <NavPath path="Review > Motion" /> page and choose **Motion Search**. In the dialog:
1. Pick the camera and time range to scan.
2. Draw a polygon on the camera frame to define the region of interest.
3. Adjust the search parameters if needed:
| Field | Description |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sensitivity Threshold** | Per-pixel luminance change required to count as motion inside the ROI. Behaves like Frigate's motion detection `threshold` setting. |
| **Minimum Change Area** | Minimum percentage of the region of interest that must change for a frame to be considered significant. Raise it to ignore small movements (leaves, distant motion); lower it when the object you care about only covers a small slice of the ROI. |
| **Frame Skip** | Number of frames to skip between samples — at a camera recording 20 fps, a skip value of 20 takes motion samples roughly once per second. Higher values scan much faster and are usually the right choice; lower it only when you need to catch the exact appearance or disappearance of a fast-moving object. |
| **Maximum Results** | Maximum number of matching timestamps to return. |
| **Parallel mode** | Process multiple recording segments in parallel. Speeds up large time ranges at the cost of higher CPU usage. |
Once running, Frigate scans the recording segments that overlap the time range and reports timestamps where changes were detected inside the polygon, along with the percentage of the ROI that changed. Clicking a result seeks the player to that moment so you can review what happened.
The status panel shows live progress and metrics such as how many segments were scanned, how many were skipped because no motion was recorded for that segment (using the stored motion heatmap), how many frames were decoded, and the total wall-clock time. Segments with no recorded motion in the selected ROI are skipped automatically, which is what makes searching long time ranges practical.
The Review page can also surface periods of motion that didn't produce a tracked object, and lets you search past recordings for motion in a region you draw. See [Reviewing Motion](/usage/review#reviewing-motion) in the Usage docs for how to use **Motion Previews** and **Motion Search**, and [Tuning Motion Detection](motion_detection.md) for configuring the underlying motion detector.
+1 -6
View File
@@ -222,12 +222,7 @@ See the [Hardware Accelerated Enrichments](/configuration/hardware_acceleration_
## Usage and Best Practices
1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results.
2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object.
3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the the ones without them are more relevant.
4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day".
5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well.
6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you.
For tips on getting the best results from Semantic Search — choosing between thumbnail and description search, phrasing queries effectively, and combining search with the other Explore filters — see [Usage and best practices](/usage/explore#usage-and-best-practices) in the Usage docs.
## Triggers
+8 -6
View File
@@ -7,13 +7,17 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
Frigate can save a snapshot image to `/media/frigate/clips` for each object that is detected named as `<camera>-<id>-clean.webp`. They are also accessible [via the api](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx)
A snapshot is a single still image that captures a tracked object at its best moment — the clearest frame Frigate saw while following that object across the scene. Unlike a [recording](./record.md), which is continuous video, a snapshot is one representative image saved per tracked object once tracking ends.
Snapshots are accessible in the UI in the Explore pane. This allows for quick submission to the Frigate+ service.
When snapshots are enabled, Frigate saves one image to `/media/frigate/clips` for each tracked object, named `<camera>-<id>-clean.webp`. A clean image is always stored without any annotations (no timestamp, bounding boxes, or cropping) so you have an unmodified copy of the original frame. Annotations like bounding boxes and timestamps are applied on demand when a snapshot is requested [via the HTTP API](../integrations/api/event-snapshot-events-event-id-snapshot-jpg-get.api.mdx) — see [Rendering](#rendering) below.
To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones)
A few things to keep in mind:
Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here.
- Snapshots are saved per tracked object, so a camera with no detected objects produces no snapshots even if recording is enabled.
- Snapshots and recordings are configured and retained independently — enabling one does not enable the other.
- Snapshots are accessible in the UI in the Explore pane, which allows for quick submission to the Frigate+ service.
- To only save snapshots for objects that enter a specific zone, [see the zone docs](./zones.md#restricting-snapshots-to-specific-zones).
- Snapshots sent via MQTT are configured separately under the camera MQTT settings, not here.
## Enabling Snapshots
@@ -107,7 +111,6 @@ Navigate to <NavPath path="Settings > Global configuration > Snapshots" />.
| Field | Description |
| -------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **Snapshot retention > Default retention** | Number of days to retain snapshots (default: 10) |
| **Snapshot retention > Retention mode** | Retention mode: `all`, `motion`, or `active_objects` |
| **Snapshot retention > Object retention > Person** | Per-object overrides for retention days (e.g., keep `person` snapshots for 15 days) |
</TabItem>
@@ -118,7 +121,6 @@ snapshots:
enabled: True
retain:
default: 10
mode: motion
objects:
person: 15
```
+39 -1
View File
@@ -5,7 +5,7 @@ title: Camera setup
Cameras configured to output H.264 video and AAC audio will offer the most compatibility with all features of Frigate and Home Assistant. H.265 has better compression, but less compatibility. Firefox 134+/136+/137+ (Windows/Mac/Linux & Android), Chrome 108+, Safari and Edge are the only browsers able to play H.265 and only support a limited number of H.265 profiles. Ideally, cameras should be configured directly for the desired resolutions and frame rates you want to use in Frigate. Reducing frame rates within Frigate will waste CPU resources decoding extra frames that are discarded. There are three different goals that you want to tune your stream configurations around.
- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The recommended frame rate is 5fps, but may need to be higher (10fps is the recommended maximum for most users) for very fast moving objects. Higher resolutions and frame rates will drive higher CPU usage on your server.
- **Detection**: This is the only stream that Frigate will decode for processing. Also, this is the stream where snapshots will be generated from. The resolution for detection should be tuned for the size of the objects you want to detect. See [Choosing a detect resolution](#choosing-a-detect-resolution) for more details. The default frame rate of 5fps is correct for almost all cameras and rarely needs to be changed; see [Choosing a detect frame rate](#choosing-a-detect-frame-rate). Higher resolutions and frame rates will drive higher CPU usage on your server.
- **Recording**: This stream should be the resolution you wish to store for reference. Typically, this will be the highest resolution your camera supports. I recommend setting this feed in your camera's firmware to 15 fps.
@@ -25,6 +25,44 @@ Larger resolutions **do** improve performance if the objects are very small in t
![Resolutions](/img/resolutions-min.jpg)
### Choosing a detect frame rate
`detect.fps` controls how many times per second Frigate runs object detection — it does **not** need to match your camera's frame rate. The default of **5** is correct for the vast majority of cameras.
:::warning
Most users who raise `detect.fps` above the default don't need to. Increasing it consumes more CPU/GPU (detection load scales directly with the frame rate) while providing **no benefit to tracking** once objects are already being followed smoothly. Leave it at **5** unless you have a specific scene that fails the test below, and confirm any change actually helps in the debug view.
:::
#### Why 5 is enough for almost everyone
Frigate follows an object by matching its bounding box from one detection frame to the next, which requires the object to be detected often enough while it is on screen. At 5 fps this is satisfied in normal scenes: an object crossing a yard, porch, driveway, or walkway is in view for several seconds and produces ~15 or more detections, which is more than enough for a reliable track and a good snapshot. This includes fast subjects such as a running person or a bolting pet, which on a wide-angle view remain on screen for several seconds.
A higher rate helps only when an object crosses the **entire frame in less than two seconds**, which is determined by camera framing rather than object speed - for example, a camera aimed down a street at fast cross-traffic. In those scenes 5 fps may produce too few detections to hold a track. Cameras covering normal approaches and open areas are unaffected.
#### Checking whether a higher rate is needed
Estimate how long an object is visible as it crosses the area of interest, aiming for roughly 810 detections during the pass:
> **`detect.fps` ≈ 10 ÷ (seconds the object is in view)**
Most objects — people walking or running, pets, and vehicles in a yard, driveway, or walkway — stay in view for two seconds or more, so the default of 5 fps is correct. Slowly try raising it to 10 (the recommended maximum) in increments only when objects routinely cross the entire frame in about a second, such as a camera aimed at a street or sidewalk with fast cross-traffic. Objects that transit in under a second cannot be tracked reliably at any practical rate, so reposition the camera instead.
:::tip
If the formula calls for more than 10, the fix is **camera placement, not frame rate**. Angle the camera so objects move toward it rather than across the view, or aim it where traffic slows. A higher `detect.fps` increases CPU load proportionally without producing more detections of a too-brief object.
:::
#### Verify in the debug view
Confirm any change in the Debug view or Debug Replay. Watch a typical object cross the scene: if its bounding box follows it smoothly while visible, the rate is sufficient. A box that jumps erratically, drops out, or splits one object into multiple events indicates the rate should be increased one step.
#### Dedicated LPR cameras
A dedicated license plate recognition camera is the most common reason to use something higher than 5 fps: the camera is highly zoomed, the plate is small, and it moves at full vehicle speed, so it transits the frame quickly. However, the same ceiling applies: above 10 fps is unnecessary, and **placement matters most**: aim LPR cameras where vehicles slow down, such as gates, driveways, and parking entrances. A tight view of a fast through-road will not likely read plates reliably at any frame rate. See [License Plate Recognition](/configuration/license_plate_recognition) for details.
### Example Camera Configuration
For the Dahua/Loryta 5442 camera, I use the following settings:
+46 -14
View File
@@ -5,20 +5,40 @@ title: Glossary
The glossary explains terms commonly used in Frigate's documentation.
## Alert
The higher-priority of the two [review item](#review-item) severities, the other being a [detection](#detection). By default a review item is an alert when it involves a `person` or `car`; the qualifying [labels](#label) and [zones](#zone) can be configured. [See the review docs for more info](/configuration/review)
## Attribute
A property detected on an [object](#object) that exists alongside its [label](#label). Unlike a [sub label](#sub-label), an object can carry several attributes at once. Some attributes come directly from the object detection [model](#model) — for example `face`, `license_plate`, or delivery carrier logos such as `amazon`, `ups`, and `fedex` — while others come from a [custom object classification model](/configuration/custom_classification/object_classification) configured with the `attribute` type. Attributes are visible in the Tracked Object Details pane in Explore, in `frigate/events` MQTT messages, and through the HTTP API.
## Bounding Box
A box returned from the object detection model that outlines an object in the frame. These have multiple colors depending on object type in the debug live view.
A box returned by the object detection [model](#model) that outlines a detected [object](#object) in the frame. In the Debug view, bounding boxes are colored by object [label](#label).
### Bounding Box Colors
- At startup different colors will be assigned to each object label
- A dark blue thin line indicates that object is not detected at this current point in time
- A gray thin line indicates that object is detected as being stationary
- A thick line indicates that object is the subject of autotracking (when enabled).
- A thick line indicates that object is the subject of autotracking (when enabled)
## Class
The categories a classification [model](#model) is trained to distinguish between. Each class is a distinct visual category the model predicts, plus a `none` class for inputs that don't fit any category. For example, a custom object classification model for `person` objects might use the classes `delivery_person`, `resident`, and `none`. The predicted class is applied to the [object](#object) as either a [sub label](#sub-label) or an [attribute](#attribute), depending on the model's configuration. [See the object classification docs for more info](/configuration/custom_classification/object_classification)
## Detection
The lower-priority of the two [review item](#review-item) severities, the other being an [alert](#alert). By default, any review item that does not qualify as an alert is a detection; the qualifying [labels](#label) and [zones](#zone) can be configured. Despite the name, a detection is a category of review item — not the same as the object detection performed by the [model](#model). [See the review docs for more info](/configuration/review)
## False Positive
An incorrect detection of an object type. For example a dog being detected as a person, a chair being detected as a dog, etc. A person being detected in an area you want to ignore is not a false positive.
An incorrect result from the object detection [model](#model), where it assigns the wrong [label](#label) to something in the frame — for example a dog identified as a person, or a chair identified as a dog. A person correctly identified in an area you want to ignore is not a false positive.
## Label
The type assigned to a detected [object](#object) by the object detection [model](#model), drawn from the model's labelmap — for example `person`, `car`, or `dog`. Frigate tracks `person` by default; additional labels are tracked by adding them to the objects configuration. [See the available objects docs for the full list](/configuration/objects)
## Mask
@@ -26,44 +46,56 @@ There are two types of masks in Frigate. [See the mask docs for more info](/conf
### Motion Mask
Motion masks prevent detection of [motion](#motion) in masked areas from triggering Frigate to run object detection, but do not prevent objects from being detected if object detection runs due to motion in nearby areas. For example: camera timestamps, skies, the tops of trees, etc.
A motion mask stops [motion](#motion) in the masked area from triggering object detection. It does not stop an object from being detected when object detection runs because of motion in a nearby area. Use motion masks for parts of the frame that change constantly but never contain objects you care about — camera timestamps, the sky, the tops of trees, and so on.
### Object Mask
Object filter masks drop any bounding boxes where the bottom center (overlap doesn't matter) is in the masked area. It forces them to be considered a [false positive](#false-positive) so that they are ignored.
An object filter mask drops any [bounding box](#bounding-box) whose bottom center falls inside the masked area (overlap elsewhere doesn't matter). The object is forced to be treated as a [false positive](#false-positive) and ignored.
## Min Score
The lowest score that an object can be detected with during tracking, any detection with a lower score will be assumed to be a false positive
The lowest score a detected object can have to be kept during tracking. Anything scoring below the minimum is assumed to be a [false positive](#false-positive) and discarded.
## Model
A machine learning model that Frigate uses to detect or classify objects. The object detection model locates [objects](#object) in each frame and returns their [labels](#label) and [bounding boxes](#bounding-box). Additional enrichment models run on tracked objects to add detail: face recognition, license plate recognition, bird classification, custom object and state classification, and the embedding models used for semantic search. [See the object detectors docs for more info](/configuration/object_detectors)
## Motion
When pixels in the current camera frame are different than previous frames. When many nearby pixels are different in the current frame they grouped together and indicated with a red motion box in the live debug view. [See the motion detection docs for more info](/configuration/motion_detection)
A change in pixels between the current camera frame and previous frames. When many nearby pixels change together, they are grouped and shown as a red motion box in the debug live view. [See the motion detection docs for more info](/configuration/motion_detection)
## Object
Something Frigate can detect and follow in a camera frame, identified by its [label](#label) (for example a person or a car). The object types Frigate watches for are set in the `objects` configuration. Once an object is detected and followed across frames it becomes a [tracked object](#tracked-object-event-in-previous-versions), which may also carry a [sub label](#sub-label) and [attributes](#attribute). [See the available objects docs for more info](/configuration/objects)
## Region
A portion of the camera frame that is sent to object detection, regions can be sent due to motion, active objects, or occasionally for stationary objects. These are represented by green boxes in the debug live view.
A portion of the camera frame sent to the object detection [model](#model). Regions are selected because of [motion](#motion), active objects, or occasionally to recheck stationary objects, and are shown as green boxes in the debug live view.
## Review Item
A review item is a time period where any number of events/tracked objects were active. [See the review docs for more info](/configuration/review)
A period of time during which one or more [tracked objects](#tracked-object-event-in-previous-versions) were active, grouped together for review. Each review item is categorized as either an [alert](#alert) or a [detection](#detection). [See the review docs for more info](/configuration/review)
## Snapshot Score
The score shown in a snapshot is the score of that object at that specific moment in time.
The object's score at the specific moment the snapshot was captured.
## Sub Label
A more specific identity assigned to a [tracked object](#tracked-object-event-in-previous-versions) in addition to its [label](#label). A `person` may get the name of a recognized face, a `car` may get the name of a known license plate, and a `bird` may get its species. An object can have only one sub label at a time. Sub labels are produced by face recognition, license plate recognition, bird classification, custom object classification configured with the `sub label` type, and semantic search triggers.
## Threshold
The threshold is the median score that an object must reach in order to be considered a true positive.
The median score an object must reach to be considered a true positive.
## Top Score
The top score for an object is the highest median score for an object.
The highest median score an object reached over its lifetime.
## Tracked Object ("event" in previous versions)
The time period starting when a tracked object entered the frame and ending when it left the frame, including any time that the object remained still. Tracked objects are saved when it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording to be saved.
An [object](#object) followed from the moment it enters the frame until it leaves, including any time it stays still. A tracked object is saved once it is considered a [true positive](#threshold) and meets the requirements for a snapshot or recording.
## Zone
Zones are areas of interest, zones can be used for notifications and for limiting the areas where Frigate will create a [review item](#review-item). [See the zone docs for more info](/configuration/zones)
A user-defined area of interest within the camera frame. Zones can be used for notifications and to limit where Frigate creates a [review item](#review-item). [See the zone docs for more info](/configuration/zones)
+4 -4
View File
@@ -68,26 +68,26 @@ Frigate supports multiple different detectors that work on different types of ha
**AMD**
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection
- [Supports limited model architectures](../../configuration/object_detectors#rocm-supported-models)
- [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector)
- Runs best on discrete AMD GPUs
**Apple Silicon**
- [Apple Silicon](#apple-silicon): Apple Silicon is usable on all M1 and newer Apple Silicon devices to provide efficient and fast object detection
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-supported-models)
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector)
- Runs well with any size models including large
- Runs via ZMQ proxy which adds some latency, only recommended for local connection
**Intel**
- [OpenVino](#openvino---intel): OpenVino can run on Intel Arc GPUs, Intel integrated GPUs, and Intel NPUs to provide efficient object detection.
- [Supports majority of model architectures](../../configuration/object_detectors#openvino-supported-models)
- [Supports majority of model architectures](../../configuration/object_detectors#openvino-detector)
- Runs best with tiny, small, or medium models
**Nvidia**
- [Nvidia GPU](#nvidia-gpus): Nvidia GPUs can provide efficient object detection.
- [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx-supported-models)
- [Supports majority of model architectures via ONNX](../../configuration/object_detectors#onnx)
- Runs well with any size models including large
- <CommunityBadge /> [Jetson](#nvidia-jetson): Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6.
+3 -3
View File
@@ -600,7 +600,7 @@ There are several variants of the App available:
If you are using hardware acceleration for ffmpeg, you **may** need to use the _Full Access_ variant of the App. This is because the Frigate App runs in a container with limited access to the host system. The _Full Access_ variant allows you to disable _Protection mode_ and give Frigate full access to the host system.
You can also edit the Frigate configuration file through the [VS Code App](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs/<addon_directory>/config.yml`, where `<addon_directory>` is specific to the variant of the Frigate App you are running. See the list of directories [here](../configuration/index.md#accessing-app-config-dir).
You can also edit the Frigate configuration file through the [VS Code App](https://github.com/hassio-addons/addon-vscode) or similar. In that case, the configuration file will be at `/addon_configs/<addon_directory>/config.yml`, where `<addon_directory>` is specific to the variant of the Frigate App you are running. See the list of directories [here](../configuration/config.md#accessing-app-config-dir).
## Kubernetes
@@ -749,7 +749,7 @@ Failure to remap port 5000 on the host will result in the WebUI and all API endp
:::
Docker containers on macOS can be orchestrated by either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [OrbStack](https://orbstack.dev) (native swift app). The difference in inference speeds is negligable, however CPU, power consumption and container start times will be lower on OrbStack because it is a native Swift application.
Docker containers on macOS can be orchestrated by either [Docker Desktop](https://docs.docker.com/desktop/setup/install/mac-install/) or [OrbStack](https://orbstack.dev) (native Swift app). The difference in inference speeds is negligible, however CPU, power consumption and container start times will be lower on OrbStack because it is a native Swift application.
To allow Frigate to use the Apple Silicon Neural Engine / Processing Unit (NPU) the host must be running [Apple Silicon Detector](../configuration/object_detectors.md#apple-silicon-detector) on the host (outside Docker)
@@ -768,7 +768,7 @@ services:
- /path/to/your/recordings:/recordings
ports:
- "8971:8971"
# If exposing on macOS map to a diffent host port like 5001 or any orher port with no conflicts
# If exposing on macOS map to a different host port like 5001 or any other port with no conflicts
# - "5001:5000" # Internal unauthenticated access. Expose carefully.
- "8554:8554" # RTSP feeds
extra_hosts:
+1 -1
View File
@@ -32,7 +32,7 @@ The following models are downloaded automatically the first time their associate
| [License plate recognition](/configuration/license_plate_recognition) | PaddleOCR (detection, classification, recognition) + YOLOv9 plate detector | GitHub |
| [Bird classification](/configuration/bird_classification) | MobileNetV2 bird model + label map | GitHub |
| [Custom classification](/configuration/custom_classification/state_classification) (training) | MobileNetV2 ImageNet base weights (via Keras) | Google storage |
| [Audio transcription](/configuration/advanced) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
| [Audio transcription](/configuration/advanced/system) | Whisper or Sherpa-ONNX streaming model | HuggingFace / OpenAI |
### Hardware-Specific Detector Models
+2
View File
@@ -42,6 +42,8 @@ Frigate requires a CPU with AVX + AVX2 instructions. Most modern CPUs (post-2011
Storage is an important consideration when planning a new installation. To get a more precise estimate of your storage requirements, you can use an IP camera storage calculator. Websites like [IPConfigure Storage Calculator](https://calculator.ipconfigure.com/) can help you determine the necessary disk space based on your camera settings.
Once running, see [Understanding storage usage](/configuration/record#understanding-storage-usage) for how Frigate measures and reports disk usage — and why its numbers won't exactly match `df` or `du`.
#### SSDs (Solid State Drives)
SSDs are an excellent choice for Frigate, offering high speed and responsiveness. The older concern that SSDs would quickly "wear out" from constant video recording is largely no longer valid for modern consumer and enterprise-grade SSDs.
-116
View File
@@ -1,116 +0,0 @@
---
id: configuring_go2rtc
title: Configuring go2rtc
---
Use of the bundled go2rtc is optional. You can still configure FFmpeg to connect directly to your cameras. However, adding go2rtc to your configuration is required for the following features:
- WebRTC or MSE for live viewing with audio, higher resolutions and frame rates than the jsmpeg stream which is limited to the detect stream and does not support audio
- Live stream support for cameras in Home Assistant Integration
- RTSP relay for use with other consumers to reduce the number of connections to your camera streams
## Setup a go2rtc stream
First, you will want to configure go2rtc to connect to your camera stream by adding the stream you want to use for live view in your Frigate config file. Avoid changing any other parts of your config at this step. Note that go2rtc supports [many different stream types](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#module-streams), not just rtsp.
:::tip
For the best experience, you should set the stream name under `go2rtc` to match the name of your camera so that Frigate will automatically map it and be able to use better live view options for the camera.
See [the live view docs](../configuration/live.md#setting-streams-for-live-ui) for more information.
:::
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
```
After adding this to the config, restart Frigate and try to watch the live stream for a single camera by clicking on it from the dashboard. It should look much clearer and more fluent than the original jsmpeg stream.
### What if my video doesn't play?
- Check Logs:
- Access the go2rtc logs in the Frigate UI under Logs in the sidebar.
- If go2rtc is having difficulty connecting to your camera, you should see some error messages in the log.
- Check go2rtc Web Interface: if you don't see any errors in the logs, try viewing the camera through go2rtc's web interface.
- Navigate to port 1984 in your browser to access go2rtc's web interface.
- If using Frigate through Home Assistant, enable the web interface at port 1984.
- If using Docker, forward port 1984 before accessing the web interface.
- Click `stream` for the specific camera to see if the camera's stream is being received.
- Check Video Codec:
- If the camera stream works in go2rtc but not in your browser, the video codec might be unsupported.
- If using H265, switch to H264. Refer to [video codec compatibility](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#codecs-madness) in go2rtc documentation.
- If unable to switch from H265 to H264, or if the stream format is different (e.g., MJPEG), re-encode the video using [FFmpeg parameters](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-ffmpeg). It supports rotating and resizing video feeds and hardware acceleration. Keep in mind that transcoding video from one format to another is a resource intensive task and you may be better off using the built-in jsmpeg view.
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- "ffmpeg:back#video=h264#hardware"
```
- Switch to FFmpeg if needed:
- Some camera streams may need to use the ffmpeg module in go2rtc. This has the downside of slower startup times, but has compatibility with more stream types.
```yaml
go2rtc:
streams:
back:
- ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
```
- If you can see the video but do not have audio, this is most likely because your camera's audio stream codec is not AAC.
- If possible, update your camera's audio settings to AAC in your camera's firmware.
- If your cameras do not support AAC audio, you will need to tell go2rtc to re-encode the audio to AAC on demand if you want audio. This will use additional CPU and add some latency. To add AAC audio on demand, you can update your go2rtc config as follows:
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- "ffmpeg:back#audio=aac"
```
If you need to convert **both** the audio and video streams, you can use the following:
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- "ffmpeg:back#video=h264#audio=aac#hardware"
```
When using the ffmpeg module, you would add AAC audio like this:
```yaml
go2rtc:
streams:
back:
- "ffmpeg:rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#video=copy#audio=copy#audio=aac#hardware"
```
:::warning
To access the go2rtc stream externally when utilizing the Frigate App (for
instance through VLC), you must first enable the RTSP Restream port.
You can do this by visiting the Frigate App configuration page within Home
Assistant and revealing the hidden options under the "Show disabled ports"
section.
:::
### Next steps
1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera).
2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router.
3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
## Homekit Configuration
To add camera streams to Homekit Frigate must be configured in docker to use `host` networking mode. Once that is done, you can use the go2rtc WebUI (accessed via port 1984, which is disabled by default) to share export a camera to Homekit. Any changes made will automatically be saved to `/config/go2rtc_homekit.yml`.
+9 -10
View File
@@ -301,7 +301,7 @@ cameras:
More details on available detectors can be found [here](../configuration/object_detectors.md).
Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they can be configured in <NavPath path="Settings > Global configuration > Objects" /> or via the [configuration file reference](../configuration/reference.md).
Restart Frigate and you should start seeing detections for `person`. If you want to track other objects, they can be configured in <NavPath path="Settings > Global configuration > Objects" /> or via the [configuration file reference](../configuration/advanced/reference.md).
### Step 5: Setup motion masks
@@ -348,7 +348,7 @@ In order to review activity in the Frigate UI, recordings need to be enabled.
<ConfigTabs>
<TabItem value="ui">
1. If you have separate streams for detect and record, navigate to <NavPath path="Settings > Camera configuration > FFmpeg" />, select your camera, and add a second input with the `record` role pointing to your high-resolution stream
1. If you have separate streams for detect and record, navigate to <NavPath path="Settings > Camera configuration > Streams (FFmpeg)" />, select your camera, and add a second input with the `record` role pointing to your high-resolution stream
2. Navigate to <NavPath path="Settings > Global configuration > Recording" /> (or <NavPath path="Settings > Camera configuration > Recording" /> for a specific camera) and set **Enable recording** to on
</TabItem>
@@ -388,21 +388,20 @@ If you only plan to use Frigate for recording, it is still recommended to define
:::
By default, Frigate will retain video of all tracked objects for 10 days. The full set of options for recording can be found [here](../configuration/reference.md).
By default, Frigate will retain video of all tracked objects for 10 days. The full set of options for recording can be found [here](../configuration/advanced/reference.md).
### Step 7: Complete config
At this point you have a complete config with basic functionality.
- View [common configuration examples](../configuration/index.md#common-configuration-examples) for a list of common configuration examples.
- View [full config reference](../configuration/reference.md) for a complete list of configuration options.
- View [common configuration examples](../configuration/config.md#common-configuration-examples) for a list of common configuration examples.
- View [full config reference](../configuration/advanced/reference.md) for a complete list of configuration options.
### Follow up
Now that you have a working install, you can use the following documentation for additional features:
1. [Configuring go2rtc](configuring_go2rtc.md) - Additional live view options and RTSP relay
2. [Zones](../configuration/zones.md)
3. [Review](../configuration/review.md)
4. [Masks](../configuration/masks.md)
5. [Home Assistant Integration](../integrations/home-assistant.md) - Integrate with Home Assistant
1. [Zones](../configuration/zones.md)
2. [Review](../configuration/review.md)
3. [Masks](../configuration/masks.md)
4. [Home Assistant Integration](../integrations/home-assistant.md) - Integrate with Home Assistant
+20 -11
View File
@@ -10,13 +10,14 @@ A reverse proxy is typically needed if you want to set up Frigate on a custom UR
Before setting up a reverse proxy, check if any of the built-in functionality in Frigate suits your needs:
|Topic|Docs|
|-|-|
|TLS|Please see the `tls` [configuration option](../configuration/tls.md)|
|TLS|Please see the `tls` [configuration option](../configuration/tls.md)|
|Authentication|Please see the [authentication](../configuration/authentication.md) documentation|
|IPv6|[Enabling IPv6](../configuration/advanced.md#enabling-ipv6)
|IPv6|[Enabling IPv6](../configuration/advanced/system.md#enabling-ipv6)
**Note about TLS**
When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port."
**Note about TLS**
When using a reverse proxy, the TLS session is usually terminated at the proxy, sending the internal request over plain HTTP. If this is the desired behavior, TLS must first be disabled in Frigate, or you will encounter an HTTP 400 error: "The plain HTTP request was sent to HTTPS port."
To disable TLS, set the following in your Frigate configuration:
```yml
tls:
enabled: false
@@ -24,18 +25,26 @@ tls:
:::warning
A reverse proxy can be used to secure access to an internal web server, but the user will be entirely reliant on the steps they have taken. You must ensure you are following security best practices.
This page does not attempt to outline the specific steps needed to secure your internal website.
This page does not attempt to outline the specific steps needed to secure your internal website.
Please use your own knowledge to assess and vet the reverse proxy software before you install anything on your system.
:::
## WebSocket support
Frigate relies on WebSockets for real-time communication between the browser and the backend. Features such as camera controls (enabling/disabling a camera, audio, detect, recordings, and other toggles), live stream playback, and other live-updating parts of the UI will not function correctly if WebSocket connections are not proxied.
Your reverse proxy must be configured to forward the `Upgrade` and `Connection` headers so that WebSocket connections can be established. Each proxy example below already includes the directives needed to do this, but if you are adapting your own configuration, ensure these headers are passed through.
Note that some proxies disable WebSocket support by default — for example, Nginx Proxy Manager has a "Websockets Support" toggle that must be enabled.
## Proxies
There are many solutions available to implement reverse proxies and the community is invited to help out documenting others through a contribution to this page.
* [Apache2](#apache2-reverse-proxy)
* [Nginx](#nginx-reverse-proxy)
* [Traefik](#traefik-reverse-proxy)
* [Caddy](#caddy-reverse-proxy)
- [Apache2](#apache2-reverse-proxy)
- [Nginx](#nginx-reverse-proxy)
- [Traefik](#traefik-reverse-proxy)
- [Caddy](#caddy-reverse-proxy)
## Apache2 Reverse Proxy
@@ -159,7 +168,7 @@ The settings below enabled connection upgrade, sets up logging (optional) and pr
## Traefik Reverse Proxy
This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance.
This example shows how to add a `label` to the Frigate Docker compose file, enabling Traefik to automatically discover your Frigate instance.
Before using the example below, you must first set up Traefik with the [Docker provider](https://doc.traefik.io/traefik/providers/docker/)
```yml
@@ -203,7 +212,7 @@ This example shows Frigate running under a subdomain with logging and a tls cert
}
frigate.YOUR_DOMAIN.TLD {
reverse_proxy http://localhost:8971
reverse_proxy http://localhost:8971
import tls
import logging frigate.YOUR_DOMAIN.TLD
}
+1 -1
View File
@@ -24,7 +24,7 @@ Video decoding is one of the most CPU-intensive tasks in Frigate. While an AI ac
### Configuration
Frigate provides preset configurations for common hardware acceleration scenarios. Set up `hwaccel_args` based on your hardware in your [configuration](../configuration/reference) as described in the [getting started guide](../guides/getting_started).
Frigate provides preset configurations for common hardware acceleration scenarios. Set up `hwaccel_args` based on your hardware in your [configuration](../configuration/advanced/reference) as described in the [getting started guide](../guides/getting_started).
### Troubleshooting Hardware Acceleration
+17 -1
View File
@@ -55,7 +55,7 @@ If you see repeated "On connect called" messages in your logs, check for another
### Error: Database Is Locked
SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced.md#database) should be used to move the database to a location on the internal drive.
SQLite does not work well on a network share, if the `/media` folder is mapped to a network share then [this guide](../configuration/advanced/system.md#database) should be used to move the database to a location on the internal drive.
### Unable to publish to MQTT: client is not connected
@@ -124,3 +124,19 @@ cameras:
width: 1280
height: 720
```
### Why does Frigate keep creating new events for my parked car?
Stationary tracking is designed to _prevent_ this — a parked car should stay one tracked object and not generate new events. If you're getting repeated events for the same car, it's likely that Frigate is losing the tracked object and re-detecting it as a new one.
Open one of the events in Explore → **Tracking Details**. If the detection scores are low (< 70% or so), the model isn't confident the parked car is a car. This is common with the free [COCO-trained](https://cocodataset.org/#explore) object detection models on steep/top-down angles, partially occluded cars, foliage, or low-light footage. When detections fall below `min_score` for too many frames the tracker loses the object, and the next confident frame creates a brand new one.
What helps:
- **Improve the view** — even a small angle change that gets more of the car visible could lift scores enough to stabilize tracking.
- **Use a more accurate model** — switching from `mobiledet` to `yolov9`, or stepping up to a larger variant like `yolov9-s` over `yolov9-t`, can help (at the cost of inference time, and still on the COCO dataset). The biggest gains usually come from fine-tuning a model on images from your own cameras so it learns your specific scene. [Frigate+](https://frigate.video/plus) is a paid option that does this - models are trained on security-camera footage and can be fine-tuned on images you submit from your own setup.
- **Don't set `detect -> stationary -> max_frames` for `car`** — it artificially ends tracking and forces re-detection as a new object. See [Stationary Objects](../configuration/stationary_objects.md).
- **Restrict alerts to the areas you care about** with `required_zones` — see [Zones](../configuration/zones.md#restricting-alerts-and-detections-to-specific-zones). Make sure those zones use the default `loitering_time: 0` unless you specifically want the review item to stay open until the car leaves.
- **Filter impossible locations** with [object filter masks](../configuration/masks.md#object-filter-masks) if cars are being detected on rooftops, treetops, etc.
See [Object Filters](../configuration/object_filters.md) for more on tuning `min_score` and `threshold` — note that raising them too high will make this exact problem worse.
+235
View File
@@ -0,0 +1,235 @@
---
id: go2rtc
title: Troubleshooting go2rtc
---
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
This page covers common problems with the bundled [go2rtc](/configuration/go2rtc) and how to resolve them, whether your cameras were added with the setup wizard or configured by hand.
When a stream won't play or behaves oddly, the most important first step is to figure out **where** in the pipeline it breaks. Frigate's live view is a chain — _camera → go2rtc → your browser_ — and each stage fails for different reasons. Work through the checks below in order, then jump to the matching problem category.
## Start by isolating the problem
### 1. Read the go2rtc logs
Access the go2rtc logs in the Frigate UI under <NavPath path="System Logs" /> in the sidebar (select the **go2rtc** tab). If go2rtc cannot connect to your camera you will usually see a clear error here — `401 Unauthorized` (bad or incorrectly encoded credentials), `Connection refused` / `timeout` (wrong IP, port, or the camera is at its connection limit), or `404 Not Found` (wrong RTSP path, or the referenced stream name does not exist).
### 2. Test the stream in the go2rtc web interface
If the logs look clean, open go2rtc's own web interface on port `1984`. This is the single most useful diagnostic, because it takes Frigate's UI out of the equation entirely.
- If using Frigate through Home Assistant, enable the web interface at port `1984` (it is disabled by default — see [Home Assistant ports](#home-assistant-and-port-access)).
- If using Docker, forward port `1984` before accessing the web interface.
Open the stream page for your camera (`http://<frigate_host>:1984/stream.html?src=back`) and try each player link:
- **If nothing plays here**, the problem is between the camera and go2rtc (codec, credentials, or transport), _not_ your browser. Fix it at the source before touching anything in Frigate.
- **If a player works here but Frigate's live view does not**, the problem is browser/codec related — compare the **MSE** and **WebRTC** links. Frigate prefers MSE and only attempts WebRTC when MSE fails (or for two-way talk). If `mode=mse` plays but `mode=webrtc` does not, you have a [WebRTC codec problem](#webrtc-and-two-way-talk); if neither plays, your browser cannot decode the codec (commonly H.265 — see [H.265 / HEVC cameras](#h265--hevc-cameras)).
### 3. Inspect the negotiated codecs
You can view detailed stream info — including the exact video and audio codecs go2rtc negotiated with the camera — at `http://frigate_ip:5000/api/go2rtc/streams` (or `http://frigate_ip:5000/api/go2rtc/streams/back` for a single camera). This is the authoritative answer to "what is my camera actually sending?" and is far more reliable than guessing from the camera's web UI. It also shows whether the audio track is `sendonly`/`recvonly`, which matters for [two-way talk](#webrtc-and-two-way-talk).
### 4. Fix the codec with the FFmpeg module
If the camera plays in go2rtc but not in your browser, the video or audio codec is unsupported. Browsers can reliably play **H.264** video and **AAC** audio; many cannot play H.265/HEVC, and some camera audio (G.711/PCM, MJPEG containers, etc.) is not playable at all. The fix is to have go2rtc re-encode the stream on demand using its FFmpeg module.
In the Frigate UI this is the **Use compatibility mode (ffmpeg)** toggle on a stream source; in YAML it is the `ffmpeg:` prefix on the source URL.
<ConfigTabs>
<TabItem value="ui">
1. Navigate to <NavPath path="Settings > System > go2rtc Streams" /> and expand your camera's stream.
2. On the source you want to convert, click the **Use compatibility mode (ffmpeg)** button (the sliders icon next to the URL). This routes the source through go2rtc's FFmpeg module and reveals the transcoding options.
3. Set **Video** to **Transcode to H.264** if your browser can't play the camera's video codec (e.g. H.265). Leave it on **Copy** to pass the video through untouched — this is much cheaper and should be your default whenever only the audio needs converting.
4. Set **Audio** to **Transcode to AAC** (for MSE) or **Transcode to Opus** (for WebRTC) if the camera's audio codec is unsupported. Leave it on **Copy** to keep the original, or **Exclude** to drop audio entirely.
5. When transcoding **video**, set **Hardware acceleration** to **Automatic (recommended)** so the encode runs on your GPU instead of the CPU. See [hardware-accelerated transcoding](#hardware-accelerated-transcoding-with-ffmpeg-8) for an important FFmpeg 8 caveat.
6. **Save** the section, then reload the live view.
</TabItem>
<TabItem value="yaml">
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
# transcode video to H.264 on the GPU; only needed if the browser can't play the source codec
- "ffmpeg:back#video=h264#hardware"
```
To convert audio only (leaving video untouched), or to convert both:
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
- "ffmpeg:back#audio=aac" # audio only — preferred when the video already plays
# or, to convert both video and audio:
# - "ffmpeg:back#video=h264#audio=aac#hardware"
```
</TabItem>
</ConfigTabs>
:::warning
The `#`-modifiers (`#video=`, `#audio=`, `#hardware`, `#backchannel=0`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing — go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you.
:::
Transcoding video is resource intensive. Always prefer `#video=copy` (the **Copy** option) and only convert the track that is actually unsupported. If you must transcode video and have no hardware encoder available, the built-in jsmpeg view may be the better option.
## Live view is black, buffering, or stuck in "low-bandwidth mode"
When the live view shows a black screen, spins forever, or repeatedly drops to the lower-quality jsmpeg player ("low-bandwidth mode"), the stream almost always contains something the browser cannot decode over MSE — usually H.265 video or a non-AAC audio track. Confirm this in the go2rtc web UI (port `1984`): if MSE won't play there, Frigate can't play it either, since it uses the same pipeline.
The fix is to produce an **H.264 + AAC** stream, either by changing your camera's firmware codecs or by transcoding in go2rtc (see [Fix the codec with the FFmpeg module](#4-fix-the-codec-with-the-ffmpeg-module)). A few other things worth checking:
- **Set the camera's I-frame (keyframe) interval to match its frame rate** (or "1x" on Reolink), and avoid "smart"/"+" codecs like _H.264+_ or _H.265+_. A long keyframe interval delays the first decodable frame past Frigate's startup timeout, which forces the fallback to jsmpeg. See [camera settings recommendations](/configuration/live#camera-settings-recommendations).
- **A spinner that never clears, even though video plays in VLC**, is often an unplayable _audio_ track stalling playback. Drop or transcode the audio (see below).
- **Remote/VPN viewing that buffers** while the LAN is fine is usually latency/jitter exceeding MSE's startup buffer — set up [WebRTC](/configuration/live#webrtc-extra-configuration), which drops late frames instead of buffering.
The general live-view behavior (smart streaming, the MSE → WebRTC → jsmpeg fallback chain, and how to read browser console errors) is documented in detail in the [Live view FAQ](/configuration/live#live-view-faq).
## H.265 / HEVC cameras
H.265/HEVC playback in the browser is unreliable and version-dependent. WebRTC does not support H.265 on some browsers, and MSE/HEVC support varies by browser, OS, and whether a hardware decoder is present. An H.265 stream that plays fine in VLC, the go2rtc web UI, and Frigate's recordings can still be blank in a live view.
For dependable live viewing, use **H.264** for the stream the live view consumes:
- Point the live view at the camera's H.264 **substream** and keep the H.265 main stream for recording only, or
- Transcode H.265 → H.264 in go2rtc with the FFmpeg module and `#hardware` (software HEVC transcoding is very CPU heavy).
Treat browser HEVC playback as best-effort. See also [H.265 cameras via Safari](/configuration/camera_specific#h265-cameras-via-safari).
## No audio in Live view
Live view audio has strict codec requirements that differ by player: **MSE requires AAC, PCMA, or PCMU**, and **WebRTC requires Opus, PCMA, or PCMU**. Many cameras default to a codec outside these sets (or to PCM/G.711), so the player loads video only and no audio control appears.
The most robust approach is to provide both an AAC track (for MSE) and an Opus track (for WebRTC) on the same stream by transcoding audio with the FFmpeg module while copying the video:
<ConfigTabs>
<TabItem value="ui">
1. Navigate to <NavPath path="Settings > System > go2rtc Streams" /> and expand the camera's stream.
2. Add a second **Source** that references the stream by name (e.g. the URL `ffmpeg:back`), enable **Use compatibility mode (ffmpeg)**, and set **Audio** to **Transcode to Opus** for WebRTC support.
3. Keep the original source as **Source 1** so MSE can use the camera's AAC (or transcode the first source's audio to AAC if the camera doesn't provide it).
4. **Save** the section.
</TabItem>
<TabItem value="yaml">
```yaml
go2rtc:
streams:
back:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2 # video + AAC for MSE
- "ffmpeg:back#audio=opus" # adds an Opus track for WebRTC
```
If the camera's native audio isn't AAC either, transcode both:
```yaml
go2rtc:
streams:
back:
- "ffmpeg:rtsp://user:password@10.0.10.10:554/live0#video=copy#audio=aac" # video copy + AAC for MSE
- "ffmpeg:back#audio=opus" # Opus for WebRTC
```
</TabItem>
</ConfigTabs>
Setting the camera firmware to AAC (and H.264) avoids transcoding entirely and is always preferable when the camera supports it. For more detail and examples, see [Audio Support](/configuration/live#audio-support).
## WebRTC and two-way talk
WebRTC is only attempted when MSE fails or when using a camera's two-way talk feature; the "All Cameras" dashboard never uses it. When it doesn't work, the cause is almost always one of:
- **Codec mismatch** — WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above.
- **Port `8555` not reachable, or no candidates set** — WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration).
- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly — go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream).
## High CPU usage
If go2rtc is using a lot of CPU, it is almost always transcoding in software. An FFmpeg source with a codec modifier like `#video=h264` or `#audio=aac` but **no** `#hardware` re-encodes on the CPU. (Frigate's `ffmpeg.hwaccel_args` only applies to Frigate's own detect/record processes — it does _not_ accelerate go2rtc's transcodes.)
To keep CPU usage down:
- Only transcode the track that is genuinely unsupported, and use `#video=copy` to pass video through untouched whenever possible.
- When you must transcode video, always add `#hardware` (the **Automatic** hardware option in the UI) so the encode runs on the GPU. Note the [FFmpeg 8 device requirement](#hardware-accelerated-transcoding-with-ffmpeg-8) below.
- Don't restream a high-resolution main stream just to feed the live view — even with `#video=copy`, muxing a 4K/8MP+ stream is inherently expensive. Use the camera's lower-resolution substream for live and detect, and let Frigate pull the main stream directly for recording.
## Connection, authentication, and complex passwords
If go2rtc logs `401 Unauthorized` for a URL that works in VLC, the password almost certainly contains reserved URL characters. **Frigate URL-encodes passwords for its own `cameras.ffmpeg.inputs`, but it does not touch what you write under `go2rtc.streams`** — go2rtc parses that URL itself. You must URL-encode special characters yourself in the `go2rtc.streams` section (`@``%40`, `#``%23`, `?``%3F`, `%``%25`, etc.).
Note the asymmetry: under `cameras.ffmpeg.inputs` you should use the **raw** password (Frigate encodes it for you) — pre-encoding it there causes a double-encode and fails. See [Handling Complex Passwords](/configuration/restream#handling-complex-passwords).
Repeated `401`/`Connection refused` errors can also mean the camera hit its **concurrent connection limit** or triggered a login lockout. Routing all roles through a single [RTSP restream](/configuration/restream#reduce-connections-to-camera) means the camera only ever sees one connection from go2rtc.
## Stream names must match everywhere
A surprising number of "the better live options aren't available" or `404 Not Found` problems come down to a name mismatch. The same string must be used consistently:
- the **go2rtc stream key** (`go2rtc.streams.<name>`),
- any `ffmpeg:<name>#…` source that references it,
- the camera's restream input path (`rtsp://127.0.0.1:8554/<name>`), and
- the camera name itself (so Frigate auto-maps it for MSE/WebRTC) — or an explicit `live -> streams` mapping pointing at the go2rtc stream **name** (never a path).
If you rename or remove a go2rtc stream while experimenting and the live stream selector then shows a blank entry, clear your browser's site data for the Frigate URL — the selected stream is cached per-device in local storage.
## Camera-specific behavior
Several camera brands have well-known quirks with go2rtc. Rather than repeat them here, see the [camera-specific configuration](/configuration/camera_specific) page, which covers them in detail. The highlights:
- **Reolink** — RTSP is unreliable on many models; the **http-flv** stream through the FFmpeg module is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras).
- **TP-Link Tapo** — use go2rtc's native `tapo://` source for stability and two-way audio; a stale RTSP credential can often be revived by clicking play once in the go2rtc web UI.
- **Ubiquiti/UniFi Protect** — use the `rtspx://` scheme (not `rtsps://…?enableSrtp`).
- **Amcrest/Dahua** — use the `/cam/realmonitor?channel=1&subtype=N` scheme, where `subtype=0` is the main stream. See [Amcrest & Dahua](/configuration/camera_specific#amcrest--dahua).
## Non-RTSP sources and the FFmpeg module
go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else — MJPEG, HTTP/HTTP-FLV, RTMP, or unusual codecs — must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example.
## Hardware-accelerated transcoding with FFmpeg 8
Frigate 0.18 ships **FFmpeg 8.0** as the default, and FFmpeg 8 is stricter about hardware-accelerated filtering than earlier versions. Whenever go2rtc transcodes video with hardware acceleration (any source using `#hardware`, `#hardware=vaapi`, or the **Automatic** hardware option in the UI), it builds a filter chain that uploads frames to the GPU with the `hwupload` filter. FFmpeg 8 now refuses to do this unless it is told **which device** to use — earlier versions selected one automatically. The result is that an otherwise-working transcode fails to start, the live view never loads, and go2rtc logs:
```
[hwupload] A hardware device reference is required to upload frames to.
[AVFilterGraph] Error initializing filters
Error opening output files: Invalid argument
```
The fix is to tell go2rtc's bundled FFmpeg which hardware device to use via the `go2rtc -> ffmpeg -> global` option. For **VAAPI**-based acceleration — which covers most Intel and AMD GPUs, and is what go2rtc selects automatically on that hardware — point it at your render device:
```yaml
go2rtc:
ffmpeg:
global: "-vaapi_device /dev/dri/renderD128"
streams:
back:
- "ffmpeg:rtsp://user:password@10.0.10.10:554/live0#video=h264#hardware"
```
`/dev/dri/renderD128` is the usual render node; on a system with more than one GPU you may need `renderD129` (or higher), and the device must be passed into the container (e.g. `devices: - /dev/dri:/dev/dri` in Docker Compose).
If you use a **different hardware acceleration backend**, you will likely need to specify its device in the same way, using the option that matches that backend instead of `-vaapi_device`. See the [go2rtc FFmpeg source documentation](https://github.com/AlexxIT/go2rtc/tree/v1.9.13#source-ffmpeg) and the upstream report ([go2rtc issue #1984](https://github.com/AlexxIT/go2rtc/issues/1984)) for background and other examples.
:::tip
If you don't transcode in go2rtc with hardware acceleration, this does not affect you. If you want to avoid the change entirely, you can pin Frigate (and the go2rtc it bundles) back to FFmpeg 7.0 by setting `ffmpeg -> path: "7.0"` in your config.
:::
## Home Assistant and port access
When running Frigate as a Home Assistant add-on, the go2rtc API (port `1984`), the RTSP restream (port `8554`), and WebRTC (port `8555`) are **disabled and hidden by default**. To use them — for example to reach the go2rtc web interface for troubleshooting, or to open a go2rtc stream externally in an app like VLC — go to <NavPath path="Settings > Add-ons > Frigate > Configuration > Network" />, click **Show disabled ports**, enable the port you need, and save. Use the host's IP address rather than an mDNS name like `homeassistant.local`.
If live view works in the Frigate UI but not in Home Assistant, the most common cause is the go2rtc stream name not matching the camera name — name the primary go2rtc stream exactly like the camera, or add a `live -> streams` mapping, so the integration can resolve the restream.
+6
View File
@@ -121,6 +121,12 @@ If segments are only ~1 second instead of ~10 seconds, the camera is sending cor
- **Changing codec, bitrate, or resolution mid-stream** — Any encoding changes during an active stream can cause unpredictable segment splitting.
- **Camera firmware bugs** — Check for firmware updates from your camera manufacturer.
:::tip
You don't have to run `ffprobe` by hand to catch this. Open a camera's **Camera Probe Info** dialog (the info icon on the System → Metrics → Cameras page) and check the **Keyframe analysis** section. It probes the record stream and flags sparse or variable keyframes, which is what smart/"+" codecs (H.264+/H.265+) and long keyframe intervals produce.
:::
### Step 4: Check for a stuck detector
If the detect stream is not processing frames, segments will accumulate. Common causes:
+101
View File
@@ -0,0 +1,101 @@
---
id: explore
title: Explore
---
import NavPath from "@site/src/components/NavPath";
**Explore** is where you browse and search every **tracked object** Frigate has saved. By default it groups recent objects by label; when [Semantic Search](/configuration/semantic_search) is enabled, you can also search by natural-language description or visual similarity. Selecting any object opens a detail pane with its snapshot, lifecycle, and metadata.
This page describes how to _use_ the Explore view. For how the underlying features are _configured_, see [Semantic Search](/configuration/semantic_search) and [Generative AI descriptions](/configuration/genai/genai_objects).
:::tip
If you just want to quickly see what happened on your cameras, it's recommended to use [Review](/usage/review) rather than Explore. Review groups overlapping and adjacent activity on a camera into **review items** and sorts them into Alerts, Detections, and Motion, so you can scan and play back footage in a few clicks instead of sifting through individual objects. Reach for Explore when you need to find a _specific_ tracked object after the fact — by label, time, zone, or description.
:::
## Browsing tracked objects
The default view shows your most recent tracked objects grouped into rows by label — _Person_, _Car_, _Dog_, and so on — each row labeled with the object type and a count. The arrow at the end of a row opens the full, filterable grid for that label.
Clicking a thumbnail opens its [detail dialog](#tracked-object-details); right-clicking or long-pressing a thumbnail opens an [actions menu](#actions-and-bulk-selection). You can switch to a denser grid layout and adjust the number of columns from the view's settings.
## Searching
When [Semantic Search](/configuration/semantic_search) is enabled, a search bar appears that combines two things in one input:
- **Natural-language search** — type a free-text query and press Enter to run a semantic search over your tracked objects.
- **Filter tokens** — type a `key:` to get suggestions, then a value, to add a structured filter. Each filter becomes a removable chip, and you can chain several together.
You can save a search with the star icon and reload it later, and clear everything with the clear-search icon. A help popover explains the token syntax, for example:
```
cameras:front_door label:person before:01012024 time_range:3:00PM-4:00PM
```
### Filter reference
The most common filter tokens are:
| Filter | Description |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| **Cameras** | Limit to one or more cameras. |
| **Labels** | Object labels (person, car, etc.). |
| **Sub Labels** | Recognized sub labels (e.g. a recognized face or name). |
| **Attributes** | Classification attributes applied to the object. |
| **Recognized License Plate** | Match a recognized plate. |
| **Zones** | Objects that entered specific zones. |
| **Before / After** | Restrict to a date range. |
| **Time Range** | Restrict to a time of day (`HH:MM-HH:MM`). |
| **Min / Max Score** | Restrict by the object's confidence score. |
| **Min / Max Speed** | Restrict by estimated speed (when speed estimation is configured). |
| **Has Snapshot / Has Clip** | Only objects that saved a snapshot or recording. |
| **Submitted to Frigate+** | Only objects already submitted (when Frigate+ is enabled). |
| **Search Type** | Whether semantic search matches the object's **Thumbnail** or its **Description**. |
### Sorting
When a filter or search is active, a **Sort** control lets you order results by **date**, **object score**, or **estimated speed** (ascending or descending). When a semantic query or similarity search is active, results can also be ordered by **relevance**.
### Thumbnail and description search
- The **Search Type** setting controls whether a text query is matched against each object's **thumbnail** or its **description**. Each result indicates which one it matched and the confidence.
Natural-language search, thumbnail search, and description search all require [Semantic Search](/configuration/semantic_search) to be enabled.
## Tracked Object Details
Selecting an object opens the **Tracked Object Details** dialog. Use the arrows (or the left/right keys) to step to the previous or next object. The dialog has two tabs:
- **Snapshot** or **Thumbnail** — the saved snapshot (or thumbnail).
- **Tracking Details** — the object's lifecycle, available when the object has a recording. It lists each significant moment (detected, entered a zone, became active or stationary, left, and so on); clicking a moment plays that part of the recording with the bounding box overlaid. A settings popover lets you show all zones and adjust the annotation offset.
The details pane shows the object's **label**, **scores**, **camera**, **timestamp**, estimated **speed**, any **recognized license plate** and **classification attributes**, and its **description**. Admins can edit the sub label, license plate, and attributes inline.
The **description** can be edited by hand, and — when [Generative AI descriptions](/configuration/genai/genai_objects) are enabled and the object's lifecycle has ended — regenerated from the snapshot or from thumbnails. For `speech` objects, a **Transcribe** action is available when audio transcription is enabled. When [Frigate+](/integrations/plus) is enabled, admins can submit a snapshot to improve their model directly from this pane.
## Actions and bulk selection
Right-clicking or long-pressing an object (in the grid or its thumbnail) opens an actions menu with options to **download** the video, snapshot, or a clean snapshot; **view tracking details**; **find similar**; **add a trigger**; **view in History**; and **delete the tracked object**.
:::note
Deleting a tracked object removes its snapshot, embeddings, and tracking-details entries, but the recorded footage of that object in [History](/usage/history) is **not** deleted.
:::
To act on many objects at once, Ctrl/Cmd-click or right-click to start a selection (selected tiles gain a blue ring), then use the toolbar to select all, clear the selection, or delete (admins).
## Semantic Search - Usage and best practices {#usage-and-best-practices}
1. Semantic Search is used in conjunction with the other filters available on the Explore page. Use a combination of traditional filtering and Semantic Search for the best results.
2. Use the thumbnail search type when searching for particular objects in the scene. Use the description search type when attempting to discern the intent of your object.
3. Because of how the AI models Frigate uses have been trained, the comparison between text and image embedding distances generally means that with multi-modal (`thumbnail` and `description`) searches, results matching `description` will appear first, even if a `thumbnail` embedding may be a better match. Play with the "Search Type" setting to help find what you are looking for. Note that if you are generating descriptions for specific objects or zones only, this may cause search results to prioritize the objects with descriptions even if the the ones without them are more relevant.
4. Make your search language and tone closely match exactly what you're looking for. If you are using thumbnail search, **phrase your query as an image caption**. Searching for "red car" may not work as well as "red sedan driving down a residential street on a sunny day".
5. Semantic search on thumbnails tends to return better results when matching large subjects that take up most of the frame. Small things like "cat" tend to not work well.
6. Experiment! Find a tracked object you want to test and start typing keywords and phrases to see what works for you.
## Triggers
From an object's actions menu, **Add trigger** sets up a per-camera trigger that uses Semantic Search to automate an action (a notification, sub label, or attribute) whenever a similar object appears. Triggers require Semantic Search and are managed under <NavPath path="Settings > Enrichments > Triggers" />. See [Triggers](/configuration/semantic_search#triggers) for full configuration and best practices.
+43
View File
@@ -0,0 +1,43 @@
---
id: exports
title: Exports
---
**Exports** are how you keep a specific piece of footage permanently.
Frigate's recordings are governed by your [retention settings](/configuration/record): once footage ages past its retention window — or, depending on your configuration, once it is only kept where motion, alerts, or detections occurred — it is deleted to free up disk space. An **export** saves a copy of a chosen time range to a separate location that is **never removed by retention**, so it stays available until you delete it yourself.
This is the answer to the common question _"how do I stop Frigate from deleting an important clip?"_ Instead of increasing retention for an entire camera (which uses far more storage to protect a single moment), export just the footage you want to keep.
:::tip
Exports are stored under `/media/frigate/exports`, separate from your recordings, and are not counted against or removed by recording retention. They remain on disk until you delete them, so be aware that they accumulate over time.
:::
## Creating an export
There are a few ways to create an export:
- **From Review** — select (right click or long-press) an individual review item directly, and choose Export from the header menu. You can also select multiple review items and export them all at once, optionally grouping them into a [case](#cases).
- **From History** — open the **Actions** menu and choose **Export**. You can export a preset duration (the last 1, 4, 8, 12, or 24 hours), enter a custom start and end time, or select a range directly on the timeline. A **multi-camera** option lets you export the same time range across several cameras at once.
In every case you can give the export a name. Frigate then saves the footage from your recordings as a single video file. Larger ranges take time to process; the export is marked _in progress_ until it finishes, and you can keep using Frigate while it runs.
## Managing exports
All of your exports live on the **Exports** page, reachable from the main navigation, where you can search for one by name. Each export offers the following actions:
- **Play** it in the browser,
- **Download** it to save the footage outside of Frigate,
- **Share** it — copies a direct link to the export (or uses your device's share sheet),
- **Rename** it, and
- **Delete** it — deleting is the only way an export is removed.
You can also select multiple exports at once to **delete** them in bulk, or to **add them to** (or **remove them from**) a [case](#cases).
## Cases
A **case** groups related exports together — for example, all the clips from a single incident across multiple cameras. On the **Exports** page you can create a case with a name and description, add existing exports to it (or create a new case while exporting), and **download the entire case as a single archive** to hand off as one package.
Exports that don't belong to a case appear under **Uncategorized Exports**. Deleting a case lets you either keep its exports (they move back to uncategorized) or delete them along with the case.
+75
View File
@@ -0,0 +1,75 @@
---
id: history
title: History
---
import NavPath from "@site/src/components/NavPath";
**History** is Frigate's full-resolution recording viewer. Unlike Live, Review, and Explore, there is no menu item for it — you reach it from within another view, then scrub the timeline, switch cameras, inspect a tracked object's lifecycle, and export or share any moment.
This page describes how to _use_ the History view. For how recordings are _configured_ (retention, pre/post capture), see [Recording](/configuration/record).
## Opening History
You can open History from several places:
- **From [Review](/usage/review):** clicking a review item opens its recording, scrubbed to just before the activity on that camera.
- **From [Live](/usage/live):** the **History** button in a camera's single-camera view opens that camera about 30 seconds in the past.
- **From a share link:** opening a shared timestamp link (see [Share Timestamp](#the-actions-menu) below) jumps straight to that camera and moment.
Use the **Back** button to return where you came from, or the **Live** button to jump to the current camera's live view.
:::tip
If you see **"No recordings found for this time"**, the most common causes are: recording was not enabled for that camera at the time of the event; the retention window has since expired and those segments were removed; or storage ran low and Frigate deleted them early to free space. See [Recording](/configuration/record) to verify your retention settings.
:::
## Timeline, Events, and Detail
A toggle (a drawer on mobile) switches the side panel between three modes:
- **Timeline** — a scrubbable vertical timeline of the selected camera. Horizontal lines down the center represent motion, with longer lines indicating more motion at that moment. Review items are marked as shaded areas (**red** for alerts, **orange** for detections), and sections with no colored background are times when no recording exists.
- **Events** — a scrollable list of the camera's review items for the time range; clicking one seeks the player to it.
- **Detail** — the [tracking details inspector](#the-detail-view) for the objects in view.
While you are selecting a range to export, the panel temporarily switches to Timeline.
## Scrubbing and previews
Drag the timeline handlebar to move through time; the main player and any secondary camera previews scrub together so everything stays in sync. Press the zoom buttons on the timeline to change its zoom level (from coarse to fine segments). Sections of the timeline with no recordings are shown as gaps.
On desktop, when more than one camera is available, a **row of secondary previews** shows the other cameras at the same moment. Clicking one of them makes it the main camera at the current timestamp, so you can follow activity across cameras without losing your place. On mobile, use the camera drawer to switch cameras.
## Filtering and the calendar
You can filter History by **cameras** and **date**. The calendar behaves the same as it does in [Review](/usage/review#filtering-and-the-calendar): an **underline** under a day means recordings exist for that day, and a **colored dot** (red for unreviewed alerts, orange for unreviewed detections) marks days with unreviewed activity.
## The Detail view
The **Detail** mode turns the side panel into a tracking details inspector. It lists one card per review item, each showing the item's severity, start time, the object labels involved, a count of tracked objects, and the duration. The active card is highlighted as the video plays, and clicking a card seeks to it.
Expanding a card reveals the **lifecycle** of each tracked object — a row for each significant moment (detected, entered a zone, became active, became stationary, left, and so on), with a progress line that follows the current playback position. Hovering a row shows that moment's score, ratio, and area, and clicking a row seeks the video to that exact timestamp.
The **Detail View Settings** at the bottom let you toggle whether the active item's objects expand automatically, and adjust the **annotation offset** — a fine timing correction that aligns the bounding-box overlays with the recorded video when your camera's snapshot and recording timestamps drift. Admins can save the offset to the camera's configuration.
## The Actions menu
On desktop, the **Actions** menu (the film icon) collects the things you can do with the footage you are viewing:
- **Export** — save a clip of a chosen time range so it is never removed by retention. The dialog pre-selects the last hour; adjust the range or drag the timeline handles, then export. See [Exports](/usage/exports) for managing and downloading exports.
- **Share Timestamp** — generate a link to the current moment (or a custom timestamp) to share with another Frigate user. This is an internal link, not a public share URL.
- **Motion Search** — scan this camera's recordings for changes in a region you draw. This is the same tool documented under [Reviewing Motion](/usage/review#motion-search).
- **Debug Replay** (admins) — replay a recorded range back through Frigate's detection pipeline to see how it would be processed.
You can also capture an instant snapshot of the current frame, and submit a frame to [Frigate+](/integrations/plus) directly from the player (admins only).
## AI review summaries
When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them as you scrub through History. A review item that has an AI summary exposes its details in a few places:
- **Over the video** — when the item is on screen, a popup appears over the player.
- **In the Events side panel** — items with a summary show the title below the thumbnail.
- **In the Detail side panel** — the item's card shows the title alongside its tracking details.
Clicking any of these opens the **AI Analysis** dialog with the generated detail and any flagged concerns for that item.
+118
View File
@@ -0,0 +1,118 @@
---
id: live
title: Live View
---
import NavPath from "@site/src/components/NavPath";
**Live view** is Frigate's real-time dashboard and the page you land on by default. It shows all of your cameras at a glance, streams your most recent alerts across the top, and lets you open any camera in a full-resolution single-camera view with audio, two-way talk, PTZ, and on-demand recording controls.
This page describes how to _use_ the Live view. For how to _configure_ live streaming — go2rtc, stream selection, smart streaming, WebRTC, and audio — see the [Live View configuration](/configuration/live) docs.
## The dashboard at a glance
The default **All Cameras** dashboard shows every camera, with a filmstrip of recent **alerts** scrolling across the top. Clicking an alert opens it in [Review](/usage/review); each card also has a check button to mark it reviewed without leaving the dashboard. Only **alerts** appear in the filmstrip — to suppress a label or zone from showing there, configure it as a detection instead (see [Alerts and Detections](/configuration/review#alerts-and-detections)).
By default Frigate uses **smart streaming**: a camera's image updates roughly once per minute while nothing is happening, and switches to a full live stream the moment activity is detected. This conserves bandwidth and resources. You can change this for each camera when using a camera group (see [Streaming settings](#streaming-settings-and-the-right-click-menu) below), and the behavior is explained in detail under [Live view technologies](/configuration/live#live-view-technologies).
On mobile, a toggle in the header switches between a **grid** layout and a single-column **list** layout. On desktop a **fullscreen** button is available in the lower-right corner.
## Switching dashboards and camera groups
The icon rail (top-left on desktop, a horizontal strip on mobile) switches between dashboards:
- The **home** icon is the **All Cameras** dashboard, which shows every camera enabled for the dashboard.
- Each **camera group** you create appears as its own icon. Selecting a group shows only that group's cameras.
Camera groups are useful for organizing cameras by location (for example, _Front of House_ or _Backyard_) and for giving each group its own dashboard layout and camera streaming preferences.
You can also view [Birdseye](/configuration/birdseye) on the dashboard, or open it directly at `http://<frigate_host>:5000/#birdseye`. Clicking a camera inside the Birdseye view jumps to that camera's live feed.
## Creating and editing camera groups
Admins can manage groups from the pencil icon next to the group rail, which opens the **Camera Groups** dialog. From there you can add a group, or edit and delete existing ones. When creating a group you choose:
- a **Name** (spaces are converted to underscores),
- the **cameras** to include — each camera has a toggle and a gear that opens its [streaming settings](#streaming-settings-and-the-right-click-menu), and
- an **icon** used for the group's button in the rail.
Deleting a group also clears any custom layout you saved for it.
## Rearranging a camera group layout
On desktop and tablet, each camera group has its own freely-arrangeable grid. Enter **Edit Layout** mode from the layout button in the lower-right corner: camera tiles gain a drag handle and corner resize handles. Drag a tile to reposition it and drag a corner to resize it (the aspect ratio is preserved). Exit edit mode to save. The layout is stored in your browser per device, so each device can have its own arrangement.
The default **All Cameras** dashboard is not manually arrangeable — it automatically sizes tiles based on each camera's aspect ratio (wide cameras span two columns, tall cameras span two rows).
## Reading the tile indicators
Each camera tile surfaces its current state with a few overlays:
- A **pulsing red dot** in the corner means **motion is currently detected** on that camera.
- A **red outline** around the tile means an **active tracked object** is on that camera.
- A small **label chip** lists the object types currently detected (for example, _Person_, _Car_).
- A **camera-name label** appears when you have enabled always-on camera names, or when a camera is offline or disabled.
- A **Stream Offline** or **Camera is off** placeholder appears when no frames are being received or the camera has been turned off.
You can optionally overlay live streaming statistics (stream type, bandwidth, latency, and frame counts) on a tile to diagnose playback issues.
## Streaming settings and the right-click menu
Right-clicking (or long-pressing) a camera tile opens a context menu with quick controls: an **audio volume** control for streams that support audio, **Mute / Unmute all cameras**, **show or hide streaming statistics**, the **debug view**, **notification** options, and — for admins — turning the camera on or off. If the audio control doesn't appear, see [Audio Support](/configuration/live#audio-support) — audio requires go2rtc configured with a compatible codec.
A **Low-bandwidth mode** notice may also appear in the context menu with a **Reset** option appears when Frigate has fallen back to the lower-quality jsmpeg stream — see the [Live view FAQ](/configuration/live#live-view-faq) for why this happens.
For non-default groups, the context menu also exposes **Streaming Settings** for that camera, which let you choose:
- the **stream** to display (the dropdown lists the streams you configured under [`live -> streams`](/configuration/live#setting-streams-for-live-ui), and indicates whether audio is available),
- the **streaming method****No Streaming**, **Smart Streaming** (recommended), or **Continuous Streaming** (higher bandwidth), and
- **compatibility mode**, for devices that have trouble rendering the default player.
These settings are saved per group and per device in your browser, not in your config file.
## The single-camera view
Clicking a camera tile opens its full-resolution single-camera view. The top bar provides:
- **Back** (also the `Esc` key) to return to the dashboard,
- **History** to jump to the [recordings](/usage/history) for this camera, starting about 30 seconds in the past,
- **Fullscreen** and **Picture-in-Picture** (if supported by your browser),
- **Two-way talk** (the microphone button — requires a supported camera and WebRTC; keyboard shortcut `t`), and
- **Camera audio muting** (the speaker button; keyboard shortcut `m`).
You can pinch or scroll to zoom into the feed. A **settings** gear provides a **stream** selector (with audio and two-way-talk availability indicators), **Play in background**, **Show stats**, and a **Debug view** that overlays Frigate's detection regions and bounding boxes.
:::tip
Two-way talk and camera audio have specific codec and port requirements. See [Audio Support](/configuration/live#audio-support) and [WebRTC](/configuration/live#webrtc-extra-configuration) for setup details.
:::
## Camera controls
Admins get a row of toggles in the single-camera view (a settings drawer on mobile) to turn camera features on and off in real time:
- **Camera** on/off,
- **Object detection**,
- **Recording** (only available when recording is enabled in the camera's config),
- **Snapshots**,
- **Audio detection**,
- **Live audio transcription** (when audio detection is enabled), and
- **Autotracking** (for [autotracking-capable PTZ cameras](/configuration/autotracking)).
These toggles change runtime behavior immediately. Whether a change persists across a restart depends on the feature — see the relevant configuration page.
## On-demand recording and snapshots
The single-camera view can capture footage on demand:
- **Start on-demand recording** begins a manual recording based on the camera's recording retention settings (the button pulses while active). If recording is disabled for the camera, only a snapshot is saved. Use **End on-demand recording** to stop.
- **Download instant snapshot** saves a still image of the current frame.
See [Recording](/configuration/record) and [Snapshots](/configuration/snapshots) for how retention is configured, and [Exports](/usage/exports) for keeping a clip permanently.
## PTZ controls
For ONVIF cameras that support it, a control panel provides pan/tilt arrows, **zoom**, **focus**, and saved **presets**. You can also enable a **click-to-move / drag-to-zoom** overlay: click a point in the frame to center the camera there, or drag a box to pan and zoom to that area (dragging top-left to bottom-right zooms in, the reverse zooms out).
For continuous, automatic tracking of a moving object, see [Autotracking](/configuration/autotracking).
+140
View File
@@ -0,0 +1,140 @@
---
id: review
title: Review
---
import NavPath from "@site/src/components/NavPath";
**Review** is where you triage what happened on your cameras. It groups activity into **review items** — segments of time on a single camera that bundle together the objects and audio that were active at once — and sorts them into **Alerts**, **Detections**, and **Motion**. From here you can scrub through activity, mark items as reviewed, filter, export, and jump to the full recording in [History](/usage/history).
This page describes how to _use_ the Review view. For how alerts and detections are _configured_ (labels, zones, required zones, retention), see the [Review configuration](/configuration/review) docs.
:::info
Review items are only created for a camera when **object tracking and recording are enabled** for that camera. See [Recording](/configuration/record).
:::
## Alerts, Detections, and Motion
Not every segment of video captured by Frigate is of the same level of interest. The people who enter your property may be a higher priority than those just walking by on the sidewalk. For this reason, Frigate sorts **review items** by importance into **alerts** and **detections**, with a separate **Motion** category for significant motion.
The toggle at the top of the page switches between these three severities. One is always selected.
| Tab | Indicator color | What it shows |
| -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Alerts** | dark red | The activity you most want to see. By default, all `person` and `car` tracked objects are alerts. |
| **Detections** | orange | Everything else Frigate tracked that wasn't promoted to an alert. |
| **Motion** | yellow | Periods of significant motion, with the ability to filter to periods which did **not** produce a tracked object. |
This same color coding is used for the ring around a selected item and the dots on the calendar. How an object is categorized as an alert vs. a detection — and how required zones refine that — is covered in [Alerts and Detections](/configuration/review#alerts-and-detections).
The **Alerts** and **Detections** tabs show a count next to their label. With **Show Reviewed** turned off (the default), this is the number of items still left to review; with it on, the count reflects every item in the selected time range.
## Marking items as reviewed
Review items are shown as a grid of thumbnail cards next to a vertical activity timeline. Hovering a card (desktop) or swiping to the right (mobile) plays a short preview inline.
- **Clicking** a card opens its recording in [History](/usage/history) and marks the item as reviewed.
- The object chip on each card is **gray** when the item is unreviewed and turns **green** once it has been reviewed.
- The **Mark these items as reviewed** button marks everything currently shown as reviewed at once.
Reviewed state is tracked per user, so marking an item reviewed does not hide it for other users. Marking an item reviewed does not delete anything — the footage and the review item itself remain until they expire via retention.
## Selecting and acting on multiple items
To act on several items at once, start a selection by **Ctrl/Cmd-clicking** a card (desktop) or **long-pressing** one (mobile). Selected cards gain a colored ring matching their severity. Keyboard shortcuts speed this up: `Ctrl+A` selects all, `R` marks the selection reviewed, and `Esc` clears it.
With items selected, an action bar appears with options to:
- **Export** the selected items (a single item exports directly; multiple items open the batch [export](/usage/exports) dialog),
- **Mark as reviewed** or **Mark as unreviewed**, and
- **Delete** them (admins only).
## Filtering and the calendar
Use the filter controls in the header to narrow what's shown. The available filters depend on the tab: Alerts and Detections can be filtered by **cameras**, **date**, **labels**, **zones**, and whether items are already reviewed; the Motion tab can be filtered by **cameras**, **date**, and **motion only**.
The **calendar** filter lets you jump to a specific day (it shows **Last 24 Hours** until you pick one). On each day:
- An **underline** under the day number means **recordings exist** for that day. Days without recordings are dimmed.
- A **colored dot** under the day number means there is **unreviewed activity** that day — a **red dot** for unreviewed alerts, or an **orange dot** for unreviewed detections when there are no unreviewed alerts. Motion is not represented by a dot.
Future dates are disabled, and the week start and time zone follow your configuration.
## Reviewing Motion
The Review page also can show periods of motion that didn't produce a tracked object, and provides a way to search past recordings for motion in a specific region. These tools complement the alerts and detections workflow above — see [Tuning Motion Detection](/configuration/motion_detection) for how the underlying motion detector is configured.
The **Motion** tab itself shows a multi-camera grid scrubbed to a shared point in time, with a draggable timeline and a playback-speed selector. A camera tile gains a colored ring when a review item or significant motion overlaps the current time, and clicking a tile opens that camera's recording at that moment. Each camera's options menu (the kebab in the corner of its tile) is where you open **Motion Previews** and **Motion Search**, described below.
### Motion Previews
The Motion Previews pane shows preview clips for periods of significant motion that did not produce a tracked object. It is useful for spotting things that motion detection picked up but object detection did not, which can help validate tuning or catch missed objects.
On the <NavPath path="Review > Motion" /> page, click the kebab menu on a camera and choose **Motion Previews**. Each card represents a continuous range of motion-only activity and plays back the recorded preview for that range. A heatmap overlay dims areas of the frame with no motion so the moving regions stand out.
The pane provides a few controls:
- **Speed** — speeds up or slows down all of the preview clips at once.
- **Dim** — controls how strongly non-motion areas are darkened by the heatmap overlay. Higher values increase motion area visibility.
- **Filter** — opens a 16×16 grid overlaid on a snapshot of the camera. Select one or more cells to only show clips with motion in those regions. This is helpful for filtering out motion in areas like a busy street while keeping motion in your driveway.
Clicking a preview clip seeks the recording player to that timestamp so you can review the full footage.
### Motion Search
Motion Search lets you scan recorded footage for changes inside a region of interest you draw on the camera. Unlike Motion Previews, which surfaces what Frigate's motion detector flagged in real time, Motion Search re-analyzes the saved recordings, so it can find changes that were missed (for example, an object that appeared while motion detection was paused by `lightning_threshold`, or in a region that is normally motion-masked).
To start a search, open the Actions menu in [History](/usage/history) or click the kebab menu on a camera in the <NavPath path="Review > Motion" /> page and choose **Motion Search**. In the dialog:
1. Pick the camera and time range to scan. In the date pickers, days that have recordings available are underlined.
2. Draw a polygon on the camera frame to define the region of interest.
3. Adjust the search parameters if needed:
| Field | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Sensitivity Threshold** | Per-pixel luminance change required to count as motion inside the ROI. Behaves like Frigate's motion detection `threshold` setting. |
| **Minimum Change Area** | Minimum size of a single moving region, as a percentage of the ROI, for a frame to count as significant. Raise it to ignore small movements (leaves, distant motion); lower it when your subject covers only a small slice of the ROI. Every result shows the percentage it scored, so you can use those values to tune this. |
| **Maximum Results** | Maximum number of matching timestamps to return. The search stops once it reaches this many results, so a lower value finishes sooner while a higher value scans further into the range. |
| **Parallel mode** | Decode multiple recording ranges at the same time. Speeds up large time ranges at the cost of higher decoding and CPU usage. |
Motion Search samples each recording's keyframes automatically, so there is no frame-rate or sampling setting to tune.
Once running, Frigate scans the recording segments that overlap the time range and reports timestamps where changes were detected inside the polygon, along with the percentage of the ROI that changed. Clicking a result seeks the player to that moment so you can review what happened.
The results panel shows the time range being scanned, a live progress bar with the timestamp currently being analyzed, and the running result count. A collapsible **Search Metrics** section reports how many segments were scanned and processed, how many were skipped because no motion was recorded in the ROI (using the stored motion heatmap), how many frames were decoded, and the total search time. Skipping segments with no recorded motion in the selected ROI is what makes searching long time ranges practical.
#### Common use cases
Frigate's main use case is to record and surface tracked objects, so Motion Search is most useful for the cases where object detection produced nothing — there is no object to find in Explore, but you suspect something happened.
- **Locating an unattributed change.** You know something appeared, disappeared, or moved in a window of footage — a package now gone, a gate left open — but no detection points to it. A search returns the candidate timestamps instead of scrubbing the timeline by hand.
- **An object that was never detected.** Something Frigate doesn't have a model label for, an object too small or distant to be detected, or movement in a region where detection isn't running. The activity left no tracked object but did change the pixels, so a search can still find it.
- **Activity while detection was effectively paused.** Changes that occurred while object detection was disabled, motion was suppressed by `skip_motion_threshold`, or inside an area covered by a motion mask, won't appear as review items or tracked objects but can be recovered by searching the recordings directly.
#### Examples
These show how to choose the ROI and **Minimum Change Area** for two common goals. Minimum Change Area is the size of a single moving region as a percentage of the ROI you draw, so the right value depends on how much of the ROI your subject — and its movement between samples — covers.
Because samples are a second or more apart, a moving subject usually appears in two places at once in the comparison, so even ordinary motion often scores tens of percent and a low threshold lets in almost everything. The most reliable approach is to **run a search, look at the percentage each result scored, and set Minimum Change Area just below the values for the events you care about.** The default is 20%; the suggestions below are starting points.
- **When did this item first appear (or disappear)?** A package was dropped off, a car parked, or a trash can was moved, and you want the exact moment. Draw a **tight ROI** around the spot the item occupies and **raise Minimum Change Area** (start around 4060%). Because the item fills most of a tight ROI, its arrival or removal is a large change, while smaller nearby motion (shadows, a passing pedestrian) stays below the threshold. The **earliest result** is when it appeared; if you only care about that moment, a low Maximum Results finishes faster. If you get no hits, the ROI is probably looser than the item — lower the threshold or tighten the ROI.
- **What's been getting into the garden?** Something has been trampling a flower bed overnight and no object was ever tracked. Draw a **looser ROI** covering the whole bed and use a **lower Minimum Change Area than the case above** — start near the 20% default and lower it (toward 510%) only if a small or distant subject is missed, since it covers just a slice of a large region. Expect more results to scan through — step through the timestamps and jump to each to see what triggered it. If wind-blown plants add noise, raise Minimum Change Area or the Sensitivity Threshold.
#### Expected performance
Motion Search analyzes the saved recordings on demand rather than reading a pre-built index, so a search over a long range takes longer than browsing Motion Previews. Cost scales mainly with how much footage has to be examined: segments with no recorded motion in your ROI are skipped using the stored motion heatmap (shown as "segments skipped" in the status panel), so a quiet range finishes quickly while a busy one takes longer.
To increase the speed of searches:
- Draw a tight ROI. Because **Minimum Change Area** is measured as a percentage of the region you draw, a tight ROI around where you expect the change makes the object fill a larger share of the area, so it clears the threshold more easily. A loose ROI makes the same object a small fraction of the region, so it can fall below the threshold and be missed — forcing you to lower Minimum Change Area, which lets in more noise.
- Narrow the time range to the window you care about, so there is less footage to examine.
- Lower **Maximum Results** when you only need the first few hits. Because the search stops once it reaches that many results, a smaller value lets a busy range finish early instead of scanning the whole window.
- Use Parallel mode to shorten wall-clock time on multi-core systems, at the cost of higher decoding and CPU usage while it runs.
## AI review summaries
When [Generative AI review](/configuration/genai/genai_review) is configured, Frigate can generate a title, description, and threat classification for review items and surface them automatically in Review and History. Clicking the summary chip opens an **AI Analysis** dialog with the generated detail and any flagged concerns.
In Review, an additional icon appears on unreviewed items that the AI classified as **suspicious** (Level 1) or **critical** (Level 2), so the activity that most warrants attention stands out before you open it. The icon goes away once the item has been reviewed.
+1
View File
@@ -29,6 +29,7 @@
"docusaurus-plugin-openapi-docs": "^4.5.1",
"docusaurus-theme-openapi-docs": "^4.5.1",
"js-yaml": "^4.1.1",
"marked": "^16.4.2",
"prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2",
"react": "^18.3.1",
+115 -79
View File
@@ -17,91 +17,126 @@ const sidebars: SidebarsConfig = {
],
Guides: [
"guides/getting_started",
"guides/configuring_go2rtc",
"guides/ha_notifications",
"guides/ha_network_storage",
"guides/reverse_proxy",
],
Configuration: {
"Configuration Files": [
"configuration/index",
"configuration/reference",
{
type: "link",
label: "Go2RTC Configuration Reference",
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration",
} as PropSidebarItemLink,
],
Detectors: [
"configuration/object_detectors",
"configuration/audio_detectors",
],
Enrichments: [
"configuration/semantic_search",
"configuration/face_recognition",
"configuration/license_plate_recognition",
"configuration/bird_classification",
{
type: "category",
label: "Custom Classification",
link: {
type: "generated-index",
title: "Custom Classification",
description: "Configuration for custom classification models",
Usage: [
"usage/live",
"usage/review",
"usage/history",
"usage/explore",
"usage/exports",
],
Configuration: [
"configuration/config",
{
type: "category",
label: "Detectors",
items: [
"configuration/object_detectors",
"configuration/audio_detectors",
],
},
{
type: "category",
label: "Enrichments",
items: [
"configuration/semantic_search",
"configuration/face_recognition",
"configuration/license_plate_recognition",
"configuration/bird_classification",
{
type: "category",
label: "Custom Classification",
link: {
type: "generated-index",
title: "Custom Classification",
description: "Configuration for custom classification models",
},
items: [
"configuration/custom_classification/state_classification",
"configuration/custom_classification/object_classification",
],
},
items: [
"configuration/custom_classification/state_classification",
"configuration/custom_classification/object_classification",
],
},
{
type: "category",
label: "Generative AI",
link: {
type: "generated-index",
title: "Generative AI",
description: "Generative AI Features",
{
type: "category",
label: "Generative AI",
link: {
type: "generated-index",
title: "Generative AI",
description: "Generative AI Features",
},
items: [
"configuration/genai/genai_config",
"configuration/genai/genai_review",
"configuration/genai/genai_objects",
],
},
items: [
"configuration/genai/genai_config",
"configuration/genai/genai_review",
"configuration/genai/genai_objects",
],
},
],
Cameras: [
"configuration/cameras",
"configuration/review",
"configuration/record",
"configuration/snapshots",
"configuration/motion_detection",
"configuration/birdseye",
"configuration/live",
"configuration/restream",
"configuration/autotracking",
"configuration/camera_specific",
],
Objects: [
"configuration/object_filters",
"configuration/masks",
"configuration/zones",
"configuration/objects",
"configuration/stationary_objects",
],
"Hardware Acceleration": [
"configuration/hardware_acceleration_video",
"configuration/hardware_acceleration_enrichments",
],
"Extra Configuration": [
"configuration/authentication",
"configuration/notifications",
"configuration/profiles",
"configuration/ffmpeg_presets",
"configuration/pwa",
"configuration/tls",
"configuration/advanced",
],
},
],
},
{
type: "category",
label: "Cameras",
items: [
"configuration/cameras",
"configuration/review",
"configuration/record",
"configuration/snapshots",
"configuration/motion_detection",
"configuration/birdseye",
"configuration/live",
"configuration/restream",
"configuration/autotracking",
"configuration/camera_specific",
],
},
{
type: "category",
label: "Objects",
items: [
"configuration/object_filters",
"configuration/masks",
"configuration/zones",
"configuration/objects",
"configuration/stationary_objects",
],
},
{
type: "category",
label: "Hardware Acceleration",
items: [
"configuration/hardware_acceleration_video",
"configuration/hardware_acceleration_enrichments",
],
},
{
type: "category",
label: "Extra Configuration",
items: [
"configuration/authentication",
"configuration/notifications",
"configuration/profiles",
"configuration/go2rtc",
"configuration/ffmpeg_presets",
"configuration/pwa",
"configuration/tls",
],
},
{
type: "category",
label: "Advanced Configuration",
items: [
"configuration/advanced/system",
"configuration/advanced/reference",
{
type: "link",
label: "Go2RTC Configuration Reference",
href: "https://github.com/AlexxIT/go2rtc/tree/v1.9.13#configuration",
} as PropSidebarItemLink,
],
},
],
Integrations: [
"integrations/plus",
"integrations/home-assistant",
@@ -130,6 +165,7 @@ const sidebars: SidebarsConfig = {
],
Troubleshooting: [
"troubleshooting/faqs",
"troubleshooting/go2rtc",
"troubleshooting/recordings",
"troubleshooting/dummy-camera",
{
@@ -0,0 +1,171 @@
import React, { useState } from "react";
import CodeBlock from "@theme/CodeBlock";
import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import { marked } from "marked";
import styles from "./styles.module.css";
marked.setOptions({ gfm: true });
/**
* @typedef {Object} Model
* @property {string} key
* @property {string} label
* @property {boolean} recommended
* @property {string} download Markdown for the "download the model" step.
* @property {string} ui Markdown for the Frigate UI configuration step.
* @property {string} yaml Raw YAML for the configuration step.
*/
// Render a markdown string to React nodes. Fenced code blocks become Docusaurus
// CodeBlock components (so they get syntax highlighting and a copy button);
// everything else is marked-parsed to HTML.
function renderBlocks(md, keyPrefix) {
if (!md.trim()) return [];
const tokens = marked.lexer(md);
const nodes = [];
let buffer = [];
let idx = 0;
const flush = () => {
if (buffer.length) {
buffer.links = tokens.links;
nodes.push(
<div
key={`${keyPrefix}-h${idx++}`}
dangerouslySetInnerHTML={{ __html: marked.parser(buffer) }}
/>,
);
buffer = [];
}
};
tokens.forEach((token) => {
if (token.type === "code") {
flush();
const language = (token.lang || "text").split(/\s+/)[0];
nodes.push(
<CodeBlock key={`${keyPrefix}-c${idx++}`} language={language}>
{token.text}
</CodeBlock>,
);
} else {
buffer.push(token);
}
});
flush();
return nodes;
}
// marked does not understand Docusaurus admonitions (:::warning ... :::), so
// render those blocks ourselves and render everything around them normally.
function renderMarkdown(md) {
if (!md) return null;
const admonition = /:::(\w+)[ \t]*([^\n]*)\n([\s\S]*?)\n:::/g;
const nodes = [];
let lastIndex = 0;
let match;
let k = 0;
while ((match = admonition.exec(md)) !== null) {
nodes.push(...renderBlocks(md.slice(lastIndex, match.index), `seg${k}`));
const [, type, title, body] = match;
const heading = (title || type).trim();
nodes.push(
<div
key={`adm${k}`}
className={`${styles.admonition} ${styles[`admonition_${type}`] || ""}`}
>
<div className={styles.admonitionTitle}>{heading}</div>
{renderBlocks(body, `adm${k}`)}
</div>,
);
lastIndex = admonition.lastIndex;
k++;
}
nodes.push(...renderBlocks(md.slice(lastIndex), `seg${k}`));
return nodes;
}
function Markdown({ children }) {
return <div className={styles.markdown}>{renderMarkdown(children)}</div>;
}
function RecommendedBadge() {
return <span className={styles.recommendedBadge}>Recommended</span>;
}
/**
* @param {{ models: Model[] }} props
*/
export default function ModelConfigDropdown({ models }) {
const [selectedModelIndex, setSelectedModelIndex] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const selectedModel = models[selectedModelIndex];
const hasChoices = models.length > 1;
const handleModelSelect = (index) => {
setSelectedModelIndex(index);
setIsOpen(false);
};
return (
<div className={styles.wrapper}>
<div className={styles.panel}>
<div className={styles.step}>
<h4 className={styles.stepTitle}>Step 1 Choose a model</h4>
<div
className={`${styles.dropdown} ${isOpen ? styles.open : ""} ${
hasChoices ? "" : styles.static
}`}
onClick={hasChoices ? () => setIsOpen(!isOpen) : undefined}
>
<div className={styles.dropdownContent}>
<span className={styles.modelName}>
{selectedModel.label}
{selectedModel.recommended && <RecommendedBadge />}
</span>
{hasChoices && (
<span className={styles.arrow}>{isOpen ? "▲" : "▼"}</span>
)}
</div>
</div>
{isOpen && hasChoices && (
<div className={styles.menu}>
{models.map((model, index) => (
<div
key={model.key}
className={`${styles.menuItem} ${
index === selectedModelIndex ? styles.menuItemActive : ""
}`}
onClick={() => handleModelSelect(index)}
>
{model.label}
{model.recommended && <RecommendedBadge />}
</div>
))}
</div>
)}
</div>
<div className={styles.step}>
<h4 className={styles.stepTitle}>Step 2 Download the model</h4>
<Markdown>{selectedModel.download}</Markdown>
</div>
<div className={styles.step}>
<h4 className={styles.stepTitle}>Step 3 Configure the detector</h4>
<ConfigTabs>
<TabItem value="ui">
<Markdown>{selectedModel.ui}</Markdown>
</TabItem>
<TabItem value="yaml">
<CodeBlock language="yaml">{selectedModel.yaml}</CodeBlock>
</TabItem>
</ConfigTabs>
</div>
</div>
</div>
);
}
@@ -0,0 +1,275 @@
/* ===================================================================
ModelConfigDropdown styles
=================================================================== */
.wrapper {
margin: 1.5rem 0;
}
/* --- Dropdown button --- */
.dropdown {
display: inline-block;
width: 360px;
max-width: 100%;
text-align: left;
border: 1px solid var(--ifm-color-emphasis-400);
border-radius: 8px;
background: var(--ifm-background-color);
cursor: pointer;
transition:
border-color 0.2s,
box-shadow 0.2s;
}
[data-theme="light"] .dropdown {
border: 1px solid #d0d7de;
background: #fff;
}
[data-theme="dark"] .dropdown {
border: 1px solid var(--ifm-color-emphasis-300);
background: #21262d;
}
.dropdown:hover {
border-color: var(--ifm-color-primary);
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
}
[data-theme="dark"] .dropdown:hover {
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
}
.dropdown.open {
border-color: var(--ifm-color-primary);
box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest);
}
[data-theme="dark"] .dropdown.open {
border-color: var(--ifm-color-primary);
}
/* Single-model detectors render the label without a clickable menu. */
.dropdown.static {
cursor: default;
}
.dropdown.static:hover {
border-color: var(--ifm-color-emphasis-400);
box-shadow: none;
}
[data-theme="light"] .dropdown.static:hover {
border-color: #d0d7de;
}
[data-theme="dark"] .dropdown.static:hover {
border-color: var(--ifm-color-emphasis-300);
}
.dropdownContent {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.8rem 1rem;
}
/* --- Model menu --- */
.menu {
margin-top: 0.25rem;
width: 360px;
max-width: 100%;
border: 1px solid var(--ifm-color-emphasis-400);
border-radius: 8px;
overflow: hidden;
background: var(--ifm-background-color);
}
[data-theme="light"] .menu {
border: 1px solid #d0d7de;
background: #fff;
}
[data-theme="dark"] .menu {
border: 1px solid var(--ifm-color-emphasis-300);
background: #21262d;
}
.menuItem {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
cursor: pointer;
font-size: 0.95rem;
color: var(--ifm-font-color-base);
transition: background 0.15s;
}
.menuItem:not(:last-child) {
border-bottom: 1px solid var(--ifm-color-emphasis-200);
}
.menuItem:hover {
background: var(--ifm-color-emphasis-100);
}
.menuItemActive {
font-weight: var(--ifm-font-weight-semibold);
background: var(--ifm-color-primary-lightest);
}
[data-theme="dark"] .menuItem:hover {
background: #2b3139;
}
[data-theme="dark"] .menuItemActive {
background: #2b3139;
}
.modelName {
font-weight: var(--ifm-font-weight-semibold);
color: var(--ifm-font-color-base);
font-size: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.recommendedBadge {
display: inline-block;
background: var(--ifm-color-success);
color: #fff;
font-size: 0.7rem;
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.arrow {
font-size: 0.7rem;
color: var(--ifm-font-color-secondary);
transition: transform 0.2s;
}
.dropdown.open .arrow {
transform: rotate(180deg);
}
/* --- Panel --- */
.panel {
margin-top: 0.5rem;
border: 1px solid var(--ifm-color-emphasis-400);
border-radius: 8px;
overflow: hidden;
background: var(--ifm-background-color);
}
[data-theme="light"] .panel {
border: 1px solid #d0d7de;
background: #fff;
}
[data-theme="dark"] .panel {
border: 1px solid var(--ifm-color-emphasis-300);
background: #21262d;
}
/* --- Steps --- */
.step {
padding: 1rem;
}
.step:not(:last-child) {
border-bottom: 1px solid var(--ifm-color-emphasis-200);
}
.stepTitle {
margin: 0 0 0.75rem 0;
font-size: 1rem;
font-weight: 600;
color: var(--ifm-font-color-base);
}
/* Rendered markdown (download + Frigate UI instructions). */
.markdown {
font-size: 0.9rem;
line-height: 1.6;
}
.markdown > :last-child {
margin-bottom: 0;
}
.markdown a {
color: var(--ifm-color-primary);
text-decoration: underline;
text-underline-offset: 2px;
}
.markdown table {
display: table;
width: 100%;
margin: 0.75rem 0;
font-size: 0.85rem;
}
/* Docusaurus-style admonitions rendered from markdown. */
.admonition {
margin: 0.75rem 0;
padding: 0.75rem 1rem;
border-left: 4px solid var(--ifm-color-info);
border-radius: 4px;
background: var(--ifm-color-info-contrast-background);
font-size: 0.85rem;
}
.admonition > :last-child {
margin-bottom: 0;
}
.admonitionTitle {
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.75rem;
margin-bottom: 0.4rem;
color: var(--ifm-color-info);
}
.admonition_warning {
border-left-color: var(--ifm-color-warning);
background: var(--ifm-color-warning-contrast-background);
}
.admonition_warning .admonitionTitle {
color: var(--ifm-color-warning-dark);
}
.admonition_danger {
border-left-color: var(--ifm-color-danger);
background: var(--ifm-color-danger-contrast-background);
}
.admonition_danger .admonitionTitle {
color: var(--ifm-color-danger-dark);
}
.admonition_tip {
border-left-color: var(--ifm-color-success);
background: var(--ifm-color-success-contrast-background);
}
.admonition_tip .admonitionTitle {
color: var(--ifm-color-success-dark);
}
+2685 -1466
View File
File diff suppressed because it is too large Load Diff
+74 -1
View File
@@ -12,6 +12,7 @@ import time
from datetime import datetime
from pathlib import Path
from typing import List, Optional
from urllib.parse import parse_qs, urlparse
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
@@ -26,7 +27,11 @@ from frigate.api.defs.request.app_body import (
AppPutRoleBody,
)
from frigate.api.defs.tags import Tags
from frigate.api.media_auth import check_camera_access, deny_response_for_media_uri
from frigate.api.media_auth import (
check_camera_access,
deny_response_for_media_uri,
is_role_restricted,
)
from frigate.config import AuthConfig, NetworkingConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
from frigate.models import User
@@ -658,6 +663,10 @@ def auth(request: Request):
if deny_status is not None:
return Response("", status_code=deny_status)
deny_status = deny_response_for_go2rtc_stream(original_url, role, request)
if deny_status is not None:
return Response("", status_code=deny_status)
return success_response
# now apply authentication
@@ -757,6 +766,10 @@ def auth(request: Request):
if deny_status is not None:
return Response("", status_code=deny_status)
deny_status = deny_response_for_go2rtc_stream(original_url, role, request)
if deny_status is not None:
return Response("", status_code=deny_status)
return success_response
except Exception as e:
logger.error(f"Error parsing jwt: {e}")
@@ -1112,6 +1125,66 @@ def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
return owner_cameras
# nginx proxies these paths straight to go2rtc with authentication-only checks
# (see auth_request.conf). Each names the desired stream via the `src` query
# param, so the camera-level check must happen here in the `/auth` subrequest —
# `require_go2rtc_stream_access` only guards the REST `/go2rtc/streams/{name}`
# endpoint, not these proxied live-stream paths.
GO2RTC_STREAM_PROXY_PATHS = frozenset(
{
"/live/mse/api/ws",
"/live/webrtc/api/ws",
"/api/go2rtc/webrtc",
}
)
def deny_response_for_go2rtc_stream(
original_url: Optional[str], role: Optional[str], request: Request
) -> Optional[int]:
"""Block role-restricted users from go2rtc live streams they cannot access.
Returns 403 when any `src` stream named in `original_url` resolves to a
camera outside the role's allow-list (or when no `src` is provided on a
stream-proxy path), otherwise None. Mirrors the resolution logic in
`require_go2rtc_stream_access` so substream names map to their owning
camera correctly.
"""
if not original_url:
return None
parsed = urlparse(original_url)
if parsed.path not in GO2RTC_STREAM_PROXY_PATHS:
return None
frigate_config = request.app.frigate_config
# admin and full-access roles (no allow-list) bypass the camera check
if not role or not is_role_restricted(role, frigate_config):
return None
sources = parse_qs(parsed.query).get("src", [])
if not sources:
# a stream-proxy request naming no stream has nothing legitimate to
# show a restricted user
return 403
allowed_cameras = set(
User.get_allowed_cameras(
role,
frigate_config.auth.roles,
set(frigate_config.cameras.keys()),
)
)
# deny if any requested source resolves outside the allow-list
for src in sources:
if not (_get_stream_owner_cameras(request, src) & allowed_cameras):
return 403
return None
async def require_go2rtc_stream_access(
stream_name: Optional[str] = None,
request: Request = None,
+137 -44
View File
@@ -34,11 +34,15 @@ from frigate.config.camera.updater import (
)
from frigate.config.env import substitute_frigate_vars
from frigate.models import User
from frigate.util.builtin import clean_camera_user_pass
from frigate.util.builtin import clean_camera_user_pass, get_record_segment_time
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
from frigate.util.config import find_config_file
from frigate.util.image import run_ffmpeg_snapshot
from frigate.util.services import ffprobe_stream, is_restricted_go2rtc_source
from frigate.util.services import (
analyze_record_keyframes,
ffprobe_stream,
is_restricted_go2rtc_source,
)
logger = logging.getLogger(__name__)
@@ -143,6 +147,19 @@ def go2rtc_camera_stream(request: Request, stream_name: str):
)
def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
"""Add or update a go2rtc stream configuration."""
if src and is_restricted_go2rtc_source(src):
logger.warning(
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
stream_name,
)
return JSONResponse(
content={
"success": False,
"message": "Restricted stream source type",
},
status_code=400,
)
try:
params = {"name": stream_name}
if src:
@@ -362,6 +379,48 @@ def ffprobe(request: Request, paths: str = "", detailed: bool = False):
return JSONResponse(content=output)
@router.get("/keyframe_analysis", dependencies=[Depends(require_role(["admin"]))])
async def keyframe_analysis(request: Request, camera: str = ""):
"""Probe a camera's record stream and classify its keyframe spacing.
Detects smart/+ codecs and long/variable GOPs that degrade recording.
"""
config: FrigateConfig = request.app.frigate_config
if camera not in config.cameras:
return JSONResponse(
content={"success": False, "message": f"{camera} is not a valid camera."},
status_code=404,
)
camera_config = config.cameras[camera]
if not camera_config.enabled:
return JSONResponse(
content={"success": False, "message": f"{camera} is not enabled."},
status_code=404,
)
# keyframe spacing only matters when this camera is recording
if not camera_config.record.enabled:
return JSONResponse(content={"severity": "record_disabled"})
# recording guarantees an input carries the record role; its index matches
# the "Stream N" numbering the ffprobe endpoint surfaces (same input order)
record_index, record_input = next(
(idx, i)
for idx, i in enumerate(camera_config.ffmpeg.inputs)
if "record" in i.roles
)
segment_time = get_record_segment_time(camera_config)
result = await analyze_record_keyframes(
config.ffmpeg, record_input.path, segment_time
)
result["stream_index"] = record_index
return JSONResponse(content=result)
@router.get("/ffprobe/snapshot", dependencies=[Depends(require_role(["admin"]))])
def ffprobe_snapshot(request: Request, url: str = "", timeout: int = 10):
"""Get a snapshot from a stream URL using ffmpeg."""
@@ -529,6 +588,68 @@ def _extract_fps(r_frame_rate: str) -> float | None:
return None
def _build_digest_transport(username: str, password: str) -> AsyncTransport:
"""Build a zeep transport backed by an httpx client using HTTP digest auth."""
auth = httpx.DigestAuth(username, password)
client = httpx.AsyncClient(auth=auth, timeout=10.0)
return AsyncTransport(client=client)
async def _connect_onvif_camera(
host: str,
port: int,
username: str,
password: str,
wsdl_base: str | None,
auth_type: str,
) -> ONVIFCamera:
"""Connect to an ONVIF device, trying both WS-Security password encodings.
Cameras disagree on whether the WS-Security UsernameToken should carry a
hashed PasswordDigest or a plaintext PasswordText. The wizard can't know
which a given camera expects, so we try PasswordDigest first (the common
case) and fall back to PasswordText when the device rejects the token. This
is independent of auth_type, which controls HTTP transport-level auth.
"""
first_error: Fault | None = None
# encrypt=True -> PasswordDigest, encrypt=False -> PasswordText
for encrypt in (True, False):
onvif_camera = ONVIFCamera(
host,
port,
username or "",
password or "",
wsdl_dir=wsdl_base,
encrypt=encrypt,
)
try:
await onvif_camera.update_xaddrs()
except Fault as e:
# A SOAP fault here is how a camera signals the wrong password
# encoding, so retry with the other encoding before giving up.
logger.debug(
"ONVIF connect with %s rejected, trying alternate encoding",
"PasswordDigest" if encrypt else "PasswordText",
)
if first_error is None:
first_error = e
continue
if auth_type == "digest" and username and password:
transport = _build_digest_transport(username, password)
for service in ("devicemgmt", "media", "ptz"):
if hasattr(onvif_camera, service):
getattr(onvif_camera, service).zeep_client.transport = transport
logger.debug("Configured digest authentication")
return onvif_camera
# Both encodings failed authentication; surface the original fault.
raise first_error
@router.get(
"/onvif/probe",
dependencies=[Depends(require_role(["admin"]))],
@@ -605,34 +726,10 @@ async def onvif_probe(
except Exception:
wsdl_base = None
onvif_camera = ONVIFCamera(
host, port, username or "", password or "", wsdl_dir=wsdl_base
onvif_camera = await _connect_onvif_camera(
host, port, username, password, wsdl_base, auth_type
)
# Configure digest authentication if requested
if auth_type == "digest" and username and password:
# Create httpx client with digest auth
auth = httpx.DigestAuth(username, password)
client = httpx.AsyncClient(auth=auth, timeout=10.0)
# Replace the transport in the zeep client
transport = AsyncTransport(client=client)
# Update the xaddr before setting transport
await onvif_camera.update_xaddrs()
# Replace transport in all services
if hasattr(onvif_camera, "devicemgmt"):
onvif_camera.devicemgmt.zeep_client.transport = transport
if hasattr(onvif_camera, "media"):
onvif_camera.media.zeep_client.transport = transport
if hasattr(onvif_camera, "ptz"):
onvif_camera.ptz.zeep_client.transport = transport
logger.debug("Configured digest authentication")
else:
await onvif_camera.update_xaddrs()
# Get device information
device_info = {
"manufacturer": "Unknown",
@@ -644,10 +741,9 @@ async def onvif_probe(
# Update transport for device service if digest auth
if auth_type == "digest" and username and password:
auth = httpx.DigestAuth(username, password)
client = httpx.AsyncClient(auth=auth, timeout=10.0)
transport = AsyncTransport(client=client)
device_service.zeep_client.transport = transport
device_service.zeep_client.transport = _build_digest_transport(
username, password
)
device_info_resp = await device_service.GetDeviceInformation()
manufacturer = getattr(device_info_resp, "Manufacturer", None) or (
@@ -685,10 +781,9 @@ async def onvif_probe(
# Update transport for media service if digest auth
if auth_type == "digest" and username and password:
auth = httpx.DigestAuth(username, password)
client = httpx.AsyncClient(auth=auth, timeout=10.0)
transport = AsyncTransport(client=client)
media_service.zeep_client.transport = transport
media_service.zeep_client.transport = _build_digest_transport(
username, password
)
profiles = await media_service.GetProfiles()
profiles_count = len(profiles) if profiles else 0
@@ -720,10 +815,9 @@ async def onvif_probe(
# Update transport for PTZ service if digest auth
if auth_type == "digest" and username and password:
auth = httpx.DigestAuth(username, password)
client = httpx.AsyncClient(auth=auth, timeout=10.0)
transport = AsyncTransport(client=client)
ptz_service.zeep_client.transport = transport
ptz_service.zeep_client.transport = _build_digest_transport(
username, password
)
# Check if PTZ service is available
try:
@@ -876,10 +970,9 @@ async def onvif_probe(
# Update transport for media service if digest auth
if auth_type == "digest" and username and password:
auth = httpx.DigestAuth(username, password)
client = httpx.AsyncClient(auth=auth, timeout=10.0)
transport = AsyncTransport(client=client)
media_service.zeep_client.transport = transport
media_service.zeep_client.transport = _build_digest_transport(
username, password
)
if profiles_count and media_service:
for p in profiles or []:
+102 -87
View File
@@ -7,7 +7,7 @@ import operator
import time
from datetime import datetime
from functools import reduce
from typing import Any, Dict, List, Optional
from typing import Any, Optional
import cv2
from fastapi import APIRouter, Body, Depends, HTTPException, Request
@@ -59,7 +59,7 @@ class ToolExecuteRequest(BaseModel):
"""Request model for tool execution."""
tool_name: str
arguments: Dict[str, Any]
arguments: dict[str, Any]
class VLMMonitorRequest(BaseModel):
@@ -68,8 +68,8 @@ class VLMMonitorRequest(BaseModel):
camera: str
condition: str
max_duration_minutes: int = 60
labels: List[str] = []
zones: List[str] = []
labels: list[str] = []
zones: list[str] = []
@router.get(
@@ -91,10 +91,10 @@ def get_tools(request: Request) -> JSONResponse:
def _resolve_zones(
zones: List[str],
zones: list[str],
config: FrigateConfig,
target_cameras: List[str],
) -> List[str]:
target_cameras: list[str],
) -> list[str]:
"""Map zone names to their canonical config keys, case-insensitively.
LLMs frequently echo a user's casing ("Front Yard") instead of the
@@ -107,7 +107,7 @@ def _resolve_zones(
if not zones:
return zones
lookup: Dict[str, str] = {}
lookup: dict[str, str] = {}
for camera_id in target_cameras:
camera_config = config.cameras.get(camera_id)
if camera_config is None:
@@ -120,8 +120,8 @@ def _resolve_zones(
async def _execute_search_objects(
request: Request,
arguments: Dict[str, Any],
allowed_cameras: List[str],
arguments: dict[str, Any],
allowed_cameras: list[str],
) -> JSONResponse:
"""
Execute the search_objects tool.
@@ -213,8 +213,8 @@ async def _execute_search_objects(
async def _execute_search_objects_semantic(
request: Request,
arguments: Dict[str, Any],
allowed_cameras: List[str],
arguments: dict[str, Any],
allowed_cameras: list[str],
semantic_query: str,
) -> JSONResponse:
"""Search objects via fused thumbnail + description embeddings.
@@ -263,8 +263,8 @@ async def _execute_search_objects_semantic(
limit = int(arguments.get("limit", 25))
limit = max(1, min(limit, 100))
visual_distances: Dict[str, float] = {}
description_distances: Dict[str, float] = {}
visual_distances: dict[str, float] = {}
description_distances: dict[str, float] = {}
try:
rows = context.search_thumbnail(semantic_query)
visual_distances = {row[0]: row[1] for row in rows}
@@ -305,7 +305,7 @@ async def _execute_search_objects_semantic(
eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))}
scored: List[tuple[str, float]] = []
scored: list[tuple[str, float]] = []
for eid in eligible:
v_score = (
distance_to_score(visual_distances[eid], context.thumb_stats)
@@ -331,9 +331,9 @@ async def _execute_search_objects_semantic(
async def _execute_find_similar_objects(
request: Request,
arguments: Dict[str, Any],
allowed_cameras: List[str],
) -> Dict[str, Any]:
arguments: dict[str, Any],
allowed_cameras: list[str],
) -> dict[str, Any]:
"""Execute the find_similar_objects tool.
Returns a plain dict (not JSONResponse) so the chat loop can embed it
@@ -403,8 +403,8 @@ async def _execute_find_similar_objects(
# version (see frigate/embeddings/__init__.py). Mirror the pattern used by
# frigate/api/event.py events_search: fetch top-k globally, then intersect
# with the structured filters via Peewee.
visual_distances: Dict[str, float] = {}
description_distances: Dict[str, float] = {}
visual_distances: dict[str, float] = {}
description_distances: dict[str, float] = {}
try:
if similarity_mode in ("visual", "fused"):
@@ -462,7 +462,7 @@ async def _execute_find_similar_objects(
eligible = {e.id: e for e in Event.select().where(reduce(operator.and_, clauses))}
# 6. Fuse and rank.
scored: List[tuple[str, float]] = []
scored: list[tuple[str, float]] = []
for eid in eligible:
v_score = (
distance_to_score(visual_distances[eid], context.thumb_stats)
@@ -503,7 +503,7 @@ async def _execute_find_similar_objects(
async def execute_tool(
request: Request,
body: ToolExecuteRequest = Body(...),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
) -> JSONResponse:
"""
Execute a tool function call.
@@ -545,8 +545,8 @@ async def execute_tool(
async def _execute_get_live_context(
request: Request,
camera: str,
allowed_cameras: List[str],
) -> Dict[str, Any]:
allowed_cameras: list[str],
) -> dict[str, Any]:
# Reject wildcards explicitly so models retry with a real camera name
# instead of silently fanning out across every camera.
if camera in ("*", "all"):
@@ -593,7 +593,7 @@ async def _execute_get_live_context(
"stationary": obj_dict.get("stationary", False),
}
result: Dict[str, Any] = {
result: dict[str, Any] = {
"camera": camera,
"timestamp": frame_time,
"detections": list(tracked_objects_dict.values()),
@@ -620,7 +620,7 @@ async def _execute_get_live_context(
async def _get_live_frame_image_url(
request: Request,
camera: str,
allowed_cameras: List[str],
allowed_cameras: list[str],
) -> Optional[str]:
"""
Fetch the current live frame for a camera as a base64 data URL.
@@ -659,8 +659,8 @@ async def _get_live_frame_image_url(
async def _execute_set_camera_state(
request: Request,
arguments: Dict[str, Any],
) -> Dict[str, Any]:
arguments: dict[str, Any],
) -> dict[str, Any]:
role = request.headers.get("remote-role", "")
if "admin" not in [r.strip() for r in role.split(",")]:
return {"error": "Admin privileges required to change camera settings."}
@@ -699,10 +699,10 @@ async def _execute_set_camera_state(
async def _execute_tool_internal(
tool_name: str,
arguments: Dict[str, Any],
arguments: dict[str, Any],
request: Request,
allowed_cameras: List[str],
) -> Dict[str, Any]:
allowed_cameras: list[str],
) -> dict[str, Any]:
"""
Internal helper to execute a tool and return the result as a dict.
@@ -763,8 +763,8 @@ async def _execute_tool_internal(
async def _execute_start_camera_watch(
request: Request,
arguments: Dict[str, Any],
) -> Dict[str, Any]:
arguments: dict[str, Any],
) -> dict[str, Any]:
camera = arguments.get("camera", "").strip()
condition = arguments.get("condition", "").strip()
max_duration_minutes = int(arguments.get("max_duration_minutes", 60))
@@ -814,14 +814,14 @@ async def _execute_start_camera_watch(
}
def _execute_stop_camera_watch() -> Dict[str, Any]:
def _execute_stop_camera_watch() -> dict[str, Any]:
cancelled = stop_vlm_watch_job()
if cancelled:
return {"success": True, "message": "Watch job cancelled."}
return {"success": False, "message": "No active watch job to cancel."}
def _execute_get_profile_status(request: Request) -> Dict[str, Any]:
def _execute_get_profile_status(request: Request) -> dict[str, Any]:
"""Return profile status including active profile and activation timestamps."""
profile_manager = getattr(request.app, "profile_manager", None)
if profile_manager is None:
@@ -846,9 +846,9 @@ def _execute_get_profile_status(request: Request) -> Dict[str, Any]:
def _execute_get_recap(
arguments: Dict[str, Any],
allowed_cameras: List[str],
) -> Dict[str, Any]:
arguments: dict[str, Any],
allowed_cameras: list[str],
) -> dict[str, Any]:
"""Fetch review segments with GenAI metadata for a time period."""
from functools import reduce
@@ -909,7 +909,7 @@ def _execute_get_recap(
.iterator()
)
events: List[Dict[str, Any]] = []
events: list[dict[str, Any]] = []
for row in rows:
data = row.get("data") or {}
@@ -920,7 +920,7 @@ def _execute_get_recap(
data = {}
camera = row["camera"]
event: Dict[str, Any] = {
event: dict[str, Any] = {
"camera": camera.replace("_", " ").title(),
"severity": row.get("severity", "detection"),
}
@@ -984,10 +984,10 @@ def _execute_get_recap(
async def _execute_pending_tools(
pending_tool_calls: List[Dict[str, Any]],
pending_tool_calls: list[dict[str, Any]],
request: Request,
allowed_cameras: List[str],
) -> tuple[List[ToolCall], List[Dict[str, Any]], List[Dict[str, Any]]]:
allowed_cameras: list[str],
) -> tuple[list[ToolCall], list[dict[str, Any]], list[dict[str, Any]]]:
"""
Execute a list of tool calls.
@@ -996,9 +996,9 @@ async def _execute_pending_tools(
tool result dicts for conversation,
extra messages to inject after tool results e.g. user messages with images)
"""
tool_calls_out: List[ToolCall] = []
tool_results: List[Dict[str, Any]] = []
extra_messages: List[Dict[str, Any]] = []
tool_calls_out: list[ToolCall] = []
tool_results: list[dict[str, Any]] = []
extra_messages: list[dict[str, Any]] = []
for tool_call in pending_tool_calls:
tool_name = tool_call["name"]
tool_args = tool_call.get("arguments") or {}
@@ -1106,7 +1106,7 @@ async def _execute_pending_tools(
async def chat_completion(
request: Request,
body: ChatCompletionRequest = Body(...),
allowed_cameras: List[str] = Depends(get_allowed_cameras_for_filter),
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""
Chat completion endpoint with tool calling support.
@@ -1138,19 +1138,23 @@ async def chat_completion(
)
conversation = []
system_prompt = build_chat_system_prompt(
config=config,
allowed_cameras=allowed_cameras,
semantic_search_enabled=semantic_search_enabled,
attribute_classifications=attribute_classifications,
)
conversation.append(
{
"role": "system",
"content": system_prompt,
}
)
# Build the system message only when the client hasn't already pinned one.
# The first turn has no system message; we generate it (with the current
# timestamp) and return the whole chain so the client persists it. Later
# turns send it back verbatim, freezing the timestamp so the prompt prefix
# stays byte-identical and the model server's prompt cache keeps hitting.
if not body.messages or body.messages[0].role != "system":
conversation.append(
{
"role": "system",
"content": build_chat_system_prompt(
config=config,
allowed_cameras=allowed_cameras,
semantic_search_enabled=semantic_search_enabled,
attribute_classifications=attribute_classifications,
),
}
)
for msg in body.messages:
msg_dict = {
@@ -1161,11 +1165,13 @@ async def chat_completion(
msg_dict["tool_call_id"] = msg.tool_call_id
if msg.name:
msg_dict["name"] = msg.name
if msg.tool_calls is not None:
msg_dict["tool_calls"] = msg.tool_calls
conversation.append(msg_dict)
tool_iterations = 0
tool_calls: List[ToolCall] = []
tool_calls: list[ToolCall] = []
max_iterations = body.max_tool_iterations
logger.debug(
@@ -1175,11 +1181,20 @@ async def chat_completion(
# True LLM streaming when client supports it and stream requested
if body.stream and hasattr(genai_client, "chat_with_tools_stream"):
stream_tool_calls: List[ToolCall] = []
stream_iterations = 0
async def stream_body_llm():
nonlocal conversation, stream_tool_calls, stream_iterations
nonlocal conversation, stream_iterations
def _emit_chain(extra: Optional[list[dict[str, Any]]] = None):
# Return the full conversation (including the system message) so
# the client persists and replays it verbatim next turn.
chain = conversation + (extra or [])
return (
json.dumps({"type": "messages", "messages": chain}).encode("utf-8")
+ b"\n"
)
while stream_iterations < max_iterations:
if await request.is_disconnected():
logger.debug("Client disconnected, stopping chat stream")
@@ -1244,31 +1259,33 @@ async def chat_completion(
)
return
(
executed_calls,
_executed_calls,
tool_results,
extra_msgs,
) = await _execute_pending_tools(
pending, request, allowed_cameras
)
stream_tool_calls.extend(executed_calls)
conversation.extend(tool_results)
conversation.extend(extra_msgs)
yield (
json.dumps(
{
"type": "tool_calls",
"tool_calls": [
tc.model_dump() for tc in stream_tool_calls
],
}
).encode("utf-8")
+ b"\n"
)
# Emit the running chain so the client can render tool
# calls live and replay them verbatim next turn.
yield _emit_chain()
break
else:
# Streaming never appends the final assistant message
# to the conversation, so add it to the chain.
yield _emit_chain(
extra=[
{
"role": "assistant",
"content": msg.get("content"),
}
]
)
yield (json.dumps({"type": "done"}).encode("utf-8") + b"\n")
return
else:
yield _emit_chain()
yield json.dumps({"type": "done"}).encode("utf-8") + b"\n"
return StreamingResponse(
@@ -1315,19 +1332,15 @@ async def chat_completion(
if body.stream:
final_reasoning = response.get("reasoning")
chain = list(conversation)
async def stream_body() -> Any:
if tool_calls:
yield (
json.dumps(
{
"type": "tool_calls",
"tool_calls": [
tc.model_dump() for tc in tool_calls
],
}
).encode("utf-8")
+ b"\n"
yield (
json.dumps({"type": "messages", "messages": chain}).encode(
"utf-8"
)
+ b"\n"
)
# Emit the full reasoning trace up front when the
# underlying client did not stream it
if final_reasoning:
@@ -1363,6 +1376,7 @@ async def chat_completion(
finish_reason=response.get("finish_reason", "stop"),
tool_iterations=tool_iterations,
tool_calls=tool_calls,
messages=list(conversation),
).model_dump(),
)
@@ -1395,6 +1409,7 @@ async def chat_completion(
finish_reason="length",
tool_iterations=tool_iterations,
tool_calls=tool_calls,
messages=list(conversation),
).model_dump(),
)
+18 -2
View File
@@ -1,6 +1,6 @@
"""Chat API request models."""
from typing import Optional
from typing import Any, Optional
from pydantic import BaseModel, Field
@@ -11,13 +11,29 @@ class ChatMessage(BaseModel):
role: str = Field(
description="Message role: 'user', 'assistant', 'system', or 'tool'"
)
content: str = Field(description="Message content")
content: Optional[Any] = Field(
default=None,
description=(
"Message content. Usually a string, but may be a multimodal content "
"list (e.g. text + image_url) or null for assistant turns that only "
"request tool calls."
),
)
tool_call_id: Optional[str] = Field(
default=None, description="For tool messages, the ID of the tool call"
)
name: Optional[str] = Field(
default=None, description="For tool messages, the tool name"
)
tool_calls: Optional[list[dict[str, Any]]] = Field(
default=None,
description=(
"For assistant messages replayed from prior turns, the OpenAI-format "
"tool calls the model previously requested. Replaying these verbatim "
"keeps the conversation prefix byte-for-byte identical so the model "
"server's prompt cache hits on follow-up turns."
),
)
class ChatCompletionRequest(BaseModel):
@@ -3,7 +3,10 @@ from typing import Optional, Union
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema
from frigate.record.export import PlaybackSourceEnum
from frigate.record.export import (
ChaptersEnum,
PlaybackSourceEnum,
)
class ExportRecordingsBody(BaseModel):
@@ -18,6 +21,14 @@ class ExportRecordingsBody(BaseModel):
max_length=30,
description="ID of the export case to assign this export to",
)
chapters: Optional[ChaptersEnum] = Field(
default=None,
title="Chapter mode",
description=(
"Optional chapter metadata to embed in the export. When omitted, "
"the camera's configured export chapter mode is used."
),
)
class ExportRecordingsCustomBody(BaseModel):
@@ -56,3 +56,12 @@ class ChatCompletionResponse(BaseModel):
default_factory=list,
description="List of tool calls that were executed during this completion",
)
messages: list[dict[str, Any]] = Field(
default_factory=list,
description=(
"The full conversation chain, including the system message. Persist "
"and replay this verbatim on the next request so the prompt prefix "
"stays byte-identical and the model server's prompt cache keeps "
"hitting."
),
)
+24
View File
@@ -68,6 +68,7 @@ from frigate.jobs.export import (
from frigate.models import Export, ExportCase, Previews, Recordings
from frigate.record.export import (
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
ChaptersEnum,
PlaybackSourceEnum,
validate_ffmpeg_args,
)
@@ -128,6 +129,15 @@ def _validate_export_case(export_case_id: Optional[str]) -> Optional[JSONRespons
def _sanitize_existing_image(
image_path: Optional[str],
) -> tuple[Optional[str], Optional[JSONResponse]]:
# sanitize_filepath normalizes "\" to "/" but leaves ".." intact, so a path
# like "clips\..\..\etc/passwd" passes the CLIPS_DIR prefix check yet still
# escapes the directory once resolved. A valid snapshot path never uses "..".
if image_path and ".." in image_path:
return None, JSONResponse(
content={"success": False, "message": "Invalid image path"},
status_code=400,
)
existing_image = sanitize_filepath(image_path) if image_path else None
if existing_image and not existing_image.startswith(CLIPS_DIR):
@@ -254,6 +264,7 @@ def _build_export_job(
ffmpeg_input_args: Optional[str] = None,
ffmpeg_output_args: Optional[str] = None,
cpu_fallback: bool = False,
chapters: Optional[ChaptersEnum] = None,
) -> ExportJob:
return ExportJob(
id=_generate_export_id(camera_name),
@@ -267,6 +278,7 @@ def _build_export_job(
ffmpeg_input_args=ffmpeg_input_args,
ffmpeg_output_args=ffmpeg_output_args,
cpu_fallback=cpu_fallback,
chapters=chapters,
)
@@ -725,6 +737,9 @@ def export_recordings_batch(
sanitized_images[index],
PlaybackSourceEnum.recordings,
export_case_id,
chapters=request.app.frigate_config.cameras[
item.camera
].record.export.chapters,
)
try:
start_export_job(request.app.frigate_config, export_job)
@@ -803,6 +818,14 @@ def export_recording(
export_case_id = body.export_case_id
# a chapters value in the request body overrides the camera's export config
camera_config = request.app.frigate_config.cameras[camera_name]
chapters = (
body.chapters
if body.chapters is not None
else camera_config.record.export.chapters
)
# Attaching to an existing case requires admin. Single-export for
# cameras the user can access is otherwise non-admin; we only gate
# the case-attachment side effect.
@@ -839,6 +862,7 @@ def export_recording(
existing_image,
playback_source,
export_case_id,
chapters=chapters,
)
try:
start_export_job(request.app.frigate_config, export_job)
+32 -13
View File
@@ -60,6 +60,19 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.media])
def _resolve_cache_age(max_cache_age: int) -> int:
"""Return max_cache_age as an int.
When a media handler is invoked directly by another handler instead of
through its route, FastAPI doesn't resolve the Query() default and
max_cache_age arrives as the Query object; fall back to its int default.
"""
if isinstance(max_cache_age, int):
return max_cache_age
return max_cache_age.default
@router.get("/{camera_name}", dependencies=[Depends(require_camera_access)])
async def mjpeg_feed(
request: Request,
@@ -413,7 +426,9 @@ async def submit_recording_snapshot_to_plus(
)
nd = cv2.imdecode(np.frombuffer(image_data, dtype=np.int8), cv2.IMREAD_COLOR)
request.app.frigate_config.plus_api.upload_image(nd, camera_name)
await asyncio.to_thread(
request.app.frigate_config.plus_api.upload_image, nd, camera_name
)
return JSONResponse(
content={
@@ -936,7 +951,7 @@ async def event_thumbnail(
thumbnail_bytes,
media_type=extension.get_mime_type(),
headers={
"Cache-Control": f"private, max-age={max_cache_age}"
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}"
if event_complete
else "no-store",
},
@@ -1270,14 +1285,14 @@ async def event_preview(request: Request, event_id: str):
end_ts = start_ts + (
min(event.end_time - event.start_time, 20) if event.end_time else 20
)
return preview_gif(request, event.camera, start_ts, end_ts)
return await preview_gif(request, event.camera, start_ts, end_ts)
@router.get(
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.gif",
dependencies=[Depends(require_camera_access)],
)
def preview_gif(
async def preview_gif(
request: Request,
camera_name: str,
start_ts: float,
@@ -1340,7 +1355,8 @@ def preview_gif(
"-",
]
process = sp.run(
process = await asyncio.to_thread(
sp.run,
ffmpeg_cmd,
capture_output=True,
)
@@ -1419,7 +1435,8 @@ def preview_gif(
"-",
]
process = sp.run(
process = await asyncio.to_thread(
sp.run,
ffmpeg_cmd,
input=str.encode("\n".join(selected_previews)),
capture_output=True,
@@ -1438,7 +1455,7 @@ def preview_gif(
gif_bytes,
media_type="image/gif",
headers={
"Cache-Control": f"private, max-age={max_cache_age}",
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
"Content-Type": "image/gif",
},
)
@@ -1448,7 +1465,7 @@ def preview_gif(
"/{camera_name}/start/{start_ts}/end/{end_ts}/preview.mp4",
dependencies=[Depends(require_camera_access)],
)
def preview_mp4(
async def preview_mp4(
request: Request,
camera_name: str,
start_ts: float,
@@ -1528,7 +1545,8 @@ def preview_mp4(
path,
]
process = sp.run(
process = await asyncio.to_thread(
sp.run,
ffmpeg_cmd,
capture_output=True,
)
@@ -1604,7 +1622,8 @@ def preview_mp4(
path,
]
process = sp.run(
process = await asyncio.to_thread(
sp.run,
ffmpeg_cmd,
input=str.encode("\n".join(selected_previews)),
capture_output=True,
@@ -1619,7 +1638,7 @@ def preview_mp4(
headers = {
"Content-Description": "File Transfer",
"Cache-Control": f"private, max-age={max_cache_age}",
"Cache-Control": f"private, max-age={_resolve_cache_age(max_cache_age)}",
"Content-Type": "video/mp4",
"Content-Length": str(os.path.getsize(path)),
# nginx: https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_ignore_headers
@@ -1657,9 +1676,9 @@ async def review_preview(
)
if format == "gif":
return preview_gif(request, review.camera, start_ts, end_ts)
return await preview_gif(request, review.camera, start_ts, end_ts)
else:
return preview_mp4(request, review.camera, start_ts, end_ts)
return await preview_mp4(request, review.camera, start_ts, end_ts)
@router.get(
+5 -7
View File
@@ -41,12 +41,6 @@ class MotionSearchRequest(BaseModel):
le=100.0,
description="Minimum change area as a percentage of the ROI",
)
frame_skip: int = Field(
default=5,
ge=1,
le=30,
description="Process every Nth frame (1=all frames, 5=every 5th frame)",
)
parallel: bool = Field(
default=False,
description="Enable parallel scanning across segments",
@@ -97,6 +91,8 @@ class MotionSearchStatusResponse(BaseModel):
total_frames_processed: Optional[int] = None
error_message: Optional[str] = None
metrics: Optional[MotionSearchMetricsResponse] = None
scanning_timestamp: Optional[float] = None
progress: Optional[float] = None
@router.post(
@@ -151,7 +147,6 @@ async def start_motion_search(
polygon_points=body.polygon_points,
threshold=body.threshold,
min_area=body.min_area,
frame_skip=body.frame_skip,
parallel=body.parallel,
max_results=body.max_results,
)
@@ -231,6 +226,9 @@ async def get_motion_search_status_endpoint(
if job.metrics:
response_content["metrics"] = job.metrics.to_dict()
response_content["scanning_timestamp"] = job.scanning_timestamp
response_content["progress"] = job.progress
return JSONResponse(content=response_content)
+36 -16
View File
@@ -1,7 +1,9 @@
"""Preview apis."""
import bisect
import logging
import os
import threading
from datetime import datetime, timedelta, timezone
import pytz
@@ -133,6 +135,32 @@ def preview_hour(
return preview_ts(camera_name, start_ts, end_ts, allowed_cameras)
# cache one sorted listing of the shared preview_frames dir
_preview_listing_lock = threading.Lock()
_preview_listing_cache: tuple[float, list[str]] = (-1.0, [])
def _get_preview_frame_listing(preview_dir: str) -> list[str]:
"""Return the sorted preview_frames listing, cached until the dir changes."""
global _preview_listing_cache
# mtime bumps when a frame is added or removed, invalidating the cache
mtime = os.stat(preview_dir).st_mtime
cached_mtime, files = _preview_listing_cache
if mtime == cached_mtime:
return files
with _preview_listing_lock:
# another thread may have refreshed the cache while we waited
cached_mtime, files = _preview_listing_cache
if mtime == cached_mtime:
return files
files = sorted(entry.name for entry in os.scandir(preview_dir))
_preview_listing_cache = (mtime, files)
return files
@router.get(
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}/frames",
response_model=PreviewFramesResponse,
@@ -149,23 +177,15 @@ def get_preview_frames_from_cache(camera_name: str, start_ts: float, end_ts: flo
start_file = f"{file_start}{start_ts}.{PREVIEW_FRAME_TYPE}"
end_file = f"{file_start}{end_ts}.{PREVIEW_FRAME_TYPE}"
camera_files = [
entry.name
for entry in os.scandir(preview_dir)
if entry.name.startswith(file_start)
files = _get_preview_frame_listing(preview_dir)
# a camera's frames form a contiguous slice of the sorted listing;
# bisect locates it without scanning the whole directory
left = bisect.bisect_left(files, start_file)
right = bisect.bisect_right(files, end_file)
selected_previews = [
file for file in files[left:right] if file.startswith(file_start)
]
camera_files.sort()
selected_previews = []
for file in camera_files:
if file < start_file:
continue
if file > end_file:
break
selected_previews.append(file)
return JSONResponse(
content=selected_previews,
+20 -6
View File
@@ -299,22 +299,36 @@ async def no_recordings(
.iterator()
)
# Convert recordings to list of (start, end) tuples
# Convert recordings to list of (start, end) tuples, ordered by start_time
recordings = [(r["start_time"], r["end_time"]) for r in data]
# Merge overlapping/adjacent recordings into covered intervals. The query
# orders by start_time, so a single pass merges them
covered: list[tuple[float, float]] = []
for rec_start, rec_end in recordings:
if covered and rec_start <= covered[-1][1]:
covered[-1] = (covered[-1][0], max(covered[-1][1], rec_end))
else:
covered.append((rec_start, rec_end))
# Iterate through time segments and check if each has any recording
no_recording_segments = []
current = after
current_gap_start = None
idx = 0
covered_count = len(covered)
while current < before:
segment_end = min(current + scale, before)
# Check if this segment overlaps with any recording
has_recording = any(
rec_start < segment_end and rec_end > current
for rec_start, rec_end in recordings
)
# Advance past covered intervals that end before this segment begins;
# they cannot overlap this or any later segment.
while idx < covered_count and covered[idx][1] <= current:
idx += 1
# A covered interval overlaps the segment when it starts before the
# segment ends (its end is already known to be > current).
has_recording = idx < covered_count and covered[idx][0] < segment_end
if not has_recording:
# This segment has no recordings
+14 -10
View File
@@ -605,9 +605,10 @@ def motion_activity(
if not filtered:
return JSONResponse(content=[])
camera_list = list(filtered)
clauses.append((Recordings.camera << camera_list))
else:
clauses.append((Recordings.camera << allowed_cameras))
camera_list = list(allowed_cameras)
clauses.append((Recordings.camera << camera_list))
data: list[Recordings] = (
Recordings.select(
@@ -635,14 +636,12 @@ def motion_activity(
df.set_index(["start_time"], inplace=True)
# normalize data
motion = (
df["motion"]
.resample(f"{scale}s")
.apply(lambda x: max(x, key=abs, default=0.0))
.fillna(0.0)
.to_frame()
)
cameras = df["camera"].resample(f"{scale}s").agg(lambda x: ",".join(set(x)))
motion = df["motion"].resample(f"{scale}s").max().fillna(0.0).to_frame()
if len(camera_list) == 1:
cameras = df["camera"].resample(f"{scale}s").first().fillna("")
else:
cameras = df["camera"].resample(f"{scale}s").agg(lambda x: ",".join(set(x)))
df = motion.join(cameras)
length = df.shape[0]
@@ -658,6 +657,11 @@ def motion_activity(
else:
df.iloc[i : i + chunk, 0] = 0.0
# Drop resample gap-fill buckets. The resample above emits a row for every
# {scale}s bucket spanning the range, and buckets with no recording get a
# motion of 0 (from fillna) and an empty camera (from joining an empty set).
df = df[df["camera"] != ""]
# change types for output
df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records")
+39 -7
View File
@@ -72,11 +72,16 @@ _WS_VIEWER_TOPICS = frozenset(
}
)
# Camera-scoped command topics a camera-authorized (non-admin) user may send.
_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
def _check_ws_authorization(
topic: str,
role_header: str | None,
separator: str,
roles_config: dict[str, list[str]] | None = None,
camera_names: set[str] | None = None,
) -> bool:
"""Check if a WebSocket message is authorized.
@@ -84,6 +89,10 @@ def _check_ws_authorization(
topic: The message topic.
role_header: The HTTP_REMOTE_ROLE header value, or None.
separator: The role separator character from proxy config.
roles_config: The auth.roles mapping (role -> allowed cameras), used to
authorize camera-scoped commands for non-admin users.
camera_names: All configured camera names, used to resolve a role's
allowed cameras.
Returns:
True if authorized, False if blocked.
@@ -93,16 +102,33 @@ def _check_ws_authorization(
return False
# No role header: default to viewer (fail-closed)
if role_header is None:
return topic in _WS_VIEWER_TOPICS
roles = [r.strip() for r in role_header.split(separator)] if role_header else []
# Check if any role is admin
roles = [r.strip() for r in role_header.split(separator)]
# Admin can send anything
if "admin" in roles:
return True
# Non-admin: only viewer topics allowed
return topic in _WS_VIEWER_TOPICS
# Read-only topics any authenticated user can send
if topic in _WS_VIEWER_TOPICS:
return True
# Camera-scoped command like "<camera>/ptz": allow when the user's role(s)
# grant access to that camera.
parts = topic.split("/")
if (
roles_config is not None
and len(parts) == 2
and parts[1] in _WS_CAMERA_COMMAND_TOPICS
):
allowed: set[str] = set()
# No role header maps to the default viewer role (e.g. proxy-only setups)
for role in roles or ["viewer"]:
allowed.update(
User.get_allowed_cameras(role, roles_config, camera_names or set())
)
return parts[0] in allowed
return False
# ---- Outbound filtering ---------------------------------------------------
@@ -449,6 +475,8 @@ class WebSocketClient(Communicator):
class _WebSocketHandler(WebSocket):
receiver = self._dispatcher
role_separator = self.config.proxy.separator or ","
roles_config = self.config.auth.roles
camera_names = set(self.config.cameras.keys())
def received_message(self, message: WebSocket.received_message) -> None: # type: ignore[name-defined]
try:
@@ -470,7 +498,11 @@ class WebSocketClient(Communicator):
self.environ.get("HTTP_REMOTE_ROLE") if self.environ else None
)
if self.environ is not None and not _check_ws_authorization(
topic, role_header, self.role_separator
topic,
role_header,
self.role_separator,
self.roles_config,
self.camera_names,
):
logger.warning(
"Blocked unauthorized WebSocket message: topic=%s, role=%s",
+2 -2
View File
@@ -9,7 +9,7 @@ from ..base import FrigateBaseModel
__all__ = ["AudioConfig", "AudioFilterConfig"]
DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "scream", "speech", "yell"]
DEFAULT_LISTEN_AUDIO = ["bark", "fire_alarm", "speech", "yell"]
class AudioFilterConfig(FrigateBaseModel):
@@ -41,7 +41,7 @@ class AudioConfig(FrigateBaseModel):
listen: list[str] = Field(
default=DEFAULT_LISTEN_AUDIO,
title="Listen types",
description="List of audio event types to detect (for example: bark, fire_alarm, scream, speech, yell).",
description="List of audio event types to detect (for example: bark, fire_alarm, speech, yell).",
)
filters: Optional[dict[str, AudioFilterConfig]] = Field(
None,
+3 -3
View File
@@ -100,8 +100,8 @@ class CameraConfig(FrigateBaseModel):
description="Settings for face detection and recognition for this camera.",
)
ffmpeg: CameraFfmpegConfig = Field(
title="FFmpeg",
description="FFmpeg settings including binary path, args, hwaccel options, and per-role output args.",
title="Streams (FFmpeg)",
description="Camera stream inputs and FFmpeg options, including binary path, args, hwaccel, and per-role output args.",
)
live: CameraLiveConfig = Field(
default_factory=CameraLiveConfig,
@@ -146,7 +146,7 @@ class CameraConfig(FrigateBaseModel):
timestamp_style: TimestampStyleConfig = Field(
default_factory=TimestampStyleConfig,
title="Timestamp style",
description="Styling options for in-feed timestamps applied to recordings and snapshots.",
description="Styling options for timestamps applied to snapshots and Debug view.",
)
# Options without global fallback
+4 -14
View File
@@ -3,7 +3,7 @@ from typing import Union
from pydantic import Field, field_validator
from frigate.const import DEFAULT_FFMPEG_VERSION, INCLUDED_FFMPEG_VERSIONS
from frigate.util.config import resolve_ffmpeg_path
from ..base import FrigateBaseModel
from ..env import EnvString
@@ -49,7 +49,7 @@ class FfmpegConfig(FrigateBaseModel):
path: str = Field(
default="default",
title="FFmpeg path",
description='Path to the FFmpeg binary to use or a version alias ("5.0" or "7.0").',
description='Path to the FFmpeg binary to use or a version alias ("7.0" or "8.0").',
)
global_args: Union[str, list[str]] = Field(
default=FFMPEG_GLOBAL_ARGS_DEFAULT,
@@ -90,21 +90,11 @@ class FfmpegConfig(FrigateBaseModel):
@property
def ffmpeg_path(self) -> str:
if self.path == "default":
return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffmpeg"
elif self.path in INCLUDED_FFMPEG_VERSIONS:
return f"/usr/lib/ffmpeg/{self.path}/bin/ffmpeg"
else:
return f"{self.path}/bin/ffmpeg"
return resolve_ffmpeg_path(self.path, "ffmpeg")
@property
def ffprobe_path(self) -> str:
if self.path == "default":
return f"/usr/lib/ffmpeg/{DEFAULT_FFMPEG_VERSION}/bin/ffprobe"
elif self.path in INCLUDED_FFMPEG_VERSIONS:
return f"/usr/lib/ffmpeg/{self.path}/bin/ffprobe"
else:
return f"{self.path}/bin/ffprobe"
return resolve_ffmpeg_path(self.path, "ffprobe")
class CameraRoleEnum(str, Enum):
+11
View File
@@ -9,6 +9,7 @@ from frigate.review.types import SeverityEnum
from ..base import FrigateBaseModel
__all__ = [
"ChaptersEnum",
"RecordConfig",
"RecordExportConfig",
"RecordPreviewConfig",
@@ -86,6 +87,12 @@ class RecordPreviewConfig(FrigateBaseModel):
)
class ChaptersEnum(str, Enum):
none = "none"
recording_segments = "recording_segments"
review_items = "review_items"
class RecordExportConfig(FrigateBaseModel):
hwaccel_args: Union[str, list[str]] = Field(
default="auto",
@@ -98,6 +105,10 @@ class RecordExportConfig(FrigateBaseModel):
title="Maximum concurrent exports",
description="Maximum number of export jobs to process at the same time.",
)
chapters: ChaptersEnum = Field(
default=ChaptersEnum.review_items,
title="Chapter metadata to embed in exported recordings",
)
class RecordConfig(FrigateBaseModel):
-6
View File
@@ -3,7 +3,6 @@ from typing import Optional
from pydantic import Field
from ..base import FrigateBaseModel
from .record import RetainModeEnum
__all__ = ["SnapshotsConfig", "RetainConfig"]
@@ -14,11 +13,6 @@ class RetainConfig(FrigateBaseModel):
title="Default retention",
description="Default number of days to retain snapshots.",
)
mode: RetainModeEnum = Field(
default=RetainModeEnum.motion,
title="Retention mode",
description="Mode for retention: all (save all segments), motion (save segments with motion), or active_objects (save segments with active objects).",
)
objects: dict[str, float] = Field(
default_factory=dict,
title="Object retention",
+5
View File
@@ -16,3 +16,8 @@ class CameraUiConfig(FrigateBaseModel):
title="Show in UI",
description="Toggle whether this camera is visible everywhere in the Frigate UI. Disabling this will require manually editing the config to view this camera in the UI again.",
)
review: bool = Field(
default=True,
title="Show in review",
description="Toggle whether this camera is visible in review (the review page and its camera filter, motion review, and the history view).",
)
+6 -1
View File
@@ -73,7 +73,12 @@ class CameraConfigUpdateSubscriber:
base_topic = "config/cameras"
if len(self.camera_configs) == 1:
# global subscribers must hear every camera; only narrow per-camera workers
is_global_subscriber = (
CameraConfigUpdateEnum.add in self.topics
or CameraConfigUpdateEnum.remove in self.topics
)
if not is_global_subscriber and len(self.camera_configs) == 1:
base_topic += f"/{list(self.camera_configs.keys())[0]}"
self.subscriber = ConfigSubscriber(
+49 -1
View File
@@ -5,7 +5,7 @@ import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
from typing import Any, Callable, Optional
from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
@@ -34,6 +34,45 @@ PROFILE_SECTION_UPDATES: dict[str, CameraConfigUpdateEnum] = {
"zones": CameraConfigUpdateEnum.zones,
}
# Retained MQTT switch topics per profile section, with a payload getter.
# Republished on profile change so MQTT/HA don't show a stale toggle.
SECTION_STATE_TOPICS: dict[str, list[tuple[str, Callable[[Any], Any]]]] = {
"audio": [("audio", lambda c: "ON" if c.audio.enabled else "OFF")],
"birdseye": [
("birdseye", lambda c: "ON" if c.birdseye.enabled else "OFF"),
(
"birdseye_mode",
lambda c: c.birdseye.mode.value.upper() if c.birdseye.enabled else "OFF",
),
],
"detect": [("detect", lambda c: "ON" if c.detect.enabled else "OFF")],
"motion": [
("motion", lambda c: "ON" if c.motion.enabled else "OFF"),
("improve_contrast", lambda c: "ON" if c.motion.improve_contrast else "OFF"),
("motion_threshold", lambda c: c.motion.threshold),
("motion_contour_area", lambda c: c.motion.contour_area),
],
"notifications": [
("notifications", lambda c: "ON" if c.notifications.enabled else "OFF"),
],
"objects": [
("object_descriptions", lambda c: "ON" if c.objects.genai.enabled else "OFF"),
],
"record": [("recordings", lambda c: "ON" if c.record.enabled else "OFF")],
"review": [
("review_alerts", lambda c: "ON" if c.review.alerts.enabled else "OFF"),
(
"review_detections",
lambda c: "ON" if c.review.detections.enabled else "OFF",
),
(
"review_descriptions",
lambda c: "ON" if c.review.genai.enabled else "OFF",
),
],
"snapshots": [("snapshots", lambda c: "ON" if c.snapshots.enabled else "OFF")],
}
PERSISTENCE_FILE = Path(CONFIG_DIR) / ".profiles"
@@ -310,6 +349,15 @@ class ProfileManager:
settings,
)
# republish MQTT switch states
if self.dispatcher is not None:
for suffix, get_payload in SECTION_STATE_TOPICS.get(section, ()):
self.dispatcher.publish(
f"{cam_name}/{suffix}/state",
get_payload(cam_config),
retain=True,
)
def _persist_active_profile(self, profile_name: Optional[str]) -> None:
"""Persist the active profile state to disk as JSON."""
try:
+1 -1
View File
@@ -45,7 +45,7 @@ class ProxyConfig(FrigateBaseModel):
default_role: Optional[str] = Field(
default="viewer",
title="Default role",
description="Default role assigned to proxy-authenticated users when no role mapping applies (admin or viewer).",
description="Default role assigned to proxy-authenticated users when no role mapping applies.",
)
separator: Optional[str] = Field(
default=",",
+1 -18
View File
@@ -5,7 +5,7 @@ from pydantic import Field
from .base import FrigateBaseModel
__all__ = ["TimeFormatEnum", "DateTimeStyleEnum", "UnitSystemEnum", "UIConfig"]
__all__ = ["TimeFormatEnum", "UnitSystemEnum", "UIConfig"]
class TimeFormatEnum(str, Enum):
@@ -14,13 +14,6 @@ class TimeFormatEnum(str, Enum):
hours24 = "24hour"
class DateTimeStyleEnum(str, Enum):
full = "full"
long = "long"
medium = "medium"
short = "short"
class UnitSystemEnum(str, Enum):
imperial = "imperial"
metric = "metric"
@@ -37,16 +30,6 @@ class UIConfig(FrigateBaseModel):
title="Time format",
description="Time format to use in the UI (browser, 12hour, or 24hour).",
)
date_style: DateTimeStyleEnum = Field(
default=DateTimeStyleEnum.short,
title="Date style",
description="Date style to use in the UI (full, long, medium, short).",
)
time_style: DateTimeStyleEnum = Field(
default=DateTimeStyleEnum.medium,
title="Time style",
description="Time style to use in the UI (full, long, medium, short).",
)
unit_system: UnitSystemEnum = Field(
default=UnitSystemEnum.metric,
title="Unit system",
+16 -13
View File
@@ -15,6 +15,9 @@ from frigate.util.rknn_converter import auto_convert_model, is_rknn_compatible
logger = logging.getLogger(__name__)
# Process-wide lock serializing all OpenVINO compile/inference calls
_OPENVINO_LOCK = threading.Lock()
def is_arm64_platform() -> bool:
"""Check if we're running on an ARM platform."""
@@ -326,19 +329,17 @@ class OpenVINOModelRunner(BaseModelRunner):
except Exception as e:
logger.debug(f"NPU_TURBO not supported by driver: {e}")
# Compile model
self.compiled_model = self.ov_core.compile_model(
model=model_path, device_name=device
)
# Compile model under the shared lock
with _OPENVINO_LOCK:
self.compiled_model = self.ov_core.compile_model(
model=model_path, device_name=device
)
# Create reusable inference request
self.infer_request = self.compiled_model.create_infer_request()
# Create reusable inference request
self.infer_request = self.compiled_model.create_infer_request()
self.input_tensor: ov.Tensor | None = None
# Thread lock to prevent concurrent inference (needed for JinaV2 which shares
# one runner between text and vision embeddings called from different threads)
self._inference_lock = threading.Lock()
if not self.complex_model:
try:
input_shape = self.compiled_model.inputs[0].get_shape()
@@ -382,9 +383,11 @@ class OpenVINOModelRunner(BaseModelRunner):
Returns:
List of output tensors
"""
# Lock prevents concurrent access to infer_request
# Needed for JinaV2: genai thread (text) + embeddings thread (vision)
with self._inference_lock:
# Shared lock serializes inference across every OpenVINO runner in this
# process — both the shared-runner JinaV2 case (genai text thread +
# embeddings vision thread) and distinct runners running on separate
# threads (e.g. the ArcFace face-model build vs the LPR detector).
with _OPENVINO_LOCK:
from frigate.embeddings.types import EnrichmentModelTypeEnum
if self.model_type in [EnrichmentModelTypeEnum.arcface.value]:
+1 -13
View File
@@ -465,16 +465,6 @@ PRESETS_RECORD_OUTPUT = {
"-c:a",
"aac",
],
# NOTE: This preset originally used "-c:a copy" to pass through audio
# without re-encoding. FFmpeg 7.x introduced a threaded pipeline where
# demuxing, encoding, and muxing run in parallel via a Scheduler. This
# broke audio streamcopy from RTSP sources: packets are demuxed correctly
# but silently dropped before reaching the muxer (0 bytes written). The
# issue is specific to RTSP + streamcopy; file inputs and transcoding both
# work. Transcoding AAC audio is very lightweight (~30KiB per 10s segment)
# and adds negligible CPU overhead, so this is an acceptable workaround.
# The benefits of FFmpeg 7.x — particularly the removal of gamma correction
# hacks required by earlier versions — outweigh this trade-off.
"preset-record-generic-audio-copy": [
"-f",
"segment",
@@ -486,10 +476,8 @@ PRESETS_RECORD_OUTPUT = {
"1",
"-strftime",
"1",
"-c:v",
"-c",
"copy",
"-c:a",
"aac",
],
"preset-record-mjpeg": [
"-f",
+33
View File
@@ -5,6 +5,7 @@ import json
import logging
import os
import re
import time
from typing import Any, AsyncGenerator, Callable, Optional
import numpy as np
@@ -50,6 +51,10 @@ def register_genai_provider(key: GenAIProviderEnum) -> Callable:
class GenAIClient:
"""Generative AI client for Frigate."""
# Minimum seconds between re-initialization attempts when the provider was
# offline at startup
REINIT_INTERVAL = 60.0
def __init__(
self,
genai_config: GenAIConfig,
@@ -60,6 +65,34 @@ class GenAIClient:
self.timeout = timeout
self.validate_model = validate_model
self.provider = self._init_provider()
self._last_init_attempt = time.monotonic()
def ensure_provider(self) -> bool:
"""Ensure a provider is available, retrying initialization if needed.
Providers can fail to initialize at startup when their backing service
isn't online yet (common when both are started together). This retries
``_init_provider`` lazily throttled to ``REINIT_INTERVAL`` so the
client recovers on its own once the service is reachable, without a
config reload.
Returns True if a provider is available.
"""
if self.provider is not None:
return True
now = time.monotonic()
if now - self._last_init_attempt < self.REINIT_INTERVAL:
return False
self._last_init_attempt = now
self.provider = self._init_provider()
if self.provider is not None:
logger.info(
"GenAI provider %s is now available",
self.genai_config.provider,
)
return self.provider is not None
def generate_review_description(
self,
+4 -2
View File
@@ -62,7 +62,9 @@ class GenAIClientManager:
def _get_client(self, name: str) -> "Optional[GenAIClient]":
"""Return the client for *name*, creating it on first access."""
if name in self._clients:
return self._clients[name]
client = self._clients[name]
client.ensure_provider()
return client
from frigate.genai import PROVIDERS
@@ -78,7 +80,7 @@ class GenAIClientManager:
return None
try:
client: "GenAIClient" = provider_cls(genai_cfg)
client = provider_cls(genai_cfg)
except Exception as e:
logger.exception(
"Failed to create GenAI client for provider %s: %s",
+3
View File
@@ -13,6 +13,7 @@ from peewee import DoesNotExist
from frigate.comms.inter_process import InterProcessRequestor
from frigate.config import FrigateConfig
from frigate.config.camera.record import ChaptersEnum
from frigate.const import UPDATE_JOB_STATE
from frigate.jobs.job import Job
from frigate.models import Export
@@ -55,6 +56,7 @@ class ExportJob(Job):
ffmpeg_input_args: Optional[str] = None
ffmpeg_output_args: Optional[str] = None
cpu_fallback: bool = False
chapters: Optional[ChaptersEnum] = None
current_step: str = "queued"
progress_percent: float = 0.0
@@ -343,6 +345,7 @@ class ExportJobManager:
job.ffmpeg_input_args,
job.ffmpeg_output_args,
job.cpu_fallback,
job.chapters,
on_progress=self._make_progress_callback(job),
)
+413 -329
View File
@@ -3,6 +3,8 @@
import logging
import os
import threading
import time
from collections.abc import Callable, Generator, Iterable
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from dataclasses import asdict, dataclass, field
from datetime import datetime
@@ -19,6 +21,18 @@ from frigate.jobs.manager import (
get_job_by_id,
set_current_job,
)
from frigate.jobs.motion_search_batch import (
build_segment_time_map,
coalesce_runs,
stream_time_to_absolute,
)
from frigate.jobs.motion_search_decode import (
iter_vod_frames,
keyframe_sampling_eligible,
probe_video_dimensions,
probe_vod_keyframe_pts,
resolve_motion_decode_args,
)
from frigate.models import Recordings
from frigate.types import JobStatusTypesEnum
@@ -26,6 +40,18 @@ logger = logging.getLogger(__name__)
# Constants
HEATMAP_GRID_SIZE = 16
# Max wall-clock span of one VOD run request (seconds). Bounds per-request size
# and gives streaming/cancel/early-exit granularity.
MAX_RUN_SECONDS = 600.0
# Treat segments within this many seconds end-to-start as time-contiguous.
RUN_GAP_EPSILON = 1.0
# Longest-side pixels for the ROI downscale before motion detection.
SCALE_TARGET = 400
# Minimum wall seconds between intra-run progress broadcasts.
PROGRESS_BROADCAST_INTERVAL = 1.0
# Output frame rate for the fixed-cadence fallback used on long-GOP cameras
# (where keyframe sampling is too sparse). Keyframe cameras ignore this.
FALLBACK_SAMPLE_FPS = 2.0
@dataclass
@@ -69,13 +95,16 @@ class MotionSearchJob(Job):
polygon_points: list[list[float]] = field(default_factory=list)
threshold: int = 30
min_area: float = 5.0
frame_skip: int = 5
parallel: bool = False
max_results: int = 25
# Track progress
total_frames_processed: int = 0
# Live progress (ride the existing to_dict() websocket broadcast)
scanning_timestamp: Optional[float] = None
progress: float = 0.0
# Metrics for observability
metrics: Optional[MotionSearchMetrics] = None
@@ -100,6 +129,113 @@ def create_polygon_mask(
return mask
def compute_roi_crop_and_scale(
polygon_points: list[list[float]],
frame_width: int,
frame_height: int,
scale_target: int,
) -> tuple[tuple[int, int, int, int], tuple[int, int]]:
"""Compute the ROI crop box and never-upscale scaled dimensions.
Returns ((crop_w, crop_h, crop_x, crop_y), (scaled_w, scaled_h)) in pixels.
The crop is the polygon's bounding box in frame pixels; the scaled size fits
the crop's longest side to ``scale_target`` without ever enlarging it.
"""
xs = [p[0] for p in polygon_points]
ys = [p[1] for p in polygon_points]
# nv12 (4:2:0) hwdownload requires even crop offsets and even crop/scale
# dimensions; otherwise ffmpeg rounds the chroma planes and the raw byte
# stream stops matching the expected frame size. Force even values, and the
# mask is built from these same values so the two stay aligned.
crop_x = int(min(xs) * frame_width)
crop_y = int(min(ys) * frame_height)
crop_x -= crop_x % 2
crop_y -= crop_y % 2
crop_w = max(2, int(max(xs) * frame_width) - crop_x)
crop_h = max(2, int(max(ys) * frame_height) - crop_y)
crop_w -= crop_w % 2
crop_h -= crop_h % 2
longest = max(crop_w, crop_h)
factor = min(1.0, scale_target / longest)
scaled_w = max(2, round(crop_w * factor))
scaled_h = max(2, round(crop_h * factor))
scaled_w -= scaled_w % 2
scaled_h -= scaled_h % 2
return (crop_w, crop_h, crop_x, crop_y), (scaled_w, scaled_h)
def build_scaled_roi_mask(
polygon_points: list[list[float]],
frame_width: int,
frame_height: int,
crop: tuple[int, int, int, int],
scaled: tuple[int, int],
) -> np.ndarray:
"""Rasterize the polygon mask at the scaled ROI size.
Builds the full-resolution mask, crops it to the ROI box, and nearest-
neighbor resizes it to the scaled dimensions so it lines up exactly with the
frames ffmpeg crops and scales.
"""
crop_w, crop_h, crop_x, crop_y = crop
scaled_w, scaled_h = scaled
full_mask = create_polygon_mask(polygon_points, frame_width, frame_height)
cropped = full_mask[crop_y : crop_y + crop_h, crop_x : crop_x + crop_w]
return cv2.resize(cropped, (scaled_w, scaled_h), interpolation=cv2.INTER_NEAREST)
def detect_motion_scaled(
frames: Iterable[tuple[int, np.ndarray]],
mask: np.ndarray,
threshold: int,
min_area: float,
timestamp_fn: Callable[[int], float],
) -> list[MotionSearchResult]:
"""Detect motion across pre-cropped, pre-scaled gray frames.
``frames`` yields (absolute_frame_index, gray_roi_frame); ``mask`` is the
scaled ROI mask. ``min_area`` is a percentage of the masked ROI. Mirrors the
full-res detection math (absdiff -> blur -> threshold -> dilate -> contours)
on the already-reduced frames.
"""
results: list[MotionSearchResult] = []
mask_area = np.count_nonzero(mask)
if mask_area == 0:
return results
min_area_pixels = int((min_area / 100.0) * mask_area)
prev: np.ndarray | None = None
for frame_idx, gray in frames:
masked = cv2.bitwise_and(gray, gray, mask=mask)
if prev is not None:
diff = cv2.absdiff(prev, masked)
diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0)
_, thresh = cv2.threshold(diff_blurred, threshold, 255, cv2.THRESH_BINARY)
thresh_dilated = cv2.dilate(thresh, None, iterations=1) # type: ignore[call-overload]
thresh_masked = cv2.bitwise_and(thresh_dilated, thresh_dilated, mask=mask)
change_pixels = cv2.countNonZero(thresh_masked)
if change_pixels > min_area_pixels:
contours, _ = cv2.findContours(
thresh_masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
total_change_area = sum(
cv2.contourArea(c)
for c in contours
if cv2.contourArea(c) >= min_area_pixels
)
if total_change_area > 0:
change_percentage = (total_change_area / mask_area) * 100
results.append(
MotionSearchResult(
timestamp=timestamp_fn(frame_idx),
change_percentage=round(change_percentage, 2),
)
)
prev = masked
return results
def compute_roi_bbox_normalized(
polygon_points: list[list[float]],
) -> tuple[float, float, float, float]:
@@ -184,6 +320,22 @@ def segment_passes_heatmap_gate(
return heatmap_overlaps_roi(heatmap, roi_bbox)
def resolve_internal_port(config: FrigateConfig) -> int:
"""Return the unauthenticated internal nginx port for VOD requests."""
listen = config.networking.listen.internal
if isinstance(listen, str):
return int(listen.split(":")[-1])
return int(listen)
def build_vod_url(internal_port: int, camera: str, start: float, end: float) -> str:
"""Build the internal VOD HLS URL for a camera time range."""
return (
f"http://127.0.0.1:{internal_port}/vod/{camera}"
f"/start/{start}/end/{end}/index.m3u8"
)
class MotionSearchRunner(threading.Thread):
"""Thread-based runner for motion search jobs with parallel verification."""
@@ -206,6 +358,23 @@ class MotionSearchRunner(threading.Thread):
cpu_count = os.cpu_count() or 1
self.max_workers = min(4, cpu_count)
# Resolved once per job in _execute_search
self.ffmpeg_path: str = "ffmpeg"
self.ffprobe_path: str = "ffprobe"
self.decode_args: list[str] = []
# Keyframe sampling decision, decided once per job from the first run's
# GOP. The fallback cadence is a fixed rate (see FALLBACK_SAMPLE_FPS).
self.use_keyframe: bool = True
self.fps_rate: float = FALLBACK_SAMPLE_FPS
# ROI crop/scale + scaled mask, computed once from the VOD-stream
# dimensions (which can differ from the detect resolution).
self.crop: tuple[int, int, int, int] = (0, 0, 0, 0)
self.scaled: tuple[int, int] = (0, 0)
self.scaled_mask: np.ndarray = np.zeros((0, 0), dtype=np.uint8)
self.channels: int = 1
self.internal_port: int = 5000
self._last_progress_broadcast: float = 0.0
def run(self) -> None:
"""Execute the motion search job."""
try:
@@ -281,6 +450,9 @@ class MotionSearchRunner(threading.Thread):
if frame_width is None or frame_height is None:
raise ValueError(f"Camera {camera_name} detect dimensions not configured")
self.ffmpeg_path = camera_config.ffmpeg.ffmpeg_path
self.ffprobe_path = camera_config.ffmpeg.ffprobe_path
# Create polygon mask
polygon_mask = create_polygon_mask(
self.job.polygon_points, frame_width, frame_height
@@ -384,205 +556,274 @@ class MotionSearchRunner(threading.Thread):
self.metrics.heatmap_roi_skip_segments,
)
if self.job.parallel:
return self._search_motion_parallel(filtered_recordings, polygon_mask)
# Resolve decode backend (allowlisted hwaccel or software), coalesce the
# gate-passing segments into time-contiguous runs, and probe the first
# run's VOD stream once for dimensions + keyframe layout. VOD output is
# what we decode, so crop/scale/mask are computed against it.
self.internal_port = resolve_internal_port(self.config)
self.decode_args = resolve_motion_decode_args(camera_config)
ffprobe_path = self.ffprobe_path
return self._search_motion_sequential(filtered_recordings, polygon_mask)
runs = coalesce_runs(filtered_recordings, MAX_RUN_SECONDS, RUN_GAP_EPSILON)
if not runs:
return []
def _search_motion_parallel(
self,
recordings: list[Recordings],
polygon_mask: np.ndarray,
) -> list[MotionSearchResult]:
"""Search for motion in parallel across segments, streaming results."""
all_results: list[MotionSearchResult] = []
total_frames = 0
next_recording_idx_to_merge = 0
first_run = runs[0]
first_url = build_vod_url(
self.internal_port,
camera_name,
float(first_run[0].start_time),
float(first_run[-1].end_time),
)
dims = probe_video_dimensions(ffprobe_path, first_url)
if dims is None:
raise ValueError(f"Could not probe VOD dimensions for camera {camera_name}")
rec_width, rec_height, _rec_fps = dims
self.crop, self.scaled = compute_roi_crop_and_scale(
self.job.polygon_points, rec_width, rec_height, SCALE_TARGET
)
self.scaled_mask = build_scaled_roi_mask(
self.job.polygon_points, rec_width, rec_height, self.crop, self.scaled
)
self.channels = 1 # always gray output
# Decide keyframe vs fixed-cadence sampling once from the first run's GOP
# (keyframe structure is a per-camera constant).
first_pts = probe_vod_keyframe_pts(ffprobe_path, first_url)
self.use_keyframe = keyframe_sampling_eligible(first_pts)
logger.debug(
"Motion search job %s: starting motion search with %d workers "
"across %d segments",
"Motion search job %s: %d runs, sampling=%s, hwaccel=%s, vod=%dx%d",
self.job.id,
self.max_workers,
len(recordings),
len(runs),
"keyframe" if self.use_keyframe else "cadence",
bool(self.decode_args),
rec_width,
rec_height,
)
# Initialize partial results on the job so they stream to the frontend
return self._search_runs(runs)
def _emit_progress(self, abs_ts: float) -> None:
"""Throttled intra-run progress broadcast (scanning cursor)."""
now = time.monotonic()
if now - self._last_progress_broadcast < PROGRESS_BROADCAST_INTERVAL:
return
self._last_progress_broadcast = now
self.job.scanning_timestamp = abs_ts
self._broadcast_status()
def _detect_with_progress(
self,
indexed_frames: list[tuple[int, np.ndarray]],
timestamp_fn: Callable[[int], float],
) -> list[MotionSearchResult]:
"""Run detection while firing throttled progress as frames are scanned."""
def _gen() -> Generator[tuple[int, np.ndarray], None, None]:
for i, frame in indexed_frames:
if not self._should_stop():
self._emit_progress(timestamp_fn(i))
yield i, frame
return detect_motion_scaled(
_gen(),
self.scaled_mask,
self.job.threshold,
self.job.min_area,
timestamp_fn,
)
def _process_run(
self, run: list[Recordings]
) -> tuple[list[MotionSearchResult], int]:
"""Decode one run's VOD stream and detect motion.
Keyframe mode compares every decoded keyframe (free recall, since they
are all decoded anyway) paired with its probed PTS; if the decoded and
probed counts disagree (the decoder ignored ``-skip_frame nokey`` or the
stream is corrupt) this run re-runs in the fixed-cadence fallback.
Returns ``(results, frame_count)``.
"""
run_start: float = run[0].start_time # type: ignore[assignment]
run_end: float = run[-1].end_time # type: ignore[assignment]
vod_url = build_vod_url(self.internal_port, self.job.camera, run_start, run_end)
time_map = build_segment_time_map(run)
if self.use_keyframe:
kf_pts = probe_vod_keyframe_pts(self.ffprobe_path, vod_url)
frames = list(
iter_vod_frames(
self.ffmpeg_path,
vod_url,
self.scaled[0],
self.scaled[1],
self.channels,
self.decode_args,
self.crop,
self.scaled,
True,
self._should_stop,
skip_nonkey=True,
fps_rate=None,
)
)
if kf_pts and len(frames) == len(kf_pts):
abs_times = [stream_time_to_absolute(time_map, p) for p in kf_pts]
indexed = list(enumerate(frames))
def _ts_kf(i: int) -> float:
return abs_times[i]
results = self._detect_with_progress(indexed, _ts_kf)
return results, len(frames)
logger.debug(
"Keyframe count mismatch (%d decoded vs %d probed), using cadence",
len(frames),
len(kf_pts),
)
return self._process_run_cadence(vod_url, time_map)
def _process_run_cadence(
self, vod_url: str, time_map: list[tuple[float, float, float]]
) -> tuple[list[MotionSearchResult], int]:
"""Fixed-cadence fallback: fps-filtered VOD decode, evenly spaced times."""
frames = list(
iter_vod_frames(
self.ffmpeg_path,
vod_url,
self.scaled[0],
self.scaled[1],
self.channels,
self.decode_args,
self.crop,
self.scaled,
True,
self._should_stop,
skip_nonkey=False,
fps_rate=self.fps_rate,
)
)
indexed = list(enumerate(frames))
def _ts_fps(i: int) -> float:
return stream_time_to_absolute(time_map, i / self.fps_rate)
results = self._detect_with_progress(indexed, _ts_fps)
return results, len(frames)
def _merge_run(
self,
run: list[Recordings],
run_results: list[MotionSearchResult],
frames: int,
state: dict[str, Any],
) -> bool:
"""Fold one run's output into the running results; stream + dedup.
Returns True once ``max_results`` deduped hits have accumulated.
"""
state["completed_runs"] += 1
state["all_results"].extend(run_results)
state["total_frames"] += frames
self.job.total_frames_processed = state["total_frames"]
self.metrics.frames_decoded = state["total_frames"]
self.metrics.segments_processed += len(run)
self.job.progress = state["completed_runs"] / state["total_runs"]
state["all_results"].sort(key=lambda r: r.timestamp)
deduped = self._deduplicate_results(state["all_results"])[
: self.job.max_results
]
self.job.results = {
"results": [r.to_dict() for r in deduped],
"total_frames_processed": state["total_frames"],
}
self._broadcast_status()
return len(deduped) >= self.job.max_results
def _search_runs(self, runs: list[list[Recordings]]) -> list[MotionSearchResult]:
"""Decode runs (parallel pool when enabled), merge in order, stream."""
state: dict[str, Any] = {
"all_results": [],
"total_frames": 0,
"completed_runs": 0,
"total_runs": len(runs),
}
self.job.results = {"results": [], "total_frames_processed": 0}
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures: dict[Future, int] = {}
completed_segments: dict[int, tuple[list[MotionSearchResult], int]] = {}
logger.debug(
"Motion search job %s: searching %d runs (parallel=%s, workers=%d)",
self.job.id,
len(runs),
self.job.parallel,
self.max_workers,
)
for idx, recording in enumerate(recordings):
if self._should_stop():
break
if self.job.parallel and len(runs) > 1:
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
futures: dict[Future, int] = {}
for idx, run in enumerate(runs):
if self._should_stop():
break
futures[executor.submit(self._process_run, run)] = idx
rec_start: float = recording.start_time # type: ignore[assignment]
rec_end: float = recording.end_time # type: ignore[assignment]
future = executor.submit(
self._process_recording_for_motion,
str(recording.path),
rec_start,
rec_end,
self.job.start_time_range,
self.job.end_time_range,
polygon_mask,
self.job.threshold,
self.job.min_area,
self.job.frame_skip,
)
futures[future] = idx
completed: dict[int, tuple[list[MotionSearchResult], int]] = {}
next_idx = 0
for future in as_completed(futures):
if self._should_stop():
break
run_idx = futures[future]
try:
completed[run_idx] = future.result()
except Exception as e:
self.metrics.segments_with_errors += 1
logger.warning("Error processing run %d: %s", run_idx, e)
completed[run_idx] = ([], 0)
for future in as_completed(futures):
if self._should_stop():
# Cancel remaining futures
for f in futures:
f.cancel()
break
recording_idx = futures[future]
recording = recordings[recording_idx]
try:
results, frames = future.result()
self.metrics.segments_processed += 1
completed_segments[recording_idx] = (results, frames)
while next_recording_idx_to_merge in completed_segments:
segment_results, segment_frames = completed_segments.pop(
next_recording_idx_to_merge
)
all_results.extend(segment_results)
total_frames += segment_frames
self.job.total_frames_processed = total_frames
self.metrics.frames_decoded = total_frames
if segment_results:
deduped = self._deduplicate_results(all_results)
self.job.results = {
"results": [
r.to_dict() for r in deduped[: self.job.max_results]
],
"total_frames_processed": total_frames,
}
self._broadcast_status()
if segment_results and len(deduped) >= self.job.max_results:
while next_idx in completed:
run_results, frames = completed.pop(next_idx)
if self._merge_run(runs[next_idx], run_results, frames, state):
self.internal_stop_event.set()
for pending_future in futures:
pending_future.cancel()
for pending in futures:
pending.cancel()
break
next_recording_idx_to_merge += 1
next_idx += 1
if self.internal_stop_event.is_set():
break
else:
for run in runs:
if self._should_stop():
break
try:
run_results, frames = self._process_run(run)
except Exception as e:
self.metrics.segments_processed += 1
self.metrics.segments_with_errors += 1
self.metrics.segments_processed += len(run)
self._broadcast_status()
logger.warning(
"Error processing segment %s: %s",
recording.path,
e,
)
self.job.total_frames_processed = total_frames
self.metrics.frames_decoded = total_frames
logger.debug(
"Motion search job %s: motion search complete, "
"found %d raw results, decoded %d frames, %d segment errors",
self.job.id,
len(all_results),
total_frames,
self.metrics.segments_with_errors,
)
# Sort and deduplicate results
all_results.sort(key=lambda x: x.timestamp)
return self._deduplicate_results(all_results)[: self.job.max_results]
def _search_motion_sequential(
self,
recordings: list[Recordings],
polygon_mask: np.ndarray,
) -> list[MotionSearchResult]:
"""Search for motion sequentially across segments, streaming results."""
all_results: list[MotionSearchResult] = []
total_frames = 0
logger.debug(
"Motion search job %s: starting sequential motion search across %d segments",
self.job.id,
len(recordings),
)
self.job.results = {"results": [], "total_frames_processed": 0}
for recording in recordings:
if self.cancel_event.is_set():
break
try:
rec_start: float = recording.start_time # type: ignore[assignment]
rec_end: float = recording.end_time # type: ignore[assignment]
results, frames = self._process_recording_for_motion(
str(recording.path),
rec_start,
rec_end,
self.job.start_time_range,
self.job.end_time_range,
polygon_mask,
self.job.threshold,
self.job.min_area,
self.job.frame_skip,
)
all_results.extend(results)
total_frames += frames
self.job.total_frames_processed = total_frames
self.metrics.frames_decoded = total_frames
self.metrics.segments_processed += 1
if results:
all_results.sort(key=lambda x: x.timestamp)
deduped = self._deduplicate_results(all_results)[
: self.job.max_results
]
self.job.results = {
"results": [r.to_dict() for r in deduped],
"total_frames_processed": total_frames,
}
self._broadcast_status()
if results and len(deduped) >= self.job.max_results:
logger.warning("Error processing run: %s", e)
continue
if self._merge_run(run, run_results, frames, state):
break
except Exception as e:
self.metrics.segments_processed += 1
self.metrics.segments_with_errors += 1
self._broadcast_status()
logger.warning("Error processing segment %s: %s", recording.path, e)
self.job.total_frames_processed = total_frames
self.metrics.frames_decoded = total_frames
all_results: list[MotionSearchResult] = state["all_results"]
self.job.total_frames_processed = state["total_frames"]
self.metrics.frames_decoded = state["total_frames"]
self.job.progress = 1.0
logger.debug(
"Motion search job %s: sequential motion search complete, "
"found %d raw results, decoded %d frames, %d segment errors",
"Motion search job %s: complete, %d raw results, %d frames, %d errors",
self.job.id,
len(all_results),
total_frames,
state["total_frames"],
self.metrics.segments_with_errors,
)
all_results.sort(key=lambda x: x.timestamp)
all_results.sort(key=lambda r: r.timestamp)
return self._deduplicate_results(all_results)[: self.job.max_results]
def _deduplicate_results(
@@ -602,160 +843,6 @@ class MotionSearchRunner(threading.Thread):
return deduplicated
def _process_recording_for_motion(
self,
recording_path: str,
recording_start: float,
recording_end: float,
search_start: float,
search_end: float,
polygon_mask: np.ndarray,
threshold: int,
min_area: float,
frame_skip: int,
) -> tuple[list[MotionSearchResult], int]:
"""Process a single recording file for motion detection.
This method is designed to be called from a thread pool.
Args:
min_area: Minimum change area as a percentage of the ROI (0-100).
"""
results: list[MotionSearchResult] = []
frames_processed = 0
if not os.path.exists(recording_path):
logger.warning("Recording file not found: %s", recording_path)
return results, frames_processed
cap = cv2.VideoCapture(recording_path)
if not cap.isOpened():
logger.error("Could not open recording: %s", recording_path)
return results, frames_processed
try:
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
recording_duration = recording_end - recording_start
# Calculate frame range
start_offset = max(0, search_start - recording_start)
end_offset = min(recording_duration, search_end - recording_start)
start_frame = int(start_offset * fps)
end_frame = int(end_offset * fps)
start_frame = max(0, min(start_frame, total_frames - 1))
end_frame = max(0, min(end_frame, total_frames))
if start_frame >= end_frame:
return results, frames_processed
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
# Get ROI bounding box
roi_bbox = cv2.boundingRect(polygon_mask)
roi_x, roi_y, roi_w, roi_h = roi_bbox
prev_frame_gray = None
frame_step = max(frame_skip, 1)
frame_idx = start_frame
while frame_idx < end_frame:
if self._should_stop():
break
ret, frame = cap.read()
if not ret:
frame_idx += 1
continue
if (frame_idx - start_frame) % frame_step != 0:
frame_idx += 1
continue
frames_processed += 1
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# Handle frame dimension changes
if gray.shape != polygon_mask.shape:
resized_mask = cv2.resize(
polygon_mask,
(gray.shape[1], gray.shape[0]),
interpolation=cv2.INTER_NEAREST,
)
current_bbox = cv2.boundingRect(resized_mask)
else:
resized_mask = polygon_mask
current_bbox = roi_bbox
roi_x, roi_y, roi_w, roi_h = current_bbox
cropped_gray = gray[roi_y : roi_y + roi_h, roi_x : roi_x + roi_w]
cropped_mask = resized_mask[
roi_y : roi_y + roi_h, roi_x : roi_x + roi_w
]
cropped_mask_area = np.count_nonzero(cropped_mask)
if cropped_mask_area == 0:
frame_idx += 1
continue
# Convert percentage to pixel count for this ROI
min_area_pixels = int((min_area / 100.0) * cropped_mask_area)
masked_gray = cv2.bitwise_and(
cropped_gray, cropped_gray, mask=cropped_mask
)
if prev_frame_gray is not None:
diff = cv2.absdiff(prev_frame_gray, masked_gray) # type: ignore[unreachable]
diff_blurred = cv2.GaussianBlur(diff, (3, 3), 0)
_, thresh = cv2.threshold(
diff_blurred, threshold, 255, cv2.THRESH_BINARY
)
thresh_dilated = cv2.dilate(thresh, None, iterations=1)
thresh_masked = cv2.bitwise_and(
thresh_dilated, thresh_dilated, mask=cropped_mask
)
change_pixels = cv2.countNonZero(thresh_masked)
if change_pixels > min_area_pixels:
contours, _ = cv2.findContours(
thresh_masked, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
)
total_change_area = sum(
cv2.contourArea(c)
for c in contours
if cv2.contourArea(c) >= min_area_pixels
)
if total_change_area > 0:
frame_time_offset = (frame_idx - start_frame) / fps
timestamp = (
recording_start + start_offset + frame_time_offset
)
change_percentage = (
total_change_area / cropped_mask_area
) * 100
results.append(
MotionSearchResult(
timestamp=timestamp,
change_percentage=round(change_percentage, 2),
)
)
prev_frame_gray = masked_gray
frame_idx += 1
finally:
cap.release()
logger.debug(
"Motion search segment complete: %s, %d frames processed, %d results found",
recording_path,
frames_processed,
len(results),
)
return results, frames_processed
# Module-level state for managing per-camera jobs
_motion_search_jobs: dict[str, tuple[MotionSearchJob, threading.Event]] = {}
@@ -779,7 +866,6 @@ def start_motion_search_job(
polygon_points: list[list[float]],
threshold: int = 30,
min_area: float = 5.0,
frame_skip: int = 5,
parallel: bool = False,
max_results: int = 25,
) -> str:
@@ -794,7 +880,6 @@ def start_motion_search_job(
polygon_points=polygon_points,
threshold=threshold,
min_area=min_area,
frame_skip=frame_skip,
parallel=parallel,
max_results=max_results,
)
@@ -812,14 +897,13 @@ def start_motion_search_job(
logger.debug(
"Started motion search job %s for camera %s: "
"time_range=%.1f-%.1f, threshold=%d, min_area=%.1f%%, "
"frame_skip=%d, parallel=%s, max_results=%d, polygon_points=%d vertices",
"parallel=%s, max_results=%d, polygon_points=%d vertices",
job.id,
camera_name,
start_time,
end_time,
threshold,
min_area,
frame_skip,
parallel,
max_results,
len(polygon_points),
+75
View File
@@ -0,0 +1,75 @@
"""Pure helpers for VOD-batched motion search.
Coalescing gate-passing segments into time-contiguous runs, mapping a frame's
VOD stream time back to an absolute timestamp, and thinning sample times to a
target interval. No I/O or ffmpeg here so the tricky math stays unit-testable.
"""
from bisect import bisect_right
from typing import Any
def coalesce_runs(
segments: list[Any], max_seconds: float, epsilon: float
) -> list[list[Any]]:
"""Group gate-passing segments into time-contiguous runs.
A run extends while each segment's ``start_time`` is within ``epsilon`` of
the previous segment's ``end_time`` (no recording gap) and the run's total
span stays at or below ``max_seconds``. A gap or the cap starts a new run.
Each segment must expose ``start_time`` / ``end_time``.
"""
runs: list[list[Any]] = []
current: list[Any] = []
for seg in segments:
if not current:
current = [seg]
continue
prev_end = float(current[-1].end_time)
run_start = float(current[0].start_time)
contiguous = abs(float(seg.start_time) - prev_end) <= epsilon
within_cap = (float(seg.end_time) - run_start) <= max_seconds
if contiguous and within_cap:
current.append(seg)
else:
runs.append(current)
current = [seg]
if current:
runs.append(current)
return runs
def build_segment_time_map(
run: list[Any],
) -> list[tuple[float, float, float]]:
"""Build a (stream_offset, abs_start, duration) row per segment in a run.
``stream_offset`` is the segment's start in continuous VOD stream time (the
cumulative sum of preceding segment durations); ``abs_start`` is its absolute
``start_time``. Built from each segment's own duration; for a gap-free run
this makes stream time equal ``run_start + offset``.
"""
rows: list[tuple[float, float, float]] = []
offset = 0.0
for seg in run:
duration = float(seg.end_time) - float(seg.start_time)
rows.append((offset, float(seg.start_time), duration))
offset += duration
return rows
def stream_time_to_absolute(
time_map: list[tuple[float, float, float]], stream_time: float
) -> float:
"""Map a VOD stream time to an absolute timestamp via the run's table.
Binary-searches the segment whose stream range contains ``stream_time`` and
returns ``abs_start + (stream_time - stream_offset)``. Times past the last
segment map into the last segment (clamped at the run edge).
"""
offsets = [row[0] for row in time_map]
idx = bisect_right(offsets, stream_time) - 1
if idx < 0:
idx = 0
stream_offset, abs_start, _duration = time_map[idx]
return abs_start + (stream_time - stream_offset)
+382
View File
@@ -0,0 +1,382 @@
"""Hardware-accelerated ffmpeg decode for motion search.
Decodes a recording run's VOD/HLS stream with an ffmpeg subprocess, optionally
selecting only keyframes, and streams raw frames over a pipe for the motion
math. Output is the requested ``pix_fmt`` (gray or ``bgr24``) with optional
crop/scale applied in the filter graph so downstream pixels are unchanged.
"""
import json
import logging
import subprocess as sp
import tempfile
from collections.abc import Callable, Generator
from typing import IO
import numpy as np
from frigate.config import CameraConfig
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_decode
from frigate.util.services import auto_detect_hwaccel
logger = logging.getLogger(__name__)
# Output-format surfaces that download cleanly to nv12 via the fixed
# ``hwdownload,format=nv12`` step the decode path appends. Other surfaces
# (drm_prime from rkmpp, vulkan, amf) need a different download step, so motion
# search decodes them in software to keep results byte-identical rather than risk
# a wrong-but-valid-sized frame the zero-frame fallback gate would not catch.
_NV12_OUTPUT_FORMATS = frozenset({"vaapi", "cuda", "qsv"})
def _hwaccel_output_format(decode_args: list[str]) -> str | None:
"""Return the ``-hwaccel_output_format`` value in ffmpeg args, or None."""
try:
idx = decode_args.index("-hwaccel_output_format")
except ValueError:
return None
return decode_args[idx + 1] if idx + 1 < len(decode_args) else None
def resolve_motion_decode_args(camera_config: CameraConfig) -> list[str]:
"""Resolve the ffmpeg hwaccel decode args for a camera's recordings.
``auto`` is resolved via ``auto_detect_hwaccel`` and the preset is expanded
by ``parse_preset_hardware_acceleration_decode`` (the same table the live
pipeline uses). Acceleration is kept only when the decoded surface downloads
cleanly to nv12 -- decided by reading ``-hwaccel_output_format`` back from the
resolved args rather than a separate preset allowlist that could drift from
``PRESETS_HW_ACCEL_DECODE``. Anything else (custom args, a software-only
preset, or an nv12-incompatible surface) returns an empty list, meaning
software decode, so results stay byte-identical.
"""
raw = camera_config.ffmpeg.hwaccel_args
preset = auto_detect_hwaccel() if raw == "auto" else raw
# Custom args (a list) decode in software so results stay byte-identical.
if not isinstance(preset, str):
return []
decode_args = parse_preset_hardware_acceleration_decode(
preset,
camera_config.detect.fps,
camera_config.detect.width or 0,
camera_config.detect.height or 0,
camera_config.ffmpeg.gpu,
)
if not decode_args:
return []
if _hwaccel_output_format(decode_args) not in _NV12_OUTPUT_FORMATS:
return []
return decode_args
def _read_exact(stream: IO[bytes], size: int) -> bytes | None:
"""Read exactly ``size`` bytes from a pipe, or None at clean EOF.
Pipe reads can return fewer bytes than requested, so loop until the frame
is complete. A short read at the start of a frame means end-of-stream.
"""
buf = bytearray()
while len(buf) < size:
chunk = stream.read(size - len(buf))
if not chunk:
return None
buf.extend(chunk)
return bytes(buf)
def _terminate(proc: sp.Popen[bytes]) -> None:
"""Stop an ffmpeg decode process promptly."""
# Close the read end first so a blocked ffmpeg write unblocks (ffmpeg then
# sees a broken pipe), then signal it. The resulting ffmpeg write error is
# harmless and goes to the captured stderr.
if proc.stdout is not None:
try:
proc.stdout.close()
except OSError:
pass
if proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=5)
except sp.TimeoutExpired:
proc.kill()
proc.wait()
KEYFRAME_MAX_GAP_SECONDS = 2.0
def keyframe_sampling_eligible(
keyframe_pts: list[float], max_gap: float = KEYFRAME_MAX_GAP_SECONDS
) -> bool:
"""True if keyframes are dense and regular enough for keyframe-only sampling.
Requires at least two keyframes and no gap longer than ``max_gap`` seconds, so
a multi-second motion event necessarily spans a sampled keyframe.
"""
if len(keyframe_pts) < 2:
return False
gaps = [b - a for a, b in zip(keyframe_pts, keyframe_pts[1:])]
return max(gaps) <= max_gap
VOD_PROTOCOL_ARGS = ["-protocol_whitelist", "pipe,file,http,tcp"]
def build_vod_decode_command(
ffmpeg_path: str,
vod_url: str,
decode_args: list[str],
crop: tuple[int, int, int, int] | None,
scale: tuple[int, int] | None,
gray: bool,
*,
skip_nonkey: bool,
fps_rate: float | None,
) -> list[str]:
"""Build the ffmpeg argv to decode a VOD HLS URL.
``skip_nonkey`` adds ``-skip_frame nokey`` (keyframe-only). ``fps_rate`` adds
an ``fps`` filter for the fixed-cadence fallback. They are mutually
exclusive: keyframe mode passes ``skip_nonkey=True``/``fps_rate=None``; the
fallback passes ``skip_nonkey=False`` with a rate.
"""
filters: list[str] = []
# With hwaccel the decoded frames are GPU surfaces; pull them back to system
# memory before the CPU fps/crop/scale filters and the rawvideo encoder.
if decode_args:
filters.append("hwdownload")
filters.append("format=nv12")
if fps_rate is not None:
filters.append(f"fps={fps_rate}")
if crop is not None:
cw, ch, cx, cy = crop
filters.append(f"crop={cw}:{ch}:{cx}:{cy}")
if scale is not None:
sw, sh = scale
filters.append(f"scale={sw}:{sh}")
pix_fmt = "gray" if gray else "bgr24"
cmd = [ffmpeg_path, "-hide_banner", "-loglevel", "error"]
if skip_nonkey:
cmd += ["-skip_frame", "nokey"]
cmd += [*decode_args, *VOD_PROTOCOL_ARGS, "-i", vod_url, "-an"]
if filters:
cmd += ["-vf", ",".join(filters)]
cmd += ["-vsync", "0", "-f", "rawvideo", "-pix_fmt", pix_fmt, "pipe:"]
return cmd
def _run_vod_decode(
ffmpeg_path: str,
vod_url: str,
out_width: int,
out_height: int,
channels: int,
decode_args: list[str],
crop: tuple[int, int, int, int] | None,
scale: tuple[int, int] | None,
gray: bool,
should_stop: Callable[[], bool],
*,
skip_nonkey: bool,
fps_rate: float | None,
software_retry: bool,
) -> Generator[np.ndarray, None, None]:
"""Run one VOD decode, yielding raw frames; retry in software if empty."""
cmd = build_vod_decode_command(
ffmpeg_path,
vod_url,
decode_args,
crop,
scale,
gray,
skip_nonkey=skip_nonkey,
fps_rate=fps_rate,
)
frame_size = out_width * out_height * channels
stderr_file = tempfile.SpooledTemporaryFile(max_size=65536)
proc = sp.Popen(cmd, stdout=sp.PIPE, stderr=stderr_file)
assert proc.stdout is not None
count = 0
try:
while True:
if should_stop():
break
buf = _read_exact(proc.stdout, frame_size)
if buf is None:
break
if channels == 1:
frame = np.frombuffer(buf, dtype=np.uint8).reshape(
(out_height, out_width)
)
else:
frame = np.frombuffer(buf, dtype=np.uint8).reshape(
(out_height, out_width, channels)
)
count += 1
yield frame
finally:
_terminate(proc)
stderr_file.close()
if count == 0 and software_retry and not should_stop():
logger.warning("Hardware VOD decode produced no frames, retrying in software")
yield from _run_vod_decode(
ffmpeg_path,
vod_url,
out_width,
out_height,
channels,
[],
crop,
scale,
gray,
should_stop,
skip_nonkey=skip_nonkey,
fps_rate=fps_rate,
software_retry=False,
)
def iter_vod_frames(
ffmpeg_path: str,
vod_url: str,
out_width: int,
out_height: int,
channels: int,
decode_args: list[str],
crop: tuple[int, int, int, int] | None,
scale: tuple[int, int] | None,
gray: bool,
should_stop: Callable[[], bool],
*,
skip_nonkey: bool,
fps_rate: float | None,
) -> Generator[np.ndarray, None, None]:
"""Decode a VOD HLS URL and yield raw frames in order.
Pair keyframe-mode output with probed keyframe PTS; pair fallback output with
a fixed cadence. Falls back once to software decode if a hwaccel decode yields
no frames.
"""
yield from _run_vod_decode(
ffmpeg_path,
vod_url,
out_width,
out_height,
channels,
decode_args,
crop,
scale,
gray,
should_stop,
skip_nonkey=skip_nonkey,
fps_rate=fps_rate,
software_retry=bool(decode_args),
)
def probe_vod_keyframe_pts(ffprobe_path: str, vod_url: str) -> list[float]:
"""Return keyframe presentation timestamps (VOD stream time) in order.
Reads packet flags via ffprobe over the VOD URL (no decode). Returns [] on
any failure so the caller can fall back.
"""
cmd = [
ffprobe_path,
"-v",
"error",
*VOD_PROTOCOL_ARGS,
"-i",
vod_url,
"-select_streams",
"v:0",
"-show_packets",
"-show_entries",
"packet=pts_time,flags",
"-of",
"json",
]
try:
completed = sp.run(cmd, capture_output=True, text=True, timeout=120)
except (OSError, sp.SubprocessError):
logger.warning("ffprobe failed for VOD keyframe probe")
return []
if completed.returncode != 0 or not completed.stdout:
return []
try:
packets = json.loads(completed.stdout).get("packets", [])
except json.JSONDecodeError:
return []
pts: list[float] = []
for pkt in packets:
flags = pkt.get("flags", "")
pts_time = pkt.get("pts_time")
if flags.startswith("K") and pts_time is not None:
try:
pts.append(float(pts_time))
except ValueError:
continue
return sorted(pts)
def probe_video_dimensions(
ffprobe_path: str, recording_path: str
) -> tuple[int, int, float] | None:
"""Return (width, height, fps) for a recording's video stream, or None.
Reads stream metadata via ffprobe (no decode). The record stream resolution
can differ from the camera's detect resolution, so this is probed once per
job against a real segment.
"""
cmd = [
ffprobe_path,
"-v",
"error",
"-select_streams",
"v:0",
"-show_entries",
"stream=width,height,avg_frame_rate",
"-of",
"json",
recording_path,
]
try:
completed = sp.run(cmd, capture_output=True, text=True, timeout=30)
except (OSError, sp.SubprocessError):
return None
if completed.returncode != 0 or not completed.stdout:
return None
try:
streams = json.loads(completed.stdout).get("streams", [])
except json.JSONDecodeError:
return None
if not streams:
return None
stream = streams[0]
width = int(stream.get("width", 0) or 0)
height = int(stream.get("height", 0) or 0)
rate = stream.get("avg_frame_rate", "0/0") or "0/0"
try:
num, _, den = rate.partition("/")
fps = float(num) / float(den) if float(den) != 0 else 0.0
except (ValueError, ZeroDivisionError):
fps = 0.0
if width <= 0 or height <= 0:
return None
return width, height, fps
+29
View File
@@ -48,6 +48,22 @@ def ptz_moving_at_frame_time(frame_time, ptz_start_time, ptz_stop_time):
)
def transform_is_finite(coord_transformations) -> bool:
"""Return True if a norfair coordinate transform contains only finite values.
A near-singular homography (common when the motion estimator can't find
enough stable features during zoom on a low-texture scene) can produce
inf/nan matrix entries. norfair accumulates the homography across frames, so
a single bad transform poisons every subsequent one and propagates nan into
the tracker's distance function, crashing the camera process.
"""
for attr in ("homography_matrix", "inverse_homography_matrix", "movement_vector"):
value = getattr(coord_transformations, attr, None)
if value is not None and not np.all(np.isfinite(value)):
return False
return True
class PtzMotionEstimator:
def __init__(self, config: CameraConfig, ptz_metrics: PTZMetrics) -> None:
self.frame_manager = SharedMemoryFrameManager()
@@ -135,6 +151,19 @@ class PtzMotionEstimator:
)
self.coord_transformations = None
# A degenerate homography can yield non-finite transform values that
# norfair would accumulate and feed to the tracker as nan estimates.
# Drop the bad transform and request a reset so the estimator rebuilds
# a fresh reference frame instead of poisoning every following frame.
if self.coord_transformations is not None and not transform_is_finite(
self.coord_transformations
):
logger.warning(
f"Autotracker: motion estimator produced a non-finite transform for {camera} at frame time {frame_time}, resetting"
)
self.coord_transformations = None
self.ptz_metrics.reset.set()
try:
logger.debug(
f"{camera}: Motion estimator transformation: {self.coord_transformations.rel_to_abs([[0, 0]])}"
+246 -43
View File
@@ -17,6 +17,7 @@ import pytz # type: ignore[import-untyped]
from peewee import DoesNotExist
from frigate.config import FfmpegConfig, FrigateConfig
from frigate.config.camera.record import ChaptersEnum
from frigate.const import (
CACHE_DIR,
CLIPS_DIR,
@@ -42,33 +43,118 @@ TIMELAPSE_DATA_INPUT_ARGS = "-an -skip_frame nokey"
# Captures the floating-point factor so we can scale expected duration.
SETPTS_FACTOR_RE = re.compile(r"setpts=([0-9]*\.?[0-9]+)\*PTS")
# ffmpeg flags that can read from or write to arbitrary files
BLOCKED_FFMPEG_ARGS = frozenset(
# Allowlisted flags that take no value.
_VALUELESS_FLAGS = frozenset({"-an", "-sn", "-dn"})
# Allowlisted filter flags. Their value is validated as a filtergraph and may
# only reference filters in _SAFE_FILTERS.
_FILTER_FLAGS = frozenset({"-vf", "-af", "-filter"})
# Allowlisted flags that take exactly one value (encoder / muxer-safe options).
_VALUE_FLAGS = frozenset(
{
"-i",
"-filter_script",
"-filter_complex",
"-lavfi",
"-vf",
"-af",
"-filter",
"-vstats_file",
"-passlogfile",
"-sdp_file",
"-dump_attachment",
"-attach",
"-c",
"-codec",
"-b",
"-crf",
"-qp",
"-q",
"-qscale",
"-preset",
"-tune",
"-profile",
"-level",
"-pix_fmt",
"-r",
"-g",
"-keyint_min",
"-sc_threshold",
"-bf",
"-refs",
"-qmin",
"-qmax",
"-maxrate",
"-minrate",
"-bufsize",
"-movflags",
"-threads",
"-aspect",
"-fps_mode",
"-vsync",
"-skip_frame",
}
)
_ALLOWED_FLAGS = _VALUELESS_FLAGS | _FILTER_FLAGS | _VALUE_FLAGS
# Filters that cannot read files, load plugins, or open network sources.
_SAFE_FILTERS = frozenset(
{
"setpts",
"fps",
"scale",
"format",
"transpose",
"hflip",
"vflip",
"crop",
"pad",
"setsar",
"setdar",
}
)
# Conservative shape for a non-filter flag value. Excludes "/" (paths /
# filtergraph division), whitespace, brackets, and a leading "-" so a value
# can never be a path or swallow a following flag. ":" is permitted for values
# like "16:9".
_SAFE_VALUE_RE = re.compile(r"^[A-Za-z0-9_.:+][A-Za-z0-9_.:+-]*$")
# Substrings inside a filtergraph that indicate a file-reading filter option.
# "movie=" also matches "amovie=" as a substring.
_BLOCKED_FILTER_VALUE_MARKERS = ("movie=", "textfile=", "filename=", "fontfile=")
def _base_flag(token: str) -> str:
"""Return a flag's base name, lowercased and without its stream specifier.
e.g. "-c:v" -> "-c", "-filter:a:0" -> "-filter".
"""
return token.lower().split(":", 1)[0]
def _validate_filtergraph(value: str) -> tuple[bool, str]:
"""Validate a filtergraph value, allowing only filters in _SAFE_FILTERS."""
# None of the safe filters need any of these
if any(token in value for token in ("://", "..", "[", "]")):
return False, "Invalid filter graph in custom ffmpeg arguments"
lowered = value.lower()
if any(marker in lowered for marker in _BLOCKED_FILTER_VALUE_MARKERS):
return False, "File-reading filters are not allowed in custom ffmpeg arguments"
# Filters are separated by "," within a chain and ";" between chains. Safe
# filters never use unescaped "," or ";" in their arguments, so splitting on
# them to recover filter names cannot hide a disallowed filter.
for spec in re.split(r"[;,]", value):
spec = spec.strip()
if not spec:
continue
name = spec.split("=", 1)[0].strip().lower()
if name not in _SAFE_FILTERS:
return False, f"Filter not allowed in custom ffmpeg arguments: {name}"
return True, ""
def validate_ffmpeg_args(args: str) -> tuple[bool, str]:
"""Validate that user-provided ffmpeg args don't allow input/output injection.
"""Validate user-provided custom export ffmpeg args with an allowlist.
Blocks:
- The -i flag and other flags that read/write arbitrary files
- Filter flags (can read files via movie=/amovie= source filters)
- Absolute/relative file paths (potential extra outputs)
- URLs and ffmpeg protocol references (data exfiltration)
Every token must be an allowlisted flag or the value of one; filter values
may only reference safe filters; and no token may become a bare input or
output URL. This structurally prevents arbitrary file read/write, network
exfiltration/SSRF, and resource-exhaustion via the export endpoint.
Admin users skip this validation entirely since they are trusted.
"""
@@ -76,26 +162,36 @@ def validate_ffmpeg_args(args: str) -> tuple[bool, str]:
return True, ""
tokens = args.split()
for token in tokens:
# Block flags that could inject inputs or write to arbitrary files
if token.lower() in BLOCKED_FFMPEG_ARGS:
i = 0
while i < len(tokens):
token = tokens[i]
# A bare (non-flag) token here would be parsed by ffmpeg as an input or
# output URL. Only the server sets inputs/outputs, never the user.
if not token.startswith("-"):
return False, f"Unexpected argument in custom ffmpeg arguments: {token}"
base = _base_flag(token)
if base not in _ALLOWED_FLAGS:
return False, f"Forbidden ffmpeg argument: {token}"
# Block tokens that look like file paths (potential output injection)
if (
token.startswith("/")
or token.startswith("./")
or token.startswith("../")
or token.startswith("~")
):
return False, "File paths are not allowed in custom ffmpeg arguments"
if base in _VALUELESS_FLAGS:
i += 1
continue
# Block URLs and ffmpeg protocol references (e.g. http://, tcp://, pipe:, file:)
if "://" in token or token.startswith("pipe:") or token.startswith("file:"):
return (
False,
"Protocol references are not allowed in custom ffmpeg arguments",
)
# Remaining flags consume exactly one value.
if i + 1 >= len(tokens):
return False, f"Missing value for ffmpeg argument: {token}"
value = tokens[i + 1]
if base in _FILTER_FLAGS:
valid, message = _validate_filtergraph(value)
if not valid:
return False, message
elif not _SAFE_VALUE_RE.match(value):
return False, f"Invalid value for {token}: {value}"
i += 2
return True, ""
@@ -122,6 +218,7 @@ class RecordingExporter(threading.Thread):
ffmpeg_input_args: Optional[str] = None,
ffmpeg_output_args: Optional[str] = None,
cpu_fallback: bool = False,
chapters: Optional[ChaptersEnum] = None,
on_progress: Optional[Callable[[str, float], None]] = None,
) -> None:
super().__init__()
@@ -137,6 +234,7 @@ class RecordingExporter(threading.Thread):
self.ffmpeg_input_args = ffmpeg_input_args
self.ffmpeg_output_args = ffmpeg_output_args
self.cpu_fallback = cpu_fallback
self.chapters = chapters
self.on_progress = on_progress
# ensure export thumb dir
@@ -414,6 +512,74 @@ class RecordingExporter(threading.Thread):
return meta_path
def _build_recording_segment_chapter_metadata_file(
self, recordings: list
) -> Optional[str]:
"""Write an FFmpeg metadata file with one chapter per recording segment.
Each chapter's title is the segment's wallclock start time in
strict ISO 8601 form so a viewer can map any point in the
export's playback timeline back to real-world time without
OCR-ing a burnt-in timestamp. Chapter offsets are computed in
*output time*: the VOD endpoint concatenates recording clips
back-to-back, so wall-clock gaps between recordings collapse in
the produced video. Returns ``None`` when there are no
recordings or every segment is empty after clipping.
"""
if not recordings:
return None
tz_name = self.config.ui.timezone
tz: Optional[datetime.tzinfo] = None
if tz_name:
try:
tz = pytz.timezone(tz_name)
except pytz.UnknownTimeZoneError:
tz = None
if tz is None:
tz = datetime.timezone.utc
chapter_blocks: list[str] = []
output_offset_ms = 0
for rec in recordings:
clipped_start = max(float(rec.start_time), float(self.start_time))
clipped_end = min(float(rec.end_time), float(self.end_time))
if clipped_end <= clipped_start:
continue
duration_ms = int(round((clipped_end - clipped_start) * 1000))
if duration_ms <= 0:
continue
title = datetime.datetime.fromtimestamp(clipped_start, tz=tz).isoformat(
timespec="seconds"
)
chapter_blocks.append(
"[CHAPTER]\n"
"TIMEBASE=1/1000\n"
f"START={output_offset_ms}\n"
f"END={output_offset_ms + duration_ms}\n"
f"title={title}"
)
output_offset_ms += duration_ms
if not chapter_blocks:
return None
meta_path = self._chapter_metadata_path()
try:
with open(meta_path, "w", encoding="utf-8") as f:
f.write(";FFMETADATA1\n")
f.write("\n".join(chapter_blocks))
f.write("\n")
except OSError:
logger.exception(
"Failed to write chapter metadata file for export %s", self.export_id
)
return None
return meta_path
def save_thumbnail(self, id: str) -> str:
thumb_path = os.path.join(CLIPS_DIR, f"export/{id}.webp")
@@ -456,7 +622,7 @@ class RecordingExporter(threading.Thread):
diff = max(0.0, float(self.start_time) - float(preview.start_time))
ffmpeg_cmd = [
"/usr/lib/ffmpeg/7.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
"/usr/lib/ffmpeg/8.0/bin/ffmpeg", # hardcode path for exports thumbnail due to missing libwebp support
"-hide_banner",
"-loglevel",
"warning",
@@ -577,7 +743,18 @@ class RecordingExporter(threading.Thread):
)
).split(" ")
else:
chapters_path = self._build_chapter_metadata_file(recordings)
# Realtime/stream-copy export. Embed chapter metadata according to
# the camera's configured chapter mode: per-recording-segment
# timestamps or per-review-item titles.
if self.chapters == ChaptersEnum.recording_segments:
chapters_path = self._build_recording_segment_chapter_metadata_file(
recordings
)
elif self.chapters == ChaptersEnum.review_items:
chapters_path = self._build_chapter_metadata_file(recordings)
else:
chapters_path = None
chapter_args = (
f" -i {chapters_path} -map 0 -dn -map_metadata 1"
if chapters_path
@@ -589,7 +766,19 @@ class RecordingExporter(threading.Thread):
# add metadata
title = f"Frigate Recording for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
creation_time = datetime.datetime.fromtimestamp(
self.start_time, tz=datetime.timezone.utc
).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
ffmpeg_cmd.extend(
[
"-metadata",
f"title={title}",
"-metadata",
f"creation_time={creation_time}",
"-metadata",
f"comment=Camera: {self.camera}",
]
)
ffmpeg_cmd.append(video_path)
@@ -675,18 +864,32 @@ class RecordingExporter(threading.Thread):
self.config.ffmpeg.ffmpeg_path,
hwaccel_args,
f"{self.ffmpeg_input_args} {TIMELAPSE_DATA_INPUT_ARGS} {ffmpeg_input}".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart {video_path}".strip(),
f"{self.ffmpeg_output_args} -movflags +faststart".strip(),
EncodeTypeEnum.timelapse,
)
).split(" ")
else:
ffmpeg_cmd = (
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart {video_path}"
f"{self.config.ffmpeg.ffmpeg_path} -hide_banner {ffmpeg_input} {codec} -movflags +faststart"
).split(" ")
# add metadata
title = f"Frigate Preview for {self.camera}, {self.get_datetime_from_timestamp(self.start_time)} - {self.get_datetime_from_timestamp(self.end_time)}"
ffmpeg_cmd.extend(["-metadata", f"title={title}"])
creation_time = datetime.datetime.fromtimestamp(
self.start_time, tz=datetime.timezone.utc
).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
ffmpeg_cmd.extend(
[
"-metadata",
f"title={title}",
"-metadata",
f"creation_time={creation_time}",
"-metadata",
f"comment=Camera: {self.camera}",
]
)
ffmpeg_cmd.append(video_path)
return ffmpeg_cmd, playlist_lines
+21
View File
@@ -42,6 +42,8 @@ from frigate.util.services import get_video_properties
logger = logging.getLogger(__name__)
STALE_RECORDINGS_INFO_TTL = MAX_SEGMENTS_IN_CACHE * MAX_SEGMENT_DURATION * 2
class SegmentInfo:
def __init__(
@@ -301,6 +303,8 @@ class RecordingMaintainer(threading.Thread):
RecordingsDataTypeEnum.saved.value,
)
self._expire_stale_recordings_info(grouped_recordings)
recordings_to_insert: list[Optional[dict[str, Any]]] = await asyncio.gather(
*tasks
)
@@ -311,6 +315,21 @@ class RecordingMaintainer(threading.Thread):
[r for r in recordings_to_insert if r is not None],
)
def _expire_stale_recordings_info(
self, grouped_recordings: defaultdict[str, list[dict[str, Any]]]
) -> None:
expire_before = datetime.datetime.now().timestamp() - STALE_RECORDINGS_INFO_TTL
for recordings_info in (
self.object_recordings_info,
self.audio_recordings_info,
):
for camera in list(recordings_info.keys()):
if camera in grouped_recordings:
continue
info = recordings_info[camera]
while info and info[0][0] < expire_before:
info.pop(0)
def drop_segment(self, cache_path: str) -> None:
Path(cache_path).unlink(missing_ok=True)
self.end_time_cache.pop(cache_path, None)
@@ -631,6 +650,8 @@ class RecordingMaintainer(threading.Thread):
"copy",
"-movflags",
"+faststart",
"-metadata",
f"creation_time={start_time.strftime('%Y-%m-%dT%H:%M:%S.%fZ')}",
file_path,
stderr=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.DEVNULL,
@@ -0,0 +1,58 @@
from unittest.mock import AsyncMock, patch
from frigate.models import Event, Recordings, ReviewSegment
from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp
class TestHttpKeyframeAnalysis(BaseTestHttp):
def setUp(self):
super().setUp([Event, Recordings, ReviewSegment])
def test_invalid_camera_returns_404(self):
app = super().create_app()
with AuthTestClient(app) as client:
response = client.get("/keyframe_analysis?camera=does_not_exist")
assert response.status_code == 404
def test_record_disabled_returns_neutral(self):
# default minimal_config has recording disabled
app = super().create_app()
with AuthTestClient(app) as client:
response = client.get("/keyframe_analysis?camera=front_door")
assert response.status_code == 200
assert response.json()["severity"] == "record_disabled"
def test_probes_record_input_and_returns_severity(self):
self.minimal_config["cameras"]["front_door"]["ffmpeg"]["inputs"] = [
{
"path": "rtsp://10.0.0.1:554/record",
"roles": ["detect", "record"],
}
]
self.minimal_config["cameras"]["front_door"]["record"] = {"enabled": True}
app = super().create_app()
canned = {
"severity": "ok",
"keyframe_count": 5,
"max_gap": 1.0,
"mean_gap": 1.0,
"min_gap": 1.0,
"segment_time": 10,
"duration_observed": 4.0,
"thresholds": {"warning": 4.0, "error": 10},
}
with patch(
"frigate.api.camera.analyze_record_keyframes",
AsyncMock(return_value=canned),
) as mock_probe:
with AuthTestClient(app) as client:
response = client.get("/keyframe_analysis?camera=front_door")
assert response.status_code == 200
assert response.json()["severity"] == "ok"
# index matches the input carrying the record role ("Stream 1")
assert response.json()["stream_index"] == 0
# the record-role input path was probed
assert mock_probe.await_args.args[1] == "rtsp://10.0.0.1:554/record"
+72
View File
@@ -403,3 +403,75 @@ class TestHttpMedia(BaseTestHttp):
assert len(summary) == 1
assert "2024-03-10" in summary
assert summary["2024-03-10"] is True
def test_recordings_unavailable_reports_gap_between_recordings(self):
"""A gap between two recordings is reported as an unavailable segment."""
with AuthTestClient(self.app) as client:
# Two recordings with a 20s gap (1010-1030) between them.
Recordings.insert(
id="rec_a",
path="/media/recordings/a.mp4",
camera="front_door",
start_time=1000,
end_time=1010,
duration=10,
motion=0,
).execute()
Recordings.insert(
id="rec_b",
path="/media/recordings/b.mp4",
camera="front_door",
start_time=1030,
end_time=1040,
duration=10,
motion=0,
).execute()
response = client.get(
"/recordings/unavailable",
params={
"after": 1000,
"before": 1040,
"scale": 5,
"cameras": "front_door",
},
)
assert response.status_code == 200
assert response.json() == [{"start_time": 1010, "end_time": 1030}]
def test_recordings_unavailable_merges_overlapping_recordings(self):
"""Overlapping recordings are merged so no false gap is reported."""
with AuthTestClient(self.app) as client:
# Overlapping recordings spanning the whole requested range.
Recordings.insert(
id="rec_a",
path="/media/recordings/a.mp4",
camera="front_door",
start_time=1000,
end_time=1020,
duration=20,
motion=0,
).execute()
Recordings.insert(
id="rec_b",
path="/media/recordings/b.mp4",
camera="front_door",
start_time=1010,
end_time=1030,
duration=20,
motion=0,
).execute()
response = client.get(
"/recordings/unavailable",
params={
"after": 1000,
"before": 1030,
"scale": 5,
"cameras": "front_door",
},
)
assert response.status_code == 200
assert response.json() == []
+4 -7
View File
@@ -610,19 +610,16 @@ class TestHttpReview(BaseTestHttp):
response = client.get("/review/activity/motion", params=params)
assert response.status_code == 200
response_json = response.json()
assert len(response_json) == 61
# Only buckets with an actual recording are returned. Empty
# gap-fill buckets between the two recordings are dropped.
assert len(response_json) == 2
self.assertDictEqual(
{"motion": 50.5, "camera": "front_door", "start_time": now + 1},
response_json[0],
)
for item in response_json[1:-1]:
self.assertDictEqual(
{"motion": 0.0, "camera": "", "start_time": item["start_time"]},
item,
)
self.assertDictEqual(
{"motion": 100.0, "camera": "front_door", "start_time": one_m + 1},
response_json[len(response_json) - 1],
response_json[1],
)
####################################################################################################################
+41
View File
@@ -0,0 +1,41 @@
"""Tests for frigate.util.builtin helpers."""
import unittest
from unittest.mock import patch
from frigate.util.builtin import EventsPerSecond
class TestEventsPerSecond(unittest.TestCase):
def test_eps_is_zero_before_any_events(self) -> None:
eps = EventsPerSecond()
with patch("frigate.util.builtin.time.monotonic", return_value=100.0):
self.assertEqual(eps.eps(), 0.0)
def test_eps_counts_events_in_window(self) -> None:
eps = EventsPerSecond(last_n_seconds=10)
clock = [1000.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
# one event per second for five seconds
for _ in range(5):
clock[0] += 1.0
eps.update()
# five events over the five seconds since start
self.assertAlmostEqual(eps.eps(), 1.0)
def test_old_timestamps_expire_from_window(self) -> None:
eps = EventsPerSecond(last_n_seconds=10)
clock = [0.0]
with patch("frigate.util.builtin.time.monotonic", side_effect=lambda: clock[0]):
eps.start()
for _ in range(10):
clock[0] += 1.0
eps.update()
# jump well past the window so every timestamp ages out
clock[0] += 100.0
self.assertEqual(eps.eps(), 0.0)
if __name__ == "__main__":
unittest.main()
+132
View File
@@ -0,0 +1,132 @@
import unittest
from frigate.record.export import validate_ffmpeg_args
class TestValidateFfmpegArgs(unittest.TestCase):
"""Tests for the non-admin custom export ffmpeg arg validator.
The validator uses a structural allowlist: every token must be an
allowlisted flag or the value of one, filter values are restricted to a
safe set of filters, and no token may become a bare input/output URL.
"""
def assertRejected(self, args: str) -> None:
valid, message = validate_ffmpeg_args(args)
self.assertFalse(valid, f"expected {args!r} to be rejected")
self.assertNotEqual(message, "")
def assertAllowed(self, args: str) -> None:
valid, message = validate_ffmpeg_args(args)
self.assertTrue(valid, f"expected {args!r} to be allowed, got: {message}")
self.assertEqual(message, "")
# --- legitimate use cases must keep working ---------------------------
def test_timelapse_setpts_allowed(self):
# The whole reason -vf cannot simply be blocked: timelapse exports.
self.assertAllowed("-vf setpts=PTS/60 -r 25")
self.assertAllowed("-vf setpts=0.04*PTS -r 30") # server default
self.assertAllowed("-filter:v setpts=PTS/60 -r 25")
def test_default_input_args_allowed(self):
self.assertAllowed("")
self.assertAllowed("-an -skip_frame nokey")
def test_encoding_args_allowed(self):
self.assertAllowed("-c:v libx264 -crf 23 -preset fast")
self.assertAllowed("-c:v copy -c:a copy")
self.assertAllowed("-c:v libx264 -b:v 2M -maxrate 2M -bufsize 4M")
self.assertAllowed("-movflags +faststart")
self.assertAllowed("-pix_fmt yuv420p -r 30 -g 30")
def test_safe_filters_allowed(self):
self.assertAllowed("-vf scale=640:480")
self.assertAllowed("-vf scale=640:480,setpts=0.5*PTS")
self.assertAllowed("-vf format=yuv420p")
self.assertAllowed("-vf transpose=1")
self.assertAllowed("-vf hflip")
self.assertAllowed("-vf fps=15")
self.assertAllowed("-vf setsar=1 -an")
self.assertAllowed("-vf setdar=16/9")
# --- the reported advisory and file-read class ------------------------
def test_reported_advisory_rejected(self):
self.assertRejected(
"-filter:v drawtext=textfile=/etc/passwd:fontcolor=white:fontsize=20"
)
def test_file_reading_filters_rejected(self):
self.assertRejected("-vf movie=/etc/passwd")
self.assertRejected("-vf drawtext=textfile=/etc/passwd")
self.assertRejected("-vf subtitles=/etc/passwd")
# marker embedded as an option of an otherwise-allowed filter name
self.assertRejected("-vf scale=movie=/etc/passwd")
def test_filtergraph_brackets_rejected(self):
# link labels aren't needed for safe filters; rejecting "[" / "]" keeps
# filtergraph validation linear (no ReDoS on attacker input)
self.assertRejected("-vf [in]scale=640:480[out]")
self.assertRejected("-vf " + "[" * 5000)
def test_preset_file_read_rejected(self):
# cwd-anchored traversal slipped past the old startswith() path check
self.assertRejected("-fpre frigate/../../../etc/passwd")
self.assertRejected("-fpre evil.preset")
self.assertRejected("-vpre x")
self.assertRejected("-apre x")
self.assertRejected("-pre x")
def test_slash_option_file_read_rejected(self):
# ffmpeg "-/option file" reads the option value from a file
self.assertRejected("-/filter:v graph.txt")
self.assertRejected("-/filter_complex graph.txt")
# --- network / SSRF class ---------------------------------------------
def test_schemeless_protocol_rejected(self):
self.assertRejected("-f mpegts tcp:10.0.0.5:4444")
self.assertRejected("tcp:10.0.0.5:4444")
self.assertRejected("udp:10.0.0.5:4444")
self.assertRejected("-progress http:attacker.example.com:80/p")
# --- file-write class --------------------------------------------------
def test_tee_write_rejected(self):
self.assertRejected("-c:v libx264 -map 0 -f tee [f=mpegts]/tmp/owned.ts")
self.assertRejected("-f tee [f=mpegts]/etc/frigate/x.ts")
self.assertRejected("tee:/tmp/x")
def test_bare_output_token_rejected(self):
self.assertRejected("evil.mp4")
self.assertRejected("-c copy evil.mp4")
self.assertRejected("x/../escaped.mkv")
def test_file_producing_muxers_rejected(self):
self.assertRejected("-f hls -hls_segment_filename pwn%03d.ts out.m3u8")
self.assertRejected("-f md5 victim.txt")
self.assertRejected("-f segment seg%03d.ts")
def test_write_flags_rejected(self):
self.assertRejected("-progress evil.log")
self.assertRejected("-stats_enc_pre evil.csv")
self.assertRejected("-report")
# --- resource exhaustion / misc ---------------------------------------
def test_dos_input_flags_rejected(self):
self.assertRejected("-stream_loop -1")
self.assertRejected("-readrate 0.001")
def test_disallowed_flags_rejected(self):
self.assertRejected("-map 0")
self.assertRejected("-i /etc/passwd")
self.assertRejected("-attach evil.bin")
self.assertRejected("-dump_attachment evil.bin")
self.assertRejected("/etc/passwd")
self.assertRejected("-metadata comment=x")
if __name__ == "__main__":
unittest.main()
+175
View File
@@ -0,0 +1,175 @@
"""Unit tests for `deny_response_for_go2rtc_stream`.
Covers the camera-level authorization enforced in the `/auth` subrequest for
the nginx-proxied go2rtc live-stream paths (MSE/WebRTC WebSockets and the
WebRTC signaling endpoint). These paths name the stream via the `src` query
param, which the static-media auth in `media_auth` does not inspect.
"""
import types
import unittest
from frigate.api.auth import deny_response_for_go2rtc_stream
from frigate.config import FrigateConfig
_CONFIG = {
"mqtt": {"host": "mqtt"},
"auth": {
"roles": {
"limited_user": ["front_door"],
"dual_user": ["front_door", "back_door"],
}
},
"cameras": {
"front_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.1:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
# go2rtc stream name differs from the camera name (substream)
"live": {"streams": {"Main Stream": "front_door_sub"}},
},
"back_door": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.2:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
"garage": {
"ffmpeg": {
"inputs": [{"path": "rtsp://10.0.0.3:554/video", "roles": ["detect"]}]
},
"detect": {"height": 1080, "width": 1920, "fps": 5},
},
},
}
def _request(config: FrigateConfig) -> types.SimpleNamespace:
return types.SimpleNamespace(app=types.SimpleNamespace(frigate_config=config))
class TestDenyResponseForGo2rtcStream(unittest.TestCase):
def setUp(self) -> None:
self.config = FrigateConfig(**_CONFIG)
self.request = _request(self.config)
def _deny(self, url: str, role: str):
return deny_response_for_go2rtc_stream(url, role, self.request)
# --- non-stream paths pass through ---
def test_non_stream_path_passes_through(self):
self.assertIsNone(
self._deny("http://host/clips/back_door-1.jpg", "limited_user")
)
def test_empty_url_passes_through(self):
self.assertIsNone(self._deny("", "limited_user"))
def test_jsmpeg_path_not_handled_here(self):
# jsmpeg is authorized per-frame in the output pipeline, not here
self.assertIsNone(
self._deny("http://host/live/jsmpeg/back_door", "limited_user")
)
# --- restricted role: allowed vs forbidden cameras ---
def test_mse_allowed_camera(self):
self.assertIsNone(
self._deny("http://host/live/mse/api/ws?src=front_door", "limited_user")
)
def test_mse_forbidden_camera_denied(self):
self.assertEqual(
self._deny("http://host/live/mse/api/ws?src=back_door", "limited_user"),
403,
)
def test_webrtc_ws_forbidden_camera_denied(self):
self.assertEqual(
self._deny("http://host/live/webrtc/api/ws?src=back_door", "limited_user"),
403,
)
def test_webrtc_signaling_forbidden_camera_denied(self):
self.assertEqual(
self._deny("http://host/api/go2rtc/webrtc?src=back_door", "limited_user"),
403,
)
def test_unknown_camera_denied(self):
self.assertEqual(
self._deny("http://host/live/mse/api/ws?src=nonexistent", "limited_user"),
403,
)
def test_missing_src_denied(self):
self.assertEqual(self._deny("http://host/live/mse/api/ws", "limited_user"), 403)
# --- multi-camera role: each assigned camera allowed, others denied ---
def test_multi_camera_role_allows_first_assigned(self):
self.assertIsNone(
self._deny("http://host/live/mse/api/ws?src=front_door", "dual_user")
)
def test_multi_camera_role_allows_second_assigned(self):
self.assertIsNone(
self._deny("http://host/live/mse/api/ws?src=back_door", "dual_user")
)
def test_multi_camera_role_denies_unassigned(self):
# garage is configured but not in dual_user's allow-list
self.assertEqual(
self._deny("http://host/live/mse/api/ws?src=garage", "dual_user"),
403,
)
# --- substream names resolve to their owning camera ---
def test_allowed_substream_resolves_to_owning_camera(self):
# front_door_sub is owned by front_door, which limited_user may access
self.assertIsNone(
self._deny("http://host/live/mse/api/ws?src=front_door_sub", "limited_user")
)
# --- multiple src values: deny if any is forbidden ---
def test_multiple_src_one_forbidden_denied(self):
self.assertEqual(
self._deny(
"http://host/live/mse/api/ws?src=front_door&src=back_door",
"limited_user",
),
403,
)
def test_multiple_src_all_allowed(self):
self.assertIsNone(
self._deny(
"http://host/live/mse/api/ws?src=front_door&src=front_door_sub",
"limited_user",
)
)
# --- privileged roles bypass the check ---
def test_admin_bypasses(self):
self.assertIsNone(
self._deny("http://host/live/mse/api/ws?src=back_door", "admin")
)
def test_builtin_viewer_role_bypasses(self):
# the built-in viewer role is not in the config allow-list map, so it
# is treated as full access
self.assertIsNone(
self._deny("http://host/live/mse/api/ws?src=back_door", "viewer")
)
def test_missing_role_bypasses(self):
self.assertIsNone(self._deny("http://host/live/mse/api/ws?src=back_door", None))
if __name__ == "__main__":
unittest.main()
+111
View File
@@ -0,0 +1,111 @@
"""Tests for keyframe-spacing analysis used to detect smart/+ codecs."""
import asyncio
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from frigate.util.services import (
analyze_record_keyframes,
classify_keyframe_gaps,
parse_keyframe_packets,
)
class TestClassifyKeyframeGaps(unittest.TestCase):
def test_ok_when_gaps_small(self):
# keyframes every ~1s
pts = [0.0, 1.0, 2.0, 3.0, 4.0]
result = classify_keyframe_gaps(pts, segment_time=10)
self.assertEqual(result["severity"], "ok")
self.assertEqual(result["max_gap"], 1.0)
self.assertEqual(result["keyframe_count"], 5)
self.assertEqual(result["thresholds"], {"warning": 4.0, "error": 10})
def test_warning_when_gap_exceeds_four_seconds(self):
pts = [0.0, 1.0, 6.5] # 5.5s gap
result = classify_keyframe_gaps(pts, segment_time=10)
self.assertEqual(result["severity"], "warning")
self.assertEqual(result["max_gap"], 5.5)
def test_error_when_gap_exceeds_segment_time(self):
pts = [0.0, 12.0] # 12s gap > 10s segment
result = classify_keyframe_gaps(pts, segment_time=10)
self.assertEqual(result["severity"], "error")
def test_error_threshold_tracks_segment_time(self):
pts = [0.0, 6.0] # 6s gap, segment_time=5 -> error
result = classify_keyframe_gaps(pts, segment_time=5)
self.assertEqual(result["severity"], "error")
def test_unknown_with_single_keyframe(self):
result = classify_keyframe_gaps([1.0], segment_time=10)
self.assertEqual(result["severity"], "unknown")
self.assertIsNone(result["max_gap"])
self.assertEqual(result["keyframe_count"], 1)
def test_unknown_with_no_keyframes(self):
result = classify_keyframe_gaps([], segment_time=10)
self.assertEqual(result["severity"], "unknown")
self.assertEqual(result["keyframe_count"], 0)
class TestParseKeyframePackets(unittest.TestCase):
def test_extracts_keyframe_pts_and_max(self):
output = "0.000000,K__\n0.033333,___\n1.000000,K__\n1.500000,___\n"
keyframe_pts, max_pts = parse_keyframe_packets(output)
self.assertEqual(keyframe_pts, [0.0, 1.0])
self.assertEqual(max_pts, 1.5)
def test_skips_unparseable_and_empty_lines(self):
output = "N/A,K__\n\n2.0,K__\nbad line\n"
keyframe_pts, max_pts = parse_keyframe_packets(output)
self.assertEqual(keyframe_pts, [2.0])
self.assertEqual(max_pts, 2.0)
def test_empty_output(self):
keyframe_pts, max_pts = parse_keyframe_packets("")
self.assertEqual(keyframe_pts, [])
self.assertIsNone(max_pts)
class TestAnalyzeRecordKeyframes(unittest.IsolatedAsyncioTestCase):
async def test_merges_duration_and_classification(self):
csv = b"0.0,K__\n1.0,___\n6.0,K__\n7.0,___\n"
proc = MagicMock()
proc.communicate = AsyncMock(return_value=(csv, b""))
ffmpeg = MagicMock()
ffmpeg.ffprobe_path = "/usr/bin/ffprobe"
with patch(
"frigate.util.services.asyncio.create_subprocess_exec",
AsyncMock(return_value=proc),
):
result = await analyze_record_keyframes(
ffmpeg, "rtsp://cam/stream", segment_time=10
)
self.assertEqual(result["severity"], "warning") # 6s gap > 4s
self.assertEqual(result["max_gap"], 6.0)
self.assertEqual(result["duration_observed"], 7.0)
async def test_timeout_returns_unknown(self):
proc = MagicMock()
proc.communicate = AsyncMock(side_effect=asyncio.TimeoutError())
proc.kill = MagicMock()
ffmpeg = MagicMock()
ffmpeg.ffprobe_path = "/usr/bin/ffprobe"
with patch(
"frigate.util.services.asyncio.create_subprocess_exec",
AsyncMock(return_value=proc),
):
result = await analyze_record_keyframes(
ffmpeg, "rtsp://cam/stream", segment_time=10
)
self.assertEqual(result["severity"], "unknown")
proc.kill.assert_called_once()
if __name__ == "__main__":
unittest.main()
+40
View File
@@ -115,6 +115,46 @@ class TestMaintainer(unittest.IsolatedAsyncioTestCase):
self.assertIsNone(result)
maintainer.drop_segment.assert_called_once_with(cache_path)
async def test_expire_stale_recordings_info_drops_only_absent_cameras(self):
config = MagicMock(spec=FrigateConfig)
config.cameras = {}
stop_event = MagicMock()
maintainer = RecordingMaintainer(config, stop_event)
now = datetime.datetime.now().timestamp()
ancient = now - 86400
recent = now - 1
maintainer.object_recordings_info["present_cam"] = [(ancient, [], [], [])]
maintainer.audio_recordings_info["present_cam"] = [(ancient, 0, [])]
maintainer.object_recordings_info["absent_cam"] = [
(ancient, [], [], []),
(recent, [], [], []),
]
maintainer.audio_recordings_info["absent_cam"] = [
(ancient, 0, []),
(recent, 0, []),
]
grouped_recordings = {"present_cam": [{"start_time": ancient}]}
maintainer._expire_stale_recordings_info(grouped_recordings)
self.assertEqual(
maintainer.object_recordings_info["present_cam"], [(ancient, [], [], [])]
)
self.assertEqual(
maintainer.audio_recordings_info["present_cam"], [(ancient, 0, [])]
)
self.assertEqual(
maintainer.object_recordings_info["absent_cam"], [(recent, [], [], [])]
)
self.assertEqual(
maintainer.audio_recordings_info["absent_cam"], [(recent, 0, [])]
)
if __name__ == "__main__":
unittest.main()
+58
View File
@@ -0,0 +1,58 @@
"""Tests for motion search batch helpers (runs + timestamp mapping)."""
import unittest
from dataclasses import dataclass
from frigate.jobs.motion_search_batch import (
build_segment_time_map,
coalesce_runs,
stream_time_to_absolute,
)
@dataclass
class _Seg:
path: str
start_time: float
end_time: float
def _run_seconds(run):
return float(run[-1].end_time) - float(run[0].start_time)
class TestCoalesceRuns(unittest.TestCase):
def test_contiguous_segments_form_one_run(self):
segs = [_Seg("a", 0.0, 10.0), _Seg("b", 10.0, 20.0), _Seg("c", 20.0, 30.0)]
runs = coalesce_runs(segs, max_seconds=600.0, epsilon=0.5)
self.assertEqual(len(runs), 1)
self.assertEqual(len(runs[0]), 3)
def test_time_gap_splits_runs(self):
# b ends 20, c starts 25 -> 5s gap > epsilon -> two runs.
segs = [_Seg("a", 0.0, 10.0), _Seg("b", 10.0, 20.0), _Seg("c", 25.0, 35.0)]
runs = coalesce_runs(segs, max_seconds=600.0, epsilon=0.5)
self.assertEqual([len(r) for r in runs], [2, 1])
def test_max_duration_caps_a_run(self):
# Five contiguous 10s segments, cap 25s.
segs = [_Seg(str(i), i * 10.0, i * 10.0 + 10.0) for i in range(5)]
runs = coalesce_runs(segs, max_seconds=25.0, epsilon=0.5)
self.assertTrue(all(_run_seconds(r) <= 30.0 for r in runs))
self.assertEqual(sum(len(r) for r in runs), 5)
def test_empty(self):
self.assertEqual(coalesce_runs([], max_seconds=600.0, epsilon=0.5), [])
class TestTimestampMapping(unittest.TestCase):
def test_gapfree_run_maps_to_start_plus_pts(self):
run = [_Seg("a", 1000.0, 1010.0), _Seg("b", 1010.0, 1020.0)]
time_map = build_segment_time_map(run)
self.assertAlmostEqual(stream_time_to_absolute(time_map, 3.0), 1003.0)
self.assertAlmostEqual(stream_time_to_absolute(time_map, 12.0), 1012.0)
def test_past_end_clamps(self):
run = [_Seg("a", 1000.0, 1010.0)]
time_map = build_segment_time_map(run)
self.assertAlmostEqual(stream_time_to_absolute(time_map, 9.9), 1009.9)
+190
View File
@@ -0,0 +1,190 @@
"""Tests for the motion search hardware-accelerated decode helpers."""
import unittest
from types import SimpleNamespace
from unittest import mock
from frigate.jobs.motion_search_decode import (
KEYFRAME_MAX_GAP_SECONDS,
build_vod_decode_command,
keyframe_sampling_eligible,
probe_video_dimensions,
probe_vod_keyframe_pts,
resolve_motion_decode_args,
)
def _fake_camera_config(
hwaccel_args, gpu=0, fps=5, width=1280, height=720, ffmpeg_path="ffmpeg"
):
return SimpleNamespace(
ffmpeg=SimpleNamespace(
hwaccel_args=hwaccel_args, gpu=gpu, ffmpeg_path=ffmpeg_path
),
detect=SimpleNamespace(fps=fps, width=width, height=height),
)
class TestResolveMotionDecodeArgs(unittest.TestCase):
def test_vaapi_preset_is_accelerated(self):
args = resolve_motion_decode_args(_fake_camera_config("preset-vaapi"))
self.assertIn("-hwaccel", args)
self.assertIn("vaapi", args)
def test_non_nv12_preset_falls_back_to_software(self):
# rkmpp produces drm_prime surfaces that do not download to nv12, so it
# must resolve to software decode (empty args) rather than risk corrupt
# frames.
self.assertEqual(
resolve_motion_decode_args(_fake_camera_config("preset-rkmpp")), []
)
def test_custom_args_fall_back_to_software(self):
# Arbitrary custom hwaccel args (a list, not a preset) decode in software
# to preserve byte-identical results.
self.assertEqual(
resolve_motion_decode_args(_fake_camera_config(["-hwaccel", "vulkan"])),
[],
)
def test_nvidia_codec_preset_is_accelerated(self):
# Codec-specific nvidia presets resolve to the same cuda decode args as
# the bare preset, so eligibility is derived from -hwaccel_output_format
# rather than a hardcoded list that omitted these aliases.
args = resolve_motion_decode_args(_fake_camera_config("preset-nvidia-h264"))
self.assertIn("-hwaccel_output_format", args)
self.assertIn("cuda", args)
def test_software_only_preset_falls_back_to_software(self):
# A preset with no -hwaccel_output_format (decoder-based, no GPU surface)
# cannot use the nv12 download step, so it decodes in software.
self.assertEqual(
resolve_motion_decode_args(_fake_camera_config("preset-rpi-64-h264")), []
)
class TestKeyframeEligibility(unittest.TestCase):
def test_regular_short_gop_is_eligible(self):
pts = [0.0, 0.5, 1.0, 1.5, 2.0] # 0.5s gaps
self.assertTrue(keyframe_sampling_eligible(pts))
def test_long_gop_is_ineligible(self):
pts = [0.0, 5.0, 10.0] # 5s gaps
self.assertFalse(keyframe_sampling_eligible(pts))
def test_irregular_gop_ineligible_when_a_gap_is_long(self):
pts = [0.0, 0.5, 1.0, 8.0] # one 7s gap
self.assertFalse(keyframe_sampling_eligible(pts))
def test_too_few_keyframes_ineligible(self):
self.assertFalse(keyframe_sampling_eligible([1.0]))
self.assertFalse(keyframe_sampling_eligible([]))
def test_default_max_gap_constant(self):
self.assertEqual(KEYFRAME_MAX_GAP_SECONDS, 2.0)
class TestVodDecodeCommand(unittest.TestCase):
URL = "http://127.0.0.1:5000/vod/cam/start/1/end/2/index.m3u8"
def test_keyframe_command_shape(self):
cmd = build_vod_decode_command(
"ffmpeg",
self.URL,
decode_args=[],
crop=(100, 80, 10, 20),
scale=(50, 40),
gray=True,
skip_nonkey=True,
fps_rate=None,
)
joined = " ".join(cmd)
self.assertIn("-skip_frame nokey", joined)
self.assertIn("-protocol_whitelist pipe,file,http,tcp", joined)
self.assertIn(f"-i {self.URL}", joined)
self.assertIn("crop=100:80:10:20", joined)
self.assertIn("scale=50:40", joined)
self.assertIn("-pix_fmt gray", joined)
self.assertNotIn("fps=", joined)
def test_fps_command_uses_fps_filter_not_skip_frame(self):
cmd = build_vod_decode_command(
"ffmpeg",
self.URL,
decode_args=[],
crop=None,
scale=None,
gray=False,
skip_nonkey=False,
fps_rate=2.0,
)
joined = " ".join(cmd)
self.assertNotIn("skip_frame", joined)
self.assertIn("fps=2.0", joined)
self.assertIn("-pix_fmt bgr24", joined)
def test_hwaccel_inserts_hwdownload(self):
cmd = build_vod_decode_command(
"ffmpeg",
self.URL,
decode_args=["-hwaccel", "vaapi"],
crop=None,
scale=None,
gray=True,
skip_nonkey=True,
fps_rate=None,
)
joined = " ".join(cmd)
self.assertIn("hwdownload", joined)
self.assertIn("format=nv12", joined)
class TestProbeVodKeyframePts(unittest.TestCase):
def test_parses_keyframe_packets(self):
sample = (
'{"packets":['
'{"pts_time":"0.000000","flags":"K__"},'
'{"pts_time":"1.000000","flags":"___"},'
'{"pts_time":"2.000000","flags":"K__"}]}'
)
completed = mock.Mock(stdout=sample, returncode=0)
with mock.patch(
"frigate.jobs.motion_search_decode.sp.run", return_value=completed
):
pts = probe_vod_keyframe_pts("ffprobe", "http://x/index.m3u8")
self.assertEqual(pts, [0.0, 2.0])
def test_returns_empty_on_failure(self):
with mock.patch(
"frigate.jobs.motion_search_decode.sp.run",
side_effect=OSError("boom"),
):
self.assertEqual(probe_vod_keyframe_pts("ffprobe", "http://x"), [])
class TestProbeVideoDimensions(unittest.TestCase):
def test_parses_dimensions_and_fps(self):
sample = (
'{"streams":[{"width":1920,"height":1080,"avg_frame_rate":"30000/1001"}]}'
)
completed = mock.Mock(stdout=sample, returncode=0)
with mock.patch(
"frigate.jobs.motion_search_decode.sp.run", return_value=completed
):
dims = probe_video_dimensions("ffprobe", "/tmp/a.mp4")
assert dims is not None
width, height, fps = dims
self.assertEqual((width, height), (1920, 1080))
self.assertAlmostEqual(fps, 29.97, places=2)
def test_returns_none_on_zero_dimensions(self):
sample = '{"streams":[{"width":0,"height":0,"avg_frame_rate":"0/0"}]}'
completed = mock.Mock(stdout=sample, returncode=0)
with mock.patch(
"frigate.jobs.motion_search_decode.sp.run", return_value=completed
):
self.assertIsNone(probe_video_dimensions("ffprobe", "/tmp/a.mp4"))
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,87 @@
"""Tests for motion search spatial (crop/scale/mask) helpers."""
import unittest
import numpy as np
from frigate.jobs.motion_search import (
build_scaled_roi_mask,
compute_roi_crop_and_scale,
detect_motion_scaled,
)
class TestComputeRoiCropAndScale(unittest.TestCase):
def test_crop_box_in_record_pixels(self):
# ROI covering x [0.25, 0.75], y [0.5, 1.0] of a 1000x600 frame.
polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]]
crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125)
cw, ch, cx, cy = crop
self.assertEqual((cx, cy), (250, 300))
self.assertEqual((cw, ch), (500, 300))
# longest side 500 -> factor 0.25 -> (125, 75), rounded down to even.
self.assertEqual(scaled, (124, 74))
def test_never_upscales(self):
polygon = [[0.0, 0.0], [0.1, 0.0], [0.1, 0.1], [0.0, 0.1]]
crop, scaled = compute_roi_crop_and_scale(polygon, 200, 200, scale_target=400)
cw, ch, _, _ = crop
# crop is 20x20; target 400 would upscale, so scaled == crop size.
self.assertEqual(scaled, (cw, ch))
def test_scaled_dims_are_at_least_one(self):
polygon = [[0.0, 0.0], [0.02, 0.0], [0.02, 0.02], [0.0, 0.02]]
crop, scaled = compute_roi_crop_and_scale(polygon, 50, 50, scale_target=1)
self.assertGreaterEqual(scaled[0], 1)
self.assertGreaterEqual(scaled[1], 1)
def test_all_dims_are_even_for_nv12(self):
# Odd-aligned ROI on an odd-ish frame must still yield even crop/scale so
# the nv12 hwdownload byte stream matches the expected frame size.
polygon = [[0.123, 0.321], [0.777, 0.321], [0.777, 0.901], [0.123, 0.901]]
crop, scaled = compute_roi_crop_and_scale(polygon, 1377, 911, scale_target=257)
for value in (*crop, *scaled):
self.assertEqual(value % 2, 0, f"{value} is not even")
class TestBuildScaledRoiMask(unittest.TestCase):
def test_mask_matches_scaled_dims_and_has_coverage(self):
polygon = [[0.25, 0.5], [0.75, 0.5], [0.75, 1.0], [0.25, 1.0]]
crop, scaled = compute_roi_crop_and_scale(polygon, 1000, 600, scale_target=125)
mask = build_scaled_roi_mask(polygon, 1000, 600, crop, scaled)
self.assertEqual(mask.shape, (scaled[1], scaled[0]))
self.assertEqual(mask.dtype, np.uint8)
# A full rectangle ROI fills its whole crop -> mask is all 255.
self.assertGreater(np.count_nonzero(mask), 0)
self.assertEqual(np.count_nonzero(mask), mask.size)
class TestDetectMotionScaled(unittest.TestCase):
def _ts(self, idx):
return float(idx)
def test_finds_change_between_frames(self):
mask = np.full((60, 80), 255, dtype=np.uint8)
f0 = np.zeros((60, 80), dtype=np.uint8)
f1 = np.zeros((60, 80), dtype=np.uint8)
f1[10:50, 20:60] = 255 # big bright block appears
frames = [(0, f0), (30, f1)]
results = detect_motion_scaled(
frames, mask, threshold=30, min_area=1.0, timestamp_fn=self._ts
)
self.assertEqual(len(results), 1)
self.assertEqual(results[0].timestamp, 30.0)
self.assertGreater(results[0].change_percentage, 0.0)
def test_no_change_yields_nothing(self):
mask = np.full((60, 80), 255, dtype=np.uint8)
f0 = np.zeros((60, 80), dtype=np.uint8)
f1 = np.zeros((60, 80), dtype=np.uint8)
results = detect_motion_scaled(
[(0, f0), (30, f1)], mask, threshold=30, min_area=1.0, timestamp_fn=self._ts
)
self.assertEqual(results, [])
if __name__ == "__main__":
unittest.main()
+91
View File
@@ -0,0 +1,91 @@
import math
import unittest
import numpy as np
from norfair.camera_motion import (
HomographyTransformation,
TranslationTransformation,
)
from frigate.ptz.autotrack import transform_is_finite
from frigate.track.norfair_tracker import distance
class TestNorfairDistance(unittest.TestCase):
"""Regression tests for the tracker distance guard.
norfair raises a hard ValueError on any nan distance, which kills the camera
process. During autotracking, an ill-conditioned homography can hand the
tracker a non-finite or degenerate estimate box, so distance() must never
return nan for any input.
"""
def setUp(self) -> None:
# boxes are [[x1, y1], [x2, y2]]
self.detection = np.array([[805.0, 402.0], [864.0, 521.0]])
self.estimate = np.array([[800.0, 400.0], [860.0, 520.0]])
def test_finite_boxes_give_finite_distance(self) -> None:
d = distance(self.detection, self.estimate)
self.assertTrue(math.isfinite(d))
def test_inf_estimate_corner_does_not_return_nan(self) -> None:
estimate = np.array([[np.inf, 400.0], [860.0, 520.0]])
d = distance(self.detection, estimate)
self.assertFalse(math.isnan(d))
self.assertEqual(d, float("inf"))
def test_nan_estimate_corner_does_not_return_nan(self) -> None:
# the actual autotracking crash: a positive-only guard would miss this
# because nan <= 0 is False
estimate = np.array([[np.nan, 400.0], [860.0, 520.0]])
d = distance(self.detection, estimate)
self.assertFalse(math.isnan(d))
self.assertEqual(d, float("inf"))
def test_zero_area_estimate_does_not_return_nan(self) -> None:
estimate = np.array([[900.0, 500.0], [900.0, 500.0]])
d = distance(self.detection, estimate)
self.assertFalse(math.isnan(d))
self.assertEqual(d, float("inf"))
def test_zero_area_detection_does_not_return_nan(self) -> None:
detection = np.array([[805.0, 402.0], [805.0, 521.0]])
d = distance(detection, self.estimate)
self.assertFalse(math.isnan(d))
self.assertEqual(d, float("inf"))
def test_inverted_estimate_corners_do_not_return_nan(self) -> None:
# Kalman estimates can occasionally cross corners (x2 < x1)
estimate = np.array([[860.0, 520.0], [800.0, 400.0]])
d = distance(self.detection, estimate)
self.assertFalse(math.isnan(d))
self.assertEqual(d, float("inf"))
class TestTransformIsFinite(unittest.TestCase):
def test_finite_homography_is_finite(self) -> None:
matrix = np.array([[1.0, 0.0, 5.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]])
self.assertTrue(transform_is_finite(HomographyTransformation(matrix)))
def test_finite_translation_is_finite(self) -> None:
self.assertTrue(
transform_is_finite(TranslationTransformation(np.array([12.0, -4.0])))
)
def test_non_finite_homography_is_not_finite(self) -> None:
transform = HomographyTransformation(np.eye(3))
# simulate accumulation overflowing to a non-finite matrix
transform.homography_matrix = np.array(
[[1.0, 0.0, np.inf], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
)
self.assertFalse(transform_is_finite(transform))
def test_nan_translation_is_not_finite(self) -> None:
self.assertFalse(
transform_is_finite(TranslationTransformation(np.array([np.nan, 0.0])))
)
if __name__ == "__main__":
unittest.main()

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