Compare commits

..
Author SHA1 Message Date
Josh Hawkins 4e98a76464 close onvif sessions on shutdown
`OnvifController.close()` only stopped its event loop, so the aiohttp sessions each `ONVIFCamera` holds and the `_poll_config_updates` task were left to be garbage collected during interpreter shutdown, when their warnings can no longer be logged. Every restart ended with a run of `Unclosed client session` and `Task was destroyed but it is pending!` logging errors, which only became visible once restart started exiting the process itself under non-root. `close()` now closes each camera's client and cancels the tasks on the loop before stopping it.
2026-09-18 07:34:55 -05:00
Josh Hawkins cf19e52722 fix genai settings keeping a stale model and dropping roles after save
Switching a GenAI entry's provider left the previous provider's model selected, so saving wrote a model the new provider doesn't serve. llama.cpp can't find that model in `/v1/models`, so the backend reported every capability as false for the entry, and once the save refetched `genai/models` the roles widget stripped `transcribe` from the form on its own. The section showed unsaved changes right after saving, and saving again would have dropped the role. Switching provider now clears the model, and the roles widget only strips a role for a model or provider picked in the form, since the entry-level capability flags only describe the saved model. A selected role stays visible when the provider can't confirm it, so it can still be switched off. The llama.cpp model list also no longer repeats a model whose alias matches its id, which is what `--alias` produces.
2026-09-18 07:31:55 -05:00
Josh Hawkins 9ab0d97278 fix mobile overflowing icons in system due to new health pane 2026-09-18 06:55:55 -05:00
Josh Hawkins e24bb8be72 show runtime overrides in the settings form
The settings form read a camera section's saved config value, but its dependent warnings (audio transcription requiring audio detection, snapshots requiring detect, etc.) read the live config instead. A runtime toggle from the live view, MQTT, or an active profile can turn a section off without touching yaml, and that override persists across restarts, so the Enable switch showed on while the warning said the feature wasn't enabled. This adds an "Overridden (Live)" badge to any field whose live value differs from what's saved, and swaps the affected warnings to runtime-specific wording when a runtime override is the actual cause instead of the config.
2026-09-18 06:55:55 -05:00
Josh Hawkins ac0f6c9dd3 fix restart failing under non-root
restart_frigate() called psutil.Process(1).terminate() to signal s6-svscan, but s6-svscan runs as root while frigate runs as uid 1000, so the call raised AccessDenied. That exception escaped every caller: the UI restart button dropped its websocket client, MQTT restart and Save & Restart just logged and did nothing, and the watchdog crashed its own monitoring thread on a dead detector. This catches AccessDenied and falls through to the existing SIGINT branch, which exits the process for s6 to restart it.
2026-09-18 06:55:36 -05:00
Josh Hawkins 56c6416556 don't edit a chat message while a reply streams
The edit button stayed active while a reply streamed. `submitConversation` returns early while loading, but the message bubble still closed its editor, so the edit was silently lost. The edit button is hidden while a reply streams, and an editor that's already open keeps its draft with send disabled until the reply ends.
2026-09-18 06:55:36 -05:00
Josh Hawkins 885d6776fd fix train image filtering for a class with a dash
The backend writes a class with a `-` as `_` in train file names, since it splits those names on `-`, while a dataset folder keeps the dash. Filtering the Train grid by `half-open` compared it with `half_open` and hid every attempt. Both sides are normalized the same way now.
2026-09-18 06:55:36 -05:00
Josh Hawkins 27db8f1d74 don't run page shortcuts for keys a dialog already handled
Radix dismisses a dialog on Escape from a capture-phase keydown listener and calls `preventDefault()` without stopping propagation, so `useKeyboardListener` still ran the page's Escape shortcut: cancelling the delete dialog in the face library or a classification model also cleared the whole selection. Keys another shortcut hook handled still get through, since their listener order changes with every render.
2026-09-18 06:55:36 -05:00
Josh Hawkins 51eaae4857 find DST transitions to the second
`get_dst_transitions` probed the offset once every 24 hours from the start time and reported a change at the first probe after it, up to a day late, so events, review items and recordings near a transition were grouped into days with the old offset. A transition after the last daily probe wasn't found at all. The end of the range is probed too now, and a probe that sees the offset change bisects the interval to the second of the transition.
2026-09-18 06:55:36 -05:00
Josh Hawkins 54ba07917d return 403 for a snapshot or thumbnail on another camera
The broad `except Exception` handlers in `event_snapshot` and `event_thumbnail` caught the `HTTPException` from `require_camera_access`, so a restricted user asking for another camera's snapshot got a 404 instead of a 403, and for an object still being tracked the snapshot was rendered before the check ran. Both endpoints now look up the event and check access in their own block, the way the other endpoints do, so a denial propagates.
2026-09-18 06:55:36 -05:00
Josh Hawkins ee35be19ac fix the has_clip self-heal for events with no recordings
`vod_event` looked for a `(body, 404)` tuple, but `vod_ts` returns a `JSONResponse`, so the check never matched and an old event whose recordings are gone kept offering a clip that can't play. It now checks the response status code.
2026-09-18 06:55:36 -05:00
Josh Hawkins 605d051b3d check for a valid frame before using its shape
With the camera offline, no preview frame, and `camera-error.jpg` missing, `latest_frame` read `frame.shape` before its `frame is None` check, so it raised `AttributeError` and answered 500 with a traceback instead of the intended "Unable to get valid frame". The check now runs first.
2026-09-18 06:55:36 -05:00
702 changed files with 8379 additions and 43987 deletions
+4 -9
View File
@@ -17,14 +17,9 @@ runs:
shell: bash
# This creates a virtual volume at /var/lib/docker to maximize the size
# As of 2/14/2024, this results in 97G for docker images
# Runners no longer have a separate /mnt disk, so the temp PV is also carved
# from root and temp-reserve-mb is what actually stays free on root. Keep 4G
# there for setup-qemu/buildx caches in ~/.docker and the tool cache
- name: Maximize build space
uses: easimon/maximize-build-space@master
with:
root-reserve-mb: 8192
temp-reserve-mb: 4096
remove-dotnet: 'true'
remove-android: 'true'
remove-haskell: 'true'
@@ -34,15 +29,15 @@ runs:
run: sudo systemctl start docker
shell: bash
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v8
uses: ASzc/change-string-case-action@v5
with:
string: ${{ github.repository }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc
with:
registry: ghcr.io
username: ${{ github.actor }}
+22 -139
View File
@@ -1,157 +1,40 @@
version: 2
updates:
- package-ecosystem: "github-actions"
# Local composite actions are only scanned when their directory is listed
directories:
- "/"
- "/.github/actions/setup"
directory: "/"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
actions:
patterns: ["*"]
actions-security:
applies-to: security-updates
patterns: ["*"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "pip"
directory: "/docker/main"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
# Sets that fail pip resolution unless they move together
peewee:
patterns: ["peewee", "peewee-migrate"]
crypto:
patterns: ["cryptography", "joserfc", "pywebpush", "py-vapid"]
pydantic-genai:
patterns: ["pydantic", "google-genai"]
pip-main-minor-patch:
patterns: ["*"]
update-types: ["minor", "patch"]
pip-main-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# numpy 2.5 needs Python 3.12; norfair, tflite_runtime and rknn-toolkit2
# cap numpy below 2
- dependency-name: "numpy"
versions: [">=2"]
# Every OpenCV wheel from 4.12 requires numpy 2
- dependency-name: "opencv-python-headless"
versions: [">=4.12"]
- dependency-name: "opencv-contrib-python"
versions: [">=4.12"]
# aarch64 only. Rockchip rknn-toolkit2 caps protobuf at 4.25.4 and
# TensorFlow 2.21 needs protobuf 6.31 or newer.
- dependency-name: "tensorflow"
versions: [">=2.20"]
# Needs Python 3.12 and numpy 2
- dependency-name: "librosa"
versions: [">=1.0"]
# Needs Python 3.12
- dependency-name: "scipy"
versions: [">=1.18"]
# Newer stubs report false errors against peewee 4
- dependency-name: "types-peewee"
versions: [">=4.1"]
# sdist only and imported by HailoRT; needs a Hailo device to test
- dependency-name: "netifaces"
versions: [">=0.11"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "pip"
directory: "/docker/tensorrt"
schedule:
interval: weekly
# No pull request check builds these images, so version updates are off.
# Security updates still open.
open-pull-requests-limit: 0
groups:
pip-trt-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# onnx 1.17 and newer write a newer IR and opset than TensorRT 8.6 on
# JetPack 6 is known to parse
- dependency-name: "onnx"
versions: [">=1.17"]
# 1.27 moved to CUDA 13, and the image ships the CUDA 12.8 wheel set
- dependency-name: "onnxruntime-gpu"
versions: [">=1.27"]
# Hand-matched CUDA 12.8 set (2c9a25e67); move as a block
- dependency-name: "nvidia-*"
# Must match the JetPack CUDA runtime
- dependency-name: "cuda-python"
versions: [">=12.9"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "npm"
directory: "/web"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
groups:
radix:
patterns: ["@radix-ui/*"]
rjsf:
patterns: ["@rjsf/*"]
web-minor-patch:
patterns: ["*"]
update-types: ["minor", "patch"]
web-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# typescript-eslint peers typescript below 6.1
- dependency-name: "typescript"
versions: [">=6.1"]
# Tailwind 4 is a config rewrite; scrollbar 4 and merge 3 depend on it
- dependency-name: "tailwindcss"
update-types: ["version-update:semver-major"]
- dependency-name: "tailwind-scrollbar"
update-types: ["version-update:semver-major"]
- dependency-name: "tailwind-merge"
update-types: ["version-update:semver-major"]
# Intermittent failure in the UI settings import spec, cause not found
- dependency-name: "zod"
update-types: ["version-update:semver-major"]
# Needs an on-device pinch test before moving
- dependency-name: "react-zoom-pan-pinch"
update-types: ["version-update:semver-major"]
# Build fails until monaco-worker-manager releases its PR 4
- dependency-name: "monaco-editor"
versions: [">=0.55.0"]
# Later versions need Node 22 and extract false keys
- dependency-name: "i18next-cli"
versions: [">1.5.11"]
# 6.11.0 refills the saved model in the GenAI settings form
- dependency-name: "@rjsf/*"
versions: ["6.11.0"]
interval: daily
open-pull-requests-limit: 10
target-branch: dev
- package-ecosystem: "npm"
directory: "/docs"
schedule:
interval: weekly
cooldown:
default-days: 3
open-pull-requests-limit: 5
interval: daily
allow:
- dependency-name: "@docusaurus/*"
groups:
docusaurus:
patterns: ["@docusaurus/*"]
docs-security:
applies-to: security-updates
patterns: ["*"]
ignore:
# A Docusaurus major needs the openapi plugin and theme moved in the
# same commit
- dependency-name: "@docusaurus/*"
update-types: ["version-update:semver-major"]
open-pull-requests-limit: 10
target-branch: dev
+1 -1
View File
@@ -586,7 +586,7 @@ jobs:
- arm64_build
steps:
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v8
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
with:
persist-credentials: false
- id: lowercaseRepo
uses: ASzc/change-string-case-action@v8
uses: ASzc/change-string-case-action@v6
with:
string: ${{ github.repository }}
- name: Log in to the Container registry
-1
View File
@@ -5,4 +5,3 @@
/docker/rockchip/ @MarcA711
/docker/rocm/ @harakas
/docker/hailo8l/ @spanner3003
/docker/deepx/ @sixfab
-131
View File
@@ -1,131 +0,0 @@
#!/bin/bash
# Installs the DEEPX NPU kernel driver and the DX-RT runtime on the Docker host,
# then enables the vendor's dxrt.service. A container cannot load kernel
# modules, so this runs outside the image; the driver creates the /dev/dxrt*
# nodes and the daemon multiplexes the NPU across host and container.
#
# Driver, runtime and firmware versions must agree or inference hangs instead
# of failing at startup. The set this script installs is pinned in
# driver_version, runtime_version and firmware_version below; move them
# together, never one at a time.
#
# DEEPX NPU support in Frigate is maintained by Sixfab (https://sixfab.com).
set -euo pipefail
driver_version="v2.6.0"
# the commit the tag resolves to, since DEEPX signs neither tags nor releases
# and this is compiled and installed as root. Update both together
driver_commit="7074748e7104f470b02f517583abba652b3f05fa"
firmware_version="v2.7.4"
sudo apt-get update
sudo apt-get install -y git build-essential "linux-headers-$(uname -r)" pciutils wget
if ! lspci -d 1ff4: | grep -q .; then
echo "No DEEPX device found on the PCIe bus (lspci -d 1ff4:)."
echo "Check that the module is seated correctly before continuing."
exit 1
fi
# fetch the pinned commit rather than cloning the tag, so a retag cannot swap
# in different source. The build directory is reused so a second run after a
# failure does not stop on the directory already being there
mkdir -p dx_rt_npu_linux_driver
cd dx_rt_npu_linux_driver
git init -q
git remote get-url origin > /dev/null 2>&1 ||
git remote add origin https://github.com/DEEPX-AI/dx_rt_npu_linux_driver.git
git fetch --depth 1 origin "${driver_commit}"
git checkout -q FETCH_HEAD
fetched_commit=$(git rev-parse HEAD)
if [[ "${fetched_commit}" != "${driver_commit}" ]]; then
echo "Fetched commit ${fetched_commit} does not match pinned driver_commit ${driver_commit}."
echo "Refusing to build unverified driver source."
exit 1
fi
cd modules
sudo ./build.sh -c install --reload
sudo depmod -A
# dx_dma is the PCIe transport, dxrt_driver the NPU driver on top of it
for module in dx_dma dxrt_driver; do
if ! sudo modprobe "${module}"; then
echo "Unable to load the ${module} kernel module, common reasons are:"
echo "- Secure Boot is enabled and is rejecting the unsigned module."
echo "- The running kernel does not match the installed linux-headers."
exit 1
fi
done
if ! compgen -G "/dev/dxrt*" > /dev/null; then
echo "Modules loaded but no /dev/dxrt* device node appeared."
echo "Run ./sanity_check.sh from the driver repo to diagnose."
exit 1
fi
runtime_version="v3.4.0"
declare -A runtime_sha256=(
[amd64]="736cfef009ce9e974ab1ab610d867239d19d72a426a53e367ddcbd53297b6e20"
[arm64]="eb6107f5f02f2ad76ae89f414e8b5f346f34fbc6f0888236136853a26be6f6a0"
)
runtime_release="${runtime_version#v}"
deb_arch=$(dpkg --print-architecture)
deb_file="/tmp/libdxrt-bin_${runtime_release}_${deb_arch}.deb"
wget -qO "${deb_file}" \
"https://raw.githubusercontent.com/DEEPX-AI/dx_rt/${runtime_version}/release/${runtime_release}/libdxrt-bin_${runtime_release}_${deb_arch}.deb"
expected_sha256="${runtime_sha256[${deb_arch}]:-}"
if [[ -z "${expected_sha256}" ]]; then
echo "No pinned SHA-256 for architecture ${deb_arch}; refusing to install."
exit 1
fi
if [[ "$(sha256sum "${deb_file}" | cut -d' ' -f1)" != "${expected_sha256}" ]]; then
echo "SHA-256 mismatch for ${deb_file}; refusing to install."
exit 1
fi
sudo dpkg -i "${deb_file}"
sudo ldconfig
rm -f "${deb_file}"
sudo cp /usr/share/libdxrt-bin/service/dxrt.service /etc/systemd/system/
# With an endpoint set, dxrtd binds that path only, so the socket goes in a
# directory Frigate can mount (kept across restarts so the mount stays valid)
# and a symlink at the default /tmp path keeps host tools that do not set the
# variable working through their own fallback.
sudo mkdir -p /etc/systemd/system/dxrt.service.d
sudo tee /etc/systemd/system/dxrt.service.d/frigate.conf > /dev/null <<'UNIT'
[Service]
RuntimeDirectory=dxrt
RuntimeDirectoryMode=0755
RuntimeDirectoryPreserve=yes
Environment=DXRT_DYNAMIC_IPC_ENDPOINT=/run/dxrt/dxrt_dynamic_ipc.sock
ExecStartPost=/bin/ln -sfn /run/dxrt/dxrt_dynamic_ipc.sock /tmp/dxrt_dynamic_ipc.sock
UNIT
sudo systemctl daemon-reload
sudo systemctl enable dxrt.service
sudo systemctl restart dxrt.service
if ! sudo systemctl is-active --quiet dxrt.service; then
echo "dxrt.service did not start. Check: sudo journalctl -u dxrt.service"
exit 1
fi
echo "DEEPX driver and runtime installation complete."
echo "Driver version: $(modinfo -F version dxrt_driver) (expected ${driver_version#v})"
echo "Runtime version: ${runtime_release}"
echo "Device node(s): $(echo /dev/dxrt*)"
echo
echo "This driver expects NPU firmware ${firmware_version}. Check it with:"
echo " dxrt-cli --status"
echo "Update the module if it does not match before starting Frigate."
-2
View File
@@ -217,8 +217,6 @@ COPY docker/main/rootfs/ /
# Frigate deps (ffmpeg, python, nginx, go2rtc, s6-overlay, etc)
FROM slim-base AS deps
LABEL org.opencontainers.image.source="https://github.com/blakeblackshear/frigate"
ARG TARGETARCH
ARG BASE_IMAGE
+1 -1
View File
@@ -1,4 +1,4 @@
ruff == 0.16.10
ruff == 0.15.20
# types
types-peewee == 4.0.*
+34 -34
View File
@@ -1,66 +1,66 @@
aiofiles == 25.1.*
click == 8.5.*
# FastAPI
aiohttp == 3.14.*
starlette == 1.7.*
aiohttp == 3.12.*
starlette == 0.47.*
starlette-context == 0.5.*
fastapi[standard-no-fastapi-cloud-cli] == 0.142.*
uvicorn == 0.54.*
fastapi[standard-no-fastapi-cloud-cli] == 0.116.*
uvicorn == 0.52.*
slowapi == 0.1.*
joserfc == 1.7.*
cryptography == 50.0.*
joserfc == 1.6.*
cryptography == 46.0.*
pathvalidate == 3.3.*
markupsafe == 3.0.*
python-multipart == 0.0.32
python-multipart == 0.0.31
# Classification Model Training
tensorflow == 2.19.* ; platform_machine == 'aarch64'
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64'
# General
mypy == 2.4.0
onvif-zeep-async == 4.3.*
mypy == 1.6.1
onvif-zeep-async == 4.0.*
paho-mqtt == 2.1.*
pandas == 3.0.*
peewee == 4.5.*
peewee_migrate == 2.3.*
psutil == 7.2.*
pydantic == 2.13.*
pandas == 2.2.*
peewee == 3.17.*
peewee_migrate == 1.14.*
psutil == 7.1.*
pydantic == 2.10.*
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
tzdata == 2026.5
pyzmq == 27.2.*
ruamel.yaml == 0.19.*
tzlocal == 5.4.4
requests == 2.34.*
types-requests == 2.33.*
pytz == 2025.*
pyzmq == 27.1.*
ruamel.yaml == 0.18.*
tzlocal == 5.2
requests == 2.33.*
types-requests == 2.32.*
norfair == 2.3.*
setproctitle == 1.3.*
ws4py == 0.6.*
ws4py == 0.5.*
unidecode == 1.4.*
titlecase == 2.4.*
# Image Manipulation
numpy == 1.26.*
opencv-python-headless == 4.11.0.*
opencv-contrib-python == 4.11.0.*
scipy == 1.17.*
scipy == 1.16.*
# OpenVino & ONNX
openvino == 2025.4.*
onnxruntime == 1.30.*
onnxruntime == 1.22.*
# Embeddings
transformers == 5.19.*
transformers == 4.45.*
# Generative AI
google-genai == 2.29.*
google-genai == 1.58.*
ollama == 0.6.*
openai == 3.26.*
openai == 1.65.*
# push notifications
py-vapid == 1.9.4
pywebpush == 2.5.*
pywebpush == 2.0.*
# alpr
pyclipper == 1.4.*
shapely == 2.2.*
rapidfuzz==3.14.*
shapely == 2.0.*
rapidfuzz==3.12.*
# HailoRT
argcomplete==3.7.*
contextlib2==21.6.*
future==1.0.*
argcomplete==2.0.*
contextlib2==0.6.*
future==0.18.*
netaddr==1.3.*
netifaces==0.10.*
prometheus-client == 0.26.*
@@ -71,6 +71,6 @@ tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.
sherpa-onnx==1.13.*
faster-whisper==1.2.*
librosa==0.11.*
soundfile==0.14.*
soundfile==0.13.*
# Memory profiling
memray == 1.20.*
@@ -37,7 +37,6 @@ device_globs=(
"/dev/nvmap"
"/dev/nvidia*"
"/dev/memx*"
"/dev/dxrt*"
)
IFS=',' read -ra extra_globs <<< "${DEVICE_ACL_PATHS:-}"
@@ -8,14 +8,13 @@ from typing import Any
from ruamel.yaml import YAML
sys.path.insert(0, "/opt/frigate")
from frigate.config.env import apply_config_env_vars, substitute_frigate_vars
from frigate.const import (
BIRDSEYE_PIPE,
LIBAVFORMAT_VERSION_MAJOR,
)
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
from frigate.util.config import find_config_file, resolve_ffmpeg_path
from frigate.util.env import apply_config_env_vars, substitute_frigate_vars
from frigate.util.live_streams import raw_transcode_streams
from frigate.util.services import (
is_go2rtc_arbitrary_exec_allowed,
is_restricted_go2rtc_source,
@@ -175,17 +174,6 @@ for name in list(go2rtc_config.get("streams", {})):
del go2rtc_config["streams"][name]
continue
# add transcoded live streams; a user stream with the same name wins here and
# fails Frigate's config validation
transcoded_streams = raw_transcode_streams(config)
if transcoded_streams:
if go2rtc_config.get("streams") is None:
go2rtc_config["streams"] = {}
for name, source in transcoded_streams.items():
go2rtc_config["streams"].setdefault(name, source)
# add birdseye restream stream if enabled
if config.get("birdseye", {}).get("restream", False):
birdseye: dict[str, Any] = config.get("birdseye")
@@ -14,10 +14,7 @@ location /auth {
proxy_pass_request_headers off;
# Pass info about the request
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $request_uri;
# set by the go2rtc stream locations; client headers are not forwarded, so
# this cannot be spoofed
proxy_set_header X-Go2rtc-Stream-Proxy $go2rtc_stream_proxy;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Server-Port $server_port;
proxy_set_header Content-Length "";
# Pass along auth related info
@@ -219,8 +219,6 @@ http {
# frigate lovelace card uses this path
location /live/mse/api/ws {
# tells /auth to apply the go2rtc stream checks
set $go2rtc_stream_proxy 1;
include auth_request.conf;
limit_except GET {
deny all;
@@ -230,8 +228,6 @@ http {
}
location /live/webrtc/api/ws {
# tells /auth to apply the go2rtc stream checks
set $go2rtc_stream_proxy 1;
include auth_request.conf;
limit_except GET {
deny all;
@@ -252,8 +248,6 @@ http {
# integration uses this to add webrtc candidate
location /api/go2rtc/webrtc {
# tells /auth to apply the go2rtc stream checks
set $go2rtc_stream_proxy 1;
include auth_request.conf;
limit_except POST {
deny all;
@@ -2,7 +2,7 @@
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-Original-URL $request_uri;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-URI $request_uri;
+1 -1
View File
@@ -14,4 +14,4 @@ nvidia-nccl-cu12==2.26.2.post1; platform_machine == 'x86_64'
nvidia-nvjitlink-cu12==12.8.93; platform_machine == 'x86_64'
onnx==1.16.*; platform_machine == 'x86_64'
onnxruntime-gpu==1.24.*; platform_machine == 'x86_64'
protobuf==7.36.*; platform_machine == 'x86_64'
protobuf==3.20.3; platform_machine == 'x86_64'
-91
View File
@@ -824,38 +824,6 @@ cpu:
models:
- devices:
- cpu:3
xdna2:
title: AMD XDNA2
models:
- key: yolov9
label: YOLOv9
recommended: true
download: |-
Prepare the model using the frigate-xdna setup instructions linked above. For local YOLO models, Frigate must have access to the same ONNX file bytes as the sidecar. The example below uses YOLOv9-C at 320x320. Frigate+ models may instead use the same `plus://MODEL_ID` in Frigate and the sidecar.
ui: |-
Navigate to **Settings > System > Detection models** and add a model. The ZMQ endpoint is not reported by the hardware probe, so set `devices` to `zmq:tcp://xdna:5555` in YAML. Then, on the same model, open the **Custom Model** tab and configure:
| Field | Value |
| ---------------------------------------- | ------------------------------------------ |
| **Custom object detector model path** | `/config/models/yolov9-c-320.onnx` |
| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
| **Object detection model input width** | `320` |
| **Object detection model input height** | `320` |
| **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
| **Model Input Tensor Shape** | `nchw` |
| **Model Input D Type** | `float` |
| **Object Detection Model Type** | `yolo-generic` |
yaml: |-
models:
- devices:
- zmq:tcp://xdna:5555
model_type: yolo-generic
width: 320
height: 320
input_tensor: nchw
input_dtype: float
path: /config/models/yolov9-c-320.onnx
labelmap_path: /labelmap/coco-80.txt
memryx:
title: MemryX
models:
@@ -999,65 +967,6 @@ memryx:
# The .zip file must contain:
# ├── ssdlite_mobilenet.dfp (a file ending with .dfp)
# └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network)
deepx:
title: DEEPX NPU
models:
- key: yolo
label: YOLO
recommended: true
download: No model is bundled with Frigate. Download a pre-compiled YOLO `.dxnn` model from the [DEEPX ModelZoo](https://developer.deepx.ai/modelzoo) or compile your own with DX-COM, then bind-mount it into the container and point the model's `path` at it. The recommended model is `yolox-s_640x640_ppu.dxnn`. Its Post-Processing Unit (PPU) compile moves candidate selection onto the NPU, which makes it the fastest ModelZoo model measured through Frigate (about 13 ms on a DX-M1). The output layout is read from the compiled model, so anchor-based, anchor-free, NMS-in-head and PPU models (anchor-based or anchor-free) all need no extra configuration; prefer a `PPU` variant whenever the ModelZoo offers one. PPU models must be compiled with DX-COM 2.4.0 or later, which writes the head layout Frigate reads into the file.
ui: |-
Navigate to **Settings > System > Detection models** and select **DEEPX NPU** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure:
| Field | Value |
| ---------------------------------------- | ---------------------------------------------------- |
| **Custom object detector model path** | `/config/model_cache/deepx/yolox-s_640x640_ppu.dxnn` |
| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
| **Object detection model input width** | `640` |
| **Object detection model input height** | `640` |
| **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
| **Model Input Tensor Shape** | `nhwc` (Frigate's default value) |
| **Model Input D Type** | `int` (Frigate's default value) |
| **Object Detection Model Type** | `yolo-generic` |
Quantization is baked into the compiled model, so no normalization is applied on the host and the input defaults do not need to be overridden.
yaml: |-
models:
- devices:
- deepx:PCIe:0
path: /config/model_cache/deepx/yolox-s_640x640_ppu.dxnn
labelmap_path: /labelmap/coco-80.txt
model_type: yolo-generic
width: 640
height: 640
- key: yolox
label: YOLOX
recommended: false
download: No model is bundled with Frigate. Download a pre-compiled YOLOX `.dxnn` model from the [DEEPX ModelZoo](https://developer.deepx.ai/modelzoo), then bind-mount it into the container and point the model's `path` at it. The `_ppu` variant is faster and also works with the `yolo-generic` model type; the plain export needs `yolox` so its raw head is decoded.
ui: |-
Navigate to **Settings > System > Detection models** and select **DEEPX NPU** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure:
| Field | Value |
| ---------------------------------------- | ---------------------------------------- |
| **Custom object detector model path** | `/config/model_cache/deepx/yolox-s_640x640.dxnn`|
| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
| **Object detection model input width** | `640` |
| **Object detection model input height** | `640` |
| **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
| **Model Input Tensor Shape** | `nhwc` (Frigate's default value) |
| **Model Input D Type** | `int` (Frigate's default value) |
| **Object Detection Model Type** | `yolox` |
The width and height must match the resolution the `.dxnn` file was compiled for.
yaml: |-
models:
- devices:
- deepx:PCIe:0
path: /config/model_cache/deepx/yolox-s_640x640.dxnn
labelmap_path: /labelmap/coco-80.txt
model_type: yolox
width: 640
height: 640
tensorrt:
title: TensorRT
models:
+9 -27
View File
@@ -152,10 +152,9 @@ auth:
models:
# Optional: the camera environment this model is for (default: shown below)
# Cameras select a model by setting detect -> scene to a matching value, and
# the model with a scene of default is used by any camera that does not set one.
# Any name made up of letters, numbers, _ and - is valid, such as thermal.
# Models that use the same model file are combined into one model.
- scene: default
# a model with a scene of all is used by any camera that does not set one.
# Valid values are all, indoor, outdoor, indoor_thermal, outdoor_thermal
- scene: all
# Required: hardware this model runs on, as <detector> or <detector>:<device>
# See https://docs.frigate.video/configuration/object_detectors for the
# detectors available and the devices each one accepts. All of a model's
@@ -294,9 +293,9 @@ ffmpeg:
# Optional: output args for detect streams (default: shown below)
detect: -threads 2 -f rawvideo -pix_fmt yuv420p
# Optional: output args for record streams (default: shown below)
record: preset-record-generic-audio-aac
record: preset-record-generic
# Optional: output args for sub stream record streams (default: the record output args above)
# record_sub: preset-record-generic-audio-aac
# record_sub: preset-record-generic
# Optional: Time in seconds to wait before ffmpeg retries connecting to the camera. (default: shown below)
# If set too low, frigate will retry a connection to the camera's stream too frequently, using up the limited streams some cameras can allow at once
# If set too high, then if a ffmpeg crash or camera stream timeout occurs, you could potentially lose up to a maximum of retry_interval second(s) of footage
@@ -317,9 +316,9 @@ detect:
# Optional: height of the frame for the input with the detect role (default: use native stream resolution)
height: 720
# Optional: the environment this camera looks at, which picks the model it runs on
# (default: the model with a scene of default)
# Must match the scene of a configured model
scene: thermal
# (default: the model with a scene of all)
# Valid values are all, indoor, outdoor, indoor_thermal, outdoor_thermal
scene: outdoor
# Optional: desired fps for your camera for the input with the detect role (default: shown below)
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
fps: 5
@@ -556,7 +555,7 @@ motion:
# Optional: Whether this mask is active (default: true)
enabled: true
# Required: Coordinates polygon for the mask
coordinates: "0.000,0.469,1.0,0.469,1.0,1.0,0.000,1.0"
coordinates: "0.000,0.469,1.000,0.469,1.000,1.000,0.000,1.000"
# Optional: improve contrast (default: shown below)
# Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive
# for daytime.
@@ -571,8 +570,6 @@ notifications:
enabled: False
# Optional: Email for push service to reach out to
# NOTE: This is required to use notifications
# NOTE: Email can be specified with an environment variable or docker secrets that must begin with 'FRIGATE_'.
# e.g. email: '{FRIGATE_NOTIFICATION_EMAIL}'
email: "admin@example.com"
# Optional: Cooldown time for notifications in seconds (default: shown below)
cooldown: 0
@@ -903,21 +900,6 @@ live:
streams:
main_stream: main_stream_name
sub_stream: sub_stream_name
# Optional: Lower-quality live streams transcoded by go2rtc while someone is watching.
# NOTE: Set at the camera level only.
transcode:
# Optional: Enable transcoded streams (default: shown below)
enabled: False
# Optional: go2rtc stream to transcode (default: the first live stream)
source: main_stream_name
# Optional: One transcoded stream per quality (default: shown below)
qualities:
- height: 720
bitrate: 1200
- height: 480
bitrate: 500
- height: 360
bitrate: 250
# Optional: Set the height of the jsmpeg stream. (default: 720)
# This must be less than or equal to the height of the detect stream. Lower resolutions
# reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio.
+1 -1
View File
@@ -289,7 +289,7 @@ The only field that is valid at the camera level is `enabled`. In particular `mo
#### GenAI Provider
Frigate can send audio to a GenAI provider for transcription when that provider has the `transcribe` role. This is useful if you already run a GenAI provider, or if you do not have the CPU/GPU headroom for a local whisper model. See [Provider support](/configuration/genai/genai_config#provider-support) for which providers can serve this role. The model must accept audio: either a dedicated ASR model such as Qwen3-ASR, or a general multimodal model that accepts audio.
Frigate can send audio to a GenAI provider for transcription when that provider has the `transcribe` role. This is useful if you already run a GenAI provider, or if you do not have the CPU/GPU headroom for a local whisper model. Supported providers are **OpenAI**, **Azure OpenAI**, **Gemini**, and **llama.cpp** with an audio-capable model (a dedicated ASR model such as Qwen3-ASR, or a general multimodal model that accepts audio). Ollama is not supported as it has no audio input.
To use a GenAI provider for audio transcription:
+3 -3
View File
@@ -169,10 +169,10 @@ The [Add Camera Wizard](cameras.md#adding-a-camera-with-the-add-camera-wizard) i
1. Click **Add Camera** in <NavPath path="Settings > Global configuration > Camera management" />.
2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand.
3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower. For higher resolution cameras, it tries the http-flv stream first and falls back to RTSP when the camera does not support it.
4. The wizard turns on **Use stream compatibility mode** for the http-flv stream it selects. Enable it for any other http-flv stream you add, such as the sub stream.
3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower, or an RTSP stream for higher resolution cameras.
4. In the validation step, enable **Use stream compatibility mode** for http-flv streams when the wizard recommends it.
If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras 5MP and lower, where http-flv is recommended, the wizard will show a warning in the validation step.
If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras where http-flv is recommended, the wizard will show a warning in the validation step.
The wizard covers standard single-camera setups. For two way talk, cameras connected through a Reolink NVR, or audio transcoding for WebRTC live view, configure the camera manually as shown below.
+1 -1
View File
@@ -235,7 +235,7 @@ The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.or
| Amcrest ASH21 | ✅ | ❌ | ONVIF service port: 80 |
| Amcrest IP4M-S2112EW-AI | ✅ | ❌ | FOV relative movement not supported. |
| Amcrest IP5M-1190EW | ✅ | ❌ | ONVIF Port: 80. FOV relative movement not supported. |
| Annke CZ504 | ✅ | ❌ | |
| Annke CZ504 | ✅ | ✅ | Annke support provide specific firmware ([V5.7.1 build 250227](https://github.com/pierrepinon/annke_cz504/raw/refs/heads/main/digicap_V5-7-1_build_250227.dav)) to fix issue with ONVIF "TranslationSpaceFov" |
| Axis Q-6155E | ✅ | ❌ | ONVIF service port: 80; Camera does not support MoveStatus. |
| Ctronics PTZ | ✅ | ❌ | |
| Dahua | ✅ | ✅ | Some low-end Dahuas (lite series, picoo series (commonly), among others) have been reported to not support autotracking. These models usually don't have a four digit model number with chassis prefix and options postfix (e.g. DH-P5AE-PV vs DH-SD49825GB-HNR). |
@@ -85,14 +85,6 @@ An optional config, `save_attempts`, can be set as a key under the model name. T
</TabItem>
</ConfigTabs>
## Review items
When a model's state changes while its camera has an active review item, the change is recorded on that review item. This includes changes in the few seconds before the item starts, such as a garage door opening just before the car is detected. State changes never create or extend review items on their own, and the first state reported after Frigate starts is not recorded as a change.
Recorded changes appear in the review item's data as `classification_state_changes` (see the [`frigate/reviews`](/integrations/mqtt#frigatereviews) MQTT topic) and are passed to [GenAI review summaries](/configuration/genai/genai_review) as facts, so a description can note that a gate was opened during the activity.
Change times are most accurate with `motion: true`. A model that only runs on an `interval` notices a change at its next run, so the change may be recorded late or attached to a later review item.
## Training the model
Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps:
+11 -13
View File
@@ -17,19 +17,17 @@ Hardware acceleration arguments tell FFmpeg to decode your camera's video stream
See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md) for details on setting up hardware acceleration for your GPU / iGPU, then select the preset that matches your hardware.
| Preset (YAML config) | UI Label | Usage | Notes |
| ------------------------- | ----------------------- | --------------------------------------------- | --------------------------------------------------------------- |
| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | |
| preset-apple-silicon-h264 | Apple Silicon (H.264) | Apple Silicon Mac under lighter, H.264 stream | Needs the `lighter.sh/video` device |
| preset-apple-silicon-h265 | Apple Silicon (H.265) | Apple Silicon Mac under lighter, H.265 stream | Needs the `lighter.sh/video` device |
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
| Preset (YAML config) | UI Label | Usage | Notes |
| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- |
| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | |
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
<ConfigTabs>
<TabItem value="ui">
+4 -25
View File
@@ -43,19 +43,7 @@ genai:
The examples on this page all use `my_provider`, but the name is arbitrary and is only used to reference the provider elsewhere in the config (for example, `semantic_search.model`).
Each provider handles one or more **roles**: `chat`, `descriptions`, `embeddings`, and `transcribe`. A provider handles the first three by default; `transcribe` must always be listed explicitly. Each role may be assigned to exactly one provider. Define a single provider if you want it to do everything, or split the roles across several providers using the `roles` option. Not every provider supports every role; see [Provider support](#provider-support).
### Provider support
| Provider | Descriptions | Chat | Embeddings | Transcription |
| ----------------------------- | :----------: | :--: | :--------: | :-----------: |
| llama.cpp (`llamacpp`) | ✅ | ✅ | ✅ | ✅ |
| Ollama (`ollama`) | ✅ | ✅ | ✅ | ❌ |
| OpenAI (`openai`) | ✅ | ✅ | ❌ | ✅ |
| Azure OpenAI (`azure_openai`) | ✅ | ✅ | ❌ | ✅ |
| Google Gemini (`gemini`) | ✅ | ✅ | ❌ | ✅ |
A ✅ means Frigate can use the provider for that feature. The configured model must also support it: a vision model for descriptions and chat, a multimodal embedding model for embeddings (see [Embedding models](#embedding-models)), and an audio-capable model for transcription. Some features also need extra provider setup, covered in each provider's section below. OpenAI-compatible servers use the `openai` provider, so they follow the OpenAI row.
Each provider handles one or more **roles**: `chat`, `descriptions`, `embeddings`, and `transcribe`. A provider handles the first three by default; `transcribe` must always be listed explicitly, and is not available on Ollama, which has no audio input. Each role may be assigned to exactly one provider. Define a single provider if you want it to do everything, or split the roles across several providers using the `roles` option.
If the provider you choose requires an API key, you may either directly paste it in your configuration, or store it in an environment variable prefixed with `FRIGATE_`.
@@ -85,10 +73,9 @@ You must use a vision-capable model with Frigate. The following models are recom
The `embeddings` role needs a different kind of model. Text queries are matched against the stored image embeddings, so the model must be trained to place images and text into the same vector space. A chat or description model will still return vectors when asked, but those vectors are not trained for retrieval and text searches will return poor matches with no error to indicate why.
| Model | Notes |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `embeddinggemma-2` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Strong semantic search accuracy with efficient inference on a small model. |
| `qwen3-vl-embedding` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Good performance, large model that requires strong hardware for inference. |
| Model | Notes |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `qwen3-vl-embedding` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Must be served by llama.cpp started with `--embeddings` and `--mmproj`. |
#### Transcription models
@@ -165,10 +152,6 @@ genai:
Frigate queries the llama.cpp server for the model's context size at startup and logs it along with the other detected capabilities. If `context_size` is set in `provider_options`, that value is always used instead, even when the server reports its own.
#### Embeddings
To serve the `embeddings` role for [Semantic Search](/configuration/semantic_search#genai-provider), start the llama.cpp server with `--embeddings`, plus `--mmproj` for image support. See the [llama.cpp server documentation](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) for details.
### Ollama
[Ollama](https://ollama.com/) allows you to self-host large language models and keep everything running locally. It is highly recommended to host this server on a machine with an Nvidia graphics card, or on a Apple silicon Mac for best performance.
@@ -214,10 +197,6 @@ genai:
</TabItem>
</ConfigTabs>
#### Embeddings
Ollama can serve the `embeddings` role for [Semantic Search](/configuration/semantic_search#genai-provider). Embedding images requires Ollama 0.40.1 or newer and an embedding model with a vision encoder, such as `embeddinggemma-2:440m`. For a saved provider, the UI hides the role unless Ollama reports its model as an embedding model. Use a separate provider entry for the embedding model rather than adding the role to a vision chat model.
### OpenAI-Compatible
Frigate supports any provider that implements the OpenAI API standard. This includes self-hosted solutions like [vLLM](https://docs.vllm.ai/), [LocalAI](https://localai.io/), and other OpenAI-compatible servers.
@@ -201,8 +201,6 @@ Review items are sent to the model as a sequence of still frames. Some models fo
The notes come from tracking data rather than from the images, so they describe activity the model may not have picked up on its own. In testing with a person carrying three waste bins to the curb one at a time, `gemma4` described a single trip on every attempt with `frames`, and consistently described multiple trips with `annotated_frames`. Models that already handle these sequences well, such as the `qwen3-vl` family, gain little and should stay on `frames`.
Changes reported by [state classification](/configuration/custom_classification/state_classification#review-items) models during the review item are listed in the prompt in both modes. `annotated_frames` also notes each change before the frame where it happened.
Annotated mode also caps the number of frames, since the notes already establish the order of events and extra near-duplicate frames tend to crowd out the middle of a clip. Longer review items are sampled more sparsely as a result, and typically use fewer tokens than `frames` mode for the same item.
:::note
@@ -43,10 +43,6 @@ Frigate supports presets for optimal hardware accelerated video decoding:
- [RKNN](#rockchip-platform): Frigate can utilize the media engine in RockChip SOCs to accelerate video decoding.
**Apple Silicon Mac** <CommunityBadge />
- [lighter](#apple-silicon-mac-lighter): Frigate can utilize the media engine in Apple Silicon Macs to accelerate video decoding, when running under the lighter container runtime.
**Other Hardware**
Depending on your system, these presets may not be compatible, and you may need to use manual hwaccel args to take advantage of your hardware. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro
@@ -136,8 +132,7 @@ ffmpeg:
Frigate reads Intel GPU utilization directly from the kernel's per-client DRM usage counters exposed at `/proc/<pid>/fdinfo/<fd>`. This requires:
- An Intel GPU from Broadwell (5th gen) or newer. Older GPUs do not expose these counters on any kernel.
- Linux kernel **5.19 or newer** for the `i915` driver (**6.5 or newer** on 12th gen and newer), or any release of the `xe` driver.
- Linux kernel **5.19 or newer** for the `i915` driver, or any release of the `xe` driver.
- Frigate running with permission to read other processes' fdinfo. Running as root inside the container (the default) satisfies this; non-root setups may need `CAP_SYS_PTRACE`.
No `intel_gpu_top` binary, `CAP_PERFMON`, privileged mode, or `perf_event_paranoid` tuning is required.
@@ -538,35 +533,3 @@ output_args:
Make sure that your SoC supports hardware acceleration for your input stream and your input stream is h264 encoding. For example, if your camera streams with h264 encoding, your SoC must be able to de- and encode with it. If you are unsure whether your SoC meets the requirements, take a look at the datasheet.
:::
## Apple Silicon Mac (lighter)
[lighter](https://github.com/fieldwork-ai/lighter) is an open-source container runtime for macOS. It gives a container the Mac's media engine as a standard V4L2 decoder, backed by VideoToolbox, so Frigate decodes H.264 and H.265 streams in hardware with the ffmpeg it already ships. It works on M1 and newer Macs with lighter 0.9.2 or newer.
Give the container the video device. With Docker Compose:
```yaml {4-5}
services:
frigate:
...
devices:
- lighter.sh/video=all
```
Or with `docker run`, add `--device lighter.sh/video=all`.
Then set the preset for the codec your cameras stream. The decoder is specific to the codec, so if your cameras mix H.264 and H.265, set the preset for the most common codec globally and override it on the other cameras:
```yaml
ffmpeg:
hwaccel_args: preset-apple-silicon-h264
cameras:
garage: # an H.265 camera
ffmpeg:
hwaccel_args: preset-apple-silicon-h265
```
The presets decode on the media engine and encode the Birdseye restream and timelapses there too. Scaling to the detect resolution runs on the CPU, as ffmpeg's V4L2 decoders cannot scale.
lighter can also run object detection on the Mac's Neural Engine; see [Apple Neural Engine (lighter)](object_detectors.md#apple-neural-engine-lighter).
@@ -378,10 +378,10 @@ Navigate to <NavPath path="Settings > Camera configuration > Object detection" /
Navigate to <NavPath path="Settings > Camera configuration > Objects" />.
| Field | Description |
| --------------------------------------------------------- | ------------------- |
| **Objects to track** | Add `license_plate` |
| **Object filters > License Plate > Confidence threshold** | Set to `0.7` |
| Field | Description |
| ---------------------------------------------- | ------------------- |
| **Objects to track** | Add `license_plate` |
| **Object filters > License Plate > Threshold** | Set to `0.7` |
Navigate to <NavPath path="Settings > Camera configuration > Motion detection" />.
+2 -29
View File
@@ -38,7 +38,7 @@ Frigate [defaults to MSE](#why-does-frigate-prefer-mse-over-webrtc-for-live-view
- The choices are saved **per device, per camera** in your browser's local storage.
- **WebRTC is only selectable when it can actually work for that stream.** When it can't, the option is shown disabled with the reason inline, and a more detailed reason (the failing codecs, or why the connectivity check failed) is logged to your browser's console. Common reasons:
- **Not configured**: no `candidates` or `ice_servers` are set under `go2rtc.webrtc`, and the connectivity check failed with go2rtc's default candidates (see [WebRTC extra configuration](#webrtc-extra-configuration)). This is a setup note rather than an error, since live view works without WebRTC.
- **Not configured**: no `candidates` or `ice_servers` are set under `go2rtc.webrtc` (see [WebRTC extra configuration](#webrtc-extra-configuration)).
- **Could not connect**: e.g. port `8555` isn't reachable, or a STUN/TURN server is misconfigured. Frigate runs a one-time WebRTC connectivity check when the Live view opens; the option may briefly show as "checking" while it runs.
- **Unsupported video codec**: the stream's video codec can't be played over WebRTC in your browser, most commonly H.265/HEVC in Firefox or Edge.
- **Unsupported audio codec**: WebRTC needs opus or G.711 audio, so a stream whose playback audio is only AAC (without an added opus/G.711 track) can't carry audio over WebRTC. See [Audio Support](#audio-support) for how to add one.
@@ -92,7 +92,7 @@ go2rtc:
### Setting Streams For Live UI
You can configure Frigate to allow manual selection of the stream you want to view in the Live UI. For example, you may want to view your camera's substream on mobile devices, but the full resolution stream on desktop devices. Setting the streams list will populate a dropdown in the UI's Live view that allows you to choose between the streams. This stream setting is _per device_ and is saved in your browser's local storage. When a camera has more than one stream, the dropdown also offers **Auto**, which is used until you pick a specific stream. Auto starts on the first stream, steps down the list when your connection can't keep up, and steps back up when it recovers. To retry the top stream right away, select **Try highest quality** under the stream picker. List streams from highest to lowest quality, and avoid names that are plain numbers (such as `720`), which the browser sorts ahead of the others. In the UI, drag streams to reorder them, or use **Auto order** to sort them by measured bitrate.
You can configure Frigate to allow manual selection of the stream you want to view in the Live UI. For example, you may want to view your camera's substream on mobile devices, but the full resolution stream on desktop devices. Setting the streams list will populate a dropdown in the UI's Live view that allows you to choose between the streams. This stream setting is _per device_ and is saved in your browser's local storage.
Additionally, when creating and editing camera groups in the UI, you can choose the stream you want to use for your camera group's Live dashboard.
@@ -158,26 +158,6 @@ cameras:
</TabItem>
</ConfigTabs>
### Transcoded streams
When a camera has no suitable sub stream, Frigate can add lower-quality streams that go2rtc transcodes to H.264 while someone is watching. They appear in the stream list like any other stream, so Auto mode can step down to them. Enable them under <NavPath path="Settings > Camera configuration > Live playback" />, or in YAML:
```yaml
cameras:
test_cam:
live:
transcode:
enabled: true
source: test_cam # optional, defaults to the first live stream
qualities:
- height: 720
bitrate: 1200 # kbps
- height: 480
bitrate: 500
```
Each quality becomes a go2rtc stream named `<camera>_transcode_<height>p`. go2rtc picks a hardware encoder automatically and falls back to the CPU, which costs CPU for each transcode while it is being watched. Check go2rtc's `api/ffmpeg/hardware` page to see which encoder it found. Using a sub stream as the `source` lowers the cost.
### WebRTC extra configuration:
WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration:
@@ -383,13 +363,6 @@ When your browser runs into problems playing back your camera streams, it will l
- `Safari reported InvalidStateError.`
- `Safari reported decoding errors.`
- **mse-codec**
- What it means: go2rtc has no codec for this stream that the browser can play.
- What to try: Pick a stream with a codec the browser supports (H.264 is the most compatible), or use a browser that supports the stream's codec. In Auto, Frigate skips this stream for the rest of the session.
- Possible console messages from the player code:
- `mse: streams: codecs not matched: ...`
- **stalled**
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
- What to try: This is usually indicative of the browser struggling to decode too many high-resolution streams at once. Try selecting a lower-bandwidth stream (substream), reduce the number of live streams open, improve the network connection, or lower the camera resolution. Also check your camera's keyframe (I-frame) interval: shorter intervals make playback start and recover faster. You can also try increasing the timeout value in <NavPath path="Settings > UI" /> .
+2 -2
View File
@@ -82,13 +82,13 @@ objects:
person_filter1:
friendly_name: "Roof area"
enabled: true
coordinates: "0.000,0.000,1.0,0.000,1.0,0.400,0.000,0.400"
coordinates: "0.000,0.000,1.000,0.000,1.000,0.400,0.000,0.400"
car:
mask:
car_filter1:
friendly_name: "Sidewalk area"
enabled: true
coordinates: "0.000,0.700,1.0,0.700,1.0,1.0,0.000,1.0"
coordinates: "0.000,0.700,1.000,0.700,1.000,1.000,0.000,1.000"
```
</TabItem>
+12 -117
View File
@@ -24,18 +24,15 @@ Frigate supports multiple different detectors that work on different types of ha
- [Coral EdgeTPU](#edge-tpu-detector): The Google Coral EdgeTPU is available in USB, Mini PCIe, and m.2 formats allowing for a wide range of compatibility with devices.
- [Hailo](#hailo): The Hailo-8, Hailo-8L and Hailo-8R AI Acceleration modules are available in m.2 format with a HAT for RPi devices, offering a wide range of compatibility with devices.
- <CommunityBadge /> [MemryX](#memryx-mx3): The MX3 Acceleration module is available in m.2 format, offering broad compatibility across various platforms.
- <CommunityBadge /> [DEEPX](#deepx-npu): The DEEPX NPU is available in m.2 format and as a HAT+ for the Raspberry Pi 5, offering broad compatibility across various platforms.
**AMD**
- [ROCm](#amdrocm-gpu-detector): ROCm can run on AMD Discrete GPUs to provide efficient object detection.
- [ONNX](#onnx): ROCm will automatically be detected and used as a detector in the `-rocm` Frigate image when a supported ONNX model is configured.
- <CommunityBadge /> [XDNA2](#amd-xdna2): AMD Ryzen AI / XDNA2 NPUs can run object detection through the community-maintained `frigate-xdna` ZMQ sidecar.
**Apple Silicon**
- [Apple Silicon](#apple-silicon-detector): Apple Silicon can run on M1 and newer Apple Silicon devices.
- <CommunityBadge /> [ONNX](#apple-neural-engine-lighter): the ONNX detector runs on the Neural Engine of M1 and newer Macs when Frigate runs under the lighter container runtime.
**Intel**
@@ -104,36 +101,32 @@ Coral EdgeTPU and MemryX accelerators can only be opened by one process, so thos
### Running more than one model
Cameras can be split across models by scene, which is useful when some cameras benefit from a differently trained model, such as thermal cameras. Each model declares the `scene` it is for, and each camera picks one with `detect -> scene`:
Cameras can be split across models by scene, which is useful when indoor and outdoor cameras benefit from differently trained models. Each model declares the `scene` it is for, and each camera picks one with `detect -> scene`:
```yaml
models:
- scene: default
path: plus://your-model
- scene: outdoor
path: plus://your-outdoor-model
devices:
- edgetpu:pci:0
- scene: thermal
path: /config/model_cache/thermal.onnx
- scene: indoor
path: /config/model_cache/indoor.onnx
model_type: yolo-generic
devices:
- openvino:GPU
cameras:
driveway:
...
backyard_thermal:
detect:
scene: thermal
scene: outdoor
...
hallway:
detect:
scene: indoor
...
```
A scene is any name made up of letters, numbers, `_`, and `-`. The model with a scene of `default` is used by every camera that does not set one (or sets a scene that no model is configured for), and `default` is used when a model does not declare a scene. Changing a camera's scene requires a restart.
:::warning
Scenes are for running **different** models. Do not configure the same model under several scenes to dedicate a detector to specific cameras: every detector of a model already serves every camera using it, and splitting them only leaves some detectors idle while others fall behind. Frigate detects models that use the same model file, even under a different path or file name, combines them into one model with all of their hardware, and logs a warning.
:::
Available scenes are `all`, `indoor`, `outdoor`, `indoor_thermal`, and `outdoor_thermal`. A model with a scene of `all` is used by every camera that does not set one, and `all` is the default when a model does not declare a scene. Changing a camera's scene requires a restart.
### Choosing a model size
@@ -490,7 +483,7 @@ See [ONNX supported models](#onnx) for supported models, there are some caveats:
## ONNX
ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, TensorRT, and a Mac's Neural Engine. On startup Frigate will automatically try to use a GPU if one is available.
ONNX is an open format for building machine learning models, Frigate supports running ONNX models on CPU, OpenVINO, ROCm, and TensorRT. On startup Frigate will automatically try to use a GPU if one is available.
:::info
@@ -506,9 +499,6 @@ If the correct build is used for your GPU then the GPU will be detected and used
- Nvidia GPUs will automatically be detected and used with the ONNX detector in the `-tensorrt` Frigate image.
- Jetson devices will automatically be detected and used with the ONNX detector in the `-tensorrt-jp6` Frigate image.
- **Apple Silicon Mac** <CommunityBadge />
- The Neural Engine will automatically be detected and used with the ONNX detector when Frigate runs under lighter with its Neural Engine device. See [Apple Neural Engine (lighter)](#apple-neural-engine-lighter).
:::
:::tip
@@ -524,22 +514,6 @@ models:
:::
### Apple Neural Engine (lighter) {#apple-neural-engine-lighter}
[lighter](https://github.com/fieldwork-ai/lighter) is an open-source container runtime for macOS. A container started with its `lighter.sh/ane` device gets an ONNX Runtime execution provider that runs models on the Mac's Neural Engine, and the ONNX detector uses it automatically, with the same models and configuration as on any other hardware. It works on M1 and newer Macs with lighter 0.9.2 or newer.
Give the Frigate container the Neural Engine device. With Docker Compose:
```yaml
services:
frigate:
image: ghcr.io/blakeblackshear/frigate:stable-standard-arm64
devices:
- lighter.sh/ane=all
```
Or with `docker run`, add `--device lighter.sh/ane=all`. Frigate then reports the Neural Engine under **Settings > System > Detection models**, and the ONNX detector's model loads on it. lighter can also decode camera streams on the Mac's media engine; see [Video Decoding](hardware_acceleration_video.md#apple-silicon-mac-lighter).
### Configuration {#configuration-onnx}
<ModelConfigDropdown detectorTitle="ONNX" models={objectDetectorsModels.onnx.models} />
@@ -568,28 +542,6 @@ When using CPU detectors, you can add one CPU detector per camera. Adding more d
# Community Supported Detectors
## AMD XDNA2
AMD Ryzen AI / XDNA2 NPUs can be used through the community-maintained
[frigate-xdna](https://github.com/mitchins/frigate-xdna) detector sidecar.
The sidecar runs separately from Frigate and connects using Frigate's ZMQ
detector interface.
Currently qualified on **Ryzen AI Max 300 / Strix Halo**. Other XDNA2 devices
are not yet qualified; XDNA1 is unsupported.
Follow the frigate-xdna setup instructions to prepare and start the sidecar
before starting Frigate.
### Configuration {#configuration-xdna2}
Using the detector config below will connect Frigate to the sidecar:
<ModelConfigDropdown detectorTitle="AMD XDNA2" models={objectDetectorsModels.xdna2.models} />
The example assumes Frigate and the sidecar share a Docker network where the
sidecar is named `xdna`.
## MemryX MX3
This detector is available for use with the MemryX MX3 accelerator M.2 module. Frigate supports the MX3 on compatible hardware platforms, providing efficient and high-performance object detection.
@@ -660,63 +612,6 @@ For detailed instructions on compiling models, refer to the [MemryX Compiler](ht
---
## DEEPX NPU
This detector is available for use with the DEEPX NPU, both the DX-M1 M.2 module and the DX-M1M on the Sixfab AI HAT+ for the Raspberry Pi 5. The configuration below applies unchanged to either form factor. DEEPX NPU support in Frigate is developed and maintained by [Sixfab](https://sixfab.com).
See the [installation docs](../frigate/installation.md#deepx-npu) for information on installing the DEEPX kernel driver and runtime on the host and passing the NPU through to the container.
To run a model on a DEEPX NPU, list a `deepx` device on that model.
:::info
The DX-RT Python bindings are not part of the Frigate image. They are downloaded and installed into `/config/.local` the first time a DEEPX device is configured, verified against pinned checksums, and updated automatically when a Frigate release pins a new version. If the container has no internet access, see [Detector runtimes](/frigate/network_requirements#detector-runtimes) for how to provide the files yourself.
:::
### Configuration {#configuration-deepx}
<ModelConfigDropdown detectorTitle="DEEPX" models={objectDetectorsModels.deepx.models} />
Frigate does not bundle a model for this detector. Models must be compiled to DEEPX's `.dxnn` format. Two model types are supported:
- `yolo-generic` for YOLO object detection models, the recommended default. The detector reads the model's output layout from the compiled file, so anchor-based, anchor-free and NMS-in-head models all work with the same configuration, as do models compiled with DEEPX's Post-Processing Unit (PPU) support.
- `yolox` for YOLOX models compiled without PPU support, whose raw head needs Frigate's YOLOX decoder. A YOLOX model compiled with PPU support works under either `yolox` or `yolo-generic`.
The quickest way to get one is the [DEEPX ModelZoo](https://developer.deepx.ai/modelzoo), which publishes pre-compiled `.dxnn` files for a range of YOLO object detection models. Download the `.dxnn`, bind-mount it into the container, and point the model's `path` at it. Alternatively, compile your own model with the DX-COM compiler. The recommended starting point is `yolox-s_640x640_ppu.dxnn`, the fastest ModelZoo model measured through Frigate:
```yaml
models:
- devices:
- deepx:PCIe:0
path: /config/model_cache/deepx/yolox-s_640x640_ppu.dxnn
labelmap_path: /labelmap/coco-80.txt
model_type: yolo-generic
width: 640
height: 640
```
For PPU models, use a `.dxnn` compiled with DX-COM 2.4.0 or later. Frigate reads the PPU head layout the compiler writes into the file and refuses to load a PPU model without it.
`model_type` must be set to `yolo-generic` or `yolox` to match the model; `yolo-generic` is the recommended default unless the model is a raw YOLOX export. Frigate defaults it to `ssd`, which this detector does not support, so the detector refuses to start on a model that leaves it unset.
`width` and `height` must match the resolution the model was compiled for. Quantization parameters are baked into the `.dxnn` file at compile time, so no normalization is applied on the host and Frigate's default `input_tensor`, `input_pixel_format`, and `input_dtype` values do not need to be overridden.
A DEEPX device is `PCIe:<index>`, as reported on the detector settings page. The NPU daemon multiplexes across processes, so the same device may be listed more than once to run additional inference processes against it:
```yaml
models:
- devices:
- deepx:PCIe:0
- deepx:PCIe:0
```
#### Label maps
The object detection models in the DEEPX ModelZoo are trained on the standard 80-class COCO label set, so `labelmap_path` must be set to `/labelmap/coco-80.txt`. Frigate's default label map uses an extended 91-class COCO scheme, and leaving it in place will cause detections to be reported as the wrong object type. For `yolo-generic` models the label map is also what the detector uses to tell the output layout, so a label map with the wrong number of classes is reported as an error at startup.
---
## NVidia TensorRT Detector
Nvidia Jetson devices may be used for object detection using the TensorRT libraries. Due to the size of the additional libraries, this detector is only provided in images with the `-tensorrt-jp6` tag suffix, e.g. `ghcr.io/blakeblackshear/frigate:stable-tensorrt-jp6`. This detector is designed to work with Yolo models for object detection.
+10 -10
View File
@@ -45,10 +45,10 @@ Any detection below `min_score` will be immediately thrown out and never tracked
Navigate to <NavPath path="Settings > Global configuration > Objects" /> to set score filters globally.
| Field | Description |
| -------------------------------------------------- | ---------------------------------------------------------------- |
| **Object filters > Person > Minimum confidence** | Minimum score for a single detection to initiate tracking |
| **Object filters > Person > Confidence threshold** | Minimum computed (median) score to be considered a true positive |
| Field | Description |
| --------------------------------------- | ---------------------------------------------------------------- |
| **Object filters > Person > Min Score** | Minimum score for a single detection to initiate tracking |
| **Object filters > Person > Threshold** | Minimum computed (median) score to be considered a true positive |
To override score filters for a specific camera, navigate to <NavPath path="Settings > Camera configuration > Objects" /> and select the camera.
@@ -103,12 +103,12 @@ Conceptually, a ratio of 1 is a square, 0.5 is a "tall skinny" box, and 2 is a "
Navigate to <NavPath path="Settings > Global configuration > Objects" /> to set shape filters globally.
| Field | Description |
| -------------------------------------------------- | ------------------------------------------------------------------------ |
| **Object filters > Person > Minimum object area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Maximum object area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Minimum aspect ratio** | Minimum width/height ratio of the bounding box |
| **Object filters > Person > Maximum aspect ratio** | Maximum width/height ratio of the bounding box |
| Field | Description |
| --------------------------------------- | ------------------------------------------------------------------------ |
| **Object filters > Person > Min Area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Max Area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Min Ratio** | Minimum width/height ratio of the bounding box |
| **Object filters > Person > Max Ratio** | Maximum width/height ratio of the bounding box |
To override shape filters for a specific camera, navigate to <NavPath path="Settings > Camera configuration > Objects" /> and select the camera.
+8 -8
View File
@@ -70,14 +70,14 @@ Object filters help reduce false positives by constraining the size, shape, and
Navigate to <NavPath path="Settings > Global configuration > Objects" />.
| Field | Description |
| -------------------------------------------------- | ------------------------------------------------------------------------ |
| **Object filters > Person > Minimum object area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Maximum object area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Minimum aspect ratio** | Minimum width/height ratio of the bounding box |
| **Object filters > Person > Maximum aspect ratio** | Maximum width/height ratio of the bounding box |
| **Object filters > Person > Minimum confidence** | Minimum score for the object to initiate tracking |
| **Object filters > Person > Confidence threshold** | Minimum computed score to be considered a true positive |
| Field | Description |
| --------------------------------------- | ------------------------------------------------------------------------ |
| **Object filters > Person > Min Area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Max Area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
| **Object filters > Person > Min Ratio** | Minimum width/height ratio of the bounding box |
| **Object filters > Person > Max Ratio** | Maximum width/height ratio of the bounding box |
| **Object filters > Person > Min Score** | Minimum score for the object to initiate tracking |
| **Object filters > Person > Threshold** | Minimum computed score to be considered a true positive |
To override filters for a specific camera, navigate to <NavPath path="Settings > Camera configuration > Objects" />.
+3 -7
View File
@@ -191,12 +191,14 @@ cameras:
detect:
enabled: false
record:
enabled: true
enabled: false
profiles:
away:
enabled: true
detect:
enabled: true
record:
enabled: true
home:
enabled: false
```
@@ -249,12 +251,6 @@ Leaving the `objects` section empty (or omitting `track`) does not clear the lis
Fields that require a Frigate restart to take effect cannot be overridden by profiles, since profiles are applied at runtime without restarting. Those fields are hidden when editing a profile override and can only be changed on the base configuration.
### Why can't a profile enable recording when it's disabled in the base config?
Frigate only sets up a camera's recording stream at startup when recording is enabled in the base config, so enabling it later from a profile has no effect. The same applies to turning recording on from the UI or MQTT.
To keep recording off by default, leave `record.enabled: true` in the base config and create a profile that sets `record.enabled: false`. Activate that profile and it will be restored automatically when Frigate starts.
### Can I schedule profiles to be enabled or disabled at certain times?
Not within Frigate itself. Frigate is an NVR, not an automation platform, so it intentionally does not include a scheduler for activating profiles. Instead, activate profiles from an automation platform that already handles time- and event-based triggers well, such as [Home Assistant](https://www.home-assistant.io/) or [Node-RED](https://nodered.org/). These integrate with Frigate and give you far more robust and flexible scheduling than a built-in scheduler could.
+8 -5
View File
@@ -133,12 +133,13 @@ Switching between V1 and V2 requires reindexing your embeddings. The embeddings
### GenAI Provider
Frigate can use a GenAI provider for semantic search embeddings when that provider has the `embeddings` role. See [Provider support](/configuration/genai/genai_config#provider-support) for which providers can serve this role.
Frigate can use a GenAI provider for semantic search embeddings when that provider has the `embeddings` role. Currently, only **llama.cpp** supports multimodal embeddings (both text and images).
To use a GenAI provider for semantic search:
To use llama.cpp for semantic search:
1. Configure a GenAI provider with `embeddings` in its `roles`, using a multimodal embedding model (both text and images). See [Embedding models](/configuration/genai/genai_config#embedding-models) for recommendations, and your provider's section of the [GenAI docs](/configuration/genai/genai_config) for any extra setup it needs.
1. Configure a GenAI provider with `embeddings` in its `roles`.
2. Set the semantic search model to the GenAI config key (e.g. `default`).
3. Start the llama.cpp server with `--embeddings` and `--mmproj` for image support.
<ConfigTabs>
<TabItem value="ui">
@@ -173,6 +174,8 @@ semantic_search:
</TabItem>
</ConfigTabs>
The llama.cpp server must be started with `--embeddings` for the embeddings API, and a multi-modal embeddings model. See the [llama.cpp server documentation](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) for details.
:::note
Switching between Jina models and a GenAI provider requires reindexing. Embeddings from different backends are incompatible.
@@ -242,8 +245,8 @@ Triggers are best configured through the Frigate UI.
1. Navigate to <NavPath path="Settings > Enrichments > Triggers" /> and select a camera from the dropdown menu.
2. Click **Add Trigger** to create a new trigger or use the pencil icon to edit an existing one.
3. In the **Create Trigger** wizard:
- Enter a **Name** for the trigger (e.g., "Red Car Alert"). Frigate derives the trigger's
internal **ID** from this name, which can be revealed and edited with the show/hide toggle.
- Enter a **Name** for the trigger (e.g., "Red Car Alert").
- Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera").
- Select the **Type** (`Thumbnail` or `Description`).
- For `Thumbnail`, select an image to trigger this action when a similar thumbnail image is detected, based on the threshold.
- For `Description`, enter text to trigger this action when a similar tracked object description is detected.
+4 -4
View File
@@ -28,7 +28,7 @@ During testing, enable the Zones option for the [Debug view](/usage/live#the-sin
1. Navigate to <NavPath path="Settings > Camera configuration > Masks / Zones" /> and select the desired camera.
2. Under the **Zones** section, click the plus icon to add a new zone.
3. Click on the camera's latest image to create the points for the zone boundary. Click the first point again to close the polygon.
4. Configure zone options such as **Name**, **Objects**, **Loitering Time**, and **Inertia** in the zone editor.
4. Configure zone options such as **Friendly name**, **Objects**, **Loitering time**, and **Inertia** in the zone editor.
5. Press **Save** when finished.
</TabItem>
@@ -200,7 +200,7 @@ When using loitering zones, a review item will behave in the following way:
1. Navigate to <NavPath path="Settings > Camera configuration > Masks / Zones" /> and select the desired camera.
2. Edit or create the zone (e.g., `sidewalk`).
- Set **Loitering Time** to the desired number of seconds (e.g., `4`)
- Set **Loitering time** to the desired number of seconds (e.g., `4`)
- Under **Objects**, add the relevant object types (e.g., `person`)
</TabItem>
@@ -291,7 +291,7 @@ Accurate real-world distance measurements are required to estimate speeds. These
1. Navigate to <NavPath path="Settings > Camera configuration > Masks / Zones" /> and select the desired camera.
2. Create or edit a zone with exactly 4 points aligned to the ground plane.
3. In the zone editor, enable **Speed Estimation** and enter the real-world **Line A distance**, **Line B distance**, **Line C distance**, and **Line D distance** between each pair of consecutive points.
3. In the zone editor, enter the real-world **Distances** between each pair of consecutive points.
- For example, if the distance between the first and second points is 10 meters, between the second and third is 12 meters, etc.
4. Distances are measured in meters (metric) or feet (imperial), depending on the **Unit system** setting.
@@ -358,7 +358,7 @@ Zones can be configured with a minimum speed requirement, meaning an object must
1. Navigate to <NavPath path="Settings > Camera configuration > Masks / Zones" /> and select the desired camera.
2. Edit or create the zone with distances configured.
- Set **Speed Threshold** to the desired minimum speed (e.g., `20`)
- Set **Speed threshold** to the desired minimum speed (e.g., `20`)
- The unit is kph or mph, depending on the **Unit system** setting
</TabItem>
+2 -2
View File
@@ -54,7 +54,7 @@ An object filter mask drops any [bounding box](#bounding-box) whose bottom cente
## Min Score
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. Set with `min_score` in the config, shown as **Minimum confidence** in the settings UI.
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
@@ -86,7 +86,7 @@ A more specific identity assigned to a [tracked object](#tracked-object-event-in
## Threshold
The median score an object must reach to be considered a true positive. Set with `threshold` in the config, shown as **Confidence threshold** in the settings UI.
The median score an object must reach to be considered a true positive.
## Top Score
+2 -72
View File
@@ -65,26 +65,14 @@ Frigate supports multiple different detectors that work on different types of ha
- [Supports many model architectures](../../configuration/object_detectors#memryx-mx3)
- Runs best with tiny, small, or medium-size models
- <CommunityBadge /> [DEEPX](#deepx-npu): The DEEPX NPU is available in m.2 format and as a HAT+ for the Raspberry Pi 5, allowing for a wide range of compatibility with devices.
- [Supports YOLO model architectures](../../configuration/object_detectors#deepx-npu)
- Runs best with tiny or small size models
- Runs efficiently on low power hardware
**AMD**
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection
- [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector)
- Runs best on discrete AMD GPUs
- <CommunityBadge /> [XDNA2 (Ryzen AI)](#amd-xdna2): AMD XDNA2 NPU (sub-watt power AI/ML processor separate to the GPU) inside Strix and other "AI" branded AMD platforms
- Has only been tested with YOLOv9, in theory other graphs may be compiled too.
- Runs via ZMQ proxy which adds some latency, only recommended for local connection
**Apple Silicon**
- [ONNX via lighter](#apple-silicon): The ONNX detector runs on the Neural Engine of M1 and newer Macs when Frigate runs in the lighter container runtime
- [Supports the same model architectures as the ONNX detector](../../configuration/object_detectors#apple-neural-engine-lighter)
- Runs inside the Frigate container, with no separate detector process to set up
- The recommended way to run Frigate on a Mac
- [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-detector)
- Runs well with any size models including large
@@ -189,7 +177,7 @@ Frigate is able to utilize an Nvidia GPU which supports the 12.x series of CUDA
#### Minimum Hardware Support
12.x series of CUDA libraries are used which have minor version compatibility. The minimum driver version on the host system must be `>=545`. The oldest supported GPU generation for object detection is Pascal (Compute Capability 6.0 or greater). Older generations such as Maxwell are not supported but can still be used by following a [community workaround](https://github.com/blakeblackshear/frigate/discussions/20088). For other GPUs, check the NVIDIA GPU Compute Capability table linked below.
12.x series of CUDA libraries are used which have minor version compatibility. The minimum driver version on the host system must be `>=545`. Also the GPU must support a Compute Capability of `5.0` or greater. This generally correlates to a Maxwell-era GPU or newer, check the NVIDIA GPU Compute Capability table linked below.
Make sure your host system has the [nvidia-container-runtime](https://docs.docker.com/config/containers/resource_constraints/#access-an-nvidia-gpu) installed to pass through the GPU to the container and the host system has a compatible driver installed for your GPU.
@@ -218,13 +206,7 @@ Inference is done with the `onnx` detector type. Speeds will vary greatly depend
### Apple Silicon
Frigate on a Mac is best run in the [lighter](https://github.com/fieldwork-ai/lighter) container runtime, where the [ONNX detector](../configuration/object_detectors.md#apple-neural-engine-lighter) runs on the Neural Engine of M1 and newer Macs from inside the Frigate container. There is no separate detector process to install or keep running, and the same container can decode video on the Mac's media engine.
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
| ---- | -------------------------------------- | ----------------------- | ---------------------- |
| M1 | t-320: 3.3 ms s-320: 7 ms s-640: 13 ms | 320: 6.6 ms | Nano-320: 38 ms |
Alternatively, with the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon.
With the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon.
:::warning
@@ -275,32 +257,6 @@ The MX3 is a pipelined architecture, where the maximum frames per second support
Inference speeds may vary depending on the host platform. The above data was measured on an **Intel 13700 CPU**. Platforms like Raspberry Pi, Orange Pi, and other ARM-based SBCs have different levels of processing capability, which may limit total FPS.
### DEEPX NPU
Frigate supports the DEEPX NPU in both of its form factors: the **DX-M1** M.2 module, which works on x86 (Intel/AMD) and ARM-based SBCs such as the Raspberry Pi 5, and the **DX-M1M** on the [Sixfab AI HAT+](https://docs.sixfab.com/docs/ai-hat-plus-raspberry-pi-5-quickstart) for the Raspberry Pi 5. Both use the same driver and runtime, so the configuration is identical for either one. DEEPX NPU support in Frigate is developed and maintained by [Sixfab](https://sixfab.com).
The DEEPX driver and runtime run on the Docker host rather than inside the Frigate container and must be installed before the NPU can be used. See the [installation docs](installation.md#deepx-npu) for the setup steps and [the detector docs](/configuration/object_detectors#deepx-npu) for the configuration.
Frigate does not bundle a model for this detector. Models use DEEPX's `.dxnn` format, and pre-compiled YOLO models can be downloaded from the [DEEPX ModelZoo](https://developer.deepx.ai/modelzoo). Prefer a model with a `_ppu` suffix whenever one is available for the architecture you want: these run part of the post-processing on the NPU itself and are considerably faster, roughly 2.5x for the same architecture and input size. **YOLOX-S with PPU is the recommended starting point.**
Inference times for a few recommended models, measured through Frigate's own stats on a DX-M1:
| Model | Input Size | DX-M1 Inference Time |
| ----------------- | ---------- | -------------------- |
| YOLOX-S (PPU) | 640 | ~ 13 ms |
| YOLOv9-t (PPU) | 640 | ~ 18 ms |
| YOLOv4 (PPU) | 512 | ~ 20 ms |
| YOLOX-S | 640 | ~ 34 ms |
| YOLOv9-s | 640 | ~ 39 ms |
Other ModelZoo YOLO variants are also supported but have not been measured. Inference speeds vary with the host platform, so a slower host such as a Raspberry Pi 5 will report higher times than those above.
:::note
A few ModelZoo models can not be used with Frigate: SSD models (they are trained on Pascal VOC, so their labels do not match Frigate's), DAMO-YOLO models, face and pose models, and the PPU builds of YOLOv7.
:::
### Nvidia Jetson
Jetson devices are supported via the TensorRT or ONNX detectors when running Jetpack 6. It will [make use of the Jetson's hardware media engine](/configuration/hardware_acceleration_video#nvidia-jetson) when configured with the [appropriate presets](/configuration/ffmpeg_presets#hwaccel-presets), and will make use of the Jetson's GPU and DLA for object detection when configured with the [TensorRT detector](/configuration/object_detectors#nvidia-tensorrt-detector).
@@ -341,32 +297,6 @@ The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms fo
| ---------------- | ----------------------------------- |
| yolov9-tiny | ~ 4 ms |
### AMD Ryzen AI / XDNA2
Frigate supports AMD XDNA2 NPUs through the community-maintained
frigate-xdna ZMQ sidecar. It works with stock Frigate and supports
Frigate+ models or compatible local YOLO ONNX models. Models are compiled
once on the target system and cached for subsequent use.
Currently qualified on **Ryzen AI Max 300 / Strix Halo**. Other XDNA2
devices are not yet qualified; XDNA1 is unsupported.
Measured YOLOv9 detector latency on Strix Halo:
| Model | 320 | 640 |
| ----- | ---: | ---: |
| YOLOv9-T | ~7.4 ms | unsupported |
| YOLOv9-S | ~9.0 ms | ~20.0 ms |
| YOLOv9-M | ~13.1 ms | ~34.4 ms |
| YOLOv9-C | ~14.1 ms | ~35.2 ms |
| YOLOv9-E | ~69.4 ms | ~224.8 ms |
**YOLOv9-C at 320 is the recommended quality/performance balance.**
C at 640 is also usable where the lower throughput is acceptable.
Setup, model preparation, and compatibility details are available
[in the frigate-xdna documentation](https://github.com/mitchins/frigate-xdna).
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
This is taken from a [user question on reddit](https://www.reddit.com/r/homeassistant/comments/q8mgau/comment/hgqbxh5/?utm_source=share&utm_medium=web2x&context=3). Modified slightly for clarity.
+2 -95
View File
@@ -78,9 +78,9 @@ Users of the Snapcraft build of Docker cannot use storage locations outside your
Frigate utilizes shared memory to store frames during processing. The default `shm-size` provided by Docker is **64MB**.
Frigate needs about **210MB** for **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). If raising the shm size does not help, check your [process and file limits](#process-and-file-limits) as well.
The default shm size of **128MB** is fine for setups with **2 cameras** detecting at **720p**. If Frigate is exiting with "Bus error" messages, it is likely because you have too many high resolution cameras and you need to specify a higher shm size, using [`--shm-size`](https://docs.docker.com/engine/reference/run/#runtime-constraints-on-resources) (or [`service.shm_size`](https://docs.docker.com/compose/compose-file/compose-file-v2/#shm_size) in Docker Compose). If raising the shm size does not help, check your [process and file limits](#process-and-file-limits) as well.
The Frigate container also stores logs and an nginx cache in shm, which can take up to **50MB**. The calculator below includes this, along with room for 2 cameras added while Frigate is running. Its result holds about 25 frames per camera, a little above the 20 frame minimum Frigate warns below. Frigate keeps up to 50 when there is room, which gives slow processes more time to read each frame.
The Frigate container also stores logs in shm, which can take up to **40MB**, so make sure to take this into account in your math as well.
<ShmCalculator/>
@@ -381,99 +381,6 @@ If you can't use Docker Compose, you can run the container with something simila
Finally, configure [hardware object detection](/configuration/object_detectors#memryx-mx3) to complete the setup.
### DEEPX NPU
The DEEPX NPU is available in two form factors, and Frigate supports both:
- **DX-M1** in the M.2 2280 form factor (like an NVMe SSD), for x86 (Intel/AMD) PCs, the Raspberry Pi 5, and other ARM SBCs with an exposed PCIe M.2 slot.
- **DX-M1M** on the [Sixfab AI HAT+](https://docs.sixfab.com/docs/ai-hat-plus-raspberry-pi-5-quickstart), a HAT+ board that connects to the Raspberry Pi 5 over PCIe Gen 3 x1.
Both present the NPU through the same PCIe driver and DX-RT runtime, so the setup below and the detector configuration are identical for either one. Nothing needs to change when moving between them.
DEEPX NPU support in Frigate is developed and maintained by [Sixfab](https://sixfab.com).
#### Versions
A DEEPX install has several separately versioned pieces, and they all have to agree. The driver, the runtime, and the daemon live on the Docker host; Frigate itself carries only the Python bindings, which it downloads on first start:
| Component | Version | Installed on | Installed by |
| -------------- | -------- | ------------ | ------------------------- |
| Kernel driver | `v2.6.0` | Host | `user_installation.sh` |
| DX-RT runtime | `v3.4.0` | Host | `user_installation.sh` |
| NPU firmware | `v2.7.4` | The module | Flashed from the host |
| DX-RT bindings | `v3.4.0` | Frigate | Downloaded at first start |
:::warning
A version mismatch does not produce a startup error. It typically shows up as inference requests that are accepted but never return a result, so detections simply stop appearing while Frigate looks healthy. If that happens after a Frigate upgrade, check every version in the table before anything else.
:::
The installation script installs the DX-RT runtime on the host and enables `dxrt.service`, so the daemon starts at boot and any other program on the host can share the NPU with Frigate. Check the firmware version with `dxrt-cli --status` and update the module if it does not match the table above.
#### Installation
The DEEPX kernel driver must be installed on the host rather than in the container, because containers share the host kernel and cannot load kernel modules. Installing it creates the `/dev/dxrt*` device nodes that are passed through to Frigate. The same script installs the DX-RT runtime and enables `dxrt.service`, the daemon that owns the NPU and hands work to it on behalf of Frigate and anything else on the host.
1. Copy or download [this script](https://github.com/blakeblackshear/frigate/blob/dev/docker/deepx/user_installation.sh).
2. Ensure it has execution permissions with `sudo chmod +x user_installation.sh`
3. Run the script with `./user_installation.sh`
4. **Restart your computer** to complete driver installation.
Confirm the NPU is visible before continuing:
```bash
ls /dev/dxrt*
```
Then confirm the daemon is running and listening in `/run/dxrt`:
```bash
systemctl is-active dxrt.service
ls /run/dxrt/
```
#### Setup
To set up Frigate, follow the default installation instructions, for example: `ghcr.io/blakeblackshear/frigate:stable`
#### Docker configuration
Frigate needs the NPU device node and the directory holding the daemon's socket:
```yaml
services:
frigate:
devices:
- /dev/dxrt0:/dev/dxrt0
volumes:
- /run/dxrt:/run/dxrt
```
If you can't use Docker Compose, add `--device /dev/dxrt0:/dev/dxrt0 -v /run/dxrt:/run/dxrt` to your `docker run` command.
Add one `--device` per NPU, contiguously from `/dev/dxrt0`, since the client stops enumerating at the first gap.
The installation script configures `dxrt.service` to place its socket in `/run/dxrt` through a systemd drop-in. Mounting the directory rather than the socket file means the container sees the new socket after `dxrt.service` is restarted, rather than holding on to a deleted one.
`dxrtd` listens on an abstract socket as well, but that one does not cross into a container, so Frigate names the filesystem socket through `DXRT_DYNAMIC_IPC_ENDPOINT` on your behalf. Set that variable on the container yourself only if the daemon listens somewhere else, which means you also set it for `dxrtd` through its own systemd drop-in. The script writes `/etc/systemd/system/dxrt.service.d/frigate.conf` for exactly that, and has `dxrt.service` link the socket to `/tmp/dxrt_dynamic_ipc.sock` when it starts, so the host's own `dxrt-cli` and `dxtop` keep finding it at the default path they fall back to.
:::note
The DX-RT client exits when `dxrt.service` stops, so restart the Frigate container after restarting `dxrt.service`.
:::
The device node is needed as well as the socket, because the client opens the NPU directly even though the daemon arbitrates access. Without it, inference fails with `Device not found`.
`/dev/shm` does not need sharing.
The DX-RT python bindings are not shipped in the Frigate image. Frigate downloads them on first start when a DEEPX detector is configured, and caches them under `/config`.
#### Configuration
Finally, configure [hardware object detection](/configuration/object_detectors#deepx-npu) to complete the setup.
### Rockchip platform
Make sure that you use a linux distribution that comes with the rockchip BSP kernel 5.10 or 6.1 and necessary drivers (especially rkvdec2 and rknpu). To check, enter the following commands:
+7 -7
View File
@@ -5,7 +5,7 @@ title: Updating
# Updating Frigate
The current stable version of Frigate is **0.18.2**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.18.2).
The current stable version of Frigate is **0.18.0**. The release notes and any breaking changes for this version can be found on the [Frigate GitHub releases page](https://github.com/blakeblackshear/frigate/releases/tag/v0.18.0).
Keeping Frigate up to date ensures you benefit from the latest features, performance improvements, and bug fixes. The update process varies slightly depending on your installation method (Docker, Home Assistant App, etc.). Below are instructions for the most common setups.
@@ -31,21 +31,21 @@ If you’re running Frigate via Docker (recommended method), follow these steps:
2. **Update and Pull the Latest Image**:
- If using Docker Compose:
- Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.18.2` instead of `0.17.1`). For example:
- Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.18.0` instead of `0.17.1`). For example:
```yaml
services:
frigate:
image: ghcr.io/blakeblackshear/frigate:0.18.2
image: ghcr.io/blakeblackshear/frigate:0.18.0
```
- Then pull the image:
```bash
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
docker pull ghcr.io/blakeblackshear/frigate:0.18.0
```
- **Note for `stable` Tag Users**: If your `docker-compose.yml` uses the `stable` tag (e.g., `ghcr.io/blakeblackshear/frigate:stable`), you don’t need to update the tag manually. The `stable` tag always points to the latest stable release after pulling.
- If using `docker run`:
- Pull the image with the appropriate tag (e.g., `0.18.2`, `0.18.2-tensorrt`, or `stable`):
- Pull the image with the appropriate tag (e.g., `0.18.0`, `0.18.0-tensorrt`, or `stable`):
```bash
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
docker pull ghcr.io/blakeblackshear/frigate:0.18.0
```
3. **Start the Container**:
@@ -100,7 +100,7 @@ If an update causes issues:
1. Stop Frigate.
2. Restore your backed-up config file and database.
3. Revert to the previous image version:
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.18.1`) in your `docker run` command.
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.17.1`) in your `docker run` command.
- For Docker Compose: Edit your `docker-compose.yml`, specify the older version tag (e.g., `ghcr.io/blakeblackshear/frigate:0.16.4`), and re-run `docker compose up -d`.
- For Home Assistant: Restore from the app/addon backup you took before you updated.
4. Verify the old version is running again.
-67
View File
@@ -45,8 +45,6 @@ There are many solutions available to implement reverse proxies and the communit
- [Nginx](#nginx-reverse-proxy)
- [Traefik](#traefik-reverse-proxy)
- [Caddy](#caddy-reverse-proxy)
- [HAProxy](#haproxy)
- [OPNSense HAProxy Add-on](#opnsense-haproxy-add-on)
## Apache2 Reverse Proxy
@@ -220,68 +218,3 @@ frigate.YOUR_DOMAIN.TLD {
}
```
## HAProxy
This example is for a pure haproxy installation on a dedicated host. It assumes you have already combined the certificate parts in de cert file.
The frontend config:
```# Public Service: SSLSecured (Frigate)
frontend SSLSecured
bind XXX.XXX.XXX.XXX:443 ssl crt /etc/haproxy/certs/frigate.cert
mode http
option http-keep-alive
default_backend NVR
# CONDITION: NVR
acl acl_frigate hdr(host) -i nvr.example.video
# RULE: NVR
use_backend NVR if acl_frigate
```
The backend config:
```# Backend Pool: NVR (Frigate)
backend NVR
mode http
balance source
# stickiness
stick-table type ipv4 size 50k expire 30m
stick on src
http-reuse safe
server Frigate XXX.XXX.XXX.XXX:8971
```
The above config is created for a Layer 7 proxy (application layer, in this case http), if streaming the camera feeds are a little spotty or somewhat unstable, you can try to change the *mode* from http to tcp in both the frontend and backend config, which sets up a Layer 4 proxy (proxy by protocol). Make sure you change hdr(host) to req.ssl_sni in the frontend Condition to make the host based condition work on tcp traffic.
## OPNSense HAProxy add-on
This guide is for the OPNSense firewall add-on *os-haproxy* and is configured and tested on version 5.1 of the add-on on OPNSense 26.7.
The guide starts where you are already done setting up the system, *have a certificate added to the certificate store* and *installed the plugin/add-on*.
Step by step plan:
1. Go to Services -> HAProxy, click on Settings;
2. Add a Real Server under *Real Servers*;
3. Fill in the fields *Name or Prefix* and *Description*;
4. set *Type* to **static**;
5. fill in the frigate IP in the *FQDN or IP* field;
6. under *Common Options* fill *Port* with the port number for your Frigate (e.g. 8971) and
7. Click **Save** to store this configuration.
8. Under *Virtual Services* open *Backend Pools*;
9. Add a *Backend Pool*;
10. Fill in the fields *Name or Prefix* and *Description* but use something else then what you used under *Real Servers* otherwise if and when rules and conditions are needed it gets really confusing really fast, the use of them is out of the scope of this guide for a basic functioning haproxy add-on;
11. set *Mode* to **HTTP (Layer 7)**;
12. set *Balancing Algorithm* to **Source-IP Hash**;
13. set *Servers* by choosing the *Real Servers* instance you added in step 2->6;
14. Click **Save** to store this configuration.
15. Under *Virtual Services* open *Public Services*;
16. Add a *Public Service*;
17. Fill in the fields *Name or Prefix* and *Description* but use something else then what you used under *Real Servers* otherwise if and when rules and conditions are needed it gets really confusing really fast, the use of them is out of the scope of this guide for a basic functioning haproxy add-on;
18. Fill in the external address the haproxy instance will be listening on in *Listen Addresses*;
19. set *Type* to **HTTP/HTTPS (SSL offloading)**
20. set *Default Backend Pool* to the option carrying *the name you used in step 9*;
21. check *Enable SSL Offloading*;
22. Under *SSL Offloading* you set *Default certificate* to the certificate you added before you started following this guide;
23. Click **Save** to store this configuration.
24. Click **Apply** to reload the haproxy config on the service running.
**NOTE: This will forward _ALL_ requests on port 443 to your frigate instance.** If that is not what you want or have more then one website alive behind the haproxy you will need to create a *Condition* to trigger when the right URL is used and a *Rules* rule to use the right backend under *Rules & Checks* when the condition is met. This is a useful hint for those who need it, though it is out of the scope of this guide.
+2 -19
View File
@@ -11,12 +11,6 @@ MQTT requires a network connection to your broker. This is typically local, but
:::
:::note
Wherever a topic below includes a camera, mask, or zone name, use its `ID` from the config, not its `friendly_name`. For example, a camera with `friendly_name: "Back Yard"` and ID `back_yard` publishes to `frigate/back_yard/...`, not `frigate/Back Yard/...`.
:::
## General Frigate Topics
### `frigate/available`
@@ -218,7 +212,6 @@ An `update` with the same ID will be published when:
- The severity changes from `detection` to `alert`
- Additional objects are detected
- An object is recognized via face, lpr, etc.
- A [state classification](/configuration/custom_classification/state_classification#review-items) model changes state
When the review activity has ended a final `end` message is published.
@@ -242,8 +235,7 @@ When the review activity has ended a final `end` message is published.
"objects": ["person", "car"],
"sub_labels": [],
"zones": [],
"audio": [],
"classification_state_changes": []
"audio": []
}
},
"after": {
@@ -262,16 +254,7 @@ When the review activity has ended a final `end` message is published.
"objects": ["person", "car"],
"sub_labels": ["Bob"],
"zones": ["front_yard"],
"audio": [],
"classification_state_changes": [
// verified changes of state classification models on this camera
{
"model": "front_gate",
"from": "closed",
"to": "open",
"timestamp": 1718987131.52
}
]
"audio": []
}
}
}
@@ -27,10 +27,6 @@ The [Advanced Camera Card](https://card.camera/#/README) is a Home Assistant das
It supports automatically setting the sub labels in Frigate for person objects that are detected and recognized.
This is a fork (with fixed errors and new features) of [original Double Take](https://github.com/jakowenko/double-take) project which, unfortunately, isn't being maintained by author.
## [frigate-abr](https://github.com/007hacky007/frigate-abr)
[frigate-abr](https://github.com/007hacky007/frigate-abr) is a drop-in Docker image of Frigate that adds adaptive bitrate (ABR) playback for recordings: a sidecar transcodes footage to lower quality tiers on demand, for reviewing over slow remote connections. Segments are transcoded when played and cached, so no additional stream is recorded. Frigate itself is not modified.
## [Frigate Notify](https://github.com/0x2142/frigate-notify)
[Frigate Notify](https://github.com/0x2142/frigate-notify) is a simple app designed to send notifications from Frigate to your favorite platforms. Intended to be used with standalone Frigate installations - Home Assistant not required, MQTT is optional but recommended.
+1 -7
View File
@@ -21,13 +21,7 @@ Yes. Models and metadata are stored in the `model_cache` directory within the co
### Can I keep using my Frigate+ models even if I do not renew my subscription?
Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models you train during an active subscription remain licensed for your continued use even after your subscription ends — models already in your model cache will keep working indefinitely. An active subscription is required to train new models and download new versions.
### Can I use Frigate+ models commercially?
A standard subscription covers use on camera systems you own or operate, including for your business. A shop, restaurant, warehouse, or office running Frigate+ at its own locations (including multiple locations) is exactly the kind of use the subscription is for.
What the standard subscription does not cover is using Frigate+ models to provide a product or service to others. If you're deploying models at your customers' sites, bundling them with hardware you sell, or running them as part of a hosted or managed service, even if your customers never receive the model files themselves, you'll need a commercial license.
Note that professional installers are fine under standard subscriptions when each customer holds their own Frigate+ subscription. The commercial license is for cases where your license powers your customers' sites.
Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models trained with your subscription are yours to keep and use forever. However, do note that the terms and conditions prohibit you from sharing, reselling, or creating derivative products from the models.
### Why can't I submit images to Frigate+?
+13 -13
View File
@@ -63,20 +63,20 @@ Frigate+ models generally have much higher scores than the default model provide
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Global configuration > Objects" />. Under **Object filters**, set **Minimum confidence** and **Confidence threshold** for each object type, then click **Save**.
Navigate to <NavPath path="Settings > Global configuration > Objects" />. Under **Object filters**, set **Min Score** and **Threshold** for each object type, then click **Save**.
| Object | Minimum confidence | Confidence threshold |
| ----------------- | ------------------ | -------------------- |
| **dog** | .7 | .9 |
| **cat** | .65 | .8 |
| **face** | .7 | |
| **package** | .65 | .9 |
| **license_plate** | .6 | |
| **amazon** | .75 | |
| **ups** | .75 | |
| **fedex** | .75 | |
| **person** | .65 | .85 |
| **car** | .65 | .85 |
| Object | Min Score | Threshold |
| ----------------- | --------- | --------- |
| **dog** | .7 | .9 |
| **cat** | .65 | .8 |
| **face** | .7 | |
| **package** | .65 | .9 |
| **license_plate** | .6 | |
| **amazon** | .75 | |
| **ups** | .75 | |
| **fedex** | .75 | |
| **person** | .65 | .85 |
| **car** | .65 | .85 |
</TabItem>
<TabItem value="yaml">
+6 -9
View File
@@ -65,11 +65,11 @@ Some users may find that Frigate+ models result in more false positives initiall
Frigate+ models support a more relevant set of objects for security cameras. The labels for annotation in Frigate+ are configurable by editing the camera in the Cameras section of Frigate+. Currently, the following objects are supported:
- **People**: `person`, `face`, `baby`
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `garbage truck`, `license_plate`
- **People**: `person`, `face`
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `license_plate`
- **Delivery Logos**: `amazon`, `usps`, `ups`, `fedex`, `dhl`, `an_post`, `purolator`, `postnl`, `nzpost`, `postnord`, `gls`, `dpd`, `canada_post`, `royal_mail`
- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo`, `possum`, `rodent`
- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella`, `baby_stroller`
- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo`
- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella`
Other object types available in the default Frigate model are not available. Additional object types will be added in future releases.
@@ -77,12 +77,9 @@ Other object types available in the default Frigate model are not available. Add
Candidate labels are also available for annotation. These labels don't have enough data to be included in the model yet, but using them will help add support sooner. You can enable these labels by editing the camera settings.
Where possible, these labels are mapped to existing labels during training. For example, any `duck` labels are mapped to `bird` until support for new labels is added.
Where possible, these labels are mapped to existing labels during training. For example, any `baby` labels are mapped to `person` until support for new labels is added.
- **Vehicles**: `tractor`, `golf_cart`, `bus`, `airplane`, `helicopter`, `rickshaw`, `scooter`
- **Delivery Logos**: `bpost`, `auspost`, `aramex`, `transoflex`, `parcelforce`, `hermes`, `cargus`, `fan_courier`, `sameday`, `la_poste`
- **Animals**: `badger`, `chicken`, `duck`, `turkey`, `groundhog`, `boar`, `hedgehog`, `wombat`, `bobcat`, `mustelid`, `mountain_lion`, `crocodile`, `monkey`, `coyote`, `porcupine`, `sheep`, `snake`, `lizard`, `heron`, `elk`, `moose`, `pig`, `donkey`, `civet`
- **Other**: `sports_ball`, `drone`, `lawnmower`
The candidate labels are: `baby`, `bpost`, `badger`, `possum`, `rodent`, `chicken`, `groundhog`, `boar`, `hedgehog`, `tractor`, `golf cart`, `garbage truck`, `bus`, `sports ball`, `la_poste`, `lawnmower`, `heron`, `rickshaw`, `wombat`, `auspost`, `aramex`, `bobcat`, `mustelid`, `transoflex`, `airplane`, `drone`, `mountain_lion`, `crocodile`, `turkey`, `baby_stroller`, `monkey`, `coyote`, `porcupine`, `parcelforce`, `sheep`, `snake`, `helicopter`, `lizard`, `duck`, `hermes`, `cargus`, `fan_courier`, `sameday`
Candidate labels are not available for automatic suggestions.
+1 -9
View File
@@ -53,14 +53,6 @@ go2rtc:
Point the camera's inputs at the restream as described in the [restream docs](/configuration/restream.md), and swap `detect -> width` and `detect -> height` to match the rotated resolution.
### Can I add a privacy mask to hide part of my camera's view?
Frigate does not have privacy masks. [Motion masks and object filter masks](../configuration/masks.md) only affect detection, they don't hide anything in live view, recordings, or snapshots.
Privacy masks are best configured in the camera's firmware settings so the area is blacked out before the video ever leaves the camera and no extra processing is needed. Check there first.
If your camera does not support privacy masks, there is no efficient alternative. Frigate copies the camera's video into recordings and live view without decoding it, so part of the image can't be hidden without transcoding (re-encoding) the stream. This can be done with a custom ffmpeg filter in go2rtc, but it is not recommended. Every masked camera needs a continuous re-encode, which significantly increases CPU usage, especially for high resolution streams.
### My mjpeg stream or snapshots look green and crazy
This almost always means that the width/height defined for your camera are not correct. Double check the resolution with VLC or another player. Also make sure you don't have the width and height values backwards.
@@ -192,6 +184,6 @@ Filters and masks only hide the incorrect result - they don't teach Frigate what
### Where do I see problems Frigate has detected?
Open System > Health. The Notices list keeps a record of problems Frigate has found. Acknowledge an entry to hide it until the problem happens again, or mute it to hide it for good. Hidden entries stay listed under Show hidden in the filter. Ongoing conditions, such as an offline camera or recordings deleted before their retention period, appear in the status bar for admins until they clear, and the status bar links to the Notices list while it has entries showing. On mobile, tap the warning icon in the bottom navigation bar to see them.
Open System > Health. The Notices list keeps a record of problems Frigate has found, and you can dismiss any entry to acknowledge it. Ongoing conditions, such as an offline camera or recordings deleted before their retention period, appear in the status bar for admins until they clear, and the status bar links to the Notices list while it has undismissed entries. On mobile, tap the warning icon in the bottom navigation bar to see them.
The Hardware section below the notices shows whether the detection hardware, hardware acceleration, and enrichment devices in your config were found and are being used, so a GPU that silently fell back to the CPU shows up as a warning. Run stream checks to probe every camera's streams for the same problems the camera wizard reports.
+10 -2
View File
@@ -397,11 +397,19 @@ dmesg | grep -i -E "gpu|drm|reset|hang"
Messages like `trying reset from guc_exec_queue_timedout_job` or similar GPU reset/hang messages indicate a driver or hardware issue. Ensure your kernel and GPU drivers (especially Intel) are up to date.
#### Step 6: Verify go2rtc stream configuration
#### Step 6: Verify hardware acceleration configuration
An incorrect `hwaccel_args` preset can cause ffmpeg to fail silently or consume excessive CPU, starving the detector of resources.
- After upgrading Frigate, verify your preset matches your hardware (e.g., `preset-intel-qsv-h264` instead of the deprecated `preset-vaapi`).
- For h265 cameras, use the corresponding h265 preset (e.g., `preset-intel-qsv-h265`).
- Note that `hwaccel_args` are only relevant for the detect stream. Frigate does not decode the record stream.
#### Step 7: Verify go2rtc stream configuration
Ensure that the ffmpeg source names in your go2rtc configuration match the correct camera stream. A misconfigured stream name (e.g., copying a config from one camera to another without updating the stream reference) will cause the wrong stream to be used or the stream to fail entirely.
#### Step 7: Check system resources
#### Step 8: Check system resources
If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host:
+22 -30
View File
@@ -3,9 +3,6 @@ import * as path from "node:path";
import type { Config, PluginConfig } from "@docusaurus/types";
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
// Bump when a new stable release ships
const STABLE_VERSION = "0.18";
const config: Config = {
title: "Frigate",
tagline: "NVR With Realtime Object Detection for IP Cameras",
@@ -26,17 +23,17 @@ const config: Config = {
mermaid: true,
},
i18n: {
defaultLocale: "en",
locales: ["en"],
defaultLocale: 'en',
locales: ['en'],
localeConfigs: {
en: {
label: "English",
},
label: 'English',
}
},
},
themeConfig: {
announcementBar: {
id: "frigate_plus",
id: 'frigate_plus',
content: `
<span style="margin-right: 8px; display: inline-block; animation: pulse 2s infinite;">🚀</span>
Get more relevant and accurate detections with Frigate+ models.
@@ -48,8 +45,8 @@ const config: Config = {
50% { transform: scale(1.1); }
}
</style>`,
backgroundColor: "#005f73",
textColor: "#e0fbfc",
backgroundColor: '#005f73',
textColor: '#e0fbfc',
isCloseable: false,
},
docs: {
@@ -86,15 +83,15 @@ const config: Config = {
},
},
prism: {
magicComments: [
magicComments:[
{
className: "theme-code-block-highlighted-line",
line: "highlight-next-line",
block: { start: "highlight-start", end: "highlight-end" },
className: 'theme-code-block-highlighted-line',
line: 'highlight-next-line',
block: {start: 'highlight-start', end: 'highlight-end'},
},
{
className: "code-block-error-line",
line: "highlight-error-line",
className: 'code-block-error-line',
line: 'highlight-error-line',
},
],
additionalLanguages: ["bash", "json"],
@@ -134,11 +131,6 @@ const config: Config = {
srcDark: "img/branding/logo-dark.svg",
},
items: [
{
href: "https://github.com/blakeblackshear/frigate/releases",
label: `${STABLE_VERSION}`,
position: "left",
},
{
to: "/",
activeBasePath: "docs",
@@ -156,19 +148,19 @@ const config: Config = {
position: "right",
},
{
type: "localeDropdown",
position: "right",
type: 'localeDropdown',
position: 'right',
dropdownItemsAfter: [
{
label: "简体中文(社区翻译)",
href: "https://docs.frigate-cn.video",
},
],
label: '简体中文(社区翻译)',
href: 'https://docs.frigate-cn.video',
}
]
},
{
href: "https://github.com/blakeblackshear/frigate",
label: "GitHub",
position: "right",
href: 'https://github.com/blakeblackshear/frigate',
label: 'GitHub',
position: 'right',
},
],
},
+25 -34
View File
@@ -7030,9 +7030,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.21",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@@ -7698,15 +7698,14 @@
}
},
"node_modules/compression": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz",
"integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==",
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
"license": "MIT",
"dependencies": {
"bytes": "3.1.2",
"compressible": "~2.0.18",
"debug": "2.6.9",
"destroy": "1.2.0",
"negotiator": "~0.6.4",
"on-headers": "~1.1.0",
"safe-buffer": "5.2.1",
@@ -7714,10 +7713,6 @@
},
"engines": {
"node": ">= 0.8.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/compression/node_modules/bytes": {
@@ -9465,9 +9460,9 @@
}
},
"node_modules/dompurify": {
"version": "3.4.16",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz",
"integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==",
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -10130,9 +10125,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"funding": [
{
"type": "github",
@@ -11380,15 +11375,15 @@
}
},
"node_modules/image-size": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz",
"integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
"license": "MIT",
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=18"
"node": ">=16.x"
}
},
"node_modules/immer": {
@@ -17453,9 +17448,9 @@
"license": "ISC"
},
"node_modules/proxy-addr": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
"license": "MIT",
"dependencies": {
"forwarded": "0.2.0",
@@ -17463,10 +17458,6 @@
},
"engines": {
"node": ">= 0.10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/proxy-addr/node_modules/ipaddr.js": {
@@ -19238,9 +19229,9 @@
}
},
"node_modules/shell-quote": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz",
"integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
"integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -19522,9 +19513,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
+30 -49
View File
@@ -2,53 +2,42 @@ import React, { useState, useEffect } from "react";
import Admonition from "@theme/Admonition";
import styles from "./styles.module.css";
// follows calculate_shm_requirements() in frigate/util/services.py, but sizes
// for 25 frames so float rounding can't land under its 20 frame warning
const MB = 1048576;
const RESERVED_MB = 50; // logs + nginx cache
const TARGET_FRAMES = 25;
const DEFAULT_FRAMES = 50;
const frameMb = (width, height) =>
Math.round(((width * height * 1.5 + 270480) / MB) * 10) / 10;
// room for 2 cameras added at runtime, sized as 720p
const addedCamerasMb = 2 * frameMb(1280, 720);
const ShmCalculator = () => {
const [width, setWidth] = useState(1280);
const [height, setHeight] = useState(720);
const [cameraCount, setCameraCount] = useState(1);
const [result, setResult] = useState("-");
const [singleCameraShm, setSingleCameraShm] = useState("-");
const [addedCamerasShm, setAddedCamerasShm] = useState("-");
const [fullRingShm, setFullRingShm] = useState("-");
const [result, setResult] = useState("26.32MB");
const [singleCameraShm, setSingleCameraShm] = useState("26.32MB");
const [totalShm, setTotalShm] = useState("26.32MB");
const calculate = () => {
if (!width || !height || !cameraCount) {
setResult("Please enter valid values");
setSingleCameraShm("-");
setAddedCamerasShm("-");
setFullRingShm("-");
setTotalShm("-");
return;
}
const cameraFramesMb = cameraCount * frameMb(width, height) + addedCamerasMb;
// Single camera base SHM calculation (excluding logs)
// Formula: (width * height * 1.5 * 20 + 270480) / 1048576
const singleCameraBase =
(width * height * 1.5 * 20 + 270480) / 1048576;
setSingleCameraShm(`${singleCameraBase.toFixed(2)}mb`);
setSingleCameraShm(formatMb(frameMb(width, height) * TARGET_FRAMES));
setAddedCamerasShm(formatMb(addedCamerasMb * TARGET_FRAMES));
setResult(formatMb(Math.round(RESERVED_MB + cameraFramesMb * TARGET_FRAMES)));
setFullRingShm(
formatMb(Math.round(RESERVED_MB + cameraFramesMb * DEFAULT_FRAMES)),
);
};
// Total SHM calculation (multiple cameras, including logs)
const totalBase = singleCameraBase * cameraCount;
const finalResult = totalBase + 40; // Default includes logs +40mb
const formatMb = (value) => {
if (value >= 1024) {
return `${(value / 1024).toFixed(2)}gb`;
setTotalShm(`${(totalBase + 40).toFixed(2)}mb`);
// Format result
if (finalResult < 1) {
setResult(`${(finalResult * 1024).toFixed(2)}kb`);
} else if (finalResult >= 1024) {
setResult(`${(finalResult / 1024).toFixed(2)}gb`);
} else {
setResult(`${finalResult.toFixed(2)}mb`);
}
return `${Number(value.toFixed(2))}mb`;
};
const formatWithUnit = (value) => {
@@ -158,27 +147,19 @@ const ShmCalculator = () => {
</div>
<div className={styles.formulaDisplay}>
<p>
<strong>Per Camera:</strong> {formatWithUnit(singleCameraShm)}
<strong>Single Camera:</strong> {formatWithUnit(singleCameraShm)}
</p>
<p>
<strong>Formula:</strong> (width × height × 1.5 + 270480) ÷
1048576 × {TARGET_FRAMES} frames
<strong>Formula:</strong> (width × height × 1.5 × 20 + 270480) ÷
1048576
</p>
{cameraCount > 1 && (
<p>
<strong>Total ({cameraCount} cameras):</strong> {formatWithUnit(totalShm)}
</p>
)}
<p>
<strong>Room for 2 Added Cameras:</strong>{" "}
{formatWithUnit(addedCamerasShm)}
</p>
<p>
<strong>Logs and Cache:</strong> + {RESERVED_MB}
<span className={styles.unit}>mb</span>
</p>
<p>
<strong>Birdseye Restream:</strong> + 8
<span className={styles.unit}>mb</span> if enabled
</p>
<p>
<strong>For {DEFAULT_FRAMES} Frames:</strong>{" "}
{formatWithUnit(fullRingShm)}
<strong>With Logs:</strong> + 40<span className={styles.unit}>mb</span>
</p>
</div>
</div>
+28 -178
View File
@@ -412,39 +412,6 @@ paths:
security:
- frigateAdminAuth: []
x-required-role: admin
/go2rtc/streams/{stream_name}/bitrate:
get:
tags:
- Camera
summary: Go2Rtc Stream Bitrate
description: |-
**Access:** Admin role required.
Measure a go2rtc stream's bitrate over a few seconds.
operationId:
go2rtc_stream_bitrate_go2rtc_streams__stream_name__bitrate_get
parameters:
- name: stream_name
in: path
required: true
schema:
type: string
title: Stream Name
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
security:
- frigateAdminAuth: []
x-required-role: admin
/ffprobe:
get:
tags:
@@ -1088,7 +1055,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1132,7 +1098,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1287,7 +1252,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1647,7 +1611,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1695,7 +1658,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1743,7 +1705,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1785,7 +1746,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -1869,7 +1829,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -2131,7 +2090,7 @@ paths:
required: false
schema:
type: string
default: UTC
default: utc
title: Timezone
responses:
'200':
@@ -3266,7 +3225,7 @@ paths:
anyOf:
- type: string
- type: 'null'
default: UTC
default: utc
title: Timezone
responses:
'200':
@@ -3491,7 +3450,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Body
responses:
'200':
@@ -4216,20 +4174,19 @@ paths:
Get notices, most severe first.
Args:
include_hidden: Also return acknowledged and muted notices, for the
hidden list
include_dismissed: Also return dismissed notices, for the history view
Returns:
The notices
operationId: get_notices_notices_get
parameters:
- name: include_hidden
- name: include_dismissed
in: query
required: false
schema:
type: boolean
default: false
title: Include Hidden
title: Include Dismissed
responses:
'200':
description: Successful Response
@@ -4264,16 +4221,16 @@ paths:
security:
- frigateAdminAuth: []
x-required-role: admin
/notices/muted_checks:
/notices/dismissed_checks:
get:
tags:
- Notices
summary: Get Muted Checks
summary: Get Dismissed Checks
description: |-
**Access:** Admin role required.
Get the muted config and stream check rows, newest first.
operationId: get_muted_checks_notices_muted_checks_get
Get the dismissed config and stream check rows, newest first.
operationId: get_dismissed_checks_notices_dismissed_checks_get
responses:
'200':
description: Successful Response
@@ -4283,16 +4240,16 @@ paths:
security:
- frigateAdminAuth: []
x-required-role: admin
/notices/hidden:
/notices/dismissed:
delete:
tags:
- Notices
summary: Unhide All Notices
summary: Purge Dismissed
description: |-
**Access:** Admin role required.
Show every acknowledged and muted notice and check row again.
operationId: unhide_all_notices_notices_hidden_delete
Delete every dismissed notice and check row so each can show again.
operationId: purge_dismissed_notices_dismissed_delete
responses:
'200':
description: Successful Response
@@ -4302,83 +4259,18 @@ paths:
security:
- frigateAdminAuth: []
x-required-role: admin
/notices/{notice_id}/acknowledge:
/notices/{notice_id}/dismiss:
post:
tags:
- Notices
summary: Acknowledge Notice
summary: Dismiss Notice
description: |-
**Access:** Admin role required.
Hide a notice until it happens again.
Hide a notice or a config or stream check row.
Config and stream check rows and the update notice never repeat, so they
can only be muted.
operationId: acknowledge_notice_notices__notice_id__acknowledge_post
parameters:
- name: notice_id
in: path
required: true
schema:
type: string
title: Notice Id
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
security:
- frigateAdminAuth: []
x-required-role: admin
/notices/{notice_id}/mute:
post:
tags:
- Notices
summary: Mute Notice
description: |-
**Access:** Admin role required.
Hide a notice or a config or stream check row for good.
operationId: mute_notice_notices__notice_id__mute_post
parameters:
- name: notice_id
in: path
required: true
schema:
type: string
title: Notice Id
responses:
'200':
description: Successful Response
content:
application/json:
schema: {}
'422':
description: Validation Error
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
security:
- frigateAdminAuth: []
x-required-role: admin
/notices/{notice_id}/hidden:
delete:
tags:
- Notices
summary: Unhide Notice
description: |-
**Access:** Admin role required.
Show an acknowledged or muted notice or check row again.
operationId: unhide_notice_notices__notice_id__hidden_delete
It stays hidden if the same problem happens again.
operationId: dismiss_notice_notices__notice_id__dismiss_post
parameters:
- name: notice_id
in: path
@@ -4502,16 +4394,6 @@ paths:
- type: 'null'
default: 100
title: Limit
- name: offset
in: query
required: false
schema:
anyOf:
- type: integer
minimum: 0
- type: 'null'
default: 0
title: Offset
- name: after
in: query
required: false
@@ -4666,7 +4548,7 @@ paths:
anyOf:
- type: string
- type: 'null'
default: UTC
default: utc
title: Timezone
responses:
'200':
@@ -4817,16 +4699,6 @@ paths:
- type: 'null'
default: 50
title: Limit
- name: offset
in: query
required: false
schema:
anyOf:
- type: integer
minimum: 0
- type: 'null'
default: 0
title: Offset
- name: cameras
in: query
required: false
@@ -4928,7 +4800,7 @@ paths:
anyOf:
- type: string
- type: 'null'
default: UTC
default: utc
title: Timezone
- name: min_score
in: query
@@ -5008,7 +4880,7 @@ paths:
anyOf:
- type: string
- type: 'null'
default: UTC
default: utc
title: Timezone
- name: has_clip
in: query
@@ -5661,7 +5533,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Response Create Trigger Embedding Trigger Embedding Post
'422':
description: Validation Error
@@ -5710,7 +5581,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Response Update Trigger Embedding Trigger Embedding
Camera Name Name Put
'422':
@@ -5753,7 +5623,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Response Delete Trigger Embedding Trigger Embedding
Camera Name Name Delete
'422':
@@ -5790,7 +5659,6 @@ paths:
application/json:
schema:
type: object
additionalProperties: true
title: Response Get Triggers Status Triggers Status Camera Name
Get
'422':
@@ -7367,7 +7235,7 @@ paths:
required: false
schema:
type: string
default: UTC
default: utc
title: Timezone
- name: cameras
in: query
@@ -7417,7 +7285,7 @@ paths:
required: false
schema:
type: string
default: UTC
default: utc
title: Timezone
responses:
'200':
@@ -7587,7 +7455,6 @@ paths:
type: array
items:
type: object
additionalProperties: true
title: Response No Recordings Recordings Unavailable Get
'422':
description: Validation Error
@@ -7791,19 +7658,13 @@ components:
title: Update Topic
config_data:
anyOf:
- additionalProperties: true
type: object
- type: object
- type: 'null'
title: Config Data
skip_save:
type: boolean
title: Skip Save
default: false
replace_paths:
items:
type: string
type: array
title: Replace Paths
type: object
title: AppConfigSetBody
AppPostLoginBody:
@@ -8032,7 +7893,7 @@ components:
properties:
file:
type: string
contentMediaType: application/octet-stream
format: binary
title: File
type: object
required:
@@ -8042,7 +7903,7 @@ components:
properties:
file:
type: string
contentMediaType: application/octet-stream
format: binary
title: File
type: object
required:
@@ -8140,7 +8001,6 @@ components:
tool_calls:
anyOf:
- items:
additionalProperties: true
type: object
type: array
- type: 'null'
@@ -8436,7 +8296,6 @@ components:
- type: 'null'
title: Model Type
data:
additionalProperties: true
type: object
title: Data
type: object
@@ -8508,8 +8367,7 @@ components:
default: true
draw:
anyOf:
- additionalProperties: true
type: object
- type: object
- type: 'null'
title: Draw
default: {}
@@ -8752,8 +8610,7 @@ components:
description: Error message for failed jobs
results:
anyOf:
- additionalProperties: true
type: object
- type: object
- type: 'null'
title: Results
description: Result metadata for completed jobs
@@ -9003,7 +8860,6 @@ components:
- type: 'null'
title: Base Url
provider_options:
additionalProperties: true
type: object
title: Provider Options
type: object
@@ -9544,7 +9400,6 @@ components:
type: string
title: Tool Name
arguments:
additionalProperties: true
type: object
title: Arguments
type: object
@@ -9622,11 +9477,6 @@ components:
type:
type: string
title: Error Type
input:
title: Input
ctx:
type: object
title: Context
type: object
required:
- loc
+1 -7
View File
@@ -15,9 +15,7 @@ from frigate.util.config import find_config_file
def main() -> None:
# Fork this manager so the forkserver does not start until the first
# worker process, after the config has applied environment_vars.
manager = mp.get_context("fork").Manager()
manager = mp.Manager()
faulthandler.enable()
# Setup the logging thread
@@ -101,10 +99,6 @@ def main() -> None:
print("*** End Config Validation Errors ***")
print("*************************************************************")
# force a non-zero exit code for config failures
if args.validate_config:
sys.exit(1)
# attempt to start Frigate in recovery mode
try:
config = FrigateConfig.load(install=True, safe_load=True)
+5 -137
View File
@@ -63,7 +63,6 @@ from frigate.util.builtin import (
flatten_config_data,
load_labels,
process_config_query_string,
split_config_key_path,
update_yaml_file_bulk,
)
from frigate.util.config import (
@@ -71,15 +70,9 @@ from frigate.util.config import (
find_config_file,
redact_credential,
)
from frigate.util.live_streams import (
generated_transcode_streams,
sync_transcode_streams,
)
from frigate.util.object_names import get_categorized_object_names
from frigate.util.schema import get_config_schema
from frigate.util.services import (
Go2rtcStreamIssue,
find_restricted_go2rtc_stream_changes,
get_nvidia_driver_info,
process_logs,
restart_frigate,
@@ -313,12 +306,9 @@ def config(request: Request):
mode="json", warnings="none", exclude_none=True
)
is_admin = request.headers.get("remote-role") == "admin"
# hide environment_vars and the notification email from non-admin users
if not is_admin:
# remove environment_vars for non-admin users
if request.headers.get("remote-role") != "admin":
config.pop("environment_vars", None)
redact_credential(config["notifications"], "email")
# redact mqtt credentials
redact_credential(config["mqtt"], "password")
@@ -375,15 +365,7 @@ def config(request: Request):
camera_name
)
if base_sections:
# copy so redaction below can't alter the profile manager's cache
camera_dict["base_config"] = copy.deepcopy(base_sections)
# cameras inherit the global notification email
if not is_admin:
redact_credential(camera_dict["notifications"], "email")
redact_credential(
camera_dict.get("base_config", {}).get("notifications", {}), "email"
)
camera_dict["base_config"] = base_sections
# remove go2rtc stream passwords
go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump(
@@ -411,11 +393,6 @@ def config(request: Request):
model_dict["non_logo_attributes"] = model.non_logo_attributes
model_dict["labelmap"] = model.merged_labelmap
# report the configured reference rather than the resolved cache path,
# so saving the config back doesn't lose the Frigate+ model
if model.plus_id:
model_dict["path"] = f"plus://{model.plus_id}"
if not config["plus"]["enabled"]:
continue
@@ -452,8 +429,6 @@ def ffmpeg_presets():
hwaccel_presets = [
"preset-rpi-64-h264",
"preset-rpi-64-h265",
"preset-apple-silicon-h264",
"preset-apple-silicon-h265",
"preset-jetson-h264",
"preset-jetson-h265",
"preset-rkmpp",
@@ -548,63 +523,6 @@ def config_raw():
)
def _go2rtc_streams(raw_config: str) -> Any:
"""Load go2rtc.streams with the same loader Frigate and go2rtc startup use.
Raises ruamel.yaml.YAMLError when the config cannot be parsed.
"""
loaded = ruamel.yaml.YAML().load(raw_config)
go2rtc = loaded.get("go2rtc") if isinstance(loaded, dict) else None
return go2rtc.get("streams") if isinstance(go2rtc, dict) else None
def _restricted_go2rtc_streams_response(
old_raw_config: str, new_raw_config: str
) -> JSONResponse | None:
"""Reject config changes that add restricted go2rtc stream sources.
The same sources are rejected by PUT /go2rtc/streams, and go2rtc would
otherwise load them from the saved config on the next restart.
"""
try:
new_streams = _go2rtc_streams(new_raw_config)
except ruamel.yaml.YAMLError:
logger.warning("Rejected config change that could not be parsed")
return JSONResponse(
content={"success": False, "message": "Unable to parse config"},
status_code=400,
)
try:
old_streams = _go2rtc_streams(old_raw_config)
except ruamel.yaml.YAMLError:
# with no readable saved config, every source is treated as new
old_streams = None
issues = find_restricted_go2rtc_stream_changes(old_streams, new_streams)
if not issues:
return None
messages = []
for issue, label in (
(Go2rtcStreamIssue.restricted, "Restricted go2rtc stream source in"),
(Go2rtcStreamIssue.unknown_variable, "Unknown variable in go2rtc stream"),
(
Go2rtcStreamIssue.invalid_placeholder,
"Invalid variable placeholder in go2rtc stream",
),
):
names = [name for name, found in issues.items() if found == issue]
if names:
messages.append(f"{label}: {', '.join(names)}")
logger.warning("Rejected config change: %s", "; ".join(messages))
return JSONResponse(
content={"success": False, "message": "; ".join(messages)},
status_code=400,
)
@router.post("/config/save", dependencies=[Depends(require_role(["admin"]))])
def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
new_config = body.decode()
@@ -681,22 +599,10 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
status_code=400,
)
config_file = find_config_file()
try:
with open(config_file) as f:
old_raw_config = f.read()
except OSError:
old_raw_config = ""
restricted_response = _restricted_go2rtc_streams_response(
old_raw_config, new_config
)
if restricted_response is not None:
return restricted_response
# Save the config to file
try:
config_file = find_config_file()
with open(config_file, "w") as f:
f.write(new_config)
f.close()
@@ -902,17 +808,6 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
)
def _config_path_exists(data: Any, key_path: str) -> bool:
"""Return whether a dotted config path is present in parsed yaml."""
for key in split_config_key_path(key_path):
if not isinstance(data, dict) or key not in data:
return False
data = data[key]
return True
@router.put("/config/set", dependencies=[Depends(require_role(["admin"]))])
def config_set(request: Request, body: AppConfigSetBody):
config_file = find_config_file()
@@ -967,19 +862,6 @@ def config_set(request: Request, body: AppConfigSetBody):
status_code=400,
)
# delete replaced paths first so their maps are rewritten in
# the order sent; update_yaml would otherwise keep old order
if body.replace_paths:
old_yaml = ruamel.yaml.YAML(typ="safe").load(old_raw_config) or {}
updates = {
**{
path: ""
for path in body.replace_paths
if _config_path_exists(old_yaml, path)
},
**updates,
}
# apply all updates in a single operation
update_yaml_file_bulk(config_file, updates)
@@ -1031,13 +913,6 @@ def config_set(request: Request, body: AppConfigSetBody):
),
status_code=400,
)
restricted_response = _restricted_go2rtc_streams_response(
old_raw_config, new_raw_config
)
if restricted_response is not None:
with open(config_file, "w") as f:
f.write(old_raw_config)
return restricted_response
except Exception as e:
logging.error(f"Error updating config: {e}")
return JSONResponse(
@@ -1050,15 +925,9 @@ def config_set(request: Request, body: AppConfigSetBody):
if request.app.dispatcher is not None:
request.app.dispatcher.clear_runtime_state_for_yaml_keys(updates.keys())
go2rtc_synced = True
if body.requires_restart == 0 or body.update_topic:
old_config: FrigateConfig = request.app.frigate_config
swap_runtime_config(request.app, config)
go2rtc_synced = sync_transcode_streams(
generated_transcode_streams(old_config),
generated_transcode_streams(config),
)
if body.update_topic:
if body.update_topic.startswith("config/cameras/"):
@@ -1118,7 +987,6 @@ def config_set(request: Request, body: AppConfigSetBody):
if body.requires_restart == 0
else "Config successfully updated, restart to apply"
),
"go2rtc_synced": go2rtc_synced,
}
),
status_code=200,
+41 -90
View File
@@ -12,9 +12,8 @@ import threading
import time
from datetime import datetime
from pathlib import Path
from urllib.parse import parse_qs
from urllib.parse import parse_qs, urlparse
import requests
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.responses import JSONResponse, RedirectResponse
from joserfc import jwt
@@ -32,8 +31,6 @@ from frigate.api.media_auth import (
check_camera_access,
deny_response_for_media_uri,
is_role_restricted,
normalize_request_path,
split_request_uri,
)
from frigate.config import AuthConfig, ProxyConfig
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
@@ -133,18 +130,23 @@ def require_admin_by_default():
if path.startswith(EXEMPT_PREFIXES):
return
# Camera routes enforce per-camera access via route-level dependencies
# (e.g. require_camera_access). Match on the route template, not the raw
# path, so a camera named like another namespace (e.g. "faces") can't
# waive the admin check for that namespace's routes.
route = request.scope.get("route")
if (
route is not None
and route.path.startswith("/{camera_name}")
and request.path_params.get("camera_name")
in request.app.frigate_config.cameras
):
return
# Dynamic camera path exemption:
# Any path whose first segment matches a configured camera name should
# bypass the global admin requirement. These endpoints enforce access
# via route-level dependencies (e.g. require_camera_access) to ensure
# per-camera authorization. This allows non-admin authenticated users
# (e.g. viewer role) to access camera-specific resources without
# needing admin privileges.
try:
if path.startswith("/"):
first_segment = path.split("/", 2)[1]
if (
first_segment
and first_segment in request.app.frigate_config.cameras
):
return
except Exception:
pass
# For all other paths, require admin role
# Internal port requests have admin role set automatically
@@ -316,19 +318,17 @@ def get_remote_addr(request: Request):
route = list(reversed(forwarded_for.split(",")))
logger.debug(f"IP Route: {[r for r in route]}")
trusted_proxies = [
ipaddress.ip_network(proxy, strict=False)
for proxy in request.app.frigate_config.auth.trusted_proxies
]
trusted_proxies = []
for proxy in request.app.frigate_config.auth.trusted_proxies:
try:
network = ipaddress.ip_network(proxy)
except ValueError:
logger.warning(f"Unable to parse trusted network: {proxy}")
trusted_proxies.append(network)
# return the first remote address that is not trusted
for addr in route:
try:
ip = ipaddress.ip_address(addr.strip())
except ValueError:
logger.debug("Invalid address in X-Forwarded-For header")
return direct_addr or "127.0.0.1"
ip = ipaddress.ip_address(addr.strip())
logger.debug(f"Checking {ip} (v{ip.version})")
trusted = False
for trusted_proxy in trusted_proxies:
@@ -473,11 +473,12 @@ def create_encoded_jwt(user, role, expiration, secret):
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
# TODO: ideally this would set secure as well, but that requires TLS
# Starlette sets SameSite=Lax by default. The cookie is still sent to
# same-site iframes (e.g. Home Assistant on the same host or domain), but
# not to cross-site ones. CSRF is also mitigated by requiring a custom
# X-CSRF-TOKEN header, which cross-origin pages cannot set without a CORS
# preflight that Frigate never grants (see check_csrf in api/fastapi_app.py).
# SameSite is intentionally left unset (browsers default to Lax). Setting
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
# iframes, breaking embedded views such as the Home Assistant Frigate card.
# CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
# cross-origin pages cannot set without a CORS preflight that Frigate never
# grants (see check_csrf in api/fastapi_app.py).
response.set_cookie(
key=cookie_name,
value=encoded_jwt,
@@ -1002,7 +1003,7 @@ def create_user(
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
config_roles = list(request.app.frigate_config.auth.roles.keys())
if not re.match(r"^(?!\.+$)[A-Za-z0-9._]+$", body.username):
if not re.match("^[A-Za-z0-9._]+$", body.username):
return JSONResponse(content={"message": "Invalid username"}, status_code=400)
if body.role not in config_roles:
@@ -1228,44 +1229,11 @@ def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
return owner_cameras
def _is_existing_go2rtc_stream(request: Request, stream_name: str) -> bool:
"""Check that `stream_name` names a stream go2rtc already knows about.
go2rtc turns any `src` that is not an existing stream name into a new ad hoc
stream and starts the producer it describes, so the proxied live-stream
paths must only ever reference existing streams.
"""
frigate_config = request.app.frigate_config
if _get_stream_owner_cameras(request, stream_name):
return True
config_streams = getattr(frigate_config.go2rtc, "streams", None) or {}
if stream_name in config_streams:
return True
# streams added at runtime through PUT /go2rtc/streams only exist in go2rtc
try:
r = requests.get("http://127.0.0.1:1984/api/streams", timeout=2)
except requests.RequestException:
logger.warning("Unable to fetch streams from go2rtc to authorize request")
return False
if not r.ok:
return False
try:
return stream_name in r.json()
except ValueError:
return False
# 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. The nginx locations are prefix
# matches, so anything under these paths is proxied too.
# endpoint, not these proxied live-stream paths.
GO2RTC_STREAM_PROXY_PATHS = frozenset(
{
"/live/mse/api/ws",
@@ -1275,48 +1243,31 @@ GO2RTC_STREAM_PROXY_PATHS = frozenset(
)
def _is_go2rtc_stream_proxy_request(path: str, request: Request) -> bool:
# nginx marks requests routed through the go2rtc stream locations, and the
# normalized path catches them if that marker is ever missing
if request.headers.get("x-go2rtc-stream-proxy") == "1":
return True
normalized = normalize_request_path(path)
return any(normalized.startswith(p) for p in GO2RTC_STREAM_PROXY_PATHS)
def deny_response_for_go2rtc_stream(
original_url: str | None, role: str | None, request: Request
) -> int | None:
"""Block go2rtc live-stream requests the current role may not make.
"""Block role-restricted users from go2rtc live streams they cannot access.
Returns 403 when any `src` named in `original_url` is not an existing go2rtc
stream (for every role), or when it resolves to a camera outside a
restricted role's allow-list (or no `src` is provided for such a role),
otherwise None. Mirrors the resolution logic in
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
path, query = split_request_uri(original_url)
if not _is_go2rtc_stream_proxy_request(path, request):
parsed = urlparse(original_url)
if parsed.path not in GO2RTC_STREAM_PROXY_PATHS:
return None
frigate_config = request.app.frigate_config
sources = parse_qs(query, keep_blank_values=True).get("src", [])
# an unknown src would make go2rtc create an ad hoc stream from it, which
# bypasses the restricted-source checks on PUT /go2rtc/streams
for src in sources:
if not _is_existing_go2rtc_stream(request, src):
return 403
# 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
+7 -51
View File
@@ -33,25 +33,16 @@ from frigate.config.camera.updater import (
CameraConfigUpdateEnum,
CameraConfigUpdateTopic,
)
from frigate.config.env import UnknownVariableError, substitute_frigate_vars
from frigate.models import User
from frigate.util.builtin import (
clean_camera_user_pass,
clear_orphaned_comments,
get_record_segment_time,
)
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.env import UnknownVariableError, substitute_frigate_vars
from frigate.util.image import run_ffmpeg_snapshot
from frigate.util.live_streams import (
generated_transcode_streams,
measure_stream_bitrate,
sync_transcode_streams,
)
from frigate.util.services import (
analyze_record_keyframes,
ffprobe_stream,
is_restricted_go2rtc_api_source,
is_restricted_go2rtc_source,
)
logger = logging.getLogger(__name__)
@@ -157,9 +148,9 @@ 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_api_source(src):
if src and is_restricted_go2rtc_source(src):
logger.warning(
"Rejected go2rtc stream '%s' with restricted source",
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
stream_name,
)
return JSONResponse(
@@ -178,9 +169,9 @@ def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
except UnknownVariableError:
resolved_src = src
if is_restricted_go2rtc_api_source(resolved_src):
if is_restricted_go2rtc_source(resolved_src):
logger.warning(
"Rejected go2rtc stream '%s' with restricted source",
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
stream_name,
)
return JSONResponse(
@@ -257,34 +248,6 @@ def go2rtc_delete_stream(stream_name: str):
)
@router.get(
"/go2rtc/streams/{stream_name}/bitrate",
dependencies=[Depends(require_role(["admin"]))],
)
async def go2rtc_stream_bitrate(request: Request, stream_name: str):
"""Measure a go2rtc stream's bitrate over a few seconds."""
config: FrigateConfig = request.app.frigate_config
known = set(config.go2rtc.model_dump().get("streams") or {}) | set(
generated_transcode_streams(config)
)
if stream_name not in known:
return JSONResponse(
content={"success": False, "message": "Unknown stream"},
status_code=404,
)
kbps = await asyncio.to_thread(measure_stream_bitrate, stream_name)
if kbps is None:
return JSONResponse(
content={"success": False, "message": "Stream sent no data"},
status_code=502,
)
return JSONResponse(content={"success": True, "kbps": round(kbps)})
@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))])
def ffprobe(request: Request, paths: str = "", detailed: bool = False):
path_param = paths
@@ -1277,7 +1240,6 @@ async def delete_camera(
# Remove camera from config
if "cameras" in data and camera_name in data["cameras"]:
del data["cameras"][camera_name]
clear_orphaned_comments(data["cameras"], data, "cameras")
# Remove camera from auth roles
auth = data.get("auth", {})
@@ -1380,12 +1342,6 @@ async def delete_camera(
except Exception:
logger.debug("Failed to remove go2rtc stream for %s", camera_name)
await asyncio.to_thread(
sync_transcode_streams,
generated_transcode_streams(frigate_config),
generated_transcode_streams(request.app.frigate_config),
)
return JSONResponse(
content={
"success": True,
+1 -8
View File
@@ -1,6 +1,5 @@
"""Object classification APIs."""
import asyncio
import datetime
import logging
import os
@@ -49,9 +48,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.classification])
# concurrent collections for one model share its temp and train directories
example_collection_lock = asyncio.Lock()
def invalid_name_response(value: str) -> JSONResponse:
"""Response for a name that cannot be used as a path component."""
@@ -1294,10 +1290,7 @@ async def generate_object_examples(request: Request, body: GenerateObjectExample
if model_name is None:
return invalid_name_response(body.model_name)
async with example_collection_lock:
await asyncio.to_thread(
collect_object_classification_examples, model_name, body.label
)
collect_object_classification_examples(model_name, body.label)
return JSONResponse(
content={"success": True, "message": "Example generation completed"},
-1
View File
@@ -51,7 +51,6 @@ def swap_runtime_config(app: FastAPI, config: FrigateConfig) -> None:
if app.stats_emitter is not None:
app.stats_emitter.config = config
app.stats_emitter.hardware_stats.set_config(config)
if app.dispatcher is not None:
app.dispatcher.config = config
@@ -7,4 +7,4 @@ class AppTimelineHourlyQueryParameters(BaseModel):
after: float | None = None
before: float | None = None
limit: int | None = 200
timezone: str | None = "UTC"
timezone: str | None = "utc"
@@ -14,7 +14,6 @@ class EventsQueryParams(BaseModel):
zone: str | None = "all"
zones: str | None = "all"
limit: int | None = 100
offset: int | None = Field(0, ge=0)
after: float | None = None
before: float | None = None
time_range: str | None = DEFAULT_TIME_RANGE
@@ -40,7 +39,7 @@ class EventsQueryParams(BaseModel):
max_length: float | None = None
event_id: str | None = None
sort: str | None = None
timezone: str | None = "UTC"
timezone: str | None = "utc"
class EventsSearchQueryParams(BaseModel):
@@ -56,7 +55,6 @@ class EventsSearchQueryParams(BaseModel):
deprecated=True,
)
limit: int | None = 50
offset: int | None = Field(0, ge=0)
cameras: str | None = "all"
labels: str | None = "all"
sub_labels: str | None = "all"
@@ -68,7 +66,7 @@ class EventsSearchQueryParams(BaseModel):
has_clip: bool | None = None
has_snapshot: bool | None = None
is_submitted: bool | None = None
timezone: str | None = "UTC"
timezone: str | None = "utc"
min_score: float | None = None
max_score: float | None = None
min_speed: float | None = None
@@ -78,6 +76,6 @@ class EventsSearchQueryParams(BaseModel):
class EventsSummaryQueryParams(BaseModel):
timezone: str | None = "UTC"
timezone: str | None = "utc"
has_clip: int | None = None
has_snapshot: int | None = None
@@ -3,7 +3,7 @@ from pydantic.json_schema import SkipJsonSchema
class MediaRecordingsSummaryQueryParams(BaseModel):
timezone: str = "UTC"
timezone: str = "utc"
cameras: str | None = "all"
@@ -19,7 +19,7 @@ class ReviewSummaryQueryParams(BaseModel):
cameras: str = "all"
labels: str = "all"
zones: str = "all"
timezone: str = "UTC"
timezone: str = "utc"
class ReviewActivityMotionQueryParams(BaseModel):
-2
View File
@@ -10,8 +10,6 @@ class AppConfigSetBody(BaseModel):
update_topic: str | None = None
config_data: dict[str, Any] | None = None
skip_save: bool = False
# paths rewritten whole, so a map saves in the order sent
replace_paths: list[str] = Field(default_factory=list)
class GenAIProbeBody(BaseModel):
+2 -11
View File
@@ -129,7 +129,6 @@ def events(
zones = zone
limit = params.limit
offset = params.offset
after = params.after
before = params.before
time_range = params.time_range
@@ -362,15 +361,11 @@ def events(
else:
order_by = Event.start_time.desc()
# offset paging needs a stable order when scores or speeds tie
tiebreaker = [Event.id] if sort and sort.startswith(("score", "speed")) else []
events = (
Event.select(*selected_columns)
.where(reduce(operator.and_, clauses))
.order_by(order_by, *tiebreaker)
.order_by(order_by)
.limit(limit)
.offset(offset)
.dicts()
.iterator()
)
@@ -539,7 +534,6 @@ def events_search(
search_type = params.search_type
include_thumbnails = params.include_thumbnails
limit = params.limit
offset = params.offset
sort = params.sort
# Filters
@@ -846,9 +840,6 @@ def events_search(
if search_results:
events_query = events_query.where(Event.id << list(search_results.keys()))
# sorts below are stable, so this orders ties for offset paging
events_query = events_query.order_by(Event.id)
# Fetch events and process them in a single pass
processed_events = []
for event in events_query.dicts():
@@ -906,7 +897,7 @@ def events_search(
processed_events.sort(key=lambda x: x["start_time"], reverse=True)
# Limit the number of events returned
processed_events = processed_events[offset:][:limit]
processed_events = processed_events[:limit]
return JSONResponse(content=processed_events)
+15 -21
View File
@@ -58,7 +58,6 @@ from frigate.api.defs.response.export_response import (
)
from frigate.api.defs.response.generic_response import GenericResponse
from frigate.api.defs.tags import Tags
from frigate.api.media_auth import deny_response_for_media_uri
from frigate.const import CLIPS_DIR, EXPORT_DIR
from frigate.jobs.export import (
ExportJob,
@@ -80,7 +79,6 @@ from frigate.record.export import (
validate_ffmpeg_args,
)
from frigate.util.path import sanitize_contained_path
from frigate.util.recording_coverage import recording_overlap
from frigate.util.time import is_current_hour
logger = logging.getLogger(__name__)
@@ -136,7 +134,6 @@ def _validate_export_case(export_case_id: str | None) -> JSONResponse | None:
def _sanitize_existing_image(
request: Request,
image_path: str | None,
) -> tuple[str | None, JSONResponse | None]:
if not image_path:
@@ -144,15 +141,6 @@ def _sanitize_existing_image(
existing_image = sanitize_contained_path(image_path, CLIPS_DIR)
# CLIPS_DIR is shared by every camera, so the caller must also be allowed
# to read the image.
if existing_image is not None and deny_response_for_media_uri(
f"/clips/{quote(os.path.relpath(existing_image, CLIPS_DIR))}",
request.headers.get("remote-role"),
request.app.frigate_config,
):
existing_image = None
if existing_image is None:
return None, JSONResponse(
content={"success": False, "message": "Invalid image path"},
@@ -178,7 +166,15 @@ def _validate_export_source(
) -> str | None:
if playback_source == PlaybackSourceEnum.recordings:
query = Recordings.select().where(
recording_overlap(start_time, end_time) & (Recordings.camera == camera_name)
(
Recordings.start_time.between(start_time, end_time)
| Recordings.end_time.between(start_time, end_time)
| (
(start_time > Recordings.start_time)
& (end_time < Recordings.end_time)
)
)
& (Recordings.camera == camera_name)
)
# a pinned export reads only that stream, so the other stream's
@@ -243,7 +239,9 @@ def _get_item_recording_export_errors(
query = Recordings.select(Recordings.start_time, Recordings.end_time).where(
Recordings.camera == camera_name,
recording_overlap(min_start, max_end),
Recordings.start_time.between(min_start, max_end)
| Recordings.end_time.between(min_start, max_end)
| ((min_start > Recordings.start_time) & (max_end < Recordings.end_time)),
)
# a pinned batch reads only that stream, so the other stream's
@@ -703,7 +701,7 @@ def export_recordings_batch(
sanitized_images: list[str | None] = []
for item in body.items:
existing_image, image_validation_error = _sanitize_existing_image(
request, item.image_path
item.image_path
)
if image_validation_error is not None:
return image_validation_error
@@ -851,9 +849,7 @@ def export_recording(
playback_source = body.source
friendly_name = body.name
existing_image, image_validation_error = _sanitize_existing_image(
request, body.image_path
)
existing_image, image_validation_error = _sanitize_existing_image(body.image_path)
if image_validation_error is not None:
return image_validation_error
@@ -1048,9 +1044,7 @@ def export_recording_custom(
playback_source = body.source
friendly_name = body.name
existing_image, image_validation_error = _sanitize_existing_image(
request, body.image_path
)
existing_image, image_validation_error = _sanitize_existing_image(body.image_path)
if image_validation_error is not None:
return image_validation_error
ffmpeg_input_args = body.ffmpeg_input_args
+9 -12
View File
@@ -1,7 +1,6 @@
import asyncio
import logging
import re
from contextlib import asynccontextmanager
from fastapi import Depends, FastAPI, Request
from fastapi.responses import JSONResponse
@@ -82,20 +81,9 @@ def create_fastapi_app(
config_holder: ConfigHolder | None = None,
notice_registry: NoticeRegistry | None = None,
):
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("FastAPI started")
asyncio.create_task(
debug_replay_auto_stop_watchdog(
replay_manager, frigate_config, config_publisher
)
)
yield
logger.info("Starting FastAPI app")
app = FastAPI(
debug=False,
lifespan=lifespan,
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
dependencies=[Depends(require_admin_by_default())]
if enforce_default_admin
@@ -131,6 +119,15 @@ def create_fastapi_app(
database.close()
return response
@app.on_event("startup")
async def startup():
logger.info("FastAPI started")
asyncio.create_task(
debug_replay_auto_stop_watchdog(
replay_manager, frigate_config, config_publisher
)
)
# Rate limiter (used for login endpoint)
if frigate_config.auth.failed_login_rate_limit is None:
limiter.enabled = False
+29 -24
View File
@@ -14,10 +14,10 @@ from enum import Enum
from pathlib import Path as FilePath
from typing import IO, Any
from urllib.parse import unquote
from zoneinfo import ZoneInfo
import cv2
import numpy as np
import pytz
from fastapi import APIRouter, Depends, Path, Query, Request, Response
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
from pathvalidate import sanitize_filename
@@ -62,10 +62,9 @@ from frigate.util.recording_coverage import (
build_spans,
null_audio_glitches,
plan_clip,
recording_overlap,
resolve_coverage,
stream_has_audio,
)
from frigate.util.time import get_normalized_tz_name
logger = logging.getLogger(__name__)
@@ -348,7 +347,10 @@ async def get_snapshot_from_recording(
Recordings.path,
Recordings.start_time,
)
.where(recording_overlap(frame_time, frame_time))
.where(
(frame_time >= Recordings.start_time)
& (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
.limit(1)
@@ -364,7 +366,10 @@ async def get_snapshot_from_recording(
Recordings.path,
Recordings.start_time,
)
.where(recording_overlap(frame_time, frame_time))
.where(
(frame_time >= Recordings.start_time)
& (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
.limit(1)
@@ -379,13 +384,8 @@ async def get_snapshot_from_recording(
mime_type = "png" if format == "png" else "jpeg"
config: FrigateConfig = request.app.frigate_config
image_data = await asyncio.to_thread(
get_image_from_recording,
config.ffmpeg,
recording.path,
time_in_segment,
codec,
height,
image_data = get_image_from_recording(
config.ffmpeg, recording.path, time_in_segment, codec, height
)
if not image_data:
@@ -427,7 +427,9 @@ async def submit_recording_snapshot_to_plus(
Recordings.path,
Recordings.start_time,
)
.where(recording_overlap(frame_time, frame_time))
.where(
(frame_time >= Recordings.start_time) & (frame_time <= Recordings.end_time)
)
.where(Recordings.camera == camera_name)
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
.limit(1)
@@ -437,12 +439,8 @@ async def submit_recording_snapshot_to_plus(
config: FrigateConfig = request.app.frigate_config
recording: Recordings = recording_query.get()
time_in_segment = frame_time - recording.start_time
image_data = await asyncio.to_thread(
get_image_from_recording,
config.ffmpeg,
recording.path,
time_in_segment,
"png",
image_data = get_image_from_recording(
config.ffmpeg, recording.path, time_in_segment, "png"
)
if not image_data:
@@ -541,7 +539,11 @@ async def recording_clip(
Recordings.start_time,
Recordings.end_time,
)
.where(recording_overlap(start_ts, end_ts))
.where(
(Recordings.start_time.between(start_ts, end_ts))
| (Recordings.end_time.between(start_ts, end_ts))
| ((start_ts > Recordings.start_time) & (end_ts < Recordings.end_time))
)
.where(Recordings.camera == camera_name)
.where(Recordings.stream_type == stream_type)
.order_by(Recordings.start_time.asc())
@@ -679,10 +681,15 @@ async def _vod_response(
end_ts,
force_discontinuity,
)
intervals = resolve_coverage(camera_name, start_ts, end_ts)
# rows contradicting their stream's audio composition are
# truncated-shutdown glitches
main_audio = stream_has_audio(intervals, main=True)
sub_audio = stream_has_audio(intervals, main=False)
spans = build_spans(
null_audio_glitches(resolve_coverage(camera_name, start_ts, end_ts)),
null_audio_glitches(intervals, main_audio, sub_audio),
stream_preference,
)
@@ -836,9 +843,7 @@ async def vod_hour(
parts = year_month.split("-")
start_date = (
datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC)
- datetime.now(
ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/")))
).utcoffset()
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
)
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
start_ts = start_date.timestamp()
+10 -42
View File
@@ -13,9 +13,8 @@ from __future__ import annotations
import logging
import os
import re
from enum import Enum
from urllib.parse import unquote
from urllib.parse import unquote, urlparse
from peewee import DoesNotExist
@@ -25,8 +24,6 @@ from frigate.models import Export, User
logger = logging.getLogger(__name__)
_URI_SCHEME_AND_AUTHORITY = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?]*")
class MediaAuthResolution(str, Enum):
"""Classification of an `X-Original-URL` path for media-auth purposes."""
@@ -44,36 +41,6 @@ class MediaAuthResolution(str, Enum):
UNKNOWN = "unknown"
def split_request_uri(request_uri: str) -> tuple[str, str]:
"""Split a raw request URI into its path and query string.
urlparse is not used since it reads a path starting with `//` as a host.
nginx ends the URI at a literal `#`, so anything after it is dropped.
"""
uri = _URI_SCHEME_AND_AUTHORITY.sub("", request_uri, count=1)
path, _, query = uri.partition("#")[0].partition("?")
return path, query
def normalize_request_path(path: str) -> str:
"""Normalize a raw request path the way nginx does before matching locations.
nginx percent-decodes the path, merges repeated slashes and resolves dot
segments, while `X-Original-URL` carries the raw `$request_uri`.
"""
segments: list[str] = []
for segment in unquote(path).split("/"):
if segment in ("", "."):
continue
if segment == "..":
if segments:
segments.pop()
continue
segments.append(segment)
return "/" + "/".join(segments)
def extract_path(original_url: str | None) -> str | None:
"""Return the decoded path component of nginx's `X-Original-URL` header.
@@ -82,14 +49,14 @@ def extract_path(original_url: str | None) -> str | None:
request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4`
would (1) parse as the allowed camera in our auth check, (2) be served
as the forbidden camera by nginx. To close the bypass we reject any URI
whose path contains `.` or `..` segments outright. Repeated slashes are
merged, as nginx does before matching locations.
whose path contains `.` or `..` segments outright.
"""
if not original_url:
return None
path, _ = split_request_uri(original_url)
decoded = re.sub(r"/{2,}", "/", unquote(path))
parsed = urlparse(original_url)
raw_path = parsed.path or original_url
decoded = unquote(raw_path)
if not decoded:
return None
@@ -285,11 +252,12 @@ def deny_response_for_media_uri(
path = extract_path(original_url)
# `extract_path` returns None for URIs containing `.` or `..` segments.
# Deny them when nginx would resolve them under a media root, and pass
# the rest through to nginx and the backend.
# For media-root URIs that's a traversal attempt — deny outright. For
# non-media URIs, pass through (nginx / the backend handle them).
if path is None:
normalized = normalize_request_path(split_request_uri(original_url)[0])
first = normalized.lstrip("/").split("/", 1)[0]
raw = urlparse(original_url).path or original_url
decoded = unquote(raw)
first = decoded.lstrip("/").split("/", 1)[0] if decoded else ""
if first in ("clips", "recordings", "exports"):
return 403
return None
+22 -50
View File
@@ -14,18 +14,17 @@ router = APIRouter(tags=[Tags.notices])
@router.get("/notices", dependencies=[Depends(require_role(["admin"]))])
def get_notices(request: Request, include_hidden: bool = False) -> JSONResponse:
def get_notices(request: Request, include_dismissed: bool = False) -> JSONResponse:
"""Get notices, most severe first.
Args:
include_hidden: Also return acknowledged and muted notices, for the
hidden list
include_dismissed: Also return dismissed notices, for the history view
Returns:
The notices
"""
return JSONResponse(
content=request.app.notice_registry.active(include_hidden=include_hidden)
content=request.app.notice_registry.active(include_dismissed=include_dismissed)
)
@@ -35,64 +34,37 @@ def get_notice_stats(request: Request) -> JSONResponse:
return JSONResponse(content=request.app.notice_registry.stats())
@router.get("/notices/muted_checks", dependencies=[Depends(require_role(["admin"]))])
def get_muted_checks(request: Request) -> JSONResponse:
"""Get the muted config and stream check rows, newest first."""
return JSONResponse(content=request.app.notice_registry.muted_checks())
@router.get(
"/notices/dismissed_checks", dependencies=[Depends(require_role(["admin"]))]
)
def get_dismissed_checks(request: Request) -> JSONResponse:
"""Get the dismissed config and stream check rows, newest first."""
return JSONResponse(content=request.app.notice_registry.dismissed_checks())
@router.delete("/notices/hidden", dependencies=[Depends(require_role(["admin"]))])
def unhide_all_notices(request: Request) -> JSONResponse:
"""Show every acknowledged and muted notice and check row again."""
request.app.notice_registry.unhide_all()
return JSONResponse(content={"success": True, "message": "Notices shown again"})
@router.delete("/notices/dismissed", dependencies=[Depends(require_role(["admin"]))])
def purge_dismissed(request: Request) -> JSONResponse:
"""Delete every dismissed notice and check row so each can show again."""
request.app.notice_registry.purge_dismissed()
return JSONResponse(
content={"success": True, "message": "Dismissed notices cleared"}
)
# model notice ids contain a slash, so the id is a path parameter
@router.post(
"/notices/{notice_id:path}/acknowledge",
"/notices/{notice_id:path}/dismiss",
dependencies=[Depends(require_role(["admin"]))],
)
def acknowledge_notice(request: Request, notice_id: str) -> JSONResponse:
"""Hide a notice until it happens again.
def dismiss_notice(request: Request, notice_id: str) -> JSONResponse:
"""Hide a notice or a config or stream check row.
Config and stream check rows and the update notice never repeat, so they
can only be muted.
It stays hidden if the same problem happens again.
"""
if not request.app.notice_registry.acknowledge(notice_id):
if not request.app.notice_registry.dismiss(notice_id):
return JSONResponse(
content={"success": False, "message": "Notice not found"},
status_code=404,
)
return JSONResponse(content={"success": True, "message": "Notice acknowledged"})
@router.post(
"/notices/{notice_id:path}/mute",
dependencies=[Depends(require_role(["admin"]))],
)
def mute_notice(request: Request, notice_id: str) -> JSONResponse:
"""Hide a notice or a config or stream check row for good."""
if not request.app.notice_registry.mute(notice_id):
return JSONResponse(
content={"success": False, "message": "Notice not found"},
status_code=404,
)
return JSONResponse(content={"success": True, "message": "Notice muted"})
@router.delete(
"/notices/{notice_id:path}/hidden",
dependencies=[Depends(require_role(["admin"]))],
)
def unhide_notice(request: Request, notice_id: str) -> JSONResponse:
"""Show an acknowledged or muted notice or check row again."""
if not request.app.notice_registry.unhide(notice_id):
return JSONResponse(
content={"success": False, "message": "Notice not found"},
status_code=404,
)
return JSONResponse(content={"success": True, "message": "Notice shown again"})
return JSONResponse(content={"success": True, "message": "Notice dismissed"})
+9 -14
View File
@@ -9,6 +9,7 @@ from urllib.parse import urlparse
from cryptography.hazmat.primitives import serialization
from fastapi import APIRouter, Depends, Request
from fastapi.responses import JSONResponse
from peewee import DoesNotExist
from py_vapid import Vapid01, utils
from frigate.api.auth import allow_any_authenticated
@@ -172,22 +173,16 @@ def register_notifications(request: Request, body: dict = None):
status_code=400,
)
updated = (
User.update(notification_tokens=User.notification_tokens.append(sub))
.where(User.username == username)
.execute()
)
if not updated:
logger.warning(
"Rejected notification registration for unknown user %s", username
try:
User.update(notification_tokens=User.notification_tokens.append(sub)).where(
User.username == username
).execute()
return JSONResponse(
content=({"success": True, "message": "Successfully saved token."}),
status_code=200,
)
except DoesNotExist:
return JSONResponse(
content=({"success": False, "message": "Could not find user."}),
status_code=404,
)
return JSONResponse(
content=({"success": True, "message": "Successfully saved token."}),
status_code=200,
)
+2 -5
View File
@@ -5,8 +5,8 @@ import logging
import os
import threading
from datetime import UTC, datetime, timedelta
from zoneinfo import ZoneInfo
import pytz
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
@@ -22,7 +22,6 @@ from frigate.api.defs.response.preview_response import (
from frigate.api.defs.tags import Tags
from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE
from frigate.models import Previews
from frigate.util.time import get_normalized_tz_name
logger = logging.getLogger(__name__)
@@ -127,9 +126,7 @@ def preview_hour(
parts = year_month.split("-")
start_date = (
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC)
- datetime.now(
ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/")))
).utcoffset()
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
)
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
start_ts = start_date.timestamp()
+6 -3
View File
@@ -36,7 +36,6 @@ from frigate.util.recording_coverage import (
coverage_spans,
known_video_codecs,
realized_timelines,
recording_overlap,
resolve_coverage,
stream_media_summary,
)
@@ -176,7 +175,7 @@ async def all_recordings_summary(
@router.get(
"/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)]
)
async def recordings_summary(camera_name: str, timezone: str = "UTC"):
async def recordings_summary(camera_name: str, timezone: str = "utc"):
"""Returns hourly summary for recordings of given camera"""
time_range_query = (
@@ -538,7 +537,11 @@ async def delete_recordings(
# Build query to find overlapping recordings
clauses = [
recording_overlap(start, end),
(
Recordings.start_time.between(start, end)
| Recordings.end_time.between(start, end)
| ((start > Recordings.start_time) & (end < Recordings.end_time))
),
(Recordings.camera << camera_list),
]
+20 -23
View File
@@ -37,7 +37,6 @@ from frigate.const import STREAM_TYPE_MAIN
from frigate.embeddings import EmbeddingsContext
from frigate.models import Recordings, ReviewSegment, UserReviewStatus
from frigate.review.types import SeverityEnum
from frigate.util.recording_coverage import recording_overlap
from frigate.util.time import get_dst_transitions
logger = logging.getLogger(__name__)
@@ -45,22 +44,6 @@ logger = logging.getLogger(__name__)
router = APIRouter(tags=[Tags.review])
def get_label_clause(label: str, include_audio: bool = True):
"""Build a clause matching a label within a review segment's data.
Verified objects are stored with a `-verified` suffix (eg. `person-verified`)
so that variant is matched as well.
"""
clause = (ReviewSegment.data["objects"].cast("text") % f'*"{label}"*') | (
ReviewSegment.data["objects"].cast("text") % f'*"{label}-verified"*'
)
if include_audio:
clause |= ReviewSegment.data["audio"].cast("text") % f'*"{label}"*'
return clause
@router.get(
"/review",
response_model=list[ReviewSegmentResponse],
@@ -110,7 +93,10 @@ async def review(
filtered_labels = labels.split(",")
for label in filtered_labels:
label_clauses.append(get_label_clause(label))
label_clauses.append(
(ReviewSegment.data["objects"].cast("text") % f'*"{label}"*')
| (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*')
)
clauses.append(reduce(operator.or_, label_clauses))
if zones != "all":
@@ -253,7 +239,10 @@ async def review_summary(
filtered_labels = labels.split(",")
for label in filtered_labels:
label_clauses.append(get_label_clause(label))
label_clauses.append(
(ReviewSegment.data["objects"].cast("text") % f'*"{label}"*')
| (ReviewSegment.data["audio"].cast("text") % f'*"{label}"*')
)
clauses.append(reduce(operator.or_, label_clauses))
if zones != "all":
# use matching so segments with multiple zones
@@ -351,8 +340,9 @@ async def review_summary(
filtered_labels = labels.split(",")
for label in filtered_labels:
label_clauses.append(get_label_clause(label, include_audio=False))
label_clauses.append(
ReviewSegment.data["objects"].cast("text") % f'*"{label}"*'
)
clauses.append(reduce(operator.or_, label_clauses))
# Find the time range of available data
@@ -586,7 +576,14 @@ def delete_reviews(body: ReviewModifyMultipleBody):
camera_name = review["camera"]
recordings = (
Recordings.select(Recordings.id, Recordings.path)
.where(recording_overlap(start_time, end_time))
.where(
Recordings.start_time.between(start_time, end_time)
| Recordings.end_time.between(start_time, end_time)
| (
(start_time > Recordings.start_time)
& (end_time < Recordings.end_time)
)
)
.where(Recordings.camera == camera_name)
.dicts()
.iterator()
@@ -696,7 +693,7 @@ def motion_activity(
df = df[df["camera"] != ""]
# change types for output
df.index = df.index.as_unit("s").astype(int)
df.index = df.index.astype(int) // (10**9)
normalized = df.reset_index().to_dict("records")
return JSONResponse(content=normalized)
+16 -42
View File
@@ -12,8 +12,8 @@ from pathlib import Path
import psutil
import uvicorn
from peewee import SqliteDatabase
from peewee_migrate import Router
from playhouse.sqlite_ext import SqliteExtDatabase
from frigate.api.auth import hash_password
from frigate.api.fastapi_app import create_fastapi_app
@@ -49,6 +49,7 @@ from frigate.debug_replay import (
DebugReplayManager,
cleanup_replay_cameras,
)
from frigate.detectors.detector_config import SceneEnum
from frigate.detectors.detector_types import api_types
from frigate.detectors.device import build_detector_config, runner_names
from frigate.embeddings import EmbeddingProcess, EmbeddingsContext
@@ -77,7 +78,7 @@ from frigate.notices.registry import NoticeRegistry
from frigate.object_detection.base import ObjectDetectProcess
from frigate.object_detection.util import detection_frame_size
from frigate.output.output import OutputProcess
from frigate.ptz.autotrack import PtzAutoTracker
from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.ptz.onvif import OnvifController
from frigate.record.cleanup import RecordingCleanup
from frigate.record.export import migrate_exports
@@ -107,7 +108,7 @@ class FrigateApp:
self.metrics_manager = manager
self.audio_process: mp.Process | None = None
self.stop_event = stop_event
self.detection_queues: dict[str, Queue] = {
self.detection_queues: dict[SceneEnum, Queue] = {
model.scene: mp.Queue() for model in config.models
}
self.detectors: dict[str, ObjectDetectProcess] = {}
@@ -166,7 +167,11 @@ class FrigateApp:
# create camera_metrics
for camera_name in self.config.cameras.keys():
self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager)
self.ptz_metrics[camera_name] = PTZMetrics()
self.ptz_metrics[camera_name] = PTZMetrics(
autotracker_enabled=self.config.cameras[
camera_name
].onvif.autotracking.enabled
)
def init_queues(self) -> None:
# Queue for cameras to push tracked objects to
@@ -186,7 +191,7 @@ class FrigateApp:
self.timeline_queue: Queue = mp.Queue()
def init_database(self) -> None:
def vacuum_db(db: SqliteDatabase) -> None:
def vacuum_db(db: SqliteExtDatabase) -> None:
logger.info("Running database vacuum")
db.execute_sql("VACUUM;")
@@ -197,7 +202,7 @@ class FrigateApp:
logger.error("Unable to write to /config to save DB state")
# Migrate DB schema
migrate_db = SqliteDatabase(self.config.database.path)
migrate_db = SqliteExtDatabase(self.config.database.path)
# Run migrations
del logging.getLogger("peewee_migrate").handlers[:]
@@ -390,7 +395,7 @@ class FrigateApp:
logger.error("Unable to prepare the %s runtime: %s", detector_type, err)
def start_detectors(self) -> None:
model_cameras: dict[str, list[str]] = {
model_cameras: dict[SceneEnum, list[str]] = {
model.scene: [] for model in self.config.models
}
@@ -439,7 +444,7 @@ class FrigateApp:
)
def start_ptz_autotracker(self) -> None:
self.ptz_autotracker_thread = PtzAutoTracker(
self.ptz_autotracker_thread = PtzAutoTrackerThread(
self.config,
self.onvif_controller,
self.ptz_metrics,
@@ -478,7 +483,7 @@ class FrigateApp:
def start_audio_processor(self) -> None:
self.audio_process = AudioProcessor(
self.config, self.camera_metrics, self.embeddings_metrics, self.stop_event
self.config, self.camera_metrics, self.stop_event
)
self.audio_process.start()
self.processes["audio_detector"] = self.audio_process.pid or 0
@@ -551,20 +556,10 @@ class FrigateApp:
"output",
lambda: OutputProcess(self.config, self.stop_event),
),
(
"audio_process",
"audio_detector",
lambda: AudioProcessor(
self.config,
self.camera_metrics,
self.embeddings_metrics,
self.stop_event,
),
),
]
for attr, key, factory in specs:
if getattr(self, attr, None) is None:
if not hasattr(self, attr):
continue
def on_restart(
@@ -581,12 +576,7 @@ class FrigateApp:
def init_auth(self) -> None:
if self.config.auth.enabled:
if (
User.select().count() == 0
or User.select()
.where((User.username == "admin") & (User.password_hash == ""))
.exists()
):
if User.select().count() == 0:
password = secrets.token_hex(16)
password_hash = hash_password(
password, iterations=self.config.auth.hash_iterations
@@ -598,9 +588,6 @@ class FrigateApp:
User.password_hash: password_hash,
User.notification_tokens: [],
}
).on_conflict(
conflict_target=[User.username],
update={User.password_hash: password_hash},
).execute()
self.config.auth.admin_first_time_login = True
@@ -631,19 +618,6 @@ class FrigateApp:
logger.info(f"*** Password: {password} ***")
logger.info("********************************************************")
logger.info("********************************************************")
else:
# Notification registrations store tokens on the admin row. Its
# empty password hash marks it for a generated password if auth
# is enabled later.
if User.select().count() == 0:
User.insert(
{
User.username: "admin",
User.role: "admin",
User.password_hash: "",
User.notification_tokens: [],
}
).execute()
def start(self) -> None:
logger.info(f"Starting Frigate ({VERSION})")
+13 -7
View File
@@ -43,6 +43,8 @@ class CameraMetrics:
class PTZMetrics:
autotracker_enabled: Synchronized
start_time: Synchronized
stop_time: Synchronized
frame_time: Synchronized
@@ -50,17 +52,21 @@ class PTZMetrics:
max_zoom: Synchronized
min_zoom: Synchronized
tracking_active: Event
motor_stopped: Event
reset: Event
def __init__(self) -> None:
self.start_time = mp.Value("d", 0)
self.stop_time = mp.Value("d", 0)
self.frame_time = mp.Value("d", 0)
self.zoom_level = mp.Value("d", 0)
self.max_zoom = mp.Value("d", 0)
self.min_zoom = mp.Value("d", 0)
def __init__(self, *, autotracker_enabled: bool):
self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment]
self.start_time = mp.Value("d", 0) # type: ignore[assignment]
self.stop_time = mp.Value("d", 0) # type: ignore[assignment]
self.frame_time = mp.Value("d", 0) # type: ignore[assignment]
self.zoom_level = mp.Value("d", 0) # type: ignore[assignment]
self.max_zoom = mp.Value("d", 0) # type: ignore[assignment]
self.min_zoom = mp.Value("d", 0) # type: ignore[assignment]
self.tracking_active = mp.Event()
self.motor_stopped = mp.Event()
self.reset = mp.Event()
+4 -6
View File
@@ -104,13 +104,12 @@ class CameraActivityManager:
all_objects: list[dict[str, Any]] = []
for camera in new_activity.keys():
camera_config = self.config.cameras.get(camera)
if camera_config is None:
if camera not in self.config.cameras:
continue
# handle cameras that were added dynamically
if camera not in self.camera_all_object_counts:
self.__init_camera(camera_config)
self.__init_camera(self.config.cameras[camera])
new_objects = new_activity[camera].get("objects", [])
all_objects.extend(new_objects)
@@ -235,13 +234,12 @@ class AudioActivityManager:
now = datetime.datetime.now().timestamp()
for camera in new_activity.keys():
camera_config = self.config.cameras.get(camera)
if camera_config is None:
if camera not in self.config.cameras:
continue
# handle cameras that were added dynamically
if camera not in self.current_audio_detections:
self.__init_camera(camera_config)
self.__init_camera(self.config.cameras[camera])
new_detections = new_activity[camera].get("detections", [])
if self.compare_audio_activity(camera, new_detections, now):
+5 -6
View File
@@ -15,6 +15,7 @@ from frigate.config.camera.updater import (
CameraConfigUpdateSubscriber,
)
from frigate.const import REPLAY_CAMERA_PREFIX
from frigate.detectors.detector_config import SceneEnum
from frigate.models import Regions
from frigate.object_detection.util import detection_frame_size
from frigate.util.builtin import empty_and_close_queue
@@ -30,7 +31,7 @@ class CameraMaintainer(threading.Thread):
def __init__(
self,
config: FrigateConfig,
detection_queues: dict[str, Queue],
detection_queues: dict[SceneEnum, Queue],
detected_frames_queue: Queue,
camera_metrics: DictProxy,
ptz_metrics: dict[str, PTZMetrics],
@@ -120,7 +121,9 @@ class CameraMaintainer(threading.Thread):
if runtime:
self.camera_metrics[name] = CameraMetrics(self.metrics_manager)
self.ptz_metrics[name] = PTZMetrics()
self.ptz_metrics[name] = PTZMetrics(
autotracker_enabled=config.onvif.autotracking.enabled
)
self.region_grids[name] = get_camera_regions_grid(
name,
config.detect,
@@ -169,10 +172,6 @@ class CameraMaintainer(threading.Thread):
frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1]
self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
# a leftover ledger from an unclean exit may be the wrong length
self.frame_manager.delete(f"{config.name}_frame_times")
self.frame_manager.create(f"{config.name}_frame_times", max(count, 1) * 8)
capture_process = CameraCapture(
config,
count,
+22 -14
View File
@@ -16,7 +16,7 @@ from frigate.config import (
ZoomingModeEnum,
)
from frigate.const import CLIPS_DIR, THUMB_DIR
from frigate.ptz.autotrack import PtzAutoTracker, calculate_max_target_box
from frigate.ptz.autotrack import PtzAutoTrackerThread
from frigate.track.tracked_object import TrackedObject
from frigate.util.image import (
SharedMemoryFrameManager,
@@ -35,7 +35,7 @@ class CameraState:
name: str,
config: FrigateConfig,
frame_manager: SharedMemoryFrameManager,
ptz_autotracker_thread: PtzAutoTracker,
ptz_autotracker_thread: PtzAutoTrackerThread,
) -> None:
self.name = name
self.config = config
@@ -115,13 +115,17 @@ class CameraState:
# draw thicker box around ptz autotracked object
if (
self.camera_config.onvif.autotracking.enabled
and self.ptz_autotracker_thread.autotracker_init.get(self.name)
and self.ptz_autotracker_thread.tracked_object[self.name]
and self.ptz_autotracker_thread.ptz_autotracker.autotracker_init.get(
self.name
)
and self.ptz_autotracker_thread.ptz_autotracker.tracked_object[
self.name
]
is not None
and obj["id"]
== self.ptz_autotracker_thread.tracked_object[ # type: ignore[union-attr]
== self.ptz_autotracker_thread.ptz_autotracker.tracked_object[
self.name
].obj_data["id"]
].obj_data["id"] # type: ignore[attr-defined]
and obj["frame_time"] == frame_time
):
thickness = 5
@@ -134,9 +138,9 @@ class CameraState:
and self.camera_config.detect.width is not None
and self.camera_config.detect.height is not None
):
max_target_box = calculate_max_target_box(
self.camera_config.onvif.autotracking.zoom_factor
)
max_target_box = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[
self.name
]["max_target_box"] # type: ignore[index]
side_length = max_target_box * (
max(
self.camera_config.detect.width,
@@ -349,7 +353,7 @@ class CameraState:
return
current_frame = self.frame_manager.get(
frame_name, self.camera_config.frame_shape_yuv, frame_time
frame_name, self.camera_config.frame_shape_yuv
)
tracked_objects = self.tracked_objects.copy()
@@ -392,6 +396,7 @@ class CameraState:
"attributes": new_obj.obj_data["attributes"],
"current_estimated_speed": 0,
"velocity_angle": 0,
"path_data": [],
"recognized_license_plate": None,
"recognized_license_plate_score": None,
}
@@ -403,7 +408,7 @@ class CameraState:
self.send_mqtt_snapshot(new_obj, object_type)
for c in self.callbacks["start"]:
c(self.name, new_obj, frame_name, frame_time)
c(self.name, new_obj, frame_name)
for id in updated_ids:
updated_obj = tracked_objects[id]
@@ -415,7 +420,7 @@ class CameraState:
if autotracker_update or significant_update:
for c in self.callbacks["autotrack"]:
c(self.name, updated_obj, frame_name, frame_time)
c(self.name, updated_obj, frame_name)
if thumb_update and current_frame is not None:
# ensure this frame is stored in the cache
@@ -465,7 +470,7 @@ class CameraState:
):
# call event handlers
for c in self.callbacks["update"]:
c(self.name, updated_obj, frame_name, frame_time)
c(self.name, updated_obj, frame_name)
updated_obj.last_published = frame_time
# send MQTT snapshot when object first enters a required zone,
@@ -487,7 +492,7 @@ class CameraState:
removed_obj.obj_data["end_time"] = frame_time
logger.debug(f"{self.name}: end callback for object {id}")
for c in self.callbacks["end"]:
c(self.name, removed_obj, frame_name, frame_time)
c(self.name, removed_obj, frame_name)
# TODO: can i switch to looking this up and only changing when an event ends?
# maintain best objects
@@ -602,6 +607,9 @@ class CameraState:
self.current_frame_time = frame_time
self._current_frame = np.copy(current_frame)
if self.previous_frame_id is not None:
self.frame_manager.close(self.previous_frame_id)
self.previous_frame_id = frame_name
def send_mqtt_snapshot(self, new_obj: TrackedObject, object_type: str) -> None:
+2 -5
View File
@@ -1,7 +1,6 @@
"""Facilitates communication between processes."""
import multiprocessing as mp
import threading
from _pickle import UnpicklingError
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
@@ -19,13 +18,11 @@ class ConfigPublisher:
self.socket = self.context.socket(zmq.PUB)
self.socket.bind(SOCKET_PUB_SUB)
self.stop_event: MpEvent = mp.Event()
self.lock = threading.Lock()
def publish(self, topic: str, payload: Any) -> None:
"""There is no communication back to the processes."""
with self.lock:
self.socket.send_string(topic, flags=zmq.SNDMORE)
self.socket.send_pyobj(payload)
self.socket.send_string(topic, flags=zmq.SNDMORE)
self.socket.send_pyobj(payload)
def stop(self) -> None:
self.stop_event.set()
-1
View File
@@ -12,7 +12,6 @@ class DetectionTypeEnum(str, Enum):
video = "video"
audio = "audio"
lpr = "lpr"
classification_state = "classification_state"
class DetectionPublisher(Publisher):
+7 -11
View File
@@ -759,13 +759,15 @@ class Dispatcher:
"Autotracking must be enabled in the config to be turned on via MQTT."
)
return
if not ptz_autotracker_settings.enabled:
if not self.ptz_metrics[camera_name].autotracker_enabled.value:
logger.info(f"Turning on ptz autotracker for {camera_name}")
self.ptz_metrics[camera_name].autotracker_enabled.value = True
self.ptz_metrics[camera_name].start_time.value = 0
ptz_autotracker_settings.enabled = True
elif payload == "OFF":
if ptz_autotracker_settings.enabled:
if self.ptz_metrics[camera_name].autotracker_enabled.value:
logger.info(f"Turning off ptz autotracker for {camera_name}")
self.ptz_metrics[camera_name].autotracker_enabled.value = False
self.ptz_metrics[camera_name].start_time.value = 0
ptz_autotracker_settings.enabled = False
@@ -780,9 +782,7 @@ class Dispatcher:
try:
payload = int(payload)
except ValueError:
logger.warning(
f"Received unsupported value for motion contour area: {payload}"
)
f"Received unsupported value for motion contour area: {payload}"
return
motion_settings = self.config.cameras[camera_name].motion
@@ -799,9 +799,7 @@ class Dispatcher:
try:
payload = int(payload)
except ValueError:
logger.warning(
f"Received unsupported value for motion threshold: {payload}"
)
f"Received unsupported value for motion threshold: {payload}"
return
motion_settings = self.config.cameras[camera_name].motion
@@ -816,9 +814,7 @@ class Dispatcher:
def _on_global_notification_command(self, payload: str) -> None:
"""Callback for global notification topic."""
if payload != "ON" and payload != "OFF":
logger.warning(
f"Received unsupported value for all notification: {payload}"
)
f"Received unsupported value for all notification: {payload}"
return
notification_settings = self.config.notifications
-8
View File
@@ -1,7 +1,6 @@
"""Facilitates communication between processes."""
import logging
import threading
from collections.abc import Callable
from enum import Enum
from typing import Any
@@ -80,21 +79,14 @@ class EmbeddingsRequestor:
self.context = zmq.Context()
self.socket = self.context.socket(zmq.REQ)
self.socket.connect(SOCKET_REP_REQ)
self.lock = threading.Lock()
def send_data(self, topic: str, data: Any) -> Any:
"""Sends data and then waits for reply."""
# an overlapping call fails fast so a slow reply can't stall the API
if not self.lock.acquire(blocking=False):
return ""
try:
self.socket.send_json((topic, data))
return self.socket.recv_json()
except zmq.ZMQError:
return ""
finally:
self.lock.release()
def stop(self) -> None:
self.socket.close()
+2 -18
View File
@@ -8,16 +8,7 @@ from .zmq_proxy import Publisher, Subscriber
class EventUpdatePublisher(
Publisher[
tuple[
EventTypeEnum,
EventStateEnum,
str | None,
str,
dict[str, Any],
float | None,
]
]
Publisher[tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]]]
):
"""Publishes events (objects, audio, manual)."""
@@ -28,14 +19,7 @@ class EventUpdatePublisher(
def publish(
self,
payload: tuple[
EventTypeEnum,
EventStateEnum,
str | None,
str,
dict[str, Any],
float | None,
],
payload: tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]],
sub_topic: str = "",
) -> None:
super().publish(payload, sub_topic)
+5 -7
View File
@@ -79,16 +79,14 @@ class InterProcessRequestor:
self.context = zmq.Context()
self.socket = self.context.socket(zmq.REQ)
self.socket.connect(SOCKET_REP_REQ)
self.lock = threading.Lock()
def send_data(self, topic: str, data: Any) -> Any:
"""Sends data and then waits for reply."""
with self.lock:
try:
self.socket.send_json((topic, data))
return self.socket.recv_json()
except zmq.ZMQError:
return ""
try:
self.socket.send_json((topic, data))
return self.socket.recv_json()
except zmq.ZMQError:
return ""
def stop(self) -> None:
self.socket.close(linger=0)
+3 -58
View File
@@ -2,8 +2,6 @@ from __future__ import annotations
import logging
import queue
import selectors
import socket
import threading
import time
from collections.abc import Callable
@@ -58,11 +56,6 @@ class MqttClient(Communicator):
self._next_connect_time = 0.0
self._last_on_connect_dispatch = 0.0
# lets other threads interrupt the worker's socket wait
self._wake_recv, self._wake_send = socket.socketpair()
self._wake_recv.setblocking(False)
self._wake_send.setblocking(False)
def subscribe(self, receiver: Callable) -> None:
"""Wrapper for allowing dispatcher to subscribe."""
self._dispatcher = receiver
@@ -92,7 +85,6 @@ class MqttClient(Communicator):
return
self._publish_queue.put(QueuedPublish(full_topic, payload, retain))
self._wake_worker()
def stop(self) -> None:
if self._worker is None:
@@ -108,11 +100,9 @@ class MqttClient(Communicator):
publish_done,
)
)
self._wake_worker()
publish_done.wait(MQTT_SHUTDOWN_FLUSH_TIMEOUT)
self._stop_event.set()
self._wake_worker()
if self.client is not None:
try:
@@ -368,7 +358,7 @@ class MqttClient(Communicator):
deadline = time.monotonic() + MQTT_SHUTDOWN_FLUSH_TIMEOUT
while not message_info.is_published() and time.monotonic() < deadline:
if (
self._loop_client(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
self.client.loop(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
!= mqtt.MQTT_ERR_SUCCESS
):
break
@@ -378,51 +368,6 @@ class MqttClient(Communicator):
exc_info=True,
)
def _wake_worker(self) -> None:
try:
self._wake_send.send(b"\0")
except BlockingIOError:
# the buffer is full, so a wake is already pending
pass
def _loop_client(self, timeout: float) -> int:
"""Drive Paho without select()'s limit on socket file descriptors."""
assert self.client is not None
client = self.client
sock = client.socket()
if sock is None:
return mqtt.MQTT_ERR_NO_CONN
events = selectors.EVENT_READ
if client.want_write():
events |= selectors.EVENT_WRITE
# TLS can have decrypted bytes buffered even when the socket is not ready.
pending = hasattr(sock, "pending") and sock.pending() > 0
with selectors.DefaultSelector() as selector:
selector.register(sock, events)
selector.register(self._wake_recv, selectors.EVENT_READ)
ready = {
key.fileobj: mask
for key, mask in selector.select(0.0 if pending else timeout)
}
if self._wake_recv in ready:
try:
self._wake_recv.recv(4096)
except BlockingIOError:
pass
ready_events = ready.get(sock, 0)
if pending or ready_events & selectors.EVENT_READ:
result = client.loop_read()
if result != mqtt.MQTT_ERR_SUCCESS or client.socket() is None:
return result
if ready_events & selectors.EVENT_WRITE:
result = client.loop_write()
if result != mqtt.MQTT_ERR_SUCCESS or client.socket() is None:
return result
return client.loop_misc()
def _mqtt_loop_worker(self) -> None:
# The worker owns all socket I/O so reconnect, subscribe, and publish
# ordering stays serialized in one place.
@@ -439,7 +384,7 @@ class MqttClient(Communicator):
assert self.client is not None
try:
result = self._loop_client(timeout=MQTT_LOOP_TIMEOUT)
result = self.client.loop(timeout=MQTT_LOOP_TIMEOUT)
except (OSError, mqtt.WebsocketConnectionError) as err:
logger.warning("MQTT loop error: %s", err)
self._schedule_reconnect()
@@ -672,7 +617,7 @@ class MqttClient(Communicator):
return
try:
result = self._loop_client(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
result = self.client.loop(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
except (OSError, mqtt.WebsocketConnectionError) as err:
logger.warning("MQTT publish wait failed: %s", err)
self._schedule_reconnect()
+30 -43
View File
@@ -6,7 +6,7 @@ import logging
import os
import queue
import threading
from collections.abc import Callable, Iterator
from collections.abc import Callable
from dataclasses import dataclass
from multiprocessing.synchronize import Event as MpEvent
from typing import Any
@@ -41,20 +41,6 @@ class PushNotification:
ttl: int = 0
def _build_web_pushers(user: str, subs: list[dict[str, Any]]) -> list[WebPusher]:
"""Build pushers for a user's stored subscriptions, skipping unusable ones."""
pushers: list[WebPusher] = []
for sub in subs:
# WebPusher decodes the stored keys and raises on malformed ones
try:
pushers.append(WebPusher(sub))
except Exception:
logger.warning("Skipping invalid notification subscription for %s", user)
return pushers
class WebPushClient(Communicator):
"""Frigate wrapper for webpush client."""
@@ -70,7 +56,6 @@ class WebPushClient(Communicator):
for c in self.config.cameras.values()
}
self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None
self.config_lock = threading.Lock()
self.last_camera_notification_time: dict[str, float] = {
c.name: 0 # type: ignore[misc]
for c in self.config.cameras.values()
@@ -87,13 +72,13 @@ class WebPushClient(Communicator):
# Pull keys from PEM or generate if they do not exist
self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem"))
users: Iterator[dict[str, Any]] = (
users: list[dict[str, Any]] = (
User.select(User.username, User.notification_tokens).dicts().iterator()
)
for user in users:
self.web_pushers[user["username"]] = _build_web_pushers(
user["username"], user["notification_tokens"]
)
self.web_pushers[user["username"]] = []
for sub in user["notification_tokens"]:
self.web_pushers[user["username"]].append(WebPusher(sub))
# notification and auth config updater
self.global_config_subscriber = ConfigSubscriber("config/")
@@ -161,7 +146,10 @@ class WebPushClient(Communicator):
User.username == user
).execute()
self.web_pushers[user] = _build_web_pushers(user, user_subs)
self.web_pushers[user] = []
for sub in user_subs:
self.web_pushers[user].append(WebPusher(sub))
logger.info(
f"Cleaned up {len(expired)} notification subscriptions for {user}"
@@ -210,30 +198,29 @@ class WebPushClient(Communicator):
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
"""Wrapper for publishing when client is in valid state."""
with self.config_lock:
# check for updated global config (notifications, auth)
while True:
config_topic, config_payload = (
self.global_config_subscriber.check_for_update()
)
if config_topic is None:
break
if config_topic == "config/notifications" and config_payload:
self.config.notifications = config_payload
elif config_topic == "config/auth":
if isinstance(config_payload, AuthConfig):
self.config.auth = config_payload
self._refresh_user_cameras()
updates = self.config_subscriber.check_for_updates()
if "add" in updates:
for camera in updates["add"]:
self.suspended_cameras[camera] = 0
self.last_camera_notification_time[camera] = 0
# check for updated global config (notifications, auth)
while True:
config_topic, config_payload = (
self.global_config_subscriber.check_for_update()
)
if config_topic is None:
break
if config_topic == "config/notifications" and config_payload:
self.config.notifications = config_payload
elif config_topic == "config/auth":
if isinstance(config_payload, AuthConfig):
self.config.auth = config_payload
self._refresh_user_cameras()
updates = self.config_subscriber.check_for_updates()
if "add" in updates:
for camera in updates["add"]:
self.suspended_cameras[camera] = 0
self.last_camera_notification_time[camera] = 0
self._refresh_user_cameras()
if topic == "reviews":
decoded = json.loads(payload)
camera = decoded["before"]["camera"]
+4 -82
View File
@@ -3,9 +3,6 @@
import errno
import json
import logging
import queue
import socket
import struct
import threading
from collections.abc import Callable
from typing import Any
@@ -79,12 +76,6 @@ _WS_VIEWER_TOPICS = frozenset(
# Camera-scoped command topics a camera-authorized (non-admin) user may send.
_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
# Max outbound messages waiting on a client's writer thread.
WS_MAX_PENDING_MESSAGES = 256
# Seconds a single send() may wait for a client to free socket buffer space.
WS_SEND_TIMEOUT = 10
def _check_ws_authorization(
topic: str,
@@ -473,78 +464,6 @@ def _materialize_for_ws(
class WebSocket(WebSocket_): # type: ignore[misc]
def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._send_queue: queue.Queue[tuple[Any, bool] | None] = queue.Queue(
maxsize=WS_MAX_PENDING_MESSAGES
)
self._writer: threading.Thread | None = None
self._aborted = False
def opened(self) -> None:
"""Set a send timeout on the socket and start this client's writer thread."""
# ws4py writes close and pong frames on the shared manager thread, so a
# stalled client must not block sendall forever. SO_SNDTIMEO leaves recv
# (and idle clients) untouched, unlike settimeout().
self.sock.setsockopt(
socket.SOL_SOCKET, socket.SO_SNDTIMEO, struct.pack("ll", WS_SEND_TIMEOUT, 0)
)
# every client gets its own writer so a client that stops reading only
# blocks itself, never the thread that called publish()
self._writer = threading.Thread(
target=self._drain_send_queue, name="ws_writer", daemon=True
)
self._writer.start()
def send(self, payload: Any, binary: bool = False) -> None:
"""Queue a message for this client's writer thread.
Returns without waiting for delivery. Disconnects the client if its queue
is full.
"""
try:
self._send_queue.put_nowait((payload, binary))
except queue.Full:
self._abort("Websocket client is not keeping up, disconnecting it")
def _drain_send_queue(self) -> None:
while True:
item = self._send_queue.get()
if item is None or self.terminated or self.sock is None:
return
try:
super().send(*item)
except Exception:
self._abort()
return
def _abort(self, reason: str | None = None) -> None:
# publish() keeps hitting a full queue until the manager thread removes
# the connection, so only act (and log) the first time
if self._aborted:
return
self._aborted = True
if reason:
logger.warning(reason)
# shutdown rather than close so the ws4py manager thread sees EOF and
# runs its normal unregister/terminate; this also unblocks a stuck sendall
sock = self.sock
if sock is not None:
try:
sock.shutdown(socket.SHUT_RDWR)
except OSError:
pass
def closed(self, code: int, reason: str | None = None) -> None:
"""Signal the writer thread to exit."""
try:
self._send_queue.put_nowait(None)
except queue.Full:
pass
def unhandled_error(self, error: Any) -> None:
"""
Handles the unfriendly socket closures on the server side
@@ -678,7 +597,10 @@ class WebSocketClient(Communicator):
)
if message is None:
continue
ws.send(message)
try:
ws.send(message)
except (ConnectionResetError, BrokenPipeError, ValueError):
pass
def stop(self) -> None:
if self.websocket_server is not None:
-10
View File
@@ -1,5 +1,3 @@
import ipaddress
from pydantic import Field, field_validator, model_validator
from .base import FrigateBaseModel
@@ -70,14 +68,6 @@ class AuthConfig(FrigateBaseModel):
),
)
@field_validator("trusted_proxies")
@classmethod
def validate_trusted_proxies(cls, v: list[str]) -> list[str]:
for proxy in v:
ipaddress.ip_network(proxy, strict=False)
return v
@field_validator("roles")
@classmethod
def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]:
+4 -5
View File
@@ -1,6 +1,6 @@
from pydantic import Field, model_validator
from frigate.detectors.detector_config import DEFAULT_SCENE, SCENE_PATTERN
from frigate.detectors.detector_config import SceneEnum
from ..base import FrigateBaseModel
@@ -62,11 +62,10 @@ class DetectConfig(FrigateBaseModel):
title="Detect width",
description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
)
scene: str = Field(
default=DEFAULT_SCENE,
pattern=SCENE_PATTERN,
scene: SceneEnum = Field(
default=SceneEnum.all,
title="Detect scene",
description="The environment this camera looks at, used to pick which of the configured models runs on it. Cameras left on 'default' run the model configured with a scene of 'default'.",
description="The environment this camera looks at, used to pick which of the configured models runs on it. Cameras left on 'all' run the model configured with a scene of 'all'.",
)
fps: int = Field(
default=5,
+1 -1
View File
@@ -3,9 +3,9 @@ from enum import Enum
from pydantic import Field, field_validator
from frigate.util.config import resolve_ffmpeg_path
from frigate.util.env import EnvString
from ..base import FrigateBaseModel
from ..env import EnvString
__all__ = [
"CameraFfmpegConfig",
+1 -2
View File
@@ -3,9 +3,8 @@ from typing import Any, Self
from pydantic import Field, model_validator
from frigate.util.env import EnvString
from ..base import FrigateBaseModel
from ..env import EnvString
__all__ = ["GenAIConfig", "GenAIProviderEnum", "GenAIRoleEnum"]
+2 -56
View File
@@ -1,57 +1,8 @@
from pydantic import Field, field_validator
from frigate.util.live_streams import DEFAULT_TRANSCODE_QUALITIES
from pydantic import Field
from ..base import FrigateBaseModel
__all__ = ["CameraLiveConfig", "LiveTranscodeConfig", "LiveTranscodeQualityConfig"]
class LiveTranscodeQualityConfig(FrigateBaseModel):
height: int = Field(
ge=144,
le=2160,
title="Height",
description="Output height in pixels; width follows the source aspect ratio.",
)
bitrate: int = Field(
ge=64,
title="Bitrate",
description="Target and maximum video bitrate in kbps.",
)
class LiveTranscodeConfig(FrigateBaseModel):
enabled: bool = Field(
default=False,
title="Enable transcoded streams",
description="Add lower-quality live streams that go2rtc transcodes in real time while someone is watching.",
)
source: str | None = Field(
default=None,
title="Source stream",
description="go2rtc stream to transcode. Defaults to the first live stream.",
)
qualities: list[LiveTranscodeQualityConfig] = Field(
default_factory=lambda: [
LiveTranscodeQualityConfig(**quality)
for quality in DEFAULT_TRANSCODE_QUALITIES
],
title="Qualities",
description="One transcoded stream is added per quality.",
)
@field_validator("qualities")
@classmethod
def validate_unique_heights(
cls, qualities: list[LiveTranscodeQualityConfig]
) -> list[LiveTranscodeQualityConfig]:
heights = [quality.height for quality in qualities]
if len(heights) != len(set(heights)):
raise ValueError("Transcoded stream heights must be unique.")
return qualities
__all__ = ["CameraLiveConfig"]
class CameraLiveConfig(FrigateBaseModel):
@@ -60,11 +11,6 @@ class CameraLiveConfig(FrigateBaseModel):
title="Live stream names",
description="Mapping of configured stream names to restream/go2rtc names used for live playback.",
)
transcode: LiveTranscodeConfig = Field(
default_factory=LiveTranscodeConfig,
title="Transcoded streams",
description="Lower-quality live streams transcoded on demand by go2rtc.",
)
height: int = Field(
default=720,
title="Live height",
+1 -3
View File
@@ -1,7 +1,5 @@
from pydantic import Field
from frigate.util.env import EnvString
from ..base import FrigateBaseModel
__all__ = ["NotificationConfig"]
@@ -13,7 +11,7 @@ class NotificationConfig(FrigateBaseModel):
title="Enable notifications",
description="Enable or disable notifications for all cameras; can be overridden per-camera.",
)
email: EnvString | None = Field(
email: str | None = Field(
default=None,
title="Notification email",
description="Email address used for push notifications or required by certain notification providers.",
+1 -2
View File
@@ -2,9 +2,8 @@ from enum import Enum
from pydantic import Field, field_validator
from frigate.util.env import EnvString
from ..base import FrigateBaseModel
from ..env import EnvString
from .objects import DEFAULT_TRACKED_OBJECTS
__all__ = ["OnvifConfig", "PtzAutotrackConfig", "ZoomingModeEnum"]
+2 -2
View File
@@ -128,8 +128,8 @@ class ZoneConfig(BaseModel):
# we know if any points are > 1 then it is using the
# old native resolution coordinates
if isinstance(coordinates, list):
explicit = any(p.split(",")[0] > "1.0" for p in coordinates)
try:
explicit = any(float(p.split(",")[0]) > 1.0 for p in coordinates)
self._contour = np.array(
[
(
@@ -157,8 +157,8 @@ class ZoneConfig(BaseModel):
)
elif isinstance(coordinates, str):
points = coordinates.split(",")
explicit = any(p > "1.0" for p in points)
try:
explicit = any(float(p) > 1.0 for p in points)
self._contour = np.array(
[
(
+23 -181
View File
@@ -19,7 +19,7 @@ from ruamel.yaml import YAML
from frigate.const import REGEX_JSON
from frigate.detectors import ModelConfig
from frigate.detectors.detector_config import DEFAULT_SCENE
from frigate.detectors.detector_config import SceneEnum
from frigate.detectors.device import DeviceParseError, DeviceSpec, parse_device
from frigate.plus import PlusApi
from frigate.util.builtin import (
@@ -34,15 +34,7 @@ from frigate.util.config import (
get_relative_coordinates,
migrate_frigate_config,
)
from frigate.util.env import EnvVars, reload_sources
from frigate.util.image import create_mask
from frigate.util.live_streams import (
default_transcode_source,
is_transcode_stream_name,
transcode_stream_name,
transcode_streams,
)
from frigate.util.runtime_deps import sha256_of
from frigate.util.services import auto_detect_hwaccel
from .auth import AuthConfig
@@ -72,6 +64,7 @@ from .classification import (
SemanticSearchModelEnum,
)
from .database import DatabaseConfig
from .env import EnvVars, reload_sources
from .logger import LoggerConfig
from .mqtt import MqttConfig
from .network import NetworkingConfig
@@ -289,78 +282,11 @@ def verify_config_roles(camera_config: CameraConfig) -> None:
)
def apply_live_transcode_streams(
frigate_config: FrigateConfig, camera_config: CameraConfig
) -> None:
"""Fold a camera's transcoded streams into its live stream list.
Enabled qualities missing from live.streams are appended, and entries the
user placed keep their position. Transcoded names that are no longer
generated are dropped unless they name a real go2rtc stream.
"""
live = camera_config.live
transcode = live.transcode
go2rtc_streams = frigate_config.go2rtc.model_dump().get("streams") or {}
generated: dict[str, str] = {}
if transcode.enabled:
if transcode.source is None:
transcode.source = default_transcode_source(
camera_config.name, live.streams
)
if transcode.source not in go2rtc_streams:
raise ValueError(
f"Camera {camera_config.name} has transcoded streams enabled, but its source {transcode.source} is not a go2rtc stream."
)
generated = transcode_streams(
camera_config.name,
transcode.source,
[quality.model_dump() for quality in transcode.qualities],
)
for name in generated:
if name in go2rtc_streams:
raise ValueError(
f"Camera {camera_config.name} generates transcoded stream {name}, which collides with a go2rtc stream of the same name."
)
streams = {
label: name
for label, name in live.streams.items()
if name in generated
or name in go2rtc_streams
or not is_transcode_stream_name(camera_config.name, name)
}
placed = set(streams.values())
for quality in transcode.qualities if transcode.enabled else []:
name = transcode_stream_name(camera_config.name, quality.height)
if name in placed:
continue
label = f"{quality.height}p"
if label in streams:
raise ValueError(
f"Camera {camera_config.name} already has a live stream named {label}; rename it or place the transcoded stream under another name."
)
streams[label] = name
live.streams = streams
def verify_valid_live_stream_names(
frigate_config: FrigateConfig, camera_config: CameraConfig
) -> ValueError | None:
"""Verify that a restream exists to use for live view."""
for _, stream_name in camera_config.live.streams.items():
if is_transcode_stream_name(camera_config.name, stream_name):
continue
if (
stream_name
not in frigate_config.go2rtc.model_dump().get("streams", {}).keys()
@@ -610,7 +536,7 @@ class FrigateConfig(FrigateBaseModel):
models: list[ModelConfig] = Field(
default_factory=_default_models,
title="Detection models",
description="Object detection models and the hardware each one runs on. Cameras pick a model by matching their detect.scene against a model's scene, falling back to the 'default' model.",
description="Object detection models and the hardware each one runs on. Cameras pick a model by matching their detect.scene against a model's scene.",
)
# GenAI config (named provider configs: name -> GenAIConfig)
@@ -725,9 +651,7 @@ class FrigateConfig(FrigateBaseModel):
)
_plus_api: PlusApi
_model_devices: dict[str, list[DeviceSpec]]
# scene -> model, including the scenes of duplicate models folded into another
_scene_models: dict[str, ModelConfig]
_model_devices: dict[SceneEnum, list[DeviceSpec]]
_camera_models: dict[str, ModelConfig]
_all_attributes: list[str]
_all_attribute_logos: list[str]
@@ -762,7 +686,7 @@ class FrigateConfig(FrigateBaseModel):
def primary_model(self) -> ModelConfig:
"""The model used when no specific camera is in play."""
for model in self.models:
if model.scene == DEFAULT_SCENE:
if model.scene == SceneEnum.all:
return model
return self.models[0]
@@ -784,7 +708,7 @@ class FrigateConfig(FrigateBaseModel):
if model is None:
camera = self.cameras.get(camera_name)
scene = camera.detect.scene if camera is not None else DEFAULT_SCENE
scene = camera.detect.scene if camera is not None else SceneEnum.all
model = self._resolve_camera_model(camera_name, scene)
self._camera_models[camera_name] = model
@@ -844,14 +768,14 @@ class FrigateConfig(FrigateBaseModel):
if not self.models:
raise ValueError("At least one model must be configured under models")
model_devices: dict[str, list[DeviceSpec]] = {}
model_devices: dict[SceneEnum, list[DeviceSpec]] = {}
# device string -> the scene of the model that already claimed it
claimed_devices: dict[str, str] = {}
claimed_devices: dict[str, SceneEnum] = {}
for index, model in enumerate(self.models):
scene = model.scene
scene = model.scene.value
if scene in model_devices:
if model.scene in model_devices:
raise ValueError(
f"Multiple models are configured with a scene of '{scene}'. Each model must use a different scene."
)
@@ -880,20 +804,17 @@ class FrigateConfig(FrigateBaseModel):
other = claimed_devices[device.raw]
where = (
f"twice by model '{scene}'"
if other == scene
else f"by both the '{other}' and '{scene}' models"
if other == model.scene
else f"by both the '{other.value}' and '{scene}' models"
)
raise ValueError(
f"Device '{device.raw}' is used {where}, but it can only run one detection process."
)
claimed_devices[device.raw] = scene
claimed_devices[device.raw] = model.scene
self.models[index] = self._load_model(model, devices[0].detector)
model_devices[scene] = devices
self._scene_models = {model.scene: model for model in self.models}
self._consolidate_duplicate_models(model_devices)
model_devices[model.scene] = devices
attributes: set[str] = set()
attribute_logos: set[str] = set()
@@ -917,107 +838,36 @@ class FrigateConfig(FrigateBaseModel):
}
self._all_labels = labels
def _consolidate_duplicate_models(
self, model_devices: dict[str, list[DeviceSpec]]
) -> None:
"""Fold models that load the same model file into a single model.
Separate scenes for one model only split the same work across separate
detection queues, so each device serves fewer cameras and is slower
overall than one shared model. The duplicate's devices are moved to the
model it duplicates and its scene resolves to that model.
Args:
model_devices: Scene to parsed devices, updated in place
"""
kept: list[ModelConfig] = []
hashes: dict[str, str | None] = {}
def file_hash(path: str) -> str | None:
if path not in hashes:
hashes[path] = sha256_of(path) if os.path.isfile(path) else None
return hashes[path]
def same_model(a: ModelConfig, b: ModelConfig) -> bool:
# a model's devices all share a detector, so folding across
# detectors would produce an invalid model
if model_devices[a.scene][0].detector != model_devices[b.scene][0].detector:
return False
if not a.path or not b.path:
return False
if os.path.realpath(a.path) == os.path.realpath(b.path):
return True
a_hash = file_hash(a.path)
return a_hash is not None and a_hash == file_hash(b.path)
for model in self.models:
original = next((other for other in kept if same_model(other, model)), None)
if original is None:
kept.append(model)
continue
# keep the default model so cameras without a scene still find it
if model.scene == DEFAULT_SCENE:
kept[kept.index(original)] = model
original, model = model, original
logger.warning(
"Models '%s' and '%s' use the same model file, so they have been combined into the '%s' model. Defining one model under several scenes to assign detectors to specific cameras is slower and less efficient than letting every detector serve every camera. Remove the '%s' model and list its devices under the '%s' model instead",
original.scene,
model.scene,
original.scene,
model.scene,
original.scene,
)
original.devices = [*original.devices, *model.devices]
model_devices[original.scene] = [
*model_devices[original.scene],
*model_devices.pop(model.scene),
]
self._scene_models[model.scene] = original
# anything already folded into the duplicate follows it
for scene, target in self._scene_models.items():
if target is model:
self._scene_models[scene] = original
self.models = kept
def _resolve_camera_model(self, name: str, scene: str) -> ModelConfig:
def _resolve_camera_model(self, name: str, scene: SceneEnum) -> ModelConfig:
"""Resolve which model a camera runs on.
A camera may name a scene no model is configured for, which is valid as
long as a 'default' model is there to fall back to.
long as an 'all' model is there to fall back to.
Args:
name: Name of the camera
scene: The camera's detect scene, which defaults to 'default'
scene: The camera's detect scene, which defaults to 'all'
Returns:
The model the camera runs on
"""
model = self._scene_models.get(scene)
by_scene = {model.scene: model for model in self.models}
model = by_scene.get(scene)
if model is not None:
return model
default = self._scene_models.get(DEFAULT_SCENE)
default = by_scene.get(SceneEnum.all)
if default is None:
raise ValueError(
f"Camera '{name}' has a detect scene of '{scene}', but no model is configured for that scene or for '{DEFAULT_SCENE}'."
f"Camera '{name}' has a detect scene of '{scene.value}', but no model is configured for that scene or for 'all'."
)
logger.warning(
"Camera '%s' has a detect scene of '%s', but no model is configured for that scene, so the '%s' model is used",
"Camera '%s' has a detect scene of '%s', but no model is configured for that scene, so the 'all' model is used",
name,
scene,
DEFAULT_SCENE,
scene.value,
)
return default
@@ -1128,8 +978,6 @@ class FrigateConfig(FrigateBaseModel):
"face_recognition": ["enabled", "min_area"],
"lpr": ["enabled", "expire_time", "min_area", "enhancement"],
"audio_transcription": ["enabled", "live_enabled"],
# transcode is camera-level only
"live": ["streams", "height", "quality"],
}
for section in allowed_fields_map:
@@ -1150,10 +998,6 @@ class FrigateConfig(FrigateBaseModel):
camera_model = self._resolve_camera_model(name, camera_config.detect.scene)
self._camera_models[name] = camera_model
# point cameras at the model their duplicate scene was folded into
if camera_config.detect.scene in self._scene_models:
camera_config.detect.scene = camera_model.scene
if camera_config.ffmpeg.hwaccel_args == "auto":
camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args
@@ -1362,8 +1206,6 @@ class FrigateConfig(FrigateBaseModel):
if not camera_config.live.streams:
camera_config.live.streams = {name: name}
apply_live_transcode_streams(self, camera_config)
# generate the ffmpeg commands
camera_config.create_ffmpeg_cmds()
self.cameras[name] = camera_config
+1 -14
View File
@@ -165,25 +165,12 @@ def reload_sources(warn: bool = True) -> None:
_rebuild(warn)
# Security overrides that must come from the container environment, which is
# controlled by the operator, rather than from the config, which admins can edit
_PROTECTED_ENV_VARS = frozenset({"GO2RTC_ALLOW_ARBITRARY_EXEC"})
def apply_config_env_vars(values: Mapping[str, object]) -> None:
"""Install the environment_vars block as the lowest priority source.
Unprefixed keys only set os.environ. Protected security overrides are
skipped.
Unprefixed keys only set os.environ.
"""
for key, value in values.items():
if key.upper() in _PROTECTED_ENV_VARS:
logger.warning(
"Ignoring %s in environment_vars, it must be set in the container environment",
key,
)
continue
resolved = str(value)
if key.startswith("FRIGATE_"):
+1 -1
View File
@@ -3,9 +3,9 @@ from typing import Self
from pydantic import Field, ValidationInfo, model_validator
from frigate.const import FREQUENCY_STATS_POINTS
from frigate.util.env import EnvString
from .base import FrigateBaseModel
from .env import EnvString
__all__ = ["MqttConfig"]
+1 -2
View File
@@ -1,8 +1,7 @@
from pydantic import Field, field_validator
from frigate.util.env import EnvString
from .base import FrigateBaseModel
from .env import EnvString
__all__ = ["ProxyConfig", "HeaderMappingConfig"]
-1
View File
@@ -8,7 +8,6 @@ MODEL_CACHE_DIR = f"{CONFIG_DIR}/model_cache"
BASE_DIR = "/media/frigate"
CLIPS_DIR = f"{BASE_DIR}/clips"
EXPORT_DIR = f"{BASE_DIR}/exports"
EXPORT_STAGING_DIR = f"{EXPORT_DIR}/staging"
FACE_DIR = f"{CLIPS_DIR}/faces"
THUMB_DIR = f"{CLIPS_DIR}/thumbs"
RECORD_DIR = f"{BASE_DIR}/recordings"
@@ -176,7 +176,6 @@ class LicensePlateProcessingMixin:
"""
input_shape = [3, 48, 320]
num_images = len(images)
outputs: list[np.ndarray] = []
for index in range(0, num_images, self.batch_size):
input_h, input_w = input_shape[1], input_shape[2]
@@ -196,11 +195,11 @@ class LicensePlateProcessingMixin:
norm_image = norm_image[np.newaxis, :]
norm_images.append(norm_image)
try:
outputs.extend(self.model_runner.recognition_model(norm_images)) # type: ignore[arg-type]
except Exception as e:
logger.warning(f"Error running LPR recognition model: {e}")
return [], []
try:
outputs = self.model_runner.recognition_model(norm_images) # type: ignore[arg-type]
except Exception as e:
logger.warning(f"Error running LPR recognition model: {e}")
return [], []
return self.ctc_decoder(outputs)
@@ -580,8 +579,8 @@ class LicensePlateProcessingMixin:
boxes = []
scores = []
for index in range(len(contours)):
contour = contours[index]
for index in range(len(contours)): # type: ignore[arg-type]
contour = contours[index] # type: ignore[index]
# get minimum bounding box (rotated rectangle) around the contour and the smallest side length.
points, sside = self._get_min_boxes(contour)
@@ -1222,7 +1221,7 @@ class LicensePlateProcessingMixin:
"""Look for license plates in image."""
self.metrics.alpr_pps.value = self.plates_rec_second.eps()
self.metrics.yolov9_lpr_pps.value = self.plates_det_second.eps()
camera: str = obj_data if dedicated_lpr else obj_data["camera"] # type: ignore[assignment]
camera = obj_data if dedicated_lpr else obj_data["camera"]
current_time = int(datetime.datetime.now().timestamp())
debug_frame_id = int(datetime.datetime.now().timestamp() * 1000)
@@ -105,10 +105,6 @@ class ObjectDescriptionProcessor(PostProcessorApi):
logger.debug(f"{camera} sending early request to GenAI")
self.early_request_sent[data["id"]] = True
if camera_config.objects.genai.debug_save_thumbnails:
self._save_debug_thumbnails(data["id"])
# Copy thumbnails to avoid holding references after cleanup
thumbnails_copy = [
data["thumbnail"][:] if data.get("thumbnail") else None
@@ -306,7 +302,26 @@ class ObjectDescriptionProcessor(PostProcessorApi):
)
if camera_config.objects.genai.debug_save_thumbnails and num_thumbnails > 0:
self._save_debug_thumbnails(event_id)
logger.debug(f"Saving {num_thumbnails} thumbnails for event {event_id}")
Path(os.path.join(CLIPS_DIR, f"genai-requests/{event_id}")).mkdir(
parents=True, exist_ok=True
)
for idx, data in enumerate(self.tracked_events[event_id], 1):
jpg_bytes: bytes | None = data["thumbnail"]
if jpg_bytes is None:
logger.warning(f"Unable to save thumbnail {idx} for {event_id}.")
else:
with open(
os.path.join(
CLIPS_DIR,
f"genai-requests/{event_id}/{idx}.jpg",
),
"wb",
) as j:
j.write(jpg_bytes)
# Generate the description. Call happens in a thread since it is network bound.
threading.Thread(
@@ -322,30 +337,6 @@ class ObjectDescriptionProcessor(PostProcessorApi):
# Clean up tracked events and early request state
self.cleanup_event(event_id)
def _save_debug_thumbnails(self, event_id: str) -> None:
"""Write the tracked thumbnails for an event to the genai-requests dir."""
tracked = self.tracked_events.get(event_id, [])
logger.debug(f"Saving {len(tracked)} thumbnails for event {event_id}")
Path(os.path.join(CLIPS_DIR, f"genai-requests/{event_id}")).mkdir(
parents=True, exist_ok=True
)
for idx, data in enumerate(tracked, 1):
jpg_bytes: bytes | None = data["thumbnail"]
if jpg_bytes is None:
logger.warning(f"Unable to save thumbnail {idx} for {event_id}.")
else:
with open(
os.path.join(
CLIPS_DIR,
f"genai-requests/{event_id}/{idx}.jpg",
),
"wb",
) as j:
j.write(jpg_bytes)
def _genai_embed_description(self, event: Event, thumbnails: list[bytes]) -> None:
"""Embed the description for an event."""
start = datetime.datetime.now().timestamp()

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