mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 21:22:50 +03:00
Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e197b82804 | ||
|
|
cd83fee1d6 | ||
|
|
afc335491d | ||
|
|
3175872a15 | ||
|
|
991af1370f | ||
|
|
35bec8499b | ||
|
|
45012d1c02 | ||
|
|
d014b62701 | ||
|
|
eef55045b6 | ||
|
|
2961449d4d | ||
|
|
39f76e4a2c | ||
|
|
b6cf95a844 | ||
|
|
0453f945c7 | ||
|
|
141410f8e9 | ||
|
|
1c3eebeebb | ||
|
|
505b976d19 | ||
|
|
5e2c59d27d | ||
|
|
d97dd29561 | ||
|
|
b2fc066e6c | ||
|
|
f0d7903965 | ||
|
|
036786cb30 | ||
|
|
9876052cb4 | ||
|
|
510e415a25 | ||
|
|
ab20815584 | ||
|
|
890512b054 | ||
|
|
a1fd978cab | ||
|
|
5de5cee3c6 | ||
|
|
e99eb77ca1 | ||
|
|
bb1e556ba9 | ||
|
|
4f0c1b8ee7 | ||
|
|
a2085e27f6 | ||
|
|
194e9bef69 | ||
|
|
9d109bfd12 | ||
|
|
dcda458a82 | ||
|
|
6c6683034e | ||
|
|
3ce3217db2 | ||
|
|
8a0c848914 | ||
|
|
fa4002cbe2 | ||
|
|
a94b532655 | ||
|
|
fec73c887e | ||
|
|
da135da0fb | ||
|
|
f5e398036e | ||
|
|
2dd700aa5a | ||
|
|
378fbec416 | ||
|
|
91a93167d2 | ||
|
|
dfe6428111 | ||
|
|
5e37b2c5c2 | ||
|
|
36607133e5 | ||
|
|
622fc97671 | ||
|
|
5d807587ab | ||
|
|
31bbf910c7 | ||
|
|
f0d7c1d7d4 | ||
|
|
a83219af56 | ||
|
|
4a2fb2f09c | ||
|
|
68893b28fc | ||
|
|
fbb904302c | ||
|
|
e425ab5f90 | ||
|
|
f486f7d57e | ||
|
|
c0adab1228 |
@@ -55,6 +55,7 @@ Dahua
|
|||||||
datasheet
|
datasheet
|
||||||
debconf
|
debconf
|
||||||
deci
|
deci
|
||||||
|
deepstack
|
||||||
defragment
|
defragment
|
||||||
devcontainer
|
devcontainer
|
||||||
DEVICEMAP
|
DEVICEMAP
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ body:
|
|||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Beta Version
|
label: Beta Version
|
||||||
description: Visible on the System Metrics page in the Web UI. Please include the full version including the build identifier (eg. 0.19.0-beta1, 0.19.0-8b72c7a, etc.)
|
description: Visible on the System Metrics page in the Web UI. Please include the full version including the build identifier (eg. 0.18.0-beta1, 0.18.0-8b72c7a, etc.)
|
||||||
placeholder: "0.19.0-beta1"
|
placeholder: "0.18.0-beta1"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ body:
|
|||||||
value: |
|
value: |
|
||||||
Use this form to submit a reproducible bug in Frigate or Frigate's UI.
|
Use this form to submit a reproducible bug in Frigate or Frigate's UI.
|
||||||
|
|
||||||
If you are running on Proxmox, please see the [Proxmox FAQ](https://github.com/blakeblackshear/frigate/discussions/23916) and reproduce the issue on a standard Docker install first (bare metal, or a VM running plain Debian/Ubuntu) before submitting here.
|
**⚠️ If you are running a beta version (0.18.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
|
||||||
|
|
||||||
**⚠️ If you are running a beta version (0.19.0-beta or similar), please use the [Beta Support template](https://github.com/blakeblackshear/frigate/discussions/new?category=beta-support) instead.**
|
|
||||||
|
|
||||||
Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community.
|
Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community.
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,9 @@ runs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
# This creates a virtual volume at /var/lib/docker to maximize the size
|
# 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
|
# 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
|
- name: Maximize build space
|
||||||
uses: easimon/maximize-build-space@master
|
uses: easimon/maximize-build-space@master
|
||||||
with:
|
with:
|
||||||
root-reserve-mb: 8192
|
|
||||||
temp-reserve-mb: 4096
|
|
||||||
remove-dotnet: 'true'
|
remove-dotnet: 'true'
|
||||||
remove-android: 'true'
|
remove-android: 'true'
|
||||||
remove-haskell: 'true'
|
remove-haskell: 'true'
|
||||||
@@ -34,15 +29,15 @@ runs:
|
|||||||
run: sudo systemctl start docker
|
run: sudo systemctl start docker
|
||||||
shell: bash
|
shell: bash
|
||||||
- id: lowercaseRepo
|
- id: lowercaseRepo
|
||||||
uses: ASzc/change-string-case-action@v8
|
uses: ASzc/change-string-case-action@v5
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository }}
|
string: ${{ github.repository }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v4
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v4
|
uses: docker/setup-buildx-action@v3
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
+22
-139
@@ -1,157 +1,40 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
# Local composite actions are only scanned when their directory is listed
|
directory: "/"
|
||||||
directories:
|
|
||||||
- "/"
|
|
||||||
- "/.github/actions/setup"
|
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
cooldown:
|
open-pull-requests-limit: 10
|
||||||
default-days: 3
|
target-branch: dev
|
||||||
open-pull-requests-limit: 5
|
- package-ecosystem: "docker"
|
||||||
groups:
|
directory: "/docker"
|
||||||
actions:
|
schedule:
|
||||||
patterns: ["*"]
|
interval: daily
|
||||||
actions-security:
|
open-pull-requests-limit: 10
|
||||||
applies-to: security-updates
|
target-branch: dev
|
||||||
patterns: ["*"]
|
|
||||||
|
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
directory: "/docker/main"
|
directory: "/docker/main"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
cooldown:
|
open-pull-requests-limit: 10
|
||||||
default-days: 3
|
target-branch: dev
|
||||||
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"]
|
|
||||||
|
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
directory: "/docker/tensorrt"
|
directory: "/docker/tensorrt"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
# No pull request check builds these images, so version updates are off.
|
open-pull-requests-limit: 10
|
||||||
# Security updates still open.
|
target-branch: dev
|
||||||
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"]
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/web"
|
directory: "/web"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
cooldown:
|
open-pull-requests-limit: 10
|
||||||
default-days: 3
|
target-branch: dev
|
||||||
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"]
|
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/docs"
|
directory: "/docs"
|
||||||
schedule:
|
schedule:
|
||||||
interval: weekly
|
interval: daily
|
||||||
cooldown:
|
|
||||||
default-days: 3
|
|
||||||
open-pull-requests-limit: 5
|
|
||||||
allow:
|
allow:
|
||||||
- dependency-name: "@docusaurus/*"
|
- dependency-name: "@docusaurus/*"
|
||||||
groups:
|
open-pull-requests-limit: 10
|
||||||
docusaurus:
|
target-branch: dev
|
||||||
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"]
|
|
||||||
|
|||||||
+16
-18
@@ -23,7 +23,7 @@ jobs:
|
|||||||
name: AMD64 Build
|
name: AMD64 Build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
- amd64_build
|
- amd64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -144,11 +144,9 @@ jobs:
|
|||||||
done'
|
done'
|
||||||
- name: Assert device access grants
|
- name: Assert device access grants
|
||||||
run: |
|
run: |
|
||||||
# a fake accelerator node created after boot, then the oneshot re-run.
|
# a fake accelerator node created after boot, then the oneshot re-run
|
||||||
# /command is on PATH only for s6-supervised services, and the
|
|
||||||
# with-contenv shebang resolves its execline helpers through PATH
|
|
||||||
docker exec frigate mknod /dev/apex_9 c 120 99
|
docker exec frigate mknod /dev/apex_9 c 120 99
|
||||||
docker exec frigate sh -c 'export PATH=/command:$PATH; exec /etc/s6-overlay/s6-rc.d/init-devices/run'
|
docker exec frigate /etc/s6-overlay/s6-rc.d/init-devices/run
|
||||||
acl=$(docker exec frigate getfacl -p /dev/apex_9)
|
acl=$(docker exec frigate getfacl -p /dev/apex_9)
|
||||||
echo "$acl"
|
echo "$acl"
|
||||||
echo "$acl" | grep -q "user:frigate:rw-"
|
echo "$acl" | grep -q "user:frigate:rw-"
|
||||||
@@ -156,7 +154,7 @@ jobs:
|
|||||||
# the usb tree gets recursive grants plus a default ACL that
|
# the usb tree gets recursive grants plus a default ACL that
|
||||||
# newly created nodes inherit (the Coral re-enumeration path)
|
# newly created nodes inherit (the Coral re-enumeration path)
|
||||||
docker exec frigate sh -c 'mkdir -p /dev/bus/usb/001 && mknod /dev/bus/usb/001/002 c 189 1'
|
docker exec frigate sh -c 'mkdir -p /dev/bus/usb/001 && mknod /dev/bus/usb/001/002 c 189 1'
|
||||||
docker exec frigate sh -c 'export PATH=/command:$PATH; exec /etc/s6-overlay/s6-rc.d/init-devices/run'
|
docker exec frigate /etc/s6-overlay/s6-rc.d/init-devices/run
|
||||||
docker exec frigate getfacl -p /dev/bus/usb/001 | grep -q "user:frigate:rwx"
|
docker exec frigate getfacl -p /dev/bus/usb/001 | grep -q "user:frigate:rwx"
|
||||||
docker exec frigate sh -c 'mknod /dev/bus/usb/001/099 c 189 98 && chmod 664 /dev/bus/usb/001/099'
|
docker exec frigate sh -c 'mknod /dev/bus/usb/001/099 c 189 98 && chmod 664 /dev/bus/usb/001/099'
|
||||||
inherited=$(docker exec frigate getfacl -p /dev/bus/usb/001/099)
|
inherited=$(docker exec frigate getfacl -p /dev/bus/usb/001/099)
|
||||||
@@ -173,7 +171,7 @@ jobs:
|
|||||||
# hardware that is absent must stay silent: the literal table entries
|
# hardware that is absent must stay silent: the literal table entries
|
||||||
# are not globs, so nullglob does not drop them and only an existence
|
# are not globs, so nullglob does not drop them and only an existence
|
||||||
# check keeps them from warning on every boot
|
# check keeps them from warning on every boot
|
||||||
out=$(docker exec frigate sh -c 'export PATH=/command:$PATH; exec /etc/s6-overlay/s6-rc.d/init-devices/run')
|
out=$(docker exec frigate /etc/s6-overlay/s6-rc.d/init-devices/run)
|
||||||
echo "$out"
|
echo "$out"
|
||||||
if echo "$out" | grep -q "WARN"; then
|
if echo "$out" | grep -q "WARN"; then
|
||||||
echo "grant warned about device nodes that do not exist"; exit 1
|
echo "grant warned about device nodes that do not exist"; exit 1
|
||||||
@@ -303,7 +301,7 @@ jobs:
|
|||||||
# /run must allow exec: S6_READ_ONLY_ROOT has s6 copy its service
|
# /run must allow exec: S6_READ_ONLY_ROOT has s6 copy its service
|
||||||
# scripts there and run them, and --tmpfs defaults to noexec
|
# scripts there and run them, and --tmpfs defaults to noexec
|
||||||
docker run -d --name frigate-ro --shm-size 256m \
|
docker run -d --name frigate-ro --shm-size 256m \
|
||||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755,uid=1000,gid=1000 \
|
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||||
--user 1000:1000 \
|
--user 1000:1000 \
|
||||||
--security-opt no-new-privileges:true \
|
--security-opt no-new-privileges:true \
|
||||||
-v /tmp/frigate-config-ro:/config \
|
-v /tmp/frigate-config-ro:/config \
|
||||||
@@ -397,7 +395,7 @@ jobs:
|
|||||||
# setfacl under a read-only rootfs, which nothing else covers:
|
# setfacl under a read-only rootfs, which nothing else covers:
|
||||||
# init-devices exits early under --user, so that path is never reached
|
# init-devices exits early under --user, so that path is never reached
|
||||||
docker exec frigate-rod mknod /dev/apex_9 c 120 99
|
docker exec frigate-rod mknod /dev/apex_9 c 120 99
|
||||||
docker exec frigate-rod sh -c 'export PATH=/command:$PATH; exec /etc/s6-overlay/s6-rc.d/init-devices/run'
|
docker exec frigate-rod /etc/s6-overlay/s6-rc.d/init-devices/run
|
||||||
docker exec frigate-rod getfacl -p /dev/apex_9 | grep -q "user:frigate:rw-"
|
docker exec frigate-rod getfacl -p /dev/apex_9 | grep -q "user:frigate:rw-"
|
||||||
docker rm -f frigate-rod
|
docker rm -f frigate-rod
|
||||||
- name: "Assert switching that install to user: still starts"
|
- name: "Assert switching that install to user: still starts"
|
||||||
@@ -405,7 +403,7 @@ jobs:
|
|||||||
# the config dir above now holds a go2rtc-owned go2rtc_homekit.yml,
|
# the config dir above now holds a go2rtc-owned go2rtc_homekit.yml,
|
||||||
# which user: keeps readable but not writable (no supplementary groups)
|
# which user: keeps readable but not writable (no supplementary groups)
|
||||||
docker run -d --name frigate-rod-user --shm-size 256m \
|
docker run -d --name frigate-rod-user --shm-size 256m \
|
||||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755,uid=1000,gid=1000 \
|
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||||
--user 1000:1000 \
|
--user 1000:1000 \
|
||||||
-v /tmp/frigate-config-rod:/config \
|
-v /tmp/frigate-config-rod:/config \
|
||||||
-v /tmp/frigate-media-rod:/media/frigate \
|
-v /tmp/frigate-media-rod:/media/frigate \
|
||||||
@@ -426,7 +424,7 @@ jobs:
|
|||||||
name: ARM Build
|
name: ARM Build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -461,7 +459,7 @@ jobs:
|
|||||||
name: Jetson Jetpack 6
|
name: Jetson Jetpack 6
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -492,7 +490,7 @@ jobs:
|
|||||||
- amd64_build
|
- amd64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -533,7 +531,7 @@ jobs:
|
|||||||
- arm64_build
|
- arm64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -558,7 +556,7 @@ jobs:
|
|||||||
- arm64_build
|
- arm64_build
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up QEMU and Buildx
|
- name: Set up QEMU and Buildx
|
||||||
@@ -586,11 +584,11 @@ jobs:
|
|||||||
- arm64_build
|
- arm64_build
|
||||||
steps:
|
steps:
|
||||||
- id: lowercaseRepo
|
- id: lowercaseRepo
|
||||||
uses: ASzc/change-string-case-action@v8
|
uses: ASzc/change-string-case-action@v6
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository }}
|
string: ${{ github.repository }}
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ jobs:
|
|||||||
name: Web - Lint
|
name: Web - Lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- run: npm install
|
- run: npm install
|
||||||
@@ -35,10 +35,10 @@ jobs:
|
|||||||
name: Web - Test
|
name: Web - Test
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- run: npm install
|
- run: npm install
|
||||||
@@ -46,15 +46,18 @@ jobs:
|
|||||||
- name: Build web
|
- name: Build web
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
|
# - name: Test
|
||||||
|
# run: npm run test
|
||||||
|
# working-directory: ./web
|
||||||
|
|
||||||
web_e2e:
|
web_e2e:
|
||||||
name: Web - E2E Tests
|
name: Web - E2E Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- run: npm install
|
- run: npm install
|
||||||
@@ -83,11 +86,11 @@ jobs:
|
|||||||
name: Python Checks
|
name: Python Checks
|
||||||
steps:
|
steps:
|
||||||
- name: Check out the repository
|
- name: Check out the repository
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v7.0.0
|
uses: actions/setup-python@v5.4.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
- name: Install requirements
|
- name: Install requirements
|
||||||
@@ -106,10 +109,10 @@ jobs:
|
|||||||
name: Python Tests
|
name: Python Tests
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20.x
|
node-version: 20.x
|
||||||
- name: Install devcontainer cli
|
- name: Install devcontainer cli
|
||||||
|
|||||||
@@ -10,15 +10,15 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: lowercaseRepo
|
- id: lowercaseRepo
|
||||||
uses: ASzc/change-string-case-action@v8
|
uses: ASzc/change-string-case-action@v6
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository }}
|
string: ${{ github.repository }}
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ When reviewing code, do NOT comment on:
|
|||||||
|
|
||||||
### Code Quality
|
### Code Quality
|
||||||
|
|
||||||
- **Linting**: ESLint (see `web/eslint.config.js`)
|
- **Linting**: ESLint (see `web/.eslintrc.cjs`)
|
||||||
- **Formatting**: Prettier with Tailwind CSS plugin
|
- **Formatting**: Prettier with Tailwind CSS plugin
|
||||||
- **Type Safety**: TypeScript strict mode enabled
|
- **Type Safety**: TypeScript strict mode enabled
|
||||||
|
|
||||||
|
|||||||
@@ -5,4 +5,3 @@
|
|||||||
/docker/rockchip/ @MarcA711
|
/docker/rockchip/ @MarcA711
|
||||||
/docker/rocm/ @harakas
|
/docker/rocm/ @harakas
|
||||||
/docker/hailo8l/ @spanner3003
|
/docker/hailo8l/ @spanner3003
|
||||||
/docker/deepx/ @sixfab
|
|
||||||
|
|||||||
@@ -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."
|
|
||||||
@@ -217,8 +217,6 @@ COPY docker/main/rootfs/ /
|
|||||||
|
|
||||||
# Frigate deps (ffmpeg, python, nginx, go2rtc, s6-overlay, etc)
|
# Frigate deps (ffmpeg, python, nginx, go2rtc, s6-overlay, etc)
|
||||||
FROM slim-base AS deps
|
FROM slim-base AS deps
|
||||||
LABEL org.opencontainers.image.source="https://github.com/blakeblackshear/frigate"
|
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG BASE_IMAGE
|
ARG BASE_IMAGE
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ruff == 0.17.0
|
ruff == 0.15.20
|
||||||
|
|
||||||
# types
|
# types
|
||||||
types-peewee == 4.0.*
|
types-peewee == 3.17.*
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
numpy
|
numpy
|
||||||
openvino >= 2026.4.1
|
openvino >= 2026.2.0
|
||||||
|
|||||||
@@ -1,76 +1,76 @@
|
|||||||
aiofiles == 25.1.*
|
aiofiles == 24.1.*
|
||||||
click == 8.5.*
|
click == 8.1.*
|
||||||
# FastAPI
|
# FastAPI
|
||||||
aiohttp == 3.14.*
|
aiohttp == 3.12.*
|
||||||
starlette == 1.7.*
|
starlette == 0.47.*
|
||||||
starlette-context == 0.5.*
|
starlette-context == 0.4.*
|
||||||
fastapi[standard-no-fastapi-cloud-cli] == 0.142.*
|
fastapi[standard-no-fastapi-cloud-cli] == 0.116.*
|
||||||
uvicorn == 0.54.*
|
uvicorn == 0.35.*
|
||||||
slowapi == 0.1.*
|
slowapi == 0.1.*
|
||||||
joserfc == 1.7.*
|
joserfc == 1.2.*
|
||||||
cryptography == 50.0.*
|
cryptography == 44.0.*
|
||||||
pathvalidate == 3.3.*
|
pathvalidate == 3.3.*
|
||||||
markupsafe == 3.0.*
|
markupsafe == 3.0.*
|
||||||
python-multipart == 0.0.32
|
python-multipart == 0.0.26
|
||||||
# Classification Model Training
|
# Classification Model Training
|
||||||
tensorflow == 2.19.* ; platform_machine == 'aarch64'
|
tensorflow == 2.19.* ; platform_machine == 'aarch64'
|
||||||
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
|
tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64'
|
||||||
# General
|
# General
|
||||||
mypy == 2.4.0
|
mypy == 1.6.1
|
||||||
onvif-zeep-async == 4.3.*
|
onvif-zeep-async == 4.0.*
|
||||||
paho-mqtt == 2.1.*
|
paho-mqtt == 2.1.*
|
||||||
pandas == 3.0.*
|
pandas == 2.2.*
|
||||||
peewee == 4.5.*
|
peewee == 3.17.*
|
||||||
peewee_migrate == 2.3.*
|
peewee_migrate == 1.14.*
|
||||||
psutil == 7.2.*
|
psutil == 7.1.*
|
||||||
pydantic == 2.14.*
|
pydantic == 2.10.*
|
||||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||||
tzdata == 2026.5
|
pytz == 2025.*
|
||||||
pyzmq == 27.2.*
|
pyzmq == 26.2.*
|
||||||
ruamel.yaml == 0.19.*
|
ruamel.yaml == 0.18.*
|
||||||
tzlocal == 5.4.4
|
tzlocal == 5.2
|
||||||
requests == 2.34.*
|
requests == 2.32.*
|
||||||
types-requests == 2.33.*
|
types-requests == 2.32.*
|
||||||
norfair == 2.3.*
|
norfair == 2.3.*
|
||||||
setproctitle == 1.3.*
|
setproctitle == 1.3.*
|
||||||
ws4py == 0.6.*
|
ws4py == 0.5.*
|
||||||
unidecode == 1.4.*
|
unidecode == 1.3.*
|
||||||
titlecase == 2.4.*
|
titlecase == 2.4.*
|
||||||
# Image Manipulation
|
# Image Manipulation
|
||||||
numpy == 1.26.*
|
numpy == 1.26.*
|
||||||
opencv-python-headless == 4.11.0.*
|
opencv-python-headless == 4.11.0.*
|
||||||
opencv-contrib-python == 4.11.0.*
|
opencv-contrib-python == 4.11.0.*
|
||||||
scipy == 1.17.*
|
scipy == 1.16.*
|
||||||
# OpenVino & ONNX
|
# OpenVino & ONNX
|
||||||
openvino == 2026.4.*
|
openvino == 2025.4.*
|
||||||
onnxruntime == 1.30.*
|
onnxruntime == 1.22.*
|
||||||
# Embeddings
|
# Embeddings
|
||||||
transformers == 5.19.*
|
transformers == 4.45.*
|
||||||
# Generative AI
|
# Generative AI
|
||||||
google-genai == 2.31.*
|
google-genai == 1.58.*
|
||||||
ollama == 0.6.*
|
ollama == 0.6.*
|
||||||
openai == 3.26.*
|
openai == 1.65.*
|
||||||
# push notifications
|
# push notifications
|
||||||
py-vapid == 1.9.6
|
py-vapid == 1.9.*
|
||||||
pywebpush == 2.5.*
|
pywebpush == 2.0.*
|
||||||
# alpr
|
# alpr
|
||||||
pyclipper == 1.4.*
|
pyclipper == 1.3.*
|
||||||
shapely == 2.2.*
|
shapely == 2.0.*
|
||||||
rapidfuzz==3.14.*
|
rapidfuzz==3.12.*
|
||||||
# HailoRT
|
# HailoRT
|
||||||
argcomplete==3.7.*
|
argcomplete==2.0.*
|
||||||
contextlib2==21.6.*
|
contextlib2==0.6.*
|
||||||
future==1.0.*
|
future==0.18.*
|
||||||
netaddr==1.3.*
|
netaddr==0.8.*
|
||||||
netifaces==0.10.*
|
netifaces==0.10.*
|
||||||
prometheus-client == 0.26.*
|
prometheus-client == 0.21.*
|
||||||
# TFLite
|
# TFLite
|
||||||
tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64'
|
tflite_runtime @ https://github.com/frigate-nvr/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_x86_64.whl; platform_machine == 'x86_64'
|
||||||
tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64'
|
tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.17.1/tflite_runtime-2.17.1-cp311-cp311-linux_aarch64.whl; platform_machine == 'aarch64'
|
||||||
# audio transcription
|
# audio transcription
|
||||||
sherpa-onnx==1.13.*
|
sherpa-onnx==1.12.*
|
||||||
faster-whisper==1.2.*
|
faster-whisper==1.1.*
|
||||||
librosa==0.11.*
|
librosa==0.11.*
|
||||||
soundfile==0.14.*
|
soundfile==0.13.*
|
||||||
# Memory profiling
|
# Memory profiling
|
||||||
memray == 1.20.*
|
memray == 1.15.*
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
scikit-build == 0.19.*
|
scikit-build == 0.18.*
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ else
|
|||||||
lefile="/config/tls/fullchain.pem"
|
lefile="/config/tls/fullchain.pem"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
tls_enabled=`python3 -I /usr/local/nginx/get_nginx_settings.py | jq -r .tls.enabled`
|
tls_enabled=`python3 /usr/local/nginx/get_nginx_settings.py | jq -r .tls.enabled`
|
||||||
listen_external_port=`python3 -I /usr/local/nginx/get_nginx_settings.py | jq -r .listen.external_port`
|
listen_external_port=`python3 /usr/local/nginx/get_nginx_settings.py | jq -r .listen.external_port`
|
||||||
|
|
||||||
while true
|
while true
|
||||||
do
|
do
|
||||||
@@ -61,7 +61,7 @@ do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
liveprint=`openssl s_client -showcerts -connect 127.0.0.1:$listen_external_port </dev/null 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'`
|
liveprint=`echo | openssl s_client -showcerts -connect 127.0.0.1:$listen_external_port 2>&1 | openssl x509 -fingerprint 2>&1 | grep -i fingerprint || echo 'failed'`
|
||||||
|
|
||||||
case "$liveprint" in
|
case "$liveprint" in
|
||||||
*Fingerprint*)
|
*Fingerprint*)
|
||||||
|
|||||||
@@ -11,13 +11,30 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# /root survives s6-setuidgid and breaks cache writes after the drop; set
|
||||||
|
# before opt_in_out so the opt-out marker lands where the service will look
|
||||||
|
if [[ "$runs_as_root" -eq 0 ]]; then
|
||||||
|
export HOME=/config
|
||||||
|
fi
|
||||||
|
|
||||||
|
# detector runtimes installed at first start (pip install --user) live under
|
||||||
|
# $HOME/.local; the dynamic loader only reads LD_LIBRARY_PATH at exec time
|
||||||
|
export LD_LIBRARY_PATH="${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}${HOME}/.local/lib"
|
||||||
|
export PATH="${PATH}:${HOME}/.local/bin"
|
||||||
|
|
||||||
|
# opt out of openvino telemetry
|
||||||
|
if [ -e /usr/local/bin/opt_in_out ]; then
|
||||||
|
/usr/local/bin/opt_in_out --opt_out > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Logs should be sent to stdout so that s6 can collect them
|
||||||
|
|
||||||
# Tell S6-Overlay not to restart this service
|
# Tell S6-Overlay not to restart this service
|
||||||
s6-svc -O .
|
s6-svc -O .
|
||||||
|
|
||||||
# Runs as root with root's HOME and LD_LIBRARY_PATH; -I skips user site-packages
|
|
||||||
function set_libva_version() {
|
function set_libva_version() {
|
||||||
local ffmpeg_path
|
local ffmpeg_path
|
||||||
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py)
|
ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py)
|
||||||
LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
|
LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
|
||||||
export LIBAVFORMAT_VERSION_MAJOR
|
export LIBAVFORMAT_VERSION_MAJOR
|
||||||
}
|
}
|
||||||
@@ -25,32 +42,14 @@ function set_libva_version() {
|
|||||||
echo "[INFO] Preparing Frigate..."
|
echo "[INFO] Preparing Frigate..."
|
||||||
set_libva_version
|
set_libva_version
|
||||||
|
|
||||||
# HOME=/root survives s6-setuidgid, so any run that is not root gets
|
|
||||||
# HOME=/config explicitly. The runtime user owns /config, so these paths go
|
|
||||||
# only to the service process, never to root. Detector runtimes installed at
|
|
||||||
# first start (pip install --user) live under $HOME/.local.
|
|
||||||
run_as=()
|
|
||||||
runtime_home="${HOME}"
|
|
||||||
if [[ "$runs_as_root" -eq 0 ]]; then
|
|
||||||
runtime_home=/config
|
|
||||||
if [[ "$(id -u)" -eq 0 ]]; then
|
|
||||||
run_as=(s6-setuidgid frigate)
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
run_as+=(env
|
|
||||||
"HOME=${runtime_home}"
|
|
||||||
"LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+${LD_LIBRARY_PATH}:}${runtime_home}/.local/lib"
|
|
||||||
"PATH=${PATH}:${runtime_home}/.local/bin")
|
|
||||||
|
|
||||||
# opt out of openvino telemetry; the marker lands under the service's HOME
|
|
||||||
if [ -e /usr/local/bin/opt_in_out ]; then
|
|
||||||
"${run_as[@]}" /usr/local/bin/opt_in_out --opt_out > /dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[INFO] Starting Frigate..."
|
echo "[INFO] Starting Frigate..."
|
||||||
|
|
||||||
cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate"
|
cd /opt/frigate || echo "[ERROR] Failed to change working directory to /opt/frigate"
|
||||||
|
|
||||||
# Replace the bash process with the Frigate process, redirecting stderr to stdout
|
# Replace the bash process with the Frigate process, redirecting stderr to stdout
|
||||||
exec 2>&1
|
exec 2>&1
|
||||||
exec "${run_as[@]}" python3 -u -m frigate
|
if [[ "$(id -u)" -ne 0 || "$runs_as_root" -eq 1 ]]; then
|
||||||
|
exec python3 -u -m frigate
|
||||||
|
else
|
||||||
|
exec s6-setuidgid frigate python3 -u -m frigate
|
||||||
|
fi
|
||||||
|
|||||||
@@ -59,33 +59,38 @@ function get_ip_and_port_from_supervisor() {
|
|||||||
|
|
||||||
function set_libva_version() {
|
function set_libva_version() {
|
||||||
local ffmpeg_path
|
local ffmpeg_path
|
||||||
ffmpeg_path=$(python3 -I /usr/local/ffmpeg/get_ffmpeg_path.py)
|
ffmpeg_path=$(python3 /usr/local/ffmpeg/get_ffmpeg_path.py)
|
||||||
LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
|
LIBAVFORMAT_VERSION_MAJOR=$("$ffmpeg_path" -version | grep -Po "libavformat\W+\K\d+")
|
||||||
export LIBAVFORMAT_VERSION_MAJOR
|
export LIBAVFORMAT_VERSION_MAJOR
|
||||||
}
|
}
|
||||||
|
|
||||||
set_libva_version
|
set_libva_version
|
||||||
|
|
||||||
# create_config.py creates the file exclusively and fails if anything
|
if [[ -f "/dev/shm/go2rtc.yaml" ]]; then
|
||||||
# reappears at this path
|
echo "[INFO] Removing stale config from last run..."
|
||||||
rm -f /dev/shm/go2rtc.yaml
|
rm /dev/shm/go2rtc.yaml
|
||||||
|
|
||||||
echo "[INFO] Preparing new go2rtc config..."
|
|
||||||
|
|
||||||
if [[ -n "${SUPERVISOR_TOKEN:-}" ]]; then
|
|
||||||
# Running as a Home Assistant Add-on, infer the IP address and port
|
|
||||||
get_ip_and_port_from_supervisor
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -I /usr/local/go2rtc/create_config.py
|
if [[ ! -f "/dev/shm/go2rtc.yaml" ]]; then
|
||||||
|
echo "[INFO] Preparing new go2rtc config..."
|
||||||
|
|
||||||
|
if [[ -n "${SUPERVISOR_TOKEN:-}" ]]; then
|
||||||
|
# Running as a Home Assistant Add-on, infer the IP address and port
|
||||||
|
get_ip_and_port_from_supervisor
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 /usr/local/go2rtc/create_config.py
|
||||||
|
else
|
||||||
|
echo "[WARNING] Unable to remove existing go2rtc config. Changes made to your frigate config file may not be recognized. Please remove the /dev/shm/go2rtc.yaml from your docker host manually."
|
||||||
|
fi
|
||||||
|
|
||||||
# HomeKit persistence. The helper is symlink-safe; hand off to go2rtc only when dropping.
|
# HomeKit persistence. The helper is symlink-safe; hand off to go2rtc only when dropping.
|
||||||
readonly homekit_config_path="/config/go2rtc_homekit.yml"
|
readonly homekit_config_path="/config/go2rtc_homekit.yml"
|
||||||
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then
|
if [[ "$(id -u)" -eq 0 && "$runs_as_root" -eq 0 ]]; then
|
||||||
python3 -I /usr/local/go2rtc/prepare_homekit.py "${homekit_config_path}" --chown
|
python3 /usr/local/go2rtc/prepare_homekit.py "${homekit_config_path}" --chown
|
||||||
chown -h go2rtc:go2rtc /dev/shm/go2rtc.yaml 2>/dev/null || true
|
chown go2rtc:go2rtc /dev/shm/go2rtc.yaml 2>/dev/null || true
|
||||||
else
|
else
|
||||||
python3 -I /usr/local/go2rtc/prepare_homekit.py "${homekit_config_path}"
|
python3 /usr/local/go2rtc/prepare_homekit.py "${homekit_config_path}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
readonly config_path="/config"
|
readonly config_path="/config"
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ device_globs=(
|
|||||||
"/dev/nvmap"
|
"/dev/nvmap"
|
||||||
"/dev/nvidia*"
|
"/dev/nvidia*"
|
||||||
"/dev/memx*"
|
"/dev/memx*"
|
||||||
"/dev/dxrt*"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
IFS=',' read -ra extra_globs <<< "${DEVICE_ACL_PATHS:-}"
|
IFS=',' read -ra extra_globs <<< "${DEVICE_ACL_PATHS:-}"
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ fi
|
|||||||
mkdir -p /etc/letsencrypt/www 2>/dev/null || true
|
mkdir -p /etc/letsencrypt/www 2>/dev/null || true
|
||||||
|
|
||||||
# nginx settings are read once; both templates consume them
|
# nginx settings are read once; both templates consume them
|
||||||
nginx_settings=$(python3 -I /usr/local/nginx/get_nginx_settings.py)
|
nginx_settings=$(python3 /usr/local/nginx/get_nginx_settings.py)
|
||||||
|
|
||||||
# build templates for optional FRIGATE_BASE_PATH environment variable
|
# build templates for optional FRIGATE_BASE_PATH environment variable
|
||||||
echo "$nginx_settings" | \
|
echo "$nginx_settings" | \
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ from typing import Any
|
|||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
sys.path.insert(0, "/opt/frigate")
|
sys.path.insert(0, "/opt/frigate")
|
||||||
|
from frigate.config.env import apply_config_env_vars, substitute_frigate_vars
|
||||||
from frigate.const import (
|
from frigate.const import (
|
||||||
BIRDSEYE_PIPE,
|
BIRDSEYE_PIPE,
|
||||||
LIBAVFORMAT_VERSION_MAJOR,
|
LIBAVFORMAT_VERSION_MAJOR,
|
||||||
)
|
)
|
||||||
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
from frigate.ffmpeg_presets import parse_preset_hardware_acceleration_encode
|
||||||
from frigate.util.config import find_config_file, resolve_ffmpeg_path
|
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 (
|
from frigate.util.services import (
|
||||||
is_go2rtc_arbitrary_exec_allowed,
|
is_go2rtc_arbitrary_exec_allowed,
|
||||||
is_restricted_go2rtc_source,
|
is_restricted_go2rtc_source,
|
||||||
@@ -175,17 +174,6 @@ for name in list(go2rtc_config.get("streams", {})):
|
|||||||
del go2rtc_config["streams"][name]
|
del go2rtc_config["streams"][name]
|
||||||
continue
|
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
|
# add birdseye restream stream if enabled
|
||||||
if config.get("birdseye", {}).get("restream", False):
|
if config.get("birdseye", {}).get("restream", False):
|
||||||
birdseye: dict[str, Any] = config.get("birdseye")
|
birdseye: dict[str, Any] = config.get("birdseye")
|
||||||
@@ -198,13 +186,9 @@ if config.get("birdseye", {}).get("restream", False):
|
|||||||
else:
|
else:
|
||||||
go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd}
|
go2rtc_config["streams"] = {"birdseye": ffmpeg_cmd}
|
||||||
|
|
||||||
# The run script removes the old file first. Exclusive create never follows a
|
# Write go2rtc_config to /dev/shm/go2rtc.yaml
|
||||||
# link the go2rtc user planted in sticky /dev/shm. The config contains camera
|
with open("/dev/shm/go2rtc.yaml", "w") as f:
|
||||||
# credentials, so it is not world-readable.
|
|
||||||
fd = os.open(
|
|
||||||
"/dev/shm/go2rtc.yaml",
|
|
||||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
|
|
||||||
0o640,
|
|
||||||
)
|
|
||||||
with os.fdopen(fd, "w") as f:
|
|
||||||
yaml.dump(go2rtc_config, f)
|
yaml.dump(go2rtc_config, f)
|
||||||
|
|
||||||
|
# config contains camera credentials; do not leave it world-readable
|
||||||
|
os.chmod("/dev/shm/go2rtc.yaml", 0o640)
|
||||||
|
|||||||
@@ -14,12 +14,8 @@ location /auth {
|
|||||||
proxy_pass_request_headers off;
|
proxy_pass_request_headers off;
|
||||||
# Pass info about the request
|
# Pass info about the request
|
||||||
proxy_set_header X-Original-Method $request_method;
|
proxy_set_header X-Original-Method $request_method;
|
||||||
proxy_set_header X-Original-URL $request_uri;
|
proxy_set_header X-Original-URL $scheme://$http_host$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-Server-Port $server_port;
|
proxy_set_header X-Server-Port $server_port;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header Content-Length "";
|
proxy_set_header Content-Length "";
|
||||||
# Pass along auth related info
|
# Pass along auth related info
|
||||||
proxy_set_header Authorization $http_authorization;
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
|||||||
@@ -166,9 +166,7 @@ http {
|
|||||||
include auth_request.conf;
|
include auth_request.conf;
|
||||||
types {
|
types {
|
||||||
video/mp4 mp4;
|
video/mp4 mp4;
|
||||||
image/jpeg jpg jpeg;
|
image/jpeg jpg;
|
||||||
image/png png;
|
|
||||||
image/webp webp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
expires 7d;
|
expires 7d;
|
||||||
@@ -219,8 +217,6 @@ http {
|
|||||||
|
|
||||||
# frigate lovelace card uses this path
|
# frigate lovelace card uses this path
|
||||||
location /live/mse/api/ws {
|
location /live/mse/api/ws {
|
||||||
# tells /auth to apply the go2rtc stream checks
|
|
||||||
set $go2rtc_stream_proxy 1;
|
|
||||||
include auth_request.conf;
|
include auth_request.conf;
|
||||||
limit_except GET {
|
limit_except GET {
|
||||||
deny all;
|
deny all;
|
||||||
@@ -230,8 +226,6 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /live/webrtc/api/ws {
|
location /live/webrtc/api/ws {
|
||||||
# tells /auth to apply the go2rtc stream checks
|
|
||||||
set $go2rtc_stream_proxy 1;
|
|
||||||
include auth_request.conf;
|
include auth_request.conf;
|
||||||
limit_except GET {
|
limit_except GET {
|
||||||
deny all;
|
deny all;
|
||||||
@@ -252,8 +246,6 @@ http {
|
|||||||
|
|
||||||
# integration uses this to add webrtc candidate
|
# integration uses this to add webrtc candidate
|
||||||
location /api/go2rtc/webrtc {
|
location /api/go2rtc/webrtc {
|
||||||
# tells /auth to apply the go2rtc stream checks
|
|
||||||
set $go2rtc_stream_proxy 1;
|
|
||||||
include auth_request.conf;
|
include auth_request.conf;
|
||||||
limit_except POST {
|
limit_except POST {
|
||||||
deny all;
|
deny all;
|
||||||
@@ -349,6 +341,13 @@ http {
|
|||||||
add_header Cache-Control "public";
|
add_header Cache-Control "public";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /fonts/ {
|
||||||
|
access_log off;
|
||||||
|
expires 1y;
|
||||||
|
include security_headers.conf;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
location /locales/ {
|
location /locales/ {
|
||||||
access_log off;
|
access_log off;
|
||||||
include security_headers.conf;
|
include security_headers.conf;
|
||||||
@@ -375,7 +374,7 @@ http {
|
|||||||
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
|
sub_filter '"/BASE_PATH/assets/' '"$http_x_ingress_path/assets/';
|
||||||
sub_filter '"/BASE_PATH/locales/' '"$http_x_ingress_path/locales/';
|
sub_filter '"/BASE_PATH/locales/' '"$http_x_ingress_path/locales/';
|
||||||
sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/';
|
sub_filter '"/BASE_PATH/monacoeditorwork/' '"$http_x_ingress_path/assets/';
|
||||||
sub_filter 'return`/BASE_PATH/`' 'return window.baseUrl';
|
sub_filter 'return"/BASE_PATH/"' 'return window.baseUrl';
|
||||||
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
|
sub_filter '<body>' '<body><script>window.baseUrl="$http_x_ingress_path/";</script>';
|
||||||
sub_filter_types text/css application/javascript;
|
sub_filter_types text/css application/javascript;
|
||||||
sub_filter_once off;
|
sub_filter_once off;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "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-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Host $http_host;
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
proxy_set_header X-Forwarded-URI $request_uri;
|
proxy_set_header X-Forwarded-URI $request_uri;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from ruamel.yaml import YAML
|
from ruamel.yaml import YAML
|
||||||
|
|
||||||
sys.path.insert(0, "/opt/frigate")
|
sys.path.insert(0, "/opt/frigate")
|
||||||
from frigate.util.config import find_config_file, parse_listen_address
|
from frigate.util.config import find_config_file
|
||||||
|
|
||||||
sys.path.remove("/opt/frigate")
|
sys.path.remove("/opt/frigate")
|
||||||
|
|
||||||
@@ -35,29 +35,20 @@ ipv6_config: dict[str, Any] = networking_config.get("ipv6", {})
|
|||||||
ipv6_config.setdefault("enabled", False)
|
ipv6_config.setdefault("enabled", False)
|
||||||
|
|
||||||
listen_config: dict[str, Any] = networking_config.get("listen", {})
|
listen_config: dict[str, Any] = networking_config.get("listen", {})
|
||||||
|
listen_config.setdefault("internal", 5000)
|
||||||
|
listen_config.setdefault("external", 8971)
|
||||||
|
|
||||||
# Frigate's own validation runs elsewhere, and a bad value here must not stop nginx
|
# handle case where internal port is a string with ip:port
|
||||||
# from serving the safe-mode UI. Valid values are kept so a fallback never widens
|
internal_port = listen_config["internal"]
|
||||||
# the unauthenticated internal listener beyond the configured address.
|
if type(internal_port) is str:
|
||||||
for key, default in (("internal", 5000), ("external", 8971)):
|
internal_port = int(internal_port.split(":")[-1])
|
||||||
try:
|
listen_config["internal_port"] = internal_port
|
||||||
value, port = parse_listen_address(listen_config.get(key, default))
|
|
||||||
except ValueError:
|
|
||||||
print(
|
|
||||||
f"[WARN] Invalid networking.listen.{key}, using {default}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
value, port = default, default
|
|
||||||
|
|
||||||
listen_config[key] = value
|
# handle case where external port is a string with ip:port
|
||||||
listen_config[f"{key}_port"] = port
|
external_port = listen_config["external"]
|
||||||
|
if type(external_port) is str:
|
||||||
if listen_config["internal_port"] == listen_config["external_port"]:
|
external_port = int(external_port.split(":")[-1])
|
||||||
print(
|
listen_config["external_port"] = external_port
|
||||||
"[ERROR] networking.listen.internal and external share a port",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
|
base_path = os.environ.get("FRIGATE_BASE_PATH", "")
|
||||||
|
|
||||||
|
|||||||
+50
-11
@@ -2,9 +2,32 @@
|
|||||||
|
|
||||||
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
ARG ROCM=1
|
||||||
ARG HSA_OVERRIDE_GFX_VERSION
|
ARG HSA_OVERRIDE_GFX_VERSION
|
||||||
ARG HSA_OVERRIDE
|
ARG HSA_OVERRIDE
|
||||||
|
|
||||||
|
#######################################################################
|
||||||
|
FROM wget AS rocm
|
||||||
|
|
||||||
|
ARG ROCM
|
||||||
|
|
||||||
|
RUN apt update -qq && \
|
||||||
|
apt install -y wget gpg && \
|
||||||
|
wget -O rocm.deb https://repo.radeon.com/amdgpu-install/7.2.3/ubuntu/jammy/amdgpu-install_7.2.3.70203-1_all.deb && \
|
||||||
|
apt install -y ./rocm.deb && \
|
||||||
|
apt update && \
|
||||||
|
apt install -qq -y rocm
|
||||||
|
|
||||||
|
RUN mkdir -p /opt/rocm-dist/opt/rocm-$ROCM/lib
|
||||||
|
RUN cd /opt/rocm-$ROCM/lib && \
|
||||||
|
cp -dpr libMIOpen*.so* libamd*.so* libhip*.so* libhsa*.so* libmigraphx*.so* librocm*.so* librocblas*.so* libroctracer*.so* librocsolver*.so* librocfft*.so* librocprofiler*.so* libroctx*.so* librocroller.so* /opt/rocm-dist/opt/rocm-$ROCM/lib/ && \
|
||||||
|
mkdir -p /opt/rocm-dist/opt/rocm-$ROCM/lib/migraphx/lib && \
|
||||||
|
cp -dpr migraphx/lib/* /opt/rocm-dist/opt/rocm-$ROCM/lib/migraphx/lib
|
||||||
|
RUN cd /opt/rocm-dist/opt/ && ln -s rocm-$ROCM rocm
|
||||||
|
|
||||||
|
RUN mkdir -p /opt/rocm-dist/etc/ld.so.conf.d/
|
||||||
|
RUN echo /opt/rocm/lib|tee /opt/rocm-dist/etc/ld.so.conf.d/rocm.conf
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
FROM deps AS deps-prelim
|
FROM deps AS deps-prelim
|
||||||
|
|
||||||
@@ -19,35 +42,51 @@ RUN echo "deb http://deb.debian.org/debian trixie main" > /etc/apt/sources.list.
|
|||||||
rm -f /etc/apt/sources.list.d/trixie.list && \
|
rm -f /etc/apt/sources.list.d/trixie.list && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /opt/frigate
|
||||||
|
COPY --from=rootfs / /
|
||||||
|
|
||||||
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
RUN wget -q https://bootstrap.pypa.io/get-pip.py -O get-pip.py \
|
||||||
&& sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \
|
&& sed -i 's/args.append("setuptools")/args.append("setuptools==77.0.3")/' get-pip.py \
|
||||||
&& python3 get-pip.py "pip" --break-system-packages
|
&& python3 get-pip.py "pip" --break-system-packages
|
||||||
RUN python3 -m pip config set global.break-system-packages true
|
RUN python3 -m pip config set global.break-system-packages true
|
||||||
|
|
||||||
# ROCm installs from AMD's wheels ahead of Frigate's code so code changes
|
|
||||||
# reuse this layer, and is pruned in the same layer so the removed files never
|
|
||||||
# reach the image. /opt/rocm links to the runtime for rocminfo and HIP headers.
|
|
||||||
COPY docker/rocm/requirements-wheels-rocm.txt /requirements.txt
|
COPY docker/rocm/requirements-wheels-rocm.txt /requirements.txt
|
||||||
RUN --mount=type=bind,source=docker/rocm/prune_rocm.sh,target=/deps/prune_rocm.sh \
|
RUN pip3 uninstall -y onnxruntime \
|
||||||
pip3 uninstall -y onnxruntime \
|
&& pip3 install -r /requirements.txt
|
||||||
&& pip3 install --no-cache-dir -r /requirements.txt \
|
|
||||||
&& ROCM_SDK_CORE="$(python3 -c 'import _rocm_sdk_core, os; print(os.path.dirname(_rocm_sdk_core.__file__))')" \
|
|
||||||
&& /deps/prune_rocm.sh "$(dirname "${ROCM_SDK_CORE}")" \
|
|
||||||
&& ln -s "${ROCM_SDK_CORE}" /opt/rocm
|
|
||||||
|
|
||||||
WORKDIR /opt/frigate
|
#######################################################################
|
||||||
COPY --from=rootfs / /
|
FROM scratch AS rocm-dist
|
||||||
|
|
||||||
|
ARG ROCM
|
||||||
|
|
||||||
|
# Copy HIP headers required for MIOpen JIT (BuildHip) / HIPRTC at runtime
|
||||||
|
COPY --from=rocm /opt/rocm-${ROCM}/include/ /opt/rocm-${ROCM}/include/
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/bin/rocminfo /opt/rocm-$ROCM/bin/migraphx-driver /opt/rocm-$ROCM/bin/
|
||||||
|
# Copy MIOpen database files for gfx10xx, gfx11xx, and gfx12xx only (RDNA2/RDNA3/RDNA4)
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx10* /opt/rocm-$ROCM/share/miopen/db/
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx11* /opt/rocm-$ROCM/share/miopen/db/
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/share/miopen/db/*gfx12* /opt/rocm-$ROCM/share/miopen/db/
|
||||||
|
# Copy rocBLAS library files for gfx10xx, gfx11xx, and gfx12xx only
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx10* /opt/rocm-$ROCM/lib/rocblas/library/
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx11* /opt/rocm-$ROCM/lib/rocblas/library/
|
||||||
|
COPY --from=rocm /opt/rocm-$ROCM/lib/rocblas/library/*gfx12* /opt/rocm-$ROCM/lib/rocblas/library/
|
||||||
|
COPY --from=rocm /opt/rocm-dist/ /
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
FROM deps-prelim AS rocm-prelim-hsa-override0
|
FROM deps-prelim AS rocm-prelim-hsa-override0
|
||||||
ENV MIGRAPHX_DISABLE_MIOPEN_FUSION=1
|
ENV MIGRAPHX_DISABLE_MIOPEN_FUSION=1
|
||||||
ENV MIGRAPHX_DISABLE_SCHEDULE_PASS=1
|
ENV MIGRAPHX_DISABLE_SCHEDULE_PASS=1
|
||||||
ENV MIGRAPHX_DISABLE_REDUCE_FUSION=1
|
ENV MIGRAPHX_DISABLE_REDUCE_FUSION=1
|
||||||
|
ENV MIGRAPHX_ENABLE_HIPRTC_WORKAROUNDS=1
|
||||||
ENV MIOPEN_CUSTOM_CACHE_DIR=/config/model_cache/migraphx
|
ENV MIOPEN_CUSTOM_CACHE_DIR=/config/model_cache/migraphx
|
||||||
ENV MIOPEN_USER_DB_PATH=/config/model_cache/migraphx
|
ENV MIOPEN_USER_DB_PATH=/config/model_cache/migraphx
|
||||||
ENV AMD_COMGR_CACHE=1
|
ENV AMD_COMGR_CACHE=1
|
||||||
ENV AMD_COMGR_CACHE_DIR=/config/model_cache/migraphx
|
ENV AMD_COMGR_CACHE_DIR=/config/model_cache/migraphx
|
||||||
|
|
||||||
|
COPY --from=rocm-dist / /
|
||||||
|
|
||||||
|
RUN ldconfig
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
FROM rocm-prelim-hsa-override0 as rocm-prelim-hsa-override1
|
FROM rocm-prelim-hsa-override0 as rocm-prelim-hsa-override1
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Remove the parts of AMD's ROCm wheels that MIGraphX inference never loads:
|
|
||||||
# compilers, debuggers, profilers, management tools, and the math libraries
|
|
||||||
# (sparse, solver, FFT, random, RCCL, image processing) outside MIGraphX's
|
|
||||||
# dependencies. Runs in the same layer as the install so the files never
|
|
||||||
# reach the image.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
site_packages="$1"
|
|
||||||
core="${site_packages}/_rocm_sdk_core"
|
|
||||||
libraries="${site_packages}/_rocm_sdk_libraries"
|
|
||||||
|
|
||||||
# rocminfo stays for checking that the GPU is visible in the container
|
|
||||||
find "${core}/bin" -mindepth 1 -maxdepth 1 \
|
|
||||||
! -name rocminfo ! -name rocm_agent_enumerator -exec rm -rf {} +
|
|
||||||
|
|
||||||
# hiprtc compiles MIGraphX's kernels through comgr, which needs libLLVM,
|
|
||||||
# libclang-cpp, the clang resource headers, and the amdgcn device libraries
|
|
||||||
rm -rf "${core}/lib/llvm/bin" "${core}/lib/llvm/include"
|
|
||||||
find "${core}/lib/llvm/lib" -mindepth 1 -maxdepth 1 \
|
|
||||||
! -name 'libLLVM.so*' ! -name 'libclang-cpp.so*' ! -name clang \
|
|
||||||
-exec rm -rf {} +
|
|
||||||
rm -rf "${core}"/lib/llvm/lib/clang/*/lib
|
|
||||||
|
|
||||||
rm -rf \
|
|
||||||
"${core}/lib/host-math" \
|
|
||||||
"${core}"/lib/python3* \
|
|
||||||
"${core}/lib/rocprofiler-sdk" \
|
|
||||||
"${core}"/lib/librocprofiler-sdk*.so* \
|
|
||||||
"${core}"/lib/librocprof-trace-decoder.so* \
|
|
||||||
"${core}"/lib/librocm-dbgapi.so* \
|
|
||||||
"${core}"/lib/librocm-debug-agent.so* \
|
|
||||||
"${core}"/lib/libamd_smi*.so* \
|
|
||||||
"${core}"/lib/libgoamdsmi_shim64.so* \
|
|
||||||
"${core}"/lib/librocdecode.so* \
|
|
||||||
"${core}"/lib/librocjpeg.so* \
|
|
||||||
"${core}"/lib/librocdxg.so* \
|
|
||||||
"${core}"/lib/librocjitsu_*.so* \
|
|
||||||
"${core}"/lib/libhsa_hotswap_rocjitsu.so* \
|
|
||||||
"${core}"/lib/*.a
|
|
||||||
|
|
||||||
rm -rf \
|
|
||||||
"${libraries}"/lib/librpp.so* \
|
|
||||||
"${libraries}"/lib/librocsparse.so* \
|
|
||||||
"${libraries}"/lib/libhipsparse.so* \
|
|
||||||
"${libraries}"/lib/libhipsparselt.so* \
|
|
||||||
"${libraries}/lib/rocsparse" \
|
|
||||||
"${libraries}"/lib/librocsolver.so* \
|
|
||||||
"${libraries}"/lib/libhipsolver.so* \
|
|
||||||
"${libraries}"/lib/libhipblas.so* \
|
|
||||||
"${libraries}"/lib/librocrand.so* \
|
|
||||||
"${libraries}"/lib/libhiprand.so* \
|
|
||||||
"${libraries}"/lib/librocfft.so* \
|
|
||||||
"${libraries}"/lib/libhipfft.so* \
|
|
||||||
"${libraries}"/lib/libhipfftw.so* \
|
|
||||||
"${libraries}"/lib/librccl.so* \
|
|
||||||
"${libraries}"/.kpack/sparse_lib_*.kpack \
|
|
||||||
"${libraries}"/.kpack/solver_lib_*.kpack \
|
|
||||||
"${libraries}"/.kpack/rand_lib_*.kpack \
|
|
||||||
"${libraries}"/.kpack/fft_lib_*.kpack \
|
|
||||||
"${libraries}"/.kpack/rccl_lib_*.kpack
|
|
||||||
@@ -1,19 +1 @@
|
|||||||
# ROCm 10.1.0, MIGraphX, and its ONNX Runtime plugin provider from AMD's wheels.
|
onnxruntime-migraphx @ https://github.com/NickM-27/frigate-onnxruntime-rocm/releases/download/v7.2.3-1/onnxruntime_migraphx-1.24.4-cp311-cp311-linux_x86_64.whl
|
||||||
onnxruntime == 1.29.*
|
|
||||||
onnxruntime-ep-migraphx @ https://stable.repo.amd.com/rocm/onnxruntime/whl-next/onnxruntime-ep-migraphx/onnxruntime_ep_migraphx-1.0.0%2Brocm10.1.0-cp311-cp311-manylinux_2_28_x86_64.whl
|
|
||||||
migraphx-libs @ https://stable.repo.amd.com/rocm/migraphx/whl-next/migraphx-libs/migraphx_libs-2.18.0%2Brocm10.1.0-py3-none-manylinux_2_28_x86_64.whl
|
|
||||||
rocm-sdk-core @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-core/rocm_sdk_core-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-libraries @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-libraries/rocm_sdk_libraries-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
# Kernels for each supported GPU target. MIGraphX has no gfx1031-gfx1036 kernels,
|
|
||||||
# so those GPUs run as gfx1030 with HSA_OVERRIDE_GFX_VERSION=10.3.0
|
|
||||||
rocm-sdk-device-gfx1030 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1030/rocm_sdk_device_gfx1030-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1100 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1100/rocm_sdk_device_gfx1100-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1101 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1101/rocm_sdk_device_gfx1101-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1102 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1102/rocm_sdk_device_gfx1102-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1103 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1103/rocm_sdk_device_gfx1103-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1150 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1150/rocm_sdk_device_gfx1150-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1151 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1151/rocm_sdk_device_gfx1151-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1152 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1152/rocm_sdk_device_gfx1152-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1153 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1153/rocm_sdk_device_gfx1153-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1200 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1200/rocm_sdk_device_gfx1200-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
rocm-sdk-device-gfx1201 @ https://stable.repo.amd.com/rocm/whl-next/rocm-sdk-device-gfx1201/rocm_sdk_device_gfx1201-10.1.0-py3-none-linux_x86_64.whl
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
variable "ROCM" {
|
||||||
|
default = "7.2.3"
|
||||||
|
}
|
||||||
variable "HSA_OVERRIDE_GFX_VERSION" {
|
variable "HSA_OVERRIDE_GFX_VERSION" {
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
@@ -5,6 +8,12 @@ variable "HSA_OVERRIDE" {
|
|||||||
default = "1"
|
default = "1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
target wget {
|
||||||
|
dockerfile = "docker/main/Dockerfile"
|
||||||
|
platforms = ["linux/amd64"]
|
||||||
|
target = "wget"
|
||||||
|
}
|
||||||
|
|
||||||
target deps {
|
target deps {
|
||||||
dockerfile = "docker/main/Dockerfile"
|
dockerfile = "docker/main/Dockerfile"
|
||||||
platforms = ["linux/amd64"]
|
platforms = ["linux/amd64"]
|
||||||
@@ -21,10 +30,12 @@ target rocm {
|
|||||||
dockerfile = "docker/rocm/Dockerfile"
|
dockerfile = "docker/rocm/Dockerfile"
|
||||||
contexts = {
|
contexts = {
|
||||||
deps = "target:deps",
|
deps = "target:deps",
|
||||||
|
wget = "target:wget",
|
||||||
rootfs = "target:rootfs"
|
rootfs = "target:rootfs"
|
||||||
}
|
}
|
||||||
platforms = ["linux/amd64"]
|
platforms = ["linux/amd64"]
|
||||||
args = {
|
args = {
|
||||||
|
ROCM = ROCM,
|
||||||
HSA_OVERRIDE_GFX_VERSION = HSA_OVERRIDE_GFX_VERSION,
|
HSA_OVERRIDE_GFX_VERSION = HSA_OVERRIDE_GFX_VERSION,
|
||||||
HSA_OVERRIDE = HSA_OVERRIDE
|
HSA_OVERRIDE = HSA_OVERRIDE
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# Nvidia ONNX Runtime GPU Support
|
# Nvidia ONNX Runtime GPU Support
|
||||||
--extra-index-url 'https://pypi.nvidia.com'
|
--extra-index-url 'https://pypi.nvidia.com'
|
||||||
|
cython==3.0.*; platform_machine == 'x86_64'
|
||||||
nvidia-cuda-cupti-cu12==12.8.90; platform_machine == 'x86_64'
|
nvidia-cuda-cupti-cu12==12.8.90; platform_machine == 'x86_64'
|
||||||
nvidia-cublas-cu12==12.8.4.1; platform_machine == 'x86_64'
|
nvidia-cublas-cu12==12.8.4.1; platform_machine == 'x86_64'
|
||||||
nvidia-cudnn-cu12==9.8.0.87; platform_machine == 'x86_64'
|
nvidia-cudnn-cu12==9.8.0.87; platform_machine == 'x86_64'
|
||||||
@@ -14,4 +15,4 @@ nvidia-nccl-cu12==2.26.2.post1; platform_machine == 'x86_64'
|
|||||||
nvidia-nvjitlink-cu12==12.8.93; platform_machine == 'x86_64'
|
nvidia-nvjitlink-cu12==12.8.93; platform_machine == 'x86_64'
|
||||||
onnx==1.16.*; platform_machine == 'x86_64'
|
onnx==1.16.*; platform_machine == 'x86_64'
|
||||||
onnxruntime-gpu==1.24.*; 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'
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ edgeTPU:
|
|||||||
height: 320 # <--- should match the imgsize of the model, typically 320
|
height: 320 # <--- should match the imgsize of the model, typically 320
|
||||||
path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite
|
path: /config/model_cache/yolov9-s-relu6-best_320_int8_edgetpu.tflite
|
||||||
labelmap_path: /config/labels-coco17.txt
|
labelmap_path: /config/labels-coco17.txt
|
||||||
hailo:
|
hailo8l:
|
||||||
title: Hailo
|
title: Hailo-8/Hailo-8L
|
||||||
models:
|
models:
|
||||||
- key: yolo
|
- key: yolo
|
||||||
label: YOLO
|
label: YOLO
|
||||||
recommended: true
|
recommended: true
|
||||||
download: If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup, choosing the build that matches the attached device. Once cached under `/config/model_cache`, the model works fully offline.
|
download: If no custom model path or URL is provided, the Hailo detector automatically downloads the default model (YOLOv6n) from the Hailo Model Zoo on first startup based on the detected hardware. Once cached under `/config/model_cache/hailo`, the model works fully offline.
|
||||||
ui: |-
|
ui: |-
|
||||||
Navigate to **Settings > System > Detection models** and select **Hailo** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure the model settings:
|
Navigate to **Settings > System > Detection models** and select **Hailo** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure the model settings:
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ hailo:
|
|||||||
yaml: |-
|
yaml: |-
|
||||||
models:
|
models:
|
||||||
- devices:
|
- devices:
|
||||||
- hailo:PCIe
|
- hailo8l:PCIe
|
||||||
width: 320
|
width: 320
|
||||||
height: 320
|
height: 320
|
||||||
input_tensor: nhwc
|
input_tensor: nhwc
|
||||||
@@ -101,7 +101,7 @@ hailo:
|
|||||||
yaml: |-
|
yaml: |-
|
||||||
models:
|
models:
|
||||||
- devices:
|
- devices:
|
||||||
- hailo:PCIe
|
- hailo8l:PCIe
|
||||||
width: 300
|
width: 300
|
||||||
height: 300
|
height: 300
|
||||||
input_tensor: nhwc
|
input_tensor: nhwc
|
||||||
@@ -824,38 +824,24 @@ cpu:
|
|||||||
models:
|
models:
|
||||||
- devices:
|
- devices:
|
||||||
- cpu:3
|
- cpu:3
|
||||||
xdna2:
|
deepstack:
|
||||||
title: AMD XDNA2
|
title: DeepStack / CodeProject.AI
|
||||||
models:
|
models:
|
||||||
- key: yolov9
|
- key: yolo
|
||||||
label: YOLOv9
|
label: YOLO
|
||||||
recommended: true
|
recommended: true
|
||||||
download: |-
|
download: This detector runs object detection over the network against a CodeProject.AI or DeepStack server, so no model is downloaded into Frigate itself. Visit the [CodeProject.AI official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to download and install the AI server on your preferred device (e.g. Raspberry Pi, Nvidia Jetson, or other compatible hardware) before configuring the detector.
|
||||||
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: |-
|
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:
|
Navigate to **Settings > System > Detection models** and add a model. The CodeProject.AI server is not reported by the hardware probe, so set `devices` to `deepstack:http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection` in YAML.
|
||||||
|
|
||||||
| Field | Value |
|
| Field | Value |
|
||||||
| ---------------------------------------- | ------------------------------------------ |
|
| ------------- | ---------------------------------------------------------------------- |
|
||||||
| **Custom object detector model path** | `/config/models/yolov9-c-320.onnx` |
|
| **API URL** | `http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection` |
|
||||||
| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
|
| **API Timeout** | `0.1` (seconds) |
|
||||||
| **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: |-
|
yaml: |-
|
||||||
models:
|
models:
|
||||||
- devices:
|
- devices:
|
||||||
- zmq:tcp://xdna:5555
|
- deepstack:http://<your_codeproject_ai_server_ip>:<port>/v1/vision/detection
|
||||||
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:
|
memryx:
|
||||||
title: MemryX
|
title: MemryX
|
||||||
models:
|
models:
|
||||||
@@ -999,65 +985,6 @@ memryx:
|
|||||||
# The .zip file must contain:
|
# The .zip file must contain:
|
||||||
# ├── ssdlite_mobilenet.dfp (a file ending with .dfp)
|
# ├── ssdlite_mobilenet.dfp (a file ending with .dfp)
|
||||||
# └── ssdlite_mobilenet_post.onnx (optional; only if the model includes a cropped post-processing network)
|
# └── 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:
|
tensorrt:
|
||||||
title: TensorRT
|
title: TensorRT
|
||||||
models:
|
models:
|
||||||
@@ -1106,7 +1033,7 @@ synaptics:
|
|||||||
- key: ssd
|
- key: ssd
|
||||||
label: SSD MobileNet
|
label: SSD MobileNet
|
||||||
recommended: true
|
recommended: true
|
||||||
download: A synap model is provided in the container at `/synaptics/mobilenet.synap` and is used by this detector type by default. The model comes from the [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80).
|
download: A synap model is provided in the container at `/mobilenet.synap` and is used by this detector type by default. The model comes from the [Synap-release Github](https://github.com/synaptics-astra/synap-release/tree/v1.5.0/models/dolphin/object_detection/coco/model/mobilenet224_full80).
|
||||||
ui: |-
|
ui: |-
|
||||||
Navigate to **Settings > System > Detection models** and select **Synaptics NPU** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure:
|
Navigate to **Settings > System > Detection models** and select **Synaptics NPU** from the **Hardware** dropdown. Then, on the same model, open the **Custom Model** tab and configure:
|
||||||
|
|
||||||
|
|||||||
@@ -117,9 +117,7 @@ auth:
|
|||||||
# Optional: Cookie to store the JWT token for native auth (default: shown below)
|
# Optional: Cookie to store the JWT token for native auth (default: shown below)
|
||||||
cookie_name: frigate_token
|
cookie_name: frigate_token
|
||||||
# Optional: Set secure flag on cookie. (default: shown below)
|
# Optional: Set secure flag on cookie. (default: shown below)
|
||||||
# NOTE: Frigate sets the flag automatically when serving TLS. Set this to True
|
# NOTE: This should be set to True if you are using TLS
|
||||||
# when a reverse proxy terminates TLS in front of Frigate. A proxy that connects
|
|
||||||
# to the TLS port must serve Frigate to browsers over HTTPS
|
|
||||||
cookie_secure: False
|
cookie_secure: False
|
||||||
# Optional: Session length in seconds (default: shown below)
|
# Optional: Session length in seconds (default: shown below)
|
||||||
session_length: 86400 # 24 hours
|
session_length: 86400 # 24 hours
|
||||||
@@ -154,10 +152,9 @@ auth:
|
|||||||
models:
|
models:
|
||||||
# Optional: the camera environment this model is for (default: shown below)
|
# Optional: the camera environment this model is for (default: shown below)
|
||||||
# Cameras select a model by setting detect -> scene to a matching value, and
|
# 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.
|
# a model with a scene of all is used by any camera that does not set one.
|
||||||
# Any name made up of letters, numbers, _ and - is valid, such as thermal.
|
# Valid values are all, indoor, outdoor, indoor_thermal, outdoor_thermal
|
||||||
# Models that use the same model file are combined into one model.
|
- scene: all
|
||||||
- scene: default
|
|
||||||
# Required: hardware this model runs on, as <detector> or <detector>:<device>
|
# Required: hardware this model runs on, as <detector> or <detector>:<device>
|
||||||
# See https://docs.frigate.video/configuration/object_detectors for the
|
# See https://docs.frigate.video/configuration/object_detectors for the
|
||||||
# detectors available and the devices each one accepts. All of a model's
|
# detectors available and the devices each one accepts. All of a model's
|
||||||
@@ -296,9 +293,9 @@ ffmpeg:
|
|||||||
# Optional: output args for detect streams (default: shown below)
|
# Optional: output args for detect streams (default: shown below)
|
||||||
detect: -threads 2 -f rawvideo -pix_fmt yuv420p
|
detect: -threads 2 -f rawvideo -pix_fmt yuv420p
|
||||||
# Optional: output args for record streams (default: shown below)
|
# 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)
|
# 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)
|
# 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 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
|
# 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
|
||||||
@@ -319,9 +316,9 @@ detect:
|
|||||||
# Optional: height of the frame for the input with the detect role (default: use native stream resolution)
|
# Optional: height of the frame for the input with the detect role (default: use native stream resolution)
|
||||||
height: 720
|
height: 720
|
||||||
# Optional: the environment this camera looks at, which picks the model it runs on
|
# Optional: the environment this camera looks at, which picks the model it runs on
|
||||||
# (default: the model with a scene of default)
|
# (default: the model with a scene of all)
|
||||||
# Must match the scene of a configured model
|
# Valid values are all, indoor, outdoor, indoor_thermal, outdoor_thermal
|
||||||
scene: thermal
|
scene: outdoor
|
||||||
# Optional: desired fps for your camera for the input with the detect role (default: shown below)
|
# 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.
|
# NOTE: Recommended value of 5. Ideally, try and reduce your FPS on the camera.
|
||||||
fps: 5
|
fps: 5
|
||||||
@@ -558,7 +555,7 @@ motion:
|
|||||||
# Optional: Whether this mask is active (default: true)
|
# Optional: Whether this mask is active (default: true)
|
||||||
enabled: true
|
enabled: true
|
||||||
# Required: Coordinates polygon for the mask
|
# 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)
|
# 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
|
# Enables dynamic contrast improvement. This should help improve night detections at the cost of making motion detection more sensitive
|
||||||
# for daytime.
|
# for daytime.
|
||||||
@@ -573,8 +570,6 @@ notifications:
|
|||||||
enabled: False
|
enabled: False
|
||||||
# Optional: Email for push service to reach out to
|
# Optional: Email for push service to reach out to
|
||||||
# NOTE: This is required to use notifications
|
# 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"
|
email: "admin@example.com"
|
||||||
# Optional: Cooldown time for notifications in seconds (default: shown below)
|
# Optional: Cooldown time for notifications in seconds (default: shown below)
|
||||||
cooldown: 0
|
cooldown: 0
|
||||||
@@ -805,7 +800,7 @@ lpr:
|
|||||||
# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at
|
# to Google or OpenAI's LLMs to generate descriptions. GenAI features can be configured at
|
||||||
# the camera level to enhance privacy for indoor cameras.
|
# the camera level to enhance privacy for indoor cameras.
|
||||||
# NOTE: genai is a map of named providers. Each key is a name you choose for the provider,
|
# NOTE: genai is a map of named providers. Each key is a name you choose for the provider,
|
||||||
# and each role (chat, descriptions, embeddings, transcribe) may be assigned to exactly one provider.
|
# and each role (chat, descriptions, embeddings) may be assigned to exactly one provider.
|
||||||
genai:
|
genai:
|
||||||
# Required: name of the provider (chosen by you, used to reference it elsewhere)
|
# Required: name of the provider (chosen by you, used to reference it elsewhere)
|
||||||
my_provider:
|
my_provider:
|
||||||
@@ -818,13 +813,11 @@ genai:
|
|||||||
# Required: The model to use with the provider.
|
# Required: The model to use with the provider.
|
||||||
model: gemini-1.5-flash
|
model: gemini-1.5-flash
|
||||||
# Optional: Roles this provider handles (default: shown below)
|
# Optional: Roles this provider handles (default: shown below)
|
||||||
# Each role (chat, descriptions, embeddings, transcribe) must be assigned to exactly
|
# Each role (chat, descriptions, embeddings) must be assigned to exactly one provider.
|
||||||
# one provider.
|
|
||||||
roles:
|
roles:
|
||||||
- chat
|
- chat
|
||||||
- descriptions
|
- descriptions
|
||||||
- embeddings
|
- embeddings
|
||||||
- transcribe
|
|
||||||
# Optional additional args to pass to the GenAI Provider (default: None)
|
# Optional additional args to pass to the GenAI Provider (default: None)
|
||||||
provider_options:
|
provider_options:
|
||||||
keep_alive: -1
|
keep_alive: -1
|
||||||
@@ -837,19 +830,13 @@ genai:
|
|||||||
audio_transcription:
|
audio_transcription:
|
||||||
# Optional: Enable live and speech event audio transcription (default: shown below)
|
# Optional: Enable live and speech event audio transcription (default: shown below)
|
||||||
enabled: False
|
enabled: False
|
||||||
# Optional: The transcription backend (default: shown below)
|
|
||||||
# Either 'whisper' for Frigate's built-in local models, or the name of a genai
|
|
||||||
# provider that has 'transcribe' in its roles. device and model_size are ignored
|
|
||||||
# when a genai provider is named.
|
|
||||||
model: whisper
|
|
||||||
# Optional: The device to run the models on for live transcription. (default: shown below)
|
# Optional: The device to run the models on for live transcription. (default: shown below)
|
||||||
device: CPU
|
device: CPU
|
||||||
# Optional: Set the model size used for live transcription. (default: shown below)
|
# Optional: Set the model size used for live transcription. (default: shown below)
|
||||||
model_size: small
|
model_size: small
|
||||||
# Optional: Set the language used for transcription translation. (default: shown below)
|
# Optional: Set the language used for transcription translation. (default: shown below)
|
||||||
# Use 'auto' to let the model detect the language, or a language code from
|
# List of language codes: https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
|
||||||
# https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10
|
language: en
|
||||||
language: auto
|
|
||||||
|
|
||||||
# Optional: Configuration for classification models
|
# Optional: Configuration for classification models
|
||||||
classification:
|
classification:
|
||||||
@@ -905,21 +892,6 @@ live:
|
|||||||
streams:
|
streams:
|
||||||
main_stream: main_stream_name
|
main_stream: main_stream_name
|
||||||
sub_stream: sub_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)
|
# 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
|
# 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.
|
# reduce bandwidth required for viewing the jsmpeg stream. Width is computed to match known aspect ratio.
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ Frequently-heard labels like `speech` can generate a lot of events, and each eve
|
|||||||
|
|
||||||
### Audio Transcription
|
### Audio Transcription
|
||||||
|
|
||||||
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`, and can alternatively offload transcription to a [GenAI provider](#genai-provider). The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service. Automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
|
Frigate supports fully local audio transcription using either `sherpa-onnx` or OpenAI's open-source Whisper models via `faster-whisper`. The goal of this feature is to support Semantic Search for `speech` audio events. Frigate is not intended to act as a continuous, fully-automatic speech transcription service. Automatically transcribing all speech (or queuing many audio events for transcription) requires substantial CPU (or GPU) resources and is impractical on most systems. For this reason, transcriptions for events are initiated manually from the UI or the API rather than being run continuously in the background.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
@@ -224,7 +224,6 @@ To enable transcription, configure it globally and optionally disable for specif
|
|||||||
**Global:** Navigate to <NavPath path="Settings > Enrichments > Audio transcription" />.
|
**Global:** Navigate to <NavPath path="Settings > Enrichments > Audio transcription" />.
|
||||||
|
|
||||||
- Set **Enable audio transcription** to on
|
- Set **Enable audio transcription** to on
|
||||||
- Set **Audio transcription model or GenAI provider name** to `whisper` for Frigate's built-in local models, or to the name of a GenAI provider
|
|
||||||
- Set **Transcription device** to the desired device
|
- Set **Transcription device** to the desired device
|
||||||
- Set **Model size** to the desired size
|
- Set **Model size** to the desired size
|
||||||
|
|
||||||
@@ -236,7 +235,6 @@ To enable transcription, configure it globally and optionally disable for specif
|
|||||||
```yaml
|
```yaml
|
||||||
audio_transcription:
|
audio_transcription:
|
||||||
enabled: True
|
enabled: True
|
||||||
model: whisper
|
|
||||||
device: ...
|
device: ...
|
||||||
model_size: ...
|
model_size: ...
|
||||||
```
|
```
|
||||||
@@ -265,88 +263,20 @@ The optional config parameters that can be set at the global level include:
|
|||||||
- **`enabled`**: Enable or disable the audio transcription feature.
|
- **`enabled`**: Enable or disable the audio transcription feature.
|
||||||
- Default: `False`
|
- Default: `False`
|
||||||
- It is recommended to only configure the features at the global level, and enable it at the individual camera level.
|
- It is recommended to only configure the features at the global level, and enable it at the individual camera level.
|
||||||
- **`model`**: The transcription backend.
|
|
||||||
- Default: `whisper`
|
|
||||||
- `whisper` uses Frigate's built-in local models, described by `device` and `model_size` below.
|
|
||||||
- Any other value must name a key in your `genai` config whose entry has `transcribe` in its `roles`. See [GenAI Provider](#genai-provider).
|
|
||||||
- **`device`**: Device to use to run transcription and translation models.
|
- **`device`**: Device to use to run transcription and translation models.
|
||||||
- Default: `CPU`
|
- Default: `CPU`
|
||||||
- This can be `CPU` or `GPU`. The `sherpa-onnx` models are lightweight and run on the CPU only. The `whisper` models can run on GPU but are only supported on CUDA hardware.
|
- This can be `CPU` or `GPU`. The `sherpa-onnx` models are lightweight and run on the CPU only. The `whisper` models can run on GPU but are only supported on CUDA hardware.
|
||||||
- Ignored when `model` names a GenAI provider.
|
|
||||||
- **`model_size`**: The size of the model used for live transcription.
|
- **`model_size`**: The size of the model used for live transcription.
|
||||||
- Default: `small`
|
- Default: `small`
|
||||||
- This can be `small` or `large`. The `small` setting uses `sherpa-onnx` models that are fast, lightweight, and always run on the CPU but are not as accurate as the `whisper` model.
|
- This can be `small` or `large`. The `small` setting uses `sherpa-onnx` models that are fast, lightweight, and always run on the CPU but are not as accurate as the `whisper` model.
|
||||||
- This config option applies to **live transcription only**. With `model: whisper`, recorded `speech` events always use a different `whisper` model (and can be accelerated for CUDA hardware if available with `device: GPU`).
|
- This config option applies to **live transcription only**. Recorded `speech` events will always use a different `whisper` model (and can be accelerated for CUDA hardware if available with `device: GPU`).
|
||||||
- Ignored when `model` names a GenAI provider.
|
- **`language`**: Defines the language used by `whisper` to translate `speech` audio events (and live audio only if using the `large` model).
|
||||||
- **`language`**: Defines the language used to transcribe and translate `speech` audio events (and live audio only if using the `large` model or a GenAI provider).
|
- Default: `en`
|
||||||
- Default: `auto`
|
- You must use a valid [language code](https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10).
|
||||||
- `auto` lets the model detect the language itself, which most models do well. Set an explicit language only if detection is picking the wrong one.
|
|
||||||
- Otherwise you must use a valid [language code](https://github.com/openai/whisper/blob/main/whisper/tokenizer.py#L10).
|
|
||||||
- Transcriptions for `speech` events are translated.
|
- Transcriptions for `speech` events are translated.
|
||||||
- Live audio is translated only if you are using the `large` model. The `small` `sherpa-onnx` model is English-only.
|
- Live audio is translated only if you are using the `large` model. The `small` `sherpa-onnx` model is English-only.
|
||||||
|
|
||||||
The only field that is valid at the camera level is `enabled`. In particular `model` is global only: the transcription backend is a process-wide resource shared by every camera.
|
The only field that is valid at the camera level is `enabled`.
|
||||||
|
|
||||||
#### 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.
|
|
||||||
|
|
||||||
To use a GenAI provider for audio transcription:
|
|
||||||
|
|
||||||
1. Configure a GenAI provider with `transcribe` in its `roles`.
|
|
||||||
2. Set the audio transcription model to that GenAI config key (e.g. `whisper_cloud`).
|
|
||||||
|
|
||||||
<ConfigTabs>
|
|
||||||
<TabItem value="ui">
|
|
||||||
|
|
||||||
Navigate to <NavPath path="Settings > Enrichments > Audio transcription" />.
|
|
||||||
|
|
||||||
| Field | Description |
|
|
||||||
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
|
||||||
| **Audio transcription model or GenAI provider name** | Set to the GenAI config key (e.g. `whisper_cloud`) to use a configured GenAI provider for transcription |
|
|
||||||
|
|
||||||
The GenAI provider must also be configured with the `transcribe` role under <NavPath path="Settings > Enrichments > Generative AI" />.
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="yaml">
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
genai:
|
|
||||||
whisper_cloud:
|
|
||||||
provider: openai
|
|
||||||
api_key: your-api-key
|
|
||||||
model: gpt-transcribe
|
|
||||||
roles:
|
|
||||||
- transcribe
|
|
||||||
|
|
||||||
audio_transcription:
|
|
||||||
enabled: True
|
|
||||||
model: whisper_cloud
|
|
||||||
language: en
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</ConfigTabs>
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
|
|
||||||
**Give `transcribe` its own `genai` entry.** A `genai` entry has a single `model` string that is shared by every role it holds, so `roles: [descriptions, transcribe]` would send the same model name to both the chat endpoint and the transcription endpoint. Transcription models and chat models are almost never the same model, so define a dedicated entry as shown above.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
:::warning
|
|
||||||
|
|
||||||
**Live transcription against a metered provider is billed continuously.** In live mode Frigate uploads an overlapping ~2 second window of audio roughly once per second, per camera, for as long as audio stays above that camera's `audio.min_volume`. Windows below that threshold are never uploaded, which is what keeps a quiet camera near zero requests, but a camera pointed at a busy street will keep sending.
|
|
||||||
|
|
||||||
Three things keep this opt-in: `transcribe` is not one of the default roles, live transcription is off by default, and the volume gate suppresses silence. Transcription of recorded `speech` events is unaffected - it remains a manual, one-request-per-event action.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
`device` and `model_size` have no effect on this path and no local model is ever downloaded.
|
|
||||||
|
|
||||||
`language` defaults to `auto`, which sends no language hint and lets the model detect it. Most audio models detect language well, so leave it on `auto` unless detection is picking the wrong one.
|
|
||||||
|
|
||||||
When set explicitly, it is sent as the transcription endpoint's native `language` parameter for OpenAI, Azure, and llama.cpp, and as part of the prompt for Gemini. This matters for dedicated ASR models such as Qwen3-ASR: they read the prompt as contextual biasing rather than as an instruction, so a language named in the prompt is ignored, while the endpoint parameter is honored.
|
|
||||||
|
|
||||||
#### Live transcription
|
#### Live transcription
|
||||||
|
|
||||||
@@ -362,8 +292,6 @@ Results can be error-prone due to a number of factors, including:
|
|||||||
|
|
||||||
For speech sources close to the camera with minimal background noise, use the `small` model.
|
For speech sources close to the camera with minimal background noise, use the `small` model.
|
||||||
|
|
||||||
A [GenAI provider](#genai-provider) is generally the most accurate option for live transcription, at the cost of a network round trip per window. That round trip has to stay under about a second to keep up with the audio; if it does not, Frigate drops the oldest buffered audio rather than letting the backlog grow.
|
|
||||||
|
|
||||||
If you have CUDA hardware, you can experiment with the `large` `whisper` model on GPU. Performance is not quite as fast as the `sherpa-onnx` `small` model, but live transcription is far more accurate. Using the `large` model with CPU will likely be too slow for real-time transcription.
|
If you have CUDA hardware, you can experiment with the `large` `whisper` model on GPU. Performance is not quite as fast as the `sherpa-onnx` `small` model, but live transcription is far more accurate. Using the `large` model with CPU will likely be too slow for real-time transcription.
|
||||||
|
|
||||||
#### Transcription and translation of `speech` audio events
|
#### Transcription and translation of `speech` audio events
|
||||||
@@ -380,7 +308,7 @@ Only one `speech` event may be transcribed at a time. Frigate does not automatic
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
With `model: whisper`, recorded `speech` events always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient. With a [GenAI provider](#genai-provider), the recorded clip is sent to the provider instead and no local model is used.
|
Recorded `speech` events will always use a `whisper` model, regardless of the `model_size` config setting. Without a supported Nvidia GPU, generating transcriptions for longer `speech` events may take a fair amount of time, so be patient.
|
||||||
|
|
||||||
#### FAQ
|
#### FAQ
|
||||||
|
|
||||||
|
|||||||
@@ -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" />.
|
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.
|
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.
|
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. 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.
|
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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ The FeatureList on the [ONVIF Conformant Products Database](https://www.onvif.or
|
|||||||
| Amcrest ASH21 | ✅ | ❌ | ONVIF service port: 80 |
|
| Amcrest ASH21 | ✅ | ❌ | ONVIF service port: 80 |
|
||||||
| Amcrest IP4M-S2112EW-AI | ✅ | ❌ | FOV relative movement not supported. |
|
| Amcrest IP4M-S2112EW-AI | ✅ | ❌ | FOV relative movement not supported. |
|
||||||
| Amcrest IP5M-1190EW | ✅ | ❌ | ONVIF Port: 80. 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. |
|
| Axis Q-6155E | ✅ | ❌ | ONVIF service port: 80; Camera does not support MoveStatus. |
|
||||||
| Ctronics PTZ | ✅ | ❌ | |
|
| 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). |
|
| 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>
|
</TabItem>
|
||||||
</ConfigTabs>
|
</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
|
## Training the model
|
||||||
|
|
||||||
Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps:
|
Creating and training the model is done within the Frigate UI using the `Classification` page. The process consists of three steps:
|
||||||
|
|||||||
@@ -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.
|
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 (YAML config) | UI Label | Usage | Notes |
|
||||||
| ------------------------- | ----------------------- | --------------------------------------------- | --------------------------------------------------------------- |
|
| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- |
|
||||||
| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
|
| 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-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-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
|
||||||
| preset-apple-silicon-h265 | Apple Silicon (H.265) | Apple Silicon Mac under lighter, H.265 stream | Needs the `lighter.sh/video` device |
|
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
|
||||||
| 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-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
|
||||||
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
|
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
|
||||||
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
|
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
|
||||||
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
|
| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
|
||||||
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
|
| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
|
||||||
| 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>
|
<ConfigTabs>
|
||||||
<TabItem value="ui">
|
<TabItem value="ui">
|
||||||
|
|||||||
@@ -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`).
|
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).
|
Each provider handles one or more **roles**: `chat`, `descriptions`, and `embeddings`. A provider handles all three by default, and 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.
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
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_`.
|
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_`.
|
||||||
|
|
||||||
@@ -75,31 +63,19 @@ Running Generative AI models on CPU is not recommended, as high inference times
|
|||||||
|
|
||||||
You must use a vision-capable model with Frigate. The following models are recommended for local deployment of the `descriptions` and `chat` roles:
|
You must use a vision-capable model with Frigate. The following models are recommended for local deployment of the `descriptions` and `chat` roles:
|
||||||
|
|
||||||
| Model | Review [frame mode](/configuration/genai/genai_review#frame-mode) | Notes |
|
| Model | Notes |
|
||||||
| ------------------- | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `qwen3-vl` | `frames` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. Follows a sequence of frames on its own. |
|
| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. |
|
||||||
| `qwen3.6`/`qwen3.8` | `frames` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
|
| `qwen3.6`/`qwen3.8` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
|
||||||
| `gemma4` | `annotated_frames` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. Loses track of activity that repeats or reverses, so it benefits from annotated frames. |
|
| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. |
|
||||||
|
|
||||||
#### Embedding models
|
#### Embedding models
|
||||||
|
|
||||||
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.
|
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 |
|
| 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). Must be served by llama.cpp started with `--embeddings` and `--mmproj`. |
|
||||||
| `qwen3-vl-embedding` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Good performance, large model that requires strong hardware for inference. |
|
|
||||||
|
|
||||||
#### Transcription models
|
|
||||||
|
|
||||||
The `transcribe` role needs a model that accepts audio input. A text-only or vision-only model cannot serve this role. The following are recommended for local deployment of the `transcribe` role:
|
|
||||||
|
|
||||||
| Model | Notes |
|
|
||||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| `qwen3-asr` | Dedicated speech recognition model covering 30 languages, and the better choice for transcription quality. It only transcribes, so it cannot be shared with the `descriptions` or `chat` roles. |
|
|
||||||
| `gemma4` | General multimodal model that accepts audio as well as images, so one served model can cover `transcribe` alongside the other roles. Transcript quality is below `qwen3-asr`, particularly on noisy audio. |
|
|
||||||
|
|
||||||
Both must be served by llama.cpp started with the matching audio `--mmproj`. llama.cpp only reports audio support when an audio projector is loaded. Without it Frigate sees the model as text-only and the `transcribe` role is unavailable in the UI. Frigate transcribes through the server's `/v1/audio/transcriptions` route, which llama.cpp serves for any audio-capable model.
|
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
@@ -165,10 +141,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.
|
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
|
||||||
|
|
||||||
[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.
|
[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 +186,6 @@ genai:
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</ConfigTabs>
|
</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
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -192,45 +192,6 @@ review:
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</ConfigTabs>
|
</ConfigTabs>
|
||||||
|
|
||||||
### Frame Mode
|
|
||||||
|
|
||||||
Review items are sent to the model as a sequence of still frames. Some models follow that sequence well on their own; others lose track of activity that repeats or reverses, and describe a single trip when the subject actually made several. The `frame_mode` option controls how those frames are presented.
|
|
||||||
|
|
||||||
- `frames` (default): the prompt followed by the frames, exactly as earlier versions of Frigate sent them.
|
|
||||||
- `annotated_frames`: each frame is preceded by its frame number and elapsed time, along with notes describing what the object tracker recorded at that moment, such as an object being first detected, starting to move, turning around, stopping, or no longer being detected.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
Annotated mode needs tracking data for the review item. If none is available, Frigate falls back to sending plain frames for that item.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
<ConfigTabs>
|
|
||||||
<TabItem value="ui">
|
|
||||||
|
|
||||||
Navigate to <NavPath path="Settings > Global configuration > Review" />.
|
|
||||||
|
|
||||||
- Set **GenAI config > Frame mode** to the desired mode (e.g., `annotated_frames`)
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
<TabItem value="yaml">
|
|
||||||
|
|
||||||
```yaml {4}
|
|
||||||
review:
|
|
||||||
genai:
|
|
||||||
enabled: true
|
|
||||||
frame_mode: annotated_frames
|
|
||||||
```
|
|
||||||
|
|
||||||
</TabItem>
|
|
||||||
</ConfigTabs>
|
|
||||||
|
|
||||||
### Response Style
|
### Response Style
|
||||||
|
|
||||||
Different models respond to the built-in prompt with very different writing styles: some produce natural narration while others sound short and mechanical. The `response_style` option selects a writing style preset that rewords the prompt's instructions for the user-facing fields (the title, short summary, and scene description). Presets replace those instructions rather than adding extra ones, so the model never receives competing style directions.
|
Different models respond to the built-in prompt with very different writing styles: some produce natural narration while others sound short and mechanical. The `response_style` option selects a writing style preset that rewords the prompt's instructions for the user-facing fields (the title, short summary, and scene description). Presets replace those instructions rather than adding extra ones, so the model never receives competing style directions.
|
||||||
|
|||||||
@@ -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.
|
- [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**
|
**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
|
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:
|
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, or any release of the `xe` driver.
|
||||||
- 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.
|
|
||||||
- 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`.
|
- 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.
|
No `intel_gpu_top` binary, `CAP_PERFMON`, privileged mode, or `perf_event_paranoid` tuning is required.
|
||||||
@@ -504,7 +499,7 @@ cameras:
|
|||||||
|
|
||||||
## Synaptics
|
## Synaptics
|
||||||
|
|
||||||
Hardware accelerated video de-/encoding is supported on Synaptics SL-series SoC.
|
Hardware accelerated video de-/encoding is supported on Synpatics SL-series SoC.
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
@@ -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.
|
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" />.
|
Navigate to <NavPath path="Settings > Camera configuration > Objects" />.
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| --------------------------------------------------------- | ------------------- |
|
| ---------------------------------------------- | ------------------- |
|
||||||
| **Objects to track** | Add `license_plate` |
|
| **Objects to track** | Add `license_plate` |
|
||||||
| **Object filters > License Plate > Confidence threshold** | Set to `0.7` |
|
| **Object filters > License Plate > Threshold** | Set to `0.7` |
|
||||||
|
|
||||||
Navigate to <NavPath path="Settings > Camera configuration > Motion detection" />.
|
Navigate to <NavPath path="Settings > Camera configuration > Motion detection" />.
|
||||||
|
|
||||||
|
|||||||
@@ -28,24 +28,6 @@ WebRTC may use an external STUN server for NAT traversal. MSE and HLS streaming
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Selecting a streaming technology
|
|
||||||
|
|
||||||
Frigate [defaults to MSE](#why-does-frigate-prefer-mse-over-webrtc-for-live-view) for restreamed cameras by design. To use WebRTC, select it explicitly from a camera's single-camera Live view settings (the settings menu in the camera's Live view header on desktop, or the settings drawer on mobile). Three related controls work together:
|
|
||||||
|
|
||||||
- **Stream**: _what_ to play. This lists the [streams you've configured](#setting-streams-for-live-ui) (for example `Main Stream` and `Sub Stream`).
|
|
||||||
- **Force low-bandwidth mode**: a switch that always plays Frigate's built-in low-bandwidth feed (the stream assigned the `detect` role, using JSMpeg) instead of the selected stream. It works anywhere without go2rtc and is useful on slow or metered connections. While it is enabled, the stream and streaming technology selectors are disabled; your stream and technology choices are restored when you turn it off.
|
|
||||||
- **Streaming Technology**: _how_ to play the selected stream, listing **MSE** and **WebRTC**. It is only shown for a restreamed stream.
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
- **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.
|
|
||||||
- **Unsupported browser**: the browser doesn't support WebRTC.
|
|
||||||
|
|
||||||
When WebRTC isn't available, Frigate automatically uses MSE (or falls back to JSMpeg), so live view keeps working regardless of the selection.
|
|
||||||
|
|
||||||
### Camera Settings Recommendations
|
### Camera Settings Recommendations
|
||||||
|
|
||||||
If you are using go2rtc, you should adjust the following settings in your camera's firmware for the best experience with Live view:
|
If you are using go2rtc, you should adjust the following settings in your camera's firmware for the best experience with Live view:
|
||||||
@@ -92,7 +74,7 @@ go2rtc:
|
|||||||
|
|
||||||
### Setting Streams For Live UI
|
### 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.
|
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 +140,6 @@ cameras:
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</ConfigTabs>
|
</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 extra configuration:
|
||||||
|
|
||||||
WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration:
|
WebRTC works by creating a TCP or UDP connection on port `8555`. However, it requires additional configuration:
|
||||||
@@ -195,17 +157,6 @@ WebRTC works by creating a TCP or UDP connection on port `8555`. However, it req
|
|||||||
- stun:8555
|
- stun:8555
|
||||||
```
|
```
|
||||||
|
|
||||||
- The web UI uses the STUN and TURN servers in `ice_servers` and falls back to Google's public STUN server when none are set:
|
|
||||||
|
|
||||||
```yaml title="config.yml"
|
|
||||||
go2rtc:
|
|
||||||
webrtc:
|
|
||||||
ice_servers:
|
|
||||||
- urls: [turn:turn.example.com:3478]
|
|
||||||
username: frigate
|
|
||||||
credential: password
|
|
||||||
```
|
|
||||||
|
|
||||||
- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.64.0.0/10` CIDR block.
|
- For access through Tailscale, the Frigate system's Tailscale IP must be added as a WebRTC candidate. Tailscale IPs all start with `100.`, and are reserved within the `100.64.0.0/10` CIDR block.
|
||||||
|
|
||||||
- Note that some browsers may not support H.265 (HEVC). You can check your browser's current version for H.265 compatibility [here](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness).
|
- Note that some browsers may not support H.265 (HEVC). You can check your browser's current version for H.265 compatibility [here](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#codecs-madness).
|
||||||
@@ -255,8 +206,6 @@ For devices that support two way talk, Frigate can be configured to use the feat
|
|||||||
- Ensure you access Frigate via https (may require [opening port 8971](/frigate/installation/#ports)).
|
- Ensure you access Frigate via https (may require [opening port 8971](/frigate/installation/#ports)).
|
||||||
- For the Home Assistant Frigate card, [follow the docs](http://card.camera/#/usage/2-way-audio) for the correct source.
|
- For the Home Assistant Frigate card, [follow the docs](http://card.camera/#/usage/2-way-audio) for the correct source.
|
||||||
|
|
||||||
The two-way talk control in the single-camera Live view is only enabled when WebRTC is available; if WebRTC isn't configured or can't connect, the control is shown disabled.
|
|
||||||
|
|
||||||
To use the Reolink Doorbell with two way talk, you should use the [recommended Reolink configuration](/configuration/camera_specific#reolink-cameras)
|
To use the Reolink Doorbell with two way talk, you should use the [recommended Reolink configuration](/configuration/camera_specific#reolink-cameras)
|
||||||
|
|
||||||
As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability.
|
As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability.
|
||||||
@@ -383,13 +332,6 @@ When your browser runs into problems playing back your camera streams, it will l
|
|||||||
- `Safari reported InvalidStateError.`
|
- `Safari reported InvalidStateError.`
|
||||||
- `Safari reported decoding errors.`
|
- `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**
|
- **stalled**
|
||||||
- What it means: Playback has stalled because the player has fallen too far behind live (extended buffering or no data arriving).
|
- 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" /> .
|
- 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" /> .
|
||||||
|
|||||||
@@ -82,13 +82,13 @@ objects:
|
|||||||
person_filter1:
|
person_filter1:
|
||||||
friendly_name: "Roof area"
|
friendly_name: "Roof area"
|
||||||
enabled: true
|
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:
|
car:
|
||||||
mask:
|
mask:
|
||||||
car_filter1:
|
car_filter1:
|
||||||
friendly_name: "Sidewalk area"
|
friendly_name: "Sidewalk area"
|
||||||
enabled: true
|
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>
|
</TabItem>
|
||||||
|
|||||||
@@ -313,16 +313,8 @@ To remove root from the container entirely, add Docker's `user:`:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
user: "1000:1000" # NOT compatible with PUID/PGID, see the run modes table
|
user: "1000:1000" # NOT compatible with PUID/PGID, see the run modes table
|
||||||
tmpfs:
|
|
||||||
- /tmp:size=256m
|
|
||||||
- /tmp/cache:size=1000000000
|
|
||||||
- /run:exec,nosuid,nodev,mode=0755,uid=1000,gid=1000,size=16m # uid must match user:
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`/run` has to be owned by that uid as well. s6 writes its runtime state there before anything else starts, and with no root in the container a root-owned `/run` stops it during init with `cannot create /run/test of writability`. Keep `uid` and `gid` in the tmpfs options matching `user:`, and don't carry that pair back into the default mode, where a root-owned `/run` is what keeps the unprivileged services out of s6's runtime state.
|
|
||||||
|
|
||||||
This only bites once root is genuinely gone. s6's init helper is setuid, so `user:` on its own still lets init regain root and correct `/run` itself. The `no-new-privileges:true` above is what blocks that, which is also what makes the `/run` ownership mandatory. Dropping it would hide the problem by handing init root again.
|
|
||||||
|
|
||||||
Two things change, and the first one will break a working install if you skip it. The startup device grants can't run, because there is no root left to run them, so every device you pass stops working until you grant that uid access yourself with `group_add:` or a udev rule; see [Manual setup](#manual-setup). Expect this to surface as a driver error rather than a permission error, like `No VA display found` from VAAPI. And every service then runs as that one uid, so go2rtc no longer gets its own restricted user. `/config` and `/media/frigate` have to be owned by that uid already, since Frigate never adjusts ownership in this mode. Switching an existing install over also leaves `/config/go2rtc_homekit.yml` owned by the go2rtc user, which this mode can't write; `chown` it to your uid or HomeKit pairing changes stop persisting. Frigate warns and starts either way.
|
Two things change, and the first one will break a working install if you skip it. The startup device grants can't run, because there is no root left to run them, so every device you pass stops working until you grant that uid access yourself with `group_add:` or a udev rule; see [Manual setup](#manual-setup). Expect this to surface as a driver error rather than a permission error, like `No VA display found` from VAAPI. And every service then runs as that one uid, so go2rtc no longer gets its own restricted user. `/config` and `/media/frigate` have to be owned by that uid already, since Frigate never adjusts ownership in this mode. Switching an existing install over also leaves `/config/go2rtc_homekit.yml` owned by the go2rtc user, which this mode can't write; `chown` it to your uid or HomeKit pairing changes stop persisting. Frigate warns and starts either way.
|
||||||
|
|
||||||
This mode can also take `cap_drop: [ALL]`, which the default mode cannot: starting as root needs `CAP_CHOWN` for the ownership sweep, `CAP_SETUID` and `CAP_SETGID` to drop to the runtime user, and `CAP_FOWNER` for the device grants.
|
This mode can also take `cap_drop: [ALL]`, which the default mode cannot: starting as root needs `CAP_CHOWN` for the ownership sweep, `CAP_SETUID` and `CAP_SETGID` to drop to the runtime user, and `CAP_FOWNER` for the device grants.
|
||||||
|
|||||||
@@ -22,20 +22,17 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
**Most Hardware**
|
**Most Hardware**
|
||||||
|
|
||||||
- [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.
|
- [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.
|
- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is 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 /> [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**
|
**AMD**
|
||||||
|
|
||||||
- [ROCm](#amdrocm-gpu-detector): ROCm can run on AMD Discrete GPUs to provide efficient object detection.
|
- [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.
|
- [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](#apple-silicon-detector): Apple Silicon can run on M1 and newer Apple Silicon devices.
|
- [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**
|
**Intel**
|
||||||
|
|
||||||
@@ -104,36 +101,32 @@ Coral EdgeTPU and MemryX accelerators can only be opened by one process, so thos
|
|||||||
|
|
||||||
### Running more than one model
|
### 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
|
```yaml
|
||||||
models:
|
models:
|
||||||
- scene: default
|
- scene: outdoor
|
||||||
path: plus://your-model
|
path: plus://your-outdoor-model
|
||||||
devices:
|
devices:
|
||||||
- edgetpu:pci:0
|
- edgetpu:pci:0
|
||||||
- scene: thermal
|
- scene: indoor
|
||||||
path: /config/model_cache/thermal.onnx
|
path: /config/model_cache/indoor.onnx
|
||||||
model_type: yolo-generic
|
model_type: yolo-generic
|
||||||
devices:
|
devices:
|
||||||
- openvino:GPU
|
- openvino:GPU
|
||||||
|
|
||||||
cameras:
|
cameras:
|
||||||
driveway:
|
driveway:
|
||||||
...
|
|
||||||
backyard_thermal:
|
|
||||||
detect:
|
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.
|
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.
|
||||||
|
|
||||||
:::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.
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
### Choosing a model size
|
### Choosing a model size
|
||||||
|
|
||||||
@@ -292,9 +285,9 @@ models:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Hailo
|
## Hailo-8
|
||||||
|
|
||||||
This detector is available for use with the Hailo-8, Hailo-8L and Hailo-8R AI Acceleration Modules. The integration identifies which of them is attached and selects the matching default model if no custom model is specified.
|
This detector is available for use with both Hailo-8 and Hailo-8L AI Acceleration Modules. The integration automatically detects your hardware architecture via the Hailo CLI and selects the appropriate default model if no custom model is specified.
|
||||||
|
|
||||||
See the [installation docs](../frigate/installation.md#hailo-8) for information on configuring the Hailo hardware.
|
See the [installation docs](../frigate/installation.md#hailo-8) for information on configuring the Hailo hardware.
|
||||||
|
|
||||||
@@ -315,11 +308,11 @@ The HailoRT runtime is not part of the Frigate image. It is downloaded and insta
|
|||||||
When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**.
|
When configuring the Hailo detector, you have two options to specify the model: a local **path** or a **URL**.
|
||||||
If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`.
|
If both are provided, the detector will first check for the model at the given local path. If the file is not found, it will download the model from the specified URL. The model file is cached under `/config/model_cache/hailo`.
|
||||||
|
|
||||||
<ModelConfigDropdown detectorTitle="Hailo" models={objectDetectorsModels.hailo.models} />
|
<ModelConfigDropdown detectorTitle="Hailo-8/Hailo-8L" models={objectDetectorsModels.hailo8l.models} />
|
||||||
|
|
||||||
For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo
|
For additional ready-to-use models, please visit: https://github.com/hailo-ai/hailo_model_zoo
|
||||||
|
|
||||||
Hailo supports all models in the Hailo Model Zoo that include HailoRT post-processing. You're welcome to choose any of these pre-configured models for your implementation.
|
Hailo8 supports all models in the Hailo Model Zoo that include HailoRT post-processing. You're welcome to choose any of these pre-configured models for your implementation.
|
||||||
|
|
||||||
> **Note:**
|
> **Note:**
|
||||||
> The config.path parameter can accept either a local file path or a URL ending with .hef. When provided, the detector will first check if the path is a local file path. If the file exists locally, it will use it directly. If the file is not found locally or if a URL was provided, it will attempt to download the model from the specified URL.
|
> The config.path parameter can accept either a local file path or a URL ending with .hef. When provided, the detector will first check if the path is a local file path. If the file exists locally, it will use it directly. If the file is not found locally or if a URL was provided, it will attempt to download the model from the specified URL.
|
||||||
@@ -369,7 +362,7 @@ Intel NPUs cannot be used under Home Assistant OS, which does not include the NP
|
|||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
The Apple Silicon detector client is being reworked. Its extra options no longer have a place in the config, so only the endpoint carried in the device string is honored right now, and `request_timeout_ms` and `linger_ms` are ignored. Anything else is dropped when your config is migrated.
|
The network-based detectors (Deepstack and the Apple Silicon client) are being reworked. Their extra options no longer have a place in the config, so only the endpoint carried in the device string is honored right now: Deepstack ignores `api_key` and `api_timeout`, and the Apple Silicon client ignores `request_timeout_ms` and `linger_ms`. Anything else is dropped when your config is migrated.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -394,22 +387,6 @@ Note that the labelmap uses a subset of the complete COCO label set that has onl
|
|||||||
|
|
||||||
Support for AMD GPUs is provided using the [ONNX detector](#onnx). In order to utilize the AMD GPU for object detection use a frigate docker image with `-rocm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-rocm`.
|
Support for AMD GPUs is provided using the [ONNX detector](#onnx). In order to utilize the AMD GPU for object detection use a frigate docker image with `-rocm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-rocm`.
|
||||||
|
|
||||||
### Supported GPUs {#supported-gpus-rocm}
|
|
||||||
|
|
||||||
The `-rocm` image includes ROCm support for these GPU chipsets, which work without any chipset override:
|
|
||||||
|
|
||||||
| Chipset | Example GPUs |
|
|
||||||
| -------------------------------------------- | ------------------------------------------------------------------------ |
|
|
||||||
| gfx1030 (RDNA2) | Radeon RX 6800 / 6800 XT / 6900 XT / 6950 XT, Radeon PRO W6800 |
|
|
||||||
| gfx1100, gfx1101, gfx1102 (RDNA3) | Radeon RX 7900 / 7800 / 7700 / 7600 series, Radeon PRO W7000 series |
|
|
||||||
| gfx1103 (RDNA3 integrated) | Radeon 780M / 760M in Ryzen 7040, 8040, 8000G, and 200 series processors |
|
|
||||||
| gfx1150, gfx1151, gfx1152, gfx1153 (RDNA3.5) | Integrated GPUs in Ryzen AI 300 / 400 series and Ryzen AI Max processors |
|
|
||||||
| gfx1200, gfx1201 (RDNA4) | Radeon RX 9060 / 9070 series, Radeon AI PRO R9700 |
|
|
||||||
|
|
||||||
Other RDNA2 GPUs (gfx1031 through gfx1036), such as the Radeon RX 6700 / 6600 / 6500 series and the Radeon 680M / 660M integrated GPUs, run on the gfx1030 support by [overriding the chipset](#docker-settings-for-overriding-the-gpu-chipset) to `10.3.0`.
|
|
||||||
|
|
||||||
Older GPUs, including RDNA1 (Radeon RX 5000 series) and Vega based dedicated and integrated GPUs, are not supported.
|
|
||||||
|
|
||||||
### Docker settings for GPU access
|
### Docker settings for GPU access
|
||||||
|
|
||||||
ROCm needs access to the `/dev/kfd` and `/dev/dri` devices. When docker or frigate is not run under root then also `video` (and possibly `render` and `ssl/_ssl`) groups should be added.
|
ROCm needs access to the `/dev/kfd` and `/dev/dri` devices. When docker or frigate is not run under root then also `video` (and possibly `render` and `ssl/_ssl`) groups should be added.
|
||||||
@@ -436,10 +413,19 @@ For reference on recommended settings see [running ROCm/pytorch in Docker](https
|
|||||||
|
|
||||||
### Docker settings for overriding the GPU chipset
|
### Docker settings for overriding the GPU chipset
|
||||||
|
|
||||||
A GPU whose chipset isn't in the [supported GPUs](#supported-gpus-rocm) list can run on the support for a closely related chipset by setting the `HSA_OVERRIDE_GFX_VERSION` environment variable. RDNA2 GPUs other than gfx1030 (gfx1031 through gfx1036) need this, using `10.3.0`:
|
Your GPU might work just fine without any special configuration but in many cases they need manual settings. AMD/ROCm software stack comes with a limited set of GPU drivers and for newer or missing models you will have to override the chipset version to an older/generic version to get things working.
|
||||||
|
|
||||||
|
Also AMD/ROCm does not "officially" support integrated GPUs. It still does work with most of them just fine but requires special settings. One has to configure the `HSA_OVERRIDE_GFX_VERSION` environment variable. See the [ROCm bug report](https://github.com/ROCm/ROCm/issues/1743) for context and examples.
|
||||||
|
|
||||||
|
For the rocm frigate build there is some automatic detection:
|
||||||
|
|
||||||
|
- gfx1031 -> 10.3.0
|
||||||
|
- gfx1103 -> 11.0.0
|
||||||
|
|
||||||
|
If you have something else you might need to override the `HSA_OVERRIDE_GFX_VERSION` at Docker launch. Suppose the version you want is `10.0.0`, then you should configure it from command line as:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker run -e HSA_OVERRIDE_GFX_VERSION=10.3.0 \
|
$ docker run -e HSA_OVERRIDE_GFX_VERSION=10.0.0 \
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -450,17 +436,16 @@ services:
|
|||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
environment:
|
environment:
|
||||||
HSA_OVERRIDE_GFX_VERSION: "10.3.0"
|
HSA_OVERRIDE_GFX_VERSION: "10.0.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
GPUs in the supported list don't need an override. An override that was set for the Radeon 780M / 760M (gfx1103) on an earlier version of Frigate is no longer needed and can be removed.
|
Figuring out what version you need can be complicated as you can't tell the chipset name and driver from the AMD brand name.
|
||||||
|
|
||||||
You can't tell the chipset from the AMD brand name, so check it in the container:
|
- first make sure that rocm environment is running properly by running `/opt/rocm/bin/rocminfo` in the frigate container -- it should list both the CPU and the GPU with their properties
|
||||||
|
- find the chipset version you have (gfxNNN) from the output of the `rocminfo` (see below)
|
||||||
- first make sure that the ROCm environment is running properly by running `/opt/rocm/bin/rocminfo` in the Frigate container. It should list both the CPU and the GPU with their properties
|
- use a search engine to query what `HSA_OVERRIDE_GFX_VERSION` you need for the given gfx name ("gfxNNN ROCm HSA_OVERRIDE_GFX_VERSION")
|
||||||
- find the chipset version you have (gfxNNN) from the output of `rocminfo` (see below)
|
- override the `HSA_OVERRIDE_GFX_VERSION` with relevant value
|
||||||
- if the chipset isn't in the supported list, set `HSA_OVERRIDE_GFX_VERSION` to the closest supported chipset, written as major.minor.stepping (for example gfx1030 is `10.3.0`)
|
- if things are not working check the frigate docker logs
|
||||||
- if things are not working check the Frigate docker logs
|
|
||||||
|
|
||||||
#### Figuring out if AMD/ROCm is working and found your GPU
|
#### Figuring out if AMD/ROCm is working and found your GPU
|
||||||
|
|
||||||
@@ -498,7 +483,7 @@ See [ONNX supported models](#onnx) for supported models, there are some caveats:
|
|||||||
|
|
||||||
## ONNX
|
## 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
|
:::info
|
||||||
|
|
||||||
@@ -514,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.
|
- 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.
|
- 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
|
:::tip
|
||||||
@@ -532,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}
|
### Configuration {#configuration-onnx}
|
||||||
|
|
||||||
<ModelConfigDropdown detectorTitle="ONNX" models={objectDetectorsModels.onnx.models} />
|
<ModelConfigDropdown detectorTitle="ONNX" models={objectDetectorsModels.onnx.models} />
|
||||||
@@ -574,30 +540,32 @@ A TensorFlow Lite model is provided in the container at `/cpu_model.tflite` and
|
|||||||
|
|
||||||
When using CPU detectors, you can add one CPU detector per camera. Adding more detectors than the number of cameras should not improve performance.
|
When using CPU detectors, you can add one CPU detector per camera. Adding more detectors than the number of cameras should not improve performance.
|
||||||
|
|
||||||
|
## Deepstack / CodeProject.AI Server Detector
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
The network-based detectors (Deepstack and the Apple Silicon client) are being reworked. Their extra options no longer have a place in the config, so only the endpoint carried in the device string is honored right now: Deepstack ignores `api_key` and `api_timeout`, and the Apple Silicon client ignores `request_timeout_ms` and `linger_ms`. Anything else is dropped when your config is migrated.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
|
The Deepstack / CodeProject.AI Server detector for Frigate allows you to integrate Deepstack and CodeProject.AI object detection capabilities into Frigate. CodeProject.AI and DeepStack are open-source AI platforms that can be run on various devices such as the Raspberry Pi, Nvidia Jetson, and other compatible hardware. It is important to note that the integration is performed over the network, so the inference times may not be as fast as native Frigate detectors, but it still provides an efficient and reliable solution for object detection and tracking.
|
||||||
|
|
||||||
|
### Setup {#setup-deepstack}
|
||||||
|
|
||||||
|
To get started with CodeProject.AI, visit their [official website](https://www.codeproject.com/Articles/5322557/CodeProject-AI-Server-AI-the-easy-way) to follow the instructions to download and install the AI server on your preferred device. Detailed setup instructions for CodeProject.AI are outside the scope of the Frigate documentation.
|
||||||
|
|
||||||
|
To integrate CodeProject.AI into Frigate, configure the detector as follows:
|
||||||
|
|
||||||
|
### Configuration {#configuration-deepstack}
|
||||||
|
|
||||||
|
<ModelConfigDropdown detectorTitle="DeepStack" models={objectDetectorsModels.deepstack.models} />
|
||||||
|
|
||||||
|
Replace `<your_codeproject_ai_server_ip>` and `<port>` with the IP address and port of your CodeProject.AI server.
|
||||||
|
|
||||||
|
To verify that the integration is working correctly, start Frigate and observe the logs for any error messages related to CodeProject.AI. Additionally, you can check the Frigate web interface to see if the objects detected by CodeProject.AI are being displayed and tracked properly.
|
||||||
|
|
||||||
# Community Supported Detectors
|
# 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
|
## 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.
|
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.
|
||||||
@@ -668,63 +636,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 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.
|
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.
|
||||||
|
|||||||
@@ -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.
|
Navigate to <NavPath path="Settings > Global configuration > Objects" /> to set score filters globally.
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| -------------------------------------------------- | ---------------------------------------------------------------- |
|
| --------------------------------------- | ---------------------------------------------------------------- |
|
||||||
| **Object filters > Person > Minimum confidence** | Minimum score for a single detection to initiate tracking |
|
| **Object filters > Person > Min Score** | Minimum score for a single detection to initiate tracking |
|
||||||
| **Object filters > Person > Confidence threshold** | Minimum computed (median) score to be considered a true positive |
|
| **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.
|
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.
|
Navigate to <NavPath path="Settings > Global configuration > Objects" /> to set shape filters globally.
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| -------------------------------------------------- | ------------------------------------------------------------------------ |
|
| --------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
| **Object filters > Person > Minimum object area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
|
| **Object filters > Person > Min 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 > Max 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 > Min 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 > 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.
|
To override shape filters for a specific camera, navigate to <NavPath path="Settings > Camera configuration > Objects" /> and select the camera.
|
||||||
|
|
||||||
|
|||||||
@@ -70,14 +70,14 @@ Object filters help reduce false positives by constraining the size, shape, and
|
|||||||
|
|
||||||
Navigate to <NavPath path="Settings > Global configuration > Objects" />.
|
Navigate to <NavPath path="Settings > Global configuration > Objects" />.
|
||||||
|
|
||||||
| Field | Description |
|
| Field | Description |
|
||||||
| -------------------------------------------------- | ------------------------------------------------------------------------ |
|
| --------------------------------------- | ------------------------------------------------------------------------ |
|
||||||
| **Object filters > Person > Minimum object area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
|
| **Object filters > Person > Min 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 > Max 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 > Min 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 > Max 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 > Min Score** | Minimum score for the object to initiate tracking |
|
||||||
| **Object filters > Person > Confidence threshold** | Minimum computed score to be considered a true positive |
|
| **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" />.
|
To override filters for a specific camera, navigate to <NavPath path="Settings > Camera configuration > Objects" />.
|
||||||
|
|
||||||
|
|||||||
@@ -191,12 +191,14 @@ cameras:
|
|||||||
detect:
|
detect:
|
||||||
enabled: false
|
enabled: false
|
||||||
record:
|
record:
|
||||||
enabled: true
|
enabled: false
|
||||||
profiles:
|
profiles:
|
||||||
away:
|
away:
|
||||||
enabled: true
|
enabled: true
|
||||||
detect:
|
detect:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
record:
|
||||||
|
enabled: true
|
||||||
home:
|
home:
|
||||||
enabled: false
|
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.
|
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?
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ For advanced use cases, the [custom export HTTP API](../integrations/api/export-
|
|||||||
POST /export/custom/{camera_name}/start/{start_time}/end/{end_time}
|
POST /export/custom/{camera_name}/start/{start_time}/end/{end_time}
|
||||||
```
|
```
|
||||||
|
|
||||||
The request body accepts `ffmpeg_input_args` and `ffmpeg_output_args` to control encoding, frame rate, filters, and other FFmpeg options. If neither is provided, Frigate defaults to time-lapse output settings (25x speed, 30 FPS) with audio removed (`-an`). When providing your own `ffmpeg_input_args`, include `-an` if you want audio stripped from the export.
|
The request body accepts `ffmpeg_input_args` and `ffmpeg_output_args` to control encoding, frame rate, filters, and other FFmpeg options. If neither is provided, Frigate defaults to time-lapse output settings (25x speed, 30 FPS).
|
||||||
|
|
||||||
The following example exports a time-lapse at 60x speed with 25 FPS:
|
The following example exports a time-lapse at 60x speed with 25 FPS:
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ For cameras that support two-way talk, go2rtc will automatically establish an au
|
|||||||
To prevent this, you must configure two separate stream instances:
|
To prevent this, you must configure two separate stream instances:
|
||||||
|
|
||||||
1. One stream instance with `#backchannel=0` for Frigate's viewing, recording, and detection (prevents go2rtc from establishing the blocking backchannel)
|
1. One stream instance with `#backchannel=0` for Frigate's viewing, recording, and detection (prevents go2rtc from establishing the blocking backchannel)
|
||||||
2. A second stream instance with no `#` parameters at all for two-way talk functionality (can be used by Frigate's WebRTC viewer or other applications)
|
2. A second stream instance without `#backchannel=0` for two-way talk functionality (can be used by Frigate's WebRTC viewer or other applications)
|
||||||
|
|
||||||
Configuration example:
|
Configuration example:
|
||||||
|
|
||||||
@@ -215,8 +215,6 @@ In this configuration:
|
|||||||
- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access.
|
- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access.
|
||||||
- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel.
|
- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel.
|
||||||
|
|
||||||
Any `#` parameter on a bare `rtsp://` source disables the backchannel unless the URL explicitly contains `#backchannel=1`. A two-way talk stream with something like `#video=h264` on it silently loses two-way audio, and Frigate will report that two-way talk is unavailable for that stream.
|
|
||||||
|
|
||||||
## Security: Restricted Stream Sources
|
## Security: Restricted Stream Sources
|
||||||
|
|
||||||
For security reasons, the `echo:`, `expr:`, and `exec:` stream sources are disabled by default in go2rtc. These sources allow arbitrary command execution and can pose security risks if misconfigured.
|
For security reasons, the `echo:`, `expr:`, and `exec:` stream sources are disabled by default in go2rtc. These sources allow arbitrary command execution and can pose security risks if misconfigured.
|
||||||
|
|||||||
@@ -133,12 +133,13 @@ Switching between V1 and V2 requires reindexing your embeddings. The embeddings
|
|||||||
|
|
||||||
### GenAI Provider
|
### 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`).
|
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>
|
<ConfigTabs>
|
||||||
<TabItem value="ui">
|
<TabItem value="ui">
|
||||||
@@ -173,6 +174,8 @@ semantic_search:
|
|||||||
</TabItem>
|
</TabItem>
|
||||||
</ConfigTabs>
|
</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
|
:::note
|
||||||
|
|
||||||
Switching between Jina models and a GenAI provider requires reindexing. Embeddings from different backends are incompatible.
|
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.
|
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.
|
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:
|
3. In the **Create Trigger** wizard:
|
||||||
- Enter a **Name** for the trigger (e.g., "Red Car Alert"). Frigate derives the trigger's
|
- Enter a **Name** for the trigger (e.g., "Red Car Alert").
|
||||||
internal **ID** from this name, which can be revealed and edited with the show/hide toggle.
|
- Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera").
|
||||||
- Select the **Type** (`Thumbnail` or `Description`).
|
- 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 `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.
|
- For `Description`, enter text to trigger this action when a similar tracked object description is detected.
|
||||||
|
|||||||
@@ -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.
|
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.
|
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.
|
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.
|
5. Press **Save** when finished.
|
||||||
|
|
||||||
</TabItem>
|
</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.
|
1. Navigate to <NavPath path="Settings > Camera configuration > Masks / Zones" /> and select the desired camera.
|
||||||
2. Edit or create the zone (e.g., `sidewalk`).
|
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`)
|
- Under **Objects**, add the relevant object types (e.g., `person`)
|
||||||
|
|
||||||
</TabItem>
|
</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.
|
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.
|
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.
|
- 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.
|
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.
|
1. Navigate to <NavPath path="Settings > Camera configuration > Masks / Zones" /> and select the desired camera.
|
||||||
2. Edit or create the zone with distances configured.
|
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
|
- The unit is kph or mph, depending on the **Unit system** setting
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|||||||
@@ -204,20 +204,11 @@ Light guidelines and advice:
|
|||||||
npm run lint
|
npm run lint
|
||||||
```
|
```
|
||||||
|
|
||||||
- Ensure the backend [unit tests](#unit-tests) pass. Your PR cannot be merged unless tests pass.
|
- Add to unit tests and ensure they pass. As much as possible, you should strive to _increase_ test coverage whenever making changes. This will help ensure features do not accidentally become broken in the future.
|
||||||
|
- If you run into error messages like "TypeError: Cannot read properties of undefined (reading 'context')" when running tests, this may be due to these issues (https://github.com/vitest-dev/vitest/issues/1910, https://github.com/vitest-dev/vitest/issues/1652) in vitest, but I haven't been able to resolve them.
|
||||||
```shell
|
|
||||||
python3 -u -m unittest
|
|
||||||
```
|
|
||||||
|
|
||||||
- Ensure the end-to-end tests pass. They run in Playwright against a production build with mocked API data, so they don't need a running Frigate instance. Add or update tests in `web/e2e/specs/` when you change UI behavior.
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
# First-time setup
|
npm run test
|
||||||
npx playwright install chromium
|
|
||||||
|
|
||||||
# Build the app and run all tests
|
|
||||||
npm run e2e:build && npm run e2e
|
|
||||||
```
|
```
|
||||||
|
|
||||||
- Test in different browsers. Firefox, Chrome, and Safari all have different quirks that make them unique targets to interact with.
|
- Test in different browsers. Firefox, Chrome, and Safari all have different quirks that make them unique targets to interact with.
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ An object filter mask drops any [bounding box](#bounding-box) whose bottom cente
|
|||||||
|
|
||||||
## Min Score
|
## 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
|
## Model
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ A more specific identity assigned to a [tracked object](#tracked-object-event-in
|
|||||||
|
|
||||||
## Threshold
|
## 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
|
## Top Score
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
**Most Hardware**
|
**Most Hardware**
|
||||||
|
|
||||||
- [Hailo](#hailo-8): 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.
|
- [Hailo](#hailo-8): The Hailo8 and Hailo8L AI Acceleration module is available in m.2 format with a HAT for RPi devices offering a wide range of compatibility with devices.
|
||||||
- [Supports many model architectures](../../configuration/object_detectors#configuration-hailo)
|
- [Supports many model architectures](../../configuration/object_detectors#configuration-hailo)
|
||||||
- Runs best with tiny or small size models
|
- Runs best with tiny or small size models
|
||||||
|
|
||||||
@@ -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)
|
- [Supports many model architectures](../../configuration/object_detectors#memryx-mx3)
|
||||||
- Runs best with tiny, small, or medium-size models
|
- 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**
|
**AMD**
|
||||||
|
|
||||||
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD discrete and recent integrated GPUs to provide efficient object detection
|
- [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)
|
- [Supports limited model architectures](../../configuration/object_detectors#amdrocm-gpu-detector)
|
||||||
- Runs best on discrete AMD GPUs
|
- 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**
|
**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
|
- [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)
|
- [Supports primarily ssdlite and mobilenet model architectures](../../configuration/object_detectors#apple-silicon-detector)
|
||||||
- Runs well with any size models including large
|
- Runs well with any size models including large
|
||||||
@@ -123,13 +111,12 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
### Hailo-8
|
### Hailo-8
|
||||||
|
|
||||||
Frigate supports the Hailo-8, Hailo-8L and Hailo-8R AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate identifies which of them is attached and selects the matching default model when a custom model isn’t provided.
|
Frigate supports both the Hailo-8 and Hailo-8L AI Acceleration Modules on compatible hardware platforms, including the Raspberry Pi 5 with the PCIe hat from the AI kit. The Hailo detector integration in Frigate automatically identifies your hardware type and selects the appropriate default model when a custom model isn’t provided.
|
||||||
|
|
||||||
**Default Model Configuration:**
|
**Default Model Configuration:**
|
||||||
|
|
||||||
- **Hailo-8L:** Default model is **YOLOv6n**, compiled for the Hailo-8L.
|
- **Hailo-8L:** Default model is **YOLOv6n**.
|
||||||
- **Hailo-8:** Default model is **YOLOv6n**, compiled for the Hailo-8.
|
- **Hailo-8:** Default model is **YOLOv6n**.
|
||||||
- **Hailo-8R:** Default model is the **Hailo-8** build of **YOLOv6n**, since the Hailo Model Zoo publishes no Hailo-8R build.
|
|
||||||
|
|
||||||
In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms, with dual PCIe lanes, yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup.
|
In real-world deployments, even with multiple cameras running concurrently, Frigate has demonstrated consistent performance. Testing on x86 platforms, with dual PCIe lanes, yields further improvements in FPS, throughput, and latency compared to the Raspberry Pi setup.
|
||||||
|
|
||||||
@@ -189,7 +176,7 @@ Frigate is able to utilize an Nvidia GPU which supports the 12.x series of CUDA
|
|||||||
|
|
||||||
#### Minimum Hardware Support
|
#### 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.
|
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 +205,7 @@ Inference is done with the `onnx` detector type. Speeds will vary greatly depend
|
|||||||
|
|
||||||
### Apple Silicon
|
### 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.
|
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.
|
||||||
|
|
||||||
| 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.
|
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
@@ -240,7 +221,7 @@ Apple Silicon can not run within a container, so a ZMQ proxy is utilized to comm
|
|||||||
|
|
||||||
### ROCm - AMD GPU
|
### ROCm - AMD GPU
|
||||||
|
|
||||||
With the [ROCm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many AMD discrete and integrated GPUs, see the [supported GPUs](../configuration/object_detectors.md#supported-gpus-rocm).
|
With the [ROCm](../configuration/object_detectors.md#amdrocm-gpu-detector) detector Frigate can take advantage of many discrete AMD GPUs.
|
||||||
|
|
||||||
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
|
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
|
||||||
| -------------- | --------------------------- | ------------------------- | ---------------------- |
|
| -------------- | --------------------------- | ------------------------- | ---------------------- |
|
||||||
@@ -275,32 +256,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.
|
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
|
### 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).
|
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 +296,6 @@ The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms fo
|
|||||||
| ---------------- | ----------------------------------- |
|
| ---------------- | ----------------------------------- |
|
||||||
| yolov9-tiny | ~ 4 ms |
|
| 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)
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -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 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/>
|
<ShmCalculator/>
|
||||||
|
|
||||||
@@ -122,7 +122,7 @@ Additionally, the USB Coral draws a considerable amount of power. If using any o
|
|||||||
|
|
||||||
### Hailo-8
|
### Hailo-8
|
||||||
|
|
||||||
The Hailo-8, Hailo-8L and Hailo-8R AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments.
|
The Hailo-8 and Hailo-8L AI accelerators are available in both M.2 and HAT form factors for the Raspberry Pi. The M.2 version typically connects to a carrier board for PCIe, which then interfaces with the Raspberry Pi 5 as part of the AI Kit. The HAT version can be mounted directly onto compatible Raspberry Pi models. Both form factors have been successfully tested on x86 platforms as well, making them versatile options for various computing environments.
|
||||||
|
|
||||||
The HailoRT runtime is not part of the Frigate image; Frigate downloads and installs it at first start once a Hailo detector is configured. Containers without internet access can provide the files themselves, see [Detector runtimes](/frigate/network_requirements#detector-runtimes).
|
The HailoRT runtime is not part of the Frigate image; Frigate downloads and installs it at first start once a Hailo detector is configured. Containers without internet access can provide the files themselves, see [Detector runtimes](/frigate/network_requirements#detector-runtimes).
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ If you are using `docker run`, add this option to your command `--device /dev/ha
|
|||||||
|
|
||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
Finally, configure [hardware object detection](/configuration/object_detectors#hailo) to complete the setup.
|
Finally, configure [hardware object detection](/configuration/object_detectors#hailo-8) to complete the setup.
|
||||||
|
|
||||||
### MemryX MX3
|
### MemryX MX3
|
||||||
|
|
||||||
@@ -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.
|
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
|
### 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:
|
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:
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ If you are using one of the following hardware detectors and have not provided y
|
|||||||
| Detector | Model Downloaded | Source |
|
| Detector | Model Downloaded | Source |
|
||||||
| ------------------------------------------------------------------ | -------------------- | ------------------------ |
|
| ------------------------------------------------------------------ | -------------------- | ------------------------ |
|
||||||
| [Rockchip RKNN](/configuration/object_detectors#rockchip-platform) | RKNN detection model | GitHub |
|
| [Rockchip RKNN](/configuration/object_detectors#rockchip-platform) | RKNN detection model | GitHub |
|
||||||
| [Hailo 8 / 8L / 8R](/configuration/object_detectors#hailo) | YOLOv6n (.hef) | Hailo Model Zoo (AWS S3) |
|
| [Hailo 8 / 8L](/configuration/object_detectors#hailo-8) | YOLOv6n (.hef) | Hailo Model Zoo (AWS S3) |
|
||||||
| [AXERA AXEngine](/configuration/object_detectors) | Detection model | HuggingFace |
|
| [AXERA AXEngine](/configuration/object_detectors) | Detection model | HuggingFace |
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
@@ -60,16 +60,16 @@ The default CPU, EdgeTPU, and OpenVINO object detection models are bundled into
|
|||||||
|
|
||||||
The SDKs for a few hardware detectors are not shipped in the Frigate image. They are downloaded the first time that detector is configured, verified against checksums pinned in the Frigate release, and installed into the Frigate user's home directory (`/config/.local` by default). Once installed they are not downloaded again until a Frigate release pins a new version.
|
The SDKs for a few hardware detectors are not shipped in the Frigate image. They are downloaded the first time that detector is configured, verified against checksums pinned in the Frigate release, and installed into the Frigate user's home directory (`/config/.local` by default). Once installed they are not downloaded again until a Frigate release pins a new version.
|
||||||
|
|
||||||
| Detector | Version | Files | Source |
|
| Detector | Version | Files | Source |
|
||||||
| ---------------------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
| -------------------------------------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
||||||
| [Hailo 8 / 8L / 8R](/configuration/object_detectors#hailo) | 4.21.0 | `hailort-debian12-amd64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_x86_64.whl` on x86, `hailort-debian12-arm64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_aarch64.whl` on arm64 | [GitHub release](https://github.com/frigate-nvr/hailort/releases/tag/v4.21.0) |
|
| [Hailo 8 / 8L](/configuration/object_detectors#hailo-8) | 4.21.0 | `hailort-debian12-amd64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_x86_64.whl` on x86, `hailort-debian12-arm64.tar.gz` and `hailort-4.21.0-cp311-cp311-linux_aarch64.whl` on arm64 | [GitHub release](https://github.com/frigate-nvr/hailort/releases/tag/v4.21.0) |
|
||||||
| [MemryX MX3](/configuration/object_detectors#memryx-mx3) | 2.1.0 | `mx_accl_frigate-2.1.0.zip` (the release source archive, renamed) | [GitHub archive](https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip) |
|
| [MemryX MX3](/configuration/object_detectors#memryx-mx3) | 2.1.0 | `mx_accl_frigate-2.1.0.zip` (the release source archive, renamed) | [GitHub archive](https://github.com/memryx/mx_accl_frigate/archive/refs/tags/v2.1.0.zip) |
|
||||||
| [AXERA AXEngine](/configuration/object_detectors#axera) | 0.1.3 | `axengine-0.1.3-py3-none-any.whl` | [GitHub release](https://github.com/AXERA-TECH/pyaxengine/releases/tag/0.1.3-frigate) |
|
| [AXERA AXEngine](/configuration/object_detectors#axera) | 0.1.3 | `axengine-0.1.3-py3-none-any.whl` | [GitHub release](https://github.com/AXERA-TECH/pyaxengine/releases/tag/0.1.3-frigate) |
|
||||||
|
|
||||||
If the container cannot reach GitHub, provide the files yourself:
|
If the container cannot reach GitHub, provide the files yourself:
|
||||||
|
|
||||||
1. Download the files for your architecture on a machine with internet access.
|
1. Download the files for your architecture on a machine with internet access.
|
||||||
2. Place them, with exactly the file names listed above, in `/config/model_cache/runtimes/<detector>/`, where `<detector>` is the detector named in your config's `devices` (`hailo`, `memryx`, or `axengine`).
|
2. Place them, with exactly the file names listed above, in `/config/model_cache/runtimes/<detector>/`, where `<detector>` is the detector `type` from your config (`hailo8l`, `memryx`, or `axengine`).
|
||||||
3. Start Frigate. Files whose checksum matches are installed without any download; a file with the wrong checksum is discarded and downloaded again, so a failed startup log names the file to replace.
|
3. Start Frigate. Files whose checksum matches are installed without any download; a file with the wrong checksum is discarded and downloaded again, so a failed startup log names the file to replace.
|
||||||
|
|
||||||
The `GITHUB_ENDPOINT` mirror variable below applies to these downloads as well.
|
The `GITHUB_ENDPOINT` mirror variable below applies to these downloads as well.
|
||||||
@@ -142,12 +142,16 @@ When [notifications](/configuration/notifications) are enabled and users have re
|
|||||||
|
|
||||||
If an [MQTT broker](/integrations/mqtt) is configured, Frigate maintains a connection to the broker's host and port. This is typically a local network connection, but will require internet if you use a cloud-hosted MQTT broker.
|
If an [MQTT broker](/integrations/mqtt) is configured, Frigate maintains a connection to the broker's host and port. This is typically a local network connection, but will require internet if you use a cloud-hosted MQTT broker.
|
||||||
|
|
||||||
|
### DeepStack / CodeProject.AI
|
||||||
|
|
||||||
|
When using the [DeepStack detector plugin](/configuration/object_detectors), Frigate sends images to the configured API endpoint for inference. This is typically local but depends on where the service is hosted.
|
||||||
|
|
||||||
## WebRTC (STUN)
|
## WebRTC (STUN)
|
||||||
|
|
||||||
For [WebRTC live streaming](/configuration/live), Frigate uses STUN for NAT traversal:
|
For [WebRTC live streaming](/configuration/live), Frigate uses STUN for NAT traversal:
|
||||||
|
|
||||||
- **go2rtc** defaults to a local STUN listener (`stun:8555`), no internet required.
|
- **go2rtc** defaults to a local STUN listener (`stun:8555`), no internet required.
|
||||||
- **The web UI** uses the servers in `go2rtc.webrtc.ice_servers` for its WebRTC player and for the WebRTC connectivity check it runs when the Live view loads. If none are set, it uses Google's public STUN server (`stun:stun.l.google.com:19302`), which requires internet access from the browser. Set `ice_servers` to a STUN or TURN server on your network to avoid this.
|
- **The web UI's WebRTC player** includes a fallback to Google's public STUN server (`stun:stun.l.google.com:19302`), which requires internet.
|
||||||
|
|
||||||
## Home Assistant Supervisor
|
## Home Assistant Supervisor
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ title: Updating
|
|||||||
|
|
||||||
# Updating Frigate
|
# 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.
|
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**:
|
2. **Update and Pull the Latest Image**:
|
||||||
- If using Docker Compose:
|
- 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
|
```yaml
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
image: ghcr.io/blakeblackshear/frigate:0.18.2
|
image: ghcr.io/blakeblackshear/frigate:0.18.0
|
||||||
```
|
```
|
||||||
- Then pull the image:
|
- Then pull the image:
|
||||||
```bash
|
```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.
|
- **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`:
|
- 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
|
```bash
|
||||||
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
|
docker pull ghcr.io/blakeblackshear/frigate:0.18.0
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Start the Container**:
|
3. **Start the Container**:
|
||||||
@@ -100,7 +100,7 @@ If an update causes issues:
|
|||||||
1. Stop Frigate.
|
1. Stop Frigate.
|
||||||
2. Restore your backed-up config file and database.
|
2. Restore your backed-up config file and database.
|
||||||
3. Revert to the previous image version:
|
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 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.
|
- For Home Assistant: Restore from the app/addon backup you took before you updated.
|
||||||
4. Verify the old version is running again.
|
4. Verify the old version is running again.
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ There are many solutions available to implement reverse proxies and the communit
|
|||||||
- [Nginx](#nginx-reverse-proxy)
|
- [Nginx](#nginx-reverse-proxy)
|
||||||
- [Traefik](#traefik-reverse-proxy)
|
- [Traefik](#traefik-reverse-proxy)
|
||||||
- [Caddy](#caddy-reverse-proxy)
|
- [Caddy](#caddy-reverse-proxy)
|
||||||
- [HAProxy](#haproxy)
|
|
||||||
- [OPNSense HAProxy Add-on](#opnsense-haproxy-add-on)
|
|
||||||
|
|
||||||
## Apache2 Reverse Proxy
|
## 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.
|
|
||||||
|
|||||||
@@ -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
|
## General Frigate Topics
|
||||||
|
|
||||||
### `frigate/available`
|
### `frigate/available`
|
||||||
@@ -218,7 +212,6 @@ An `update` with the same ID will be published when:
|
|||||||
- The severity changes from `detection` to `alert`
|
- The severity changes from `detection` to `alert`
|
||||||
- Additional objects are detected
|
- Additional objects are detected
|
||||||
- An object is recognized via face, lpr, etc.
|
- 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.
|
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"],
|
"objects": ["person", "car"],
|
||||||
"sub_labels": [],
|
"sub_labels": [],
|
||||||
"zones": [],
|
"zones": [],
|
||||||
"audio": [],
|
"audio": []
|
||||||
"classification_state_changes": []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"after": {
|
"after": {
|
||||||
@@ -262,16 +254,7 @@ When the review activity has ended a final `end` message is published.
|
|||||||
"objects": ["person", "car"],
|
"objects": ["person", "car"],
|
||||||
"sub_labels": ["Bob"],
|
"sub_labels": ["Bob"],
|
||||||
"zones": ["front_yard"],
|
"zones": ["front_yard"],
|
||||||
"audio": [],
|
"audio": []
|
||||||
"classification_state_changes": [
|
|
||||||
// verified changes of state classification models on this camera
|
|
||||||
{
|
|
||||||
"model": "front_gate",
|
|
||||||
"from": "closed",
|
|
||||||
"to": "open",
|
|
||||||
"timestamp": 1718987131.52
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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.
|
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)
|
||||||
|
|
||||||
[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.
|
[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.
|
||||||
|
|||||||
@@ -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?
|
### 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.
|
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.
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
### Why can't I submit images to Frigate+?
|
### Why can't I submit images to Frigate+?
|
||||||
|
|
||||||
|
|||||||
@@ -63,20 +63,20 @@ Frigate+ models generally have much higher scores than the default model provide
|
|||||||
<ConfigTabs>
|
<ConfigTabs>
|
||||||
<TabItem value="ui">
|
<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 |
|
| Object | Min Score | Threshold |
|
||||||
| ----------------- | ------------------ | -------------------- |
|
| ----------------- | --------- | --------- |
|
||||||
| **dog** | .7 | .9 |
|
| **dog** | .7 | .9 |
|
||||||
| **cat** | .65 | .8 |
|
| **cat** | .65 | .8 |
|
||||||
| **face** | .7 | |
|
| **face** | .7 | |
|
||||||
| **package** | .65 | .9 |
|
| **package** | .65 | .9 |
|
||||||
| **license_plate** | .6 | |
|
| **license_plate** | .6 | |
|
||||||
| **amazon** | .75 | |
|
| **amazon** | .75 | |
|
||||||
| **ups** | .75 | |
|
| **ups** | .75 | |
|
||||||
| **fedex** | .75 | |
|
| **fedex** | .75 | |
|
||||||
| **person** | .65 | .85 |
|
| **person** | .65 | .85 |
|
||||||
| **car** | .65 | .85 |
|
| **car** | .65 | .85 |
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem value="yaml">
|
<TabItem value="yaml">
|
||||||
|
|||||||
+8
-11
@@ -41,7 +41,7 @@ Rockchip models are automatically converted as of 0.17. For 0.16, YOLOv9 onnx mo
|
|||||||
|
|
||||||
## Supported detector types
|
## Supported detector types
|
||||||
|
|
||||||
Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo`), and Rockchip (`rknn`) detectors.
|
Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVino (`openvino`), ONNX (`onnx`), Hailo (`hailo8l`), and Rockchip (`rknn`) detectors.
|
||||||
|
|
||||||
| Hardware | Recommended Detector Type | Recommended Model Type |
|
| Hardware | Recommended Detector Type | Recommended Model Type |
|
||||||
| -------------------------------------------------------------------------------- | ------------------------- | ---------------------- |
|
| -------------------------------------------------------------------------------- | ------------------------- | ---------------------- |
|
||||||
@@ -50,7 +50,7 @@ Currently, Frigate+ models support CPU (`cpu`), Google Coral (`edgetpu`), OpenVi
|
|||||||
| [Intel](/configuration/object_detectors.md#openvino-detector) | `openvino` | `yolov9` |
|
| [Intel](/configuration/object_detectors.md#openvino-detector) | `openvino` | `yolov9` |
|
||||||
| [NVidia GPU](/configuration/object_detectors#onnx) | `onnx` | `yolov9` |
|
| [NVidia GPU](/configuration/object_detectors#onnx) | `onnx` | `yolov9` |
|
||||||
| [AMD ROCm GPU](/configuration/object_detectors#amdrocm-gpu-detector) | `onnx` | `yolov9` |
|
| [AMD ROCm GPU](/configuration/object_detectors#amdrocm-gpu-detector) | `onnx` | `yolov9` |
|
||||||
| [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo) | `hailo` | `yolov9` |
|
| [Hailo8/Hailo8L/Hailo8R](/configuration/object_detectors#hailo-8) | `hailo8l` | `yolov9` |
|
||||||
| [Rockchip NPU](/configuration/object_detectors#rockchip-platform) | `rknn` | `yolov9` |
|
| [Rockchip NPU](/configuration/object_detectors#rockchip-platform) | `rknn` | `yolov9` |
|
||||||
|
|
||||||
## Improving your model
|
## Improving your model
|
||||||
@@ -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:
|
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`
|
- **People**: `person`, `face`
|
||||||
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `garbage truck`, `license_plate`
|
- **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`
|
- **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`
|
- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo`
|
||||||
- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella`, `baby_stroller`
|
- **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.
|
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.
|
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`
|
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`
|
||||||
- **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`
|
|
||||||
|
|
||||||
Candidate labels are not available for automatic suggestions.
|
Candidate labels are not available for automatic suggestions.
|
||||||
|
|
||||||
|
|||||||
@@ -39,28 +39,6 @@ To do this efficiently the following setup is required:
|
|||||||
|
|
||||||
When this is done correctly, the GPU will do the decoding and scaling which will result in a small increase in CPU usage but with better results.
|
When this is done correctly, the GPU will do the decoding and scaling which will result in a small increase in CPU usage but with better results.
|
||||||
|
|
||||||
### How can I rotate my camera's video feed?
|
|
||||||
|
|
||||||
Rotation is best done in the camera's firmware settings (usually called rotate, flip, or corridor mode) so the video arrives already rotated and no extra processing is needed. Check there first.
|
|
||||||
|
|
||||||
If your camera does not support rotation, go2rtc's ffmpeg module can rotate the stream with the `#rotate` parameter (`90`, `180`, `270`, or `-90`), but this is not recommended: rotation requires transcoding (re-encoding) the video, which significantly increases CPU usage, especially for high resolution streams.
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
go2rtc:
|
|
||||||
streams:
|
|
||||||
my_camera: "ffmpeg:rtsp://user:password@192.168.1.10:554/stream#video=h264#hardware#rotate=90"
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
### 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.
|
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 +170,6 @@ Filters and masks only hide the incorrect result - they don't teach Frigate what
|
|||||||
|
|
||||||
### Where do I see problems Frigate has detected?
|
### 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.
|
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.
|
||||||
|
|||||||
@@ -78,9 +78,7 @@ go2rtc:
|
|||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
The transcoding modifiers (`#video=`, `#audio=`, `#hardware`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing: go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you.
|
The `#`-modifiers (`#video=`, `#audio=`, `#hardware`, `#backchannel=0`, …) **only take effect on a source that is prefixed with `ffmpeg:`**. Adding them to a bare `rtsp://…#audio=opus` source does nothing: go2rtc ignores them. Likewise, when a source references another stream by name (e.g. `ffmpeg:back#audio=aac`), the name must match the stream key **exactly** (it is case sensitive), or the transcode is silently never produced. This is the single most common configuration mistake. In the Frigate UI, the **Use compatibility mode (ffmpeg)** toggle adds the `ffmpeg:` prefix for you.
|
||||||
|
|
||||||
A bare `rtsp://` source reads a different set of modifiers: `#backchannel=`, `#media=`, `#timeout=`, and `#transport=`. These do nothing on an `ffmpeg:` source. Adding **any** modifier to a bare `rtsp://` source also disables the camera's backchannel unless the URL explicitly contains `#backchannel=1`, so a stream dedicated to two-way talk should carry no modifiers at all.
|
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -155,7 +153,7 @@ WebRTC is only attempted when MSE fails or when using a camera's two-way talk fe
|
|||||||
|
|
||||||
- **Codec mismatch**: WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above.
|
- **Codec mismatch**: WebRTC cannot carry H.265 or AAC. The stream backing the WebRTC view must provide Opus (or PCMA/PCMU) audio and H.264 video. Add an `ffmpeg:back#audio=opus` source as shown above.
|
||||||
- **Port `8555` not reachable, or no candidates set**: WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration).
|
- **Port `8555` not reachable, or no candidates set**: WebRTC needs port `8555` (both TCP and UDP) open and a reachable candidate advertised. On Docker installs running on a custom/overlay network, go2rtc may advertise unreachable container IPs as ICE candidates; setting `webrtc.filters.candidates: []` and supplying only your host's LAN IP resolves this. See [WebRTC extra configuration](/configuration/live#webrtc-extra-configuration).
|
||||||
- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly: go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, carrying no `#` modifiers of any kind, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream).
|
- **Two-way talk** additionally requires a secure context (HTTPS or the authenticated port `8971`, because browsers block microphone access on plain HTTP). The camera's RTSP backchannel must also be handled correctly: go2rtc seizes the backchannel by default, which blocks two-way audio for other consumers and can inject static. Disable it on the primary stream with `#backchannel=0` and use a separate dedicated stream for talk, as documented in [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream).
|
||||||
|
|
||||||
## High CPU usage
|
## High CPU usage
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
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.
|
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:
|
If none of the above apply, the issue may be a general resource constraint. Monitor the following on your host:
|
||||||
|
|
||||||
|
|||||||
+22
-30
@@ -3,9 +3,6 @@ import * as path from "node:path";
|
|||||||
import type { Config, PluginConfig } from "@docusaurus/types";
|
import type { Config, PluginConfig } from "@docusaurus/types";
|
||||||
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
|
import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs";
|
||||||
|
|
||||||
// Bump when a new stable release ships
|
|
||||||
const STABLE_VERSION = "0.18";
|
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
title: "Frigate",
|
title: "Frigate",
|
||||||
tagline: "NVR With Realtime Object Detection for IP Cameras",
|
tagline: "NVR With Realtime Object Detection for IP Cameras",
|
||||||
@@ -26,17 +23,17 @@ const config: Config = {
|
|||||||
mermaid: true,
|
mermaid: true,
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
defaultLocale: "en",
|
defaultLocale: 'en',
|
||||||
locales: ["en"],
|
locales: ['en'],
|
||||||
localeConfigs: {
|
localeConfigs: {
|
||||||
en: {
|
en: {
|
||||||
label: "English",
|
label: 'English',
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
announcementBar: {
|
announcementBar: {
|
||||||
id: "frigate_plus",
|
id: 'frigate_plus',
|
||||||
content: `
|
content: `
|
||||||
<span style="margin-right: 8px; display: inline-block; animation: pulse 2s infinite;">🚀</span>
|
<span style="margin-right: 8px; display: inline-block; animation: pulse 2s infinite;">🚀</span>
|
||||||
Get more relevant and accurate detections with Frigate+ models.
|
Get more relevant and accurate detections with Frigate+ models.
|
||||||
@@ -48,8 +45,8 @@ const config: Config = {
|
|||||||
50% { transform: scale(1.1); }
|
50% { transform: scale(1.1); }
|
||||||
}
|
}
|
||||||
</style>`,
|
</style>`,
|
||||||
backgroundColor: "#005f73",
|
backgroundColor: '#005f73',
|
||||||
textColor: "#e0fbfc",
|
textColor: '#e0fbfc',
|
||||||
isCloseable: false,
|
isCloseable: false,
|
||||||
},
|
},
|
||||||
docs: {
|
docs: {
|
||||||
@@ -86,15 +83,15 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
prism: {
|
prism: {
|
||||||
magicComments: [
|
magicComments:[
|
||||||
{
|
{
|
||||||
className: "theme-code-block-highlighted-line",
|
className: 'theme-code-block-highlighted-line',
|
||||||
line: "highlight-next-line",
|
line: 'highlight-next-line',
|
||||||
block: { start: "highlight-start", end: "highlight-end" },
|
block: {start: 'highlight-start', end: 'highlight-end'},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
className: "code-block-error-line",
|
className: 'code-block-error-line',
|
||||||
line: "highlight-error-line",
|
line: 'highlight-error-line',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
additionalLanguages: ["bash", "json"],
|
additionalLanguages: ["bash", "json"],
|
||||||
@@ -134,11 +131,6 @@ const config: Config = {
|
|||||||
srcDark: "img/branding/logo-dark.svg",
|
srcDark: "img/branding/logo-dark.svg",
|
||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
{
|
|
||||||
href: "https://github.com/blakeblackshear/frigate/releases",
|
|
||||||
label: `${STABLE_VERSION}`,
|
|
||||||
position: "left",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
to: "/",
|
to: "/",
|
||||||
activeBasePath: "docs",
|
activeBasePath: "docs",
|
||||||
@@ -156,19 +148,19 @@ const config: Config = {
|
|||||||
position: "right",
|
position: "right",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "localeDropdown",
|
type: 'localeDropdown',
|
||||||
position: "right",
|
position: 'right',
|
||||||
dropdownItemsAfter: [
|
dropdownItemsAfter: [
|
||||||
{
|
{
|
||||||
label: "简体中文(社区翻译)",
|
label: '简体中文(社区翻译)',
|
||||||
href: "https://docs.frigate-cn.video",
|
href: 'https://docs.frigate-cn.video',
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: "https://github.com/blakeblackshear/frigate",
|
href: 'https://github.com/blakeblackshear/frigate',
|
||||||
label: "GitHub",
|
label: 'GitHub',
|
||||||
position: "right",
|
position: 'right',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+3918
-2095
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -18,17 +18,17 @@
|
|||||||
"write-heading-ids": "docusaurus write-heading-ids"
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "^3.10.2",
|
"@docusaurus/core": "^3.7.0",
|
||||||
"@docusaurus/plugin-content-docs": "^3.10.2",
|
"@docusaurus/plugin-content-docs": "^3.7.0",
|
||||||
"@docusaurus/preset-classic": "^3.10.2",
|
"@docusaurus/preset-classic": "^3.7.0",
|
||||||
"@docusaurus/theme-mermaid": "^3.10.2",
|
"@docusaurus/theme-mermaid": "^3.7.0",
|
||||||
"@inkeep/docusaurus": "^2.0.16",
|
"@inkeep/docusaurus": "^2.0.16",
|
||||||
"@mdx-js/react": "^3.1.0",
|
"@mdx-js/react": "^3.1.0",
|
||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"docusaurus-plugin-openapi-docs": "^5.2.0",
|
"docusaurus-plugin-openapi-docs": "^4.5.1",
|
||||||
"docusaurus-theme-openapi-docs": "^5.2.0",
|
"docusaurus-theme-openapi-docs": "^4.5.1",
|
||||||
"js-yaml": "^4.3.2",
|
"js-yaml": "^4.1.1",
|
||||||
"marked": "^16.4.2",
|
"marked": "^16.4.2",
|
||||||
"prism-react-renderer": "^2.4.1",
|
"prism-react-renderer": "^2.4.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
@@ -48,11 +48,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "^3.10.2",
|
"@docusaurus/module-type-aliases": "^3.7.0",
|
||||||
"@docusaurus/types": "^3.10.2",
|
"@docusaurus/types": "^3.7.0",
|
||||||
"@types/react": "^18.3.27"
|
"@types/react": "^18.3.27"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19"
|
"node": ">=18.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,53 +2,42 @@ import React, { useState, useEffect } from "react";
|
|||||||
import Admonition from "@theme/Admonition";
|
import Admonition from "@theme/Admonition";
|
||||||
import styles from "./styles.module.css";
|
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 ShmCalculator = () => {
|
||||||
const [width, setWidth] = useState(1280);
|
const [width, setWidth] = useState(1280);
|
||||||
const [height, setHeight] = useState(720);
|
const [height, setHeight] = useState(720);
|
||||||
const [cameraCount, setCameraCount] = useState(1);
|
const [cameraCount, setCameraCount] = useState(1);
|
||||||
const [result, setResult] = useState("-");
|
const [result, setResult] = useState("26.32MB");
|
||||||
const [singleCameraShm, setSingleCameraShm] = useState("-");
|
const [singleCameraShm, setSingleCameraShm] = useState("26.32MB");
|
||||||
const [addedCamerasShm, setAddedCamerasShm] = useState("-");
|
const [totalShm, setTotalShm] = useState("26.32MB");
|
||||||
const [fullRingShm, setFullRingShm] = useState("-");
|
|
||||||
|
|
||||||
const calculate = () => {
|
const calculate = () => {
|
||||||
if (!width || !height || !cameraCount) {
|
if (!width || !height || !cameraCount) {
|
||||||
setResult("Please enter valid values");
|
setResult("Please enter valid values");
|
||||||
setSingleCameraShm("-");
|
setSingleCameraShm("-");
|
||||||
setAddedCamerasShm("-");
|
setTotalShm("-");
|
||||||
setFullRingShm("-");
|
|
||||||
return;
|
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));
|
// Total SHM calculation (multiple cameras, including logs)
|
||||||
setAddedCamerasShm(formatMb(addedCamerasMb * TARGET_FRAMES));
|
const totalBase = singleCameraBase * cameraCount;
|
||||||
setResult(formatMb(Math.round(RESERVED_MB + cameraFramesMb * TARGET_FRAMES)));
|
const finalResult = totalBase + 40; // Default includes logs +40mb
|
||||||
setFullRingShm(
|
|
||||||
formatMb(Math.round(RESERVED_MB + cameraFramesMb * DEFAULT_FRAMES)),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatMb = (value) => {
|
setTotalShm(`${(totalBase + 40).toFixed(2)}mb`);
|
||||||
if (value >= 1024) {
|
|
||||||
return `${(value / 1024).toFixed(2)}gb`;
|
// 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) => {
|
const formatWithUnit = (value) => {
|
||||||
@@ -158,27 +147,19 @@ const ShmCalculator = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.formulaDisplay}>
|
<div className={styles.formulaDisplay}>
|
||||||
<p>
|
<p>
|
||||||
<strong>Per Camera:</strong> {formatWithUnit(singleCameraShm)}
|
<strong>Single Camera:</strong> {formatWithUnit(singleCameraShm)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Formula:</strong> (width × height × 1.5 + 270480) ÷
|
<strong>Formula:</strong> (width × height × 1.5 × 20 + 270480) ÷
|
||||||
1048576 × {TARGET_FRAMES} frames
|
1048576
|
||||||
</p>
|
</p>
|
||||||
|
{cameraCount > 1 && (
|
||||||
|
<p>
|
||||||
|
<strong>Total ({cameraCount} cameras):</strong> {formatWithUnit(totalShm)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<p>
|
<p>
|
||||||
<strong>Room for 2 Added Cameras:</strong>{" "}
|
<strong>With Logs:</strong> + 40<span className={styles.unit}>mb</span>
|
||||||
{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)}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Vendored
+28
-178
@@ -412,39 +412,6 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
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:
|
/ffprobe:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -1088,7 +1055,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1132,7 +1098,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1287,7 +1252,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1647,7 +1611,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1695,7 +1658,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1743,7 +1705,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1785,7 +1746,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1869,7 +1829,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -2131,7 +2090,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -3266,7 +3225,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -3491,7 +3450,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -4216,20 +4174,19 @@ paths:
|
|||||||
Get notices, most severe first.
|
Get notices, most severe first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
include_hidden: Also return acknowledged and muted notices, for the
|
include_dismissed: Also return dismissed notices, for the history view
|
||||||
hidden list
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The notices
|
The notices
|
||||||
operationId: get_notices_notices_get
|
operationId: get_notices_notices_get
|
||||||
parameters:
|
parameters:
|
||||||
- name: include_hidden
|
- name: include_dismissed
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
title: Include Hidden
|
title: Include Dismissed
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful Response
|
description: Successful Response
|
||||||
@@ -4264,16 +4221,16 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
x-required-role: admin
|
||||||
/notices/muted_checks:
|
/notices/dismissed_checks:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Notices
|
- Notices
|
||||||
summary: Get Muted Checks
|
summary: Get Dismissed Checks
|
||||||
description: |-
|
description: |-
|
||||||
**Access:** Admin role required.
|
**Access:** Admin role required.
|
||||||
|
|
||||||
Get the muted config and stream check rows, newest first.
|
Get the dismissed config and stream check rows, newest first.
|
||||||
operationId: get_muted_checks_notices_muted_checks_get
|
operationId: get_dismissed_checks_notices_dismissed_checks_get
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful Response
|
description: Successful Response
|
||||||
@@ -4283,16 +4240,16 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
x-required-role: admin
|
||||||
/notices/hidden:
|
/notices/dismissed:
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- Notices
|
- Notices
|
||||||
summary: Unhide All Notices
|
summary: Purge Dismissed
|
||||||
description: |-
|
description: |-
|
||||||
**Access:** Admin role required.
|
**Access:** Admin role required.
|
||||||
|
|
||||||
Show every acknowledged and muted notice and check row again.
|
Delete every dismissed notice and check row so each can show again.
|
||||||
operationId: unhide_all_notices_notices_hidden_delete
|
operationId: purge_dismissed_notices_dismissed_delete
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful Response
|
description: Successful Response
|
||||||
@@ -4302,83 +4259,18 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
x-required-role: admin
|
||||||
/notices/{notice_id}/acknowledge:
|
/notices/{notice_id}/dismiss:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Notices
|
- Notices
|
||||||
summary: Acknowledge Notice
|
summary: Dismiss Notice
|
||||||
description: |-
|
description: |-
|
||||||
**Access:** Admin role required.
|
**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
|
It stays hidden if the same problem happens again.
|
||||||
can only be muted.
|
operationId: dismiss_notice_notices__notice_id__dismiss_post
|
||||||
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
|
|
||||||
parameters:
|
parameters:
|
||||||
- name: notice_id
|
- name: notice_id
|
||||||
in: path
|
in: path
|
||||||
@@ -4502,16 +4394,6 @@ paths:
|
|||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: 100
|
default: 100
|
||||||
title: Limit
|
title: Limit
|
||||||
- name: offset
|
|
||||||
in: query
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
anyOf:
|
|
||||||
- type: integer
|
|
||||||
minimum: 0
|
|
||||||
- type: 'null'
|
|
||||||
default: 0
|
|
||||||
title: Offset
|
|
||||||
- name: after
|
- name: after
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
@@ -4666,7 +4548,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -4817,16 +4699,6 @@ paths:
|
|||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: 50
|
default: 50
|
||||||
title: Limit
|
title: Limit
|
||||||
- name: offset
|
|
||||||
in: query
|
|
||||||
required: false
|
|
||||||
schema:
|
|
||||||
anyOf:
|
|
||||||
- type: integer
|
|
||||||
minimum: 0
|
|
||||||
- type: 'null'
|
|
||||||
default: 0
|
|
||||||
title: Offset
|
|
||||||
- name: cameras
|
- name: cameras
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
@@ -4928,7 +4800,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
- name: min_score
|
- name: min_score
|
||||||
in: query
|
in: query
|
||||||
@@ -5008,7 +4880,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
- name: has_clip
|
- name: has_clip
|
||||||
in: query
|
in: query
|
||||||
@@ -5661,7 +5533,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Response Create Trigger Embedding Trigger Embedding Post
|
title: Response Create Trigger Embedding Trigger Embedding Post
|
||||||
'422':
|
'422':
|
||||||
description: Validation Error
|
description: Validation Error
|
||||||
@@ -5710,7 +5581,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Response Update Trigger Embedding Trigger Embedding
|
title: Response Update Trigger Embedding Trigger Embedding
|
||||||
Camera Name Name Put
|
Camera Name Name Put
|
||||||
'422':
|
'422':
|
||||||
@@ -5753,7 +5623,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Response Delete Trigger Embedding Trigger Embedding
|
title: Response Delete Trigger Embedding Trigger Embedding
|
||||||
Camera Name Name Delete
|
Camera Name Name Delete
|
||||||
'422':
|
'422':
|
||||||
@@ -5790,7 +5659,6 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Response Get Triggers Status Triggers Status Camera Name
|
title: Response Get Triggers Status Triggers Status Camera Name
|
||||||
Get
|
Get
|
||||||
'422':
|
'422':
|
||||||
@@ -7367,7 +7235,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
- name: cameras
|
- name: cameras
|
||||||
in: query
|
in: query
|
||||||
@@ -7417,7 +7285,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: UTC
|
default: utc
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -7587,7 +7455,6 @@ paths:
|
|||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: true
|
|
||||||
title: Response No Recordings Recordings Unavailable Get
|
title: Response No Recordings Recordings Unavailable Get
|
||||||
'422':
|
'422':
|
||||||
description: Validation Error
|
description: Validation Error
|
||||||
@@ -7791,19 +7658,13 @@ components:
|
|||||||
title: Update Topic
|
title: Update Topic
|
||||||
config_data:
|
config_data:
|
||||||
anyOf:
|
anyOf:
|
||||||
- additionalProperties: true
|
- type: object
|
||||||
type: object
|
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Config Data
|
title: Config Data
|
||||||
skip_save:
|
skip_save:
|
||||||
type: boolean
|
type: boolean
|
||||||
title: Skip Save
|
title: Skip Save
|
||||||
default: false
|
default: false
|
||||||
replace_paths:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
title: Replace Paths
|
|
||||||
type: object
|
type: object
|
||||||
title: AppConfigSetBody
|
title: AppConfigSetBody
|
||||||
AppPostLoginBody:
|
AppPostLoginBody:
|
||||||
@@ -8032,7 +7893,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
contentMediaType: application/octet-stream
|
format: binary
|
||||||
title: File
|
title: File
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -8042,7 +7903,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
contentMediaType: application/octet-stream
|
format: binary
|
||||||
title: File
|
title: File
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -8140,7 +8001,6 @@ components:
|
|||||||
tool_calls:
|
tool_calls:
|
||||||
anyOf:
|
anyOf:
|
||||||
- items:
|
- items:
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
@@ -8436,7 +8296,6 @@ components:
|
|||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Model Type
|
title: Model Type
|
||||||
data:
|
data:
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
type: object
|
||||||
title: Data
|
title: Data
|
||||||
type: object
|
type: object
|
||||||
@@ -8508,8 +8367,7 @@ components:
|
|||||||
default: true
|
default: true
|
||||||
draw:
|
draw:
|
||||||
anyOf:
|
anyOf:
|
||||||
- additionalProperties: true
|
- type: object
|
||||||
type: object
|
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Draw
|
title: Draw
|
||||||
default: {}
|
default: {}
|
||||||
@@ -8752,8 +8610,7 @@ components:
|
|||||||
description: Error message for failed jobs
|
description: Error message for failed jobs
|
||||||
results:
|
results:
|
||||||
anyOf:
|
anyOf:
|
||||||
- additionalProperties: true
|
- type: object
|
||||||
type: object
|
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Results
|
title: Results
|
||||||
description: Result metadata for completed jobs
|
description: Result metadata for completed jobs
|
||||||
@@ -9003,7 +8860,6 @@ components:
|
|||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Base Url
|
title: Base Url
|
||||||
provider_options:
|
provider_options:
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
type: object
|
||||||
title: Provider Options
|
title: Provider Options
|
||||||
type: object
|
type: object
|
||||||
@@ -9544,7 +9400,6 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
title: Tool Name
|
title: Tool Name
|
||||||
arguments:
|
arguments:
|
||||||
additionalProperties: true
|
|
||||||
type: object
|
type: object
|
||||||
title: Arguments
|
title: Arguments
|
||||||
type: object
|
type: object
|
||||||
@@ -9622,11 +9477,6 @@ components:
|
|||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
title: Error Type
|
title: Error Type
|
||||||
input:
|
|
||||||
title: Input
|
|
||||||
ctx:
|
|
||||||
type: object
|
|
||||||
title: Context
|
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- loc
|
- loc
|
||||||
|
|||||||
+1
-7
@@ -15,9 +15,7 @@ from frigate.util.config import find_config_file
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
# Fork this manager so the forkserver does not start until the first
|
manager = mp.Manager()
|
||||||
# worker process, after the config has applied environment_vars.
|
|
||||||
manager = mp.get_context("fork").Manager()
|
|
||||||
faulthandler.enable()
|
faulthandler.enable()
|
||||||
|
|
||||||
# Setup the logging thread
|
# Setup the logging thread
|
||||||
@@ -101,10 +99,6 @@ def main() -> None:
|
|||||||
print("*** End Config Validation Errors ***")
|
print("*** End Config Validation Errors ***")
|
||||||
print("*************************************************************")
|
print("*************************************************************")
|
||||||
|
|
||||||
# force a non-zero exit code for config failures
|
|
||||||
if args.validate_config:
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# attempt to start Frigate in recovery mode
|
# attempt to start Frigate in recovery mode
|
||||||
try:
|
try:
|
||||||
config = FrigateConfig.load(install=True, safe_load=True)
|
config = FrigateConfig.load(install=True, safe_load=True)
|
||||||
|
|||||||
+47
-311
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -10,6 +11,7 @@ import urllib
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from pathlib import Path as FilePath
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
@@ -54,17 +56,14 @@ from frigate.jobs.media_sync import (
|
|||||||
start_media_sync_job,
|
start_media_sync_job,
|
||||||
)
|
)
|
||||||
from frigate.models import Event, Timeline
|
from frigate.models import Event, Timeline
|
||||||
from frigate.plus import load_plus_model_info
|
|
||||||
from frigate.stats.prometheus import get_metrics, update_metrics
|
from frigate.stats.prometheus import get_metrics, update_metrics
|
||||||
from frigate.types import JobStatusTypesEnum
|
from frigate.types import JobStatusTypesEnum
|
||||||
from frigate.util.builtin import (
|
from frigate.util.builtin import (
|
||||||
clean_camera_user_pass,
|
clean_camera_user_pass,
|
||||||
deep_merge,
|
deep_merge,
|
||||||
flatten_config_data,
|
flatten_config_data,
|
||||||
is_masked_camera_path,
|
|
||||||
load_labels,
|
load_labels,
|
||||||
process_config_query_string,
|
process_config_query_string,
|
||||||
split_config_key_path,
|
|
||||||
update_yaml_file_bulk,
|
update_yaml_file_bulk,
|
||||||
)
|
)
|
||||||
from frigate.util.config import (
|
from frigate.util.config import (
|
||||||
@@ -72,16 +71,9 @@ from frigate.util.config import (
|
|||||||
find_config_file,
|
find_config_file,
|
||||||
redact_credential,
|
redact_credential,
|
||||||
)
|
)
|
||||||
from frigate.util.env import substitute_frigate_vars
|
|
||||||
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.object_names import get_categorized_object_names
|
||||||
from frigate.util.schema import get_config_schema
|
from frigate.util.schema import get_config_schema
|
||||||
from frigate.util.services import (
|
from frigate.util.services import (
|
||||||
Go2rtcStreamIssue,
|
|
||||||
find_restricted_go2rtc_stream_changes,
|
|
||||||
get_nvidia_driver_info,
|
get_nvidia_driver_info,
|
||||||
process_logs,
|
process_logs,
|
||||||
restart_frigate,
|
restart_frigate,
|
||||||
@@ -315,12 +307,9 @@ def config(request: Request):
|
|||||||
mode="json", warnings="none", exclude_none=True
|
mode="json", warnings="none", exclude_none=True
|
||||||
)
|
)
|
||||||
|
|
||||||
is_admin = request.headers.get("remote-role") == "admin"
|
# remove environment_vars for non-admin users
|
||||||
|
if request.headers.get("remote-role") != "admin":
|
||||||
# hide environment_vars and the notification email from non-admin users
|
|
||||||
if not is_admin:
|
|
||||||
config.pop("environment_vars", None)
|
config.pop("environment_vars", None)
|
||||||
redact_credential(config["notifications"], "email")
|
|
||||||
|
|
||||||
# redact mqtt credentials
|
# redact mqtt credentials
|
||||||
redact_credential(config["mqtt"], "password")
|
redact_credential(config["mqtt"], "password")
|
||||||
@@ -377,15 +366,7 @@ def config(request: Request):
|
|||||||
camera_name
|
camera_name
|
||||||
)
|
)
|
||||||
if base_sections:
|
if base_sections:
|
||||||
# copy so redaction below can't alter the profile manager's cache
|
camera_dict["base_config"] = base_sections
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
# remove go2rtc stream passwords
|
# remove go2rtc stream passwords
|
||||||
go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump(
|
go2rtc: dict[str, Any] = config_obj.go2rtc.model_dump(
|
||||||
@@ -404,8 +385,6 @@ def config(request: Request):
|
|||||||
|
|
||||||
config["go2rtc"]["streams"][stream_name] = cleaned
|
config["go2rtc"]["streams"][stream_name] = cleaned
|
||||||
|
|
||||||
_redact_go2rtc_credentials(config["go2rtc"])
|
|
||||||
|
|
||||||
config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()}
|
config["plus"] = {"enabled": request.app.frigate_config.plus_api.is_active()}
|
||||||
|
|
||||||
for index, model in enumerate(config_obj.models):
|
for index, model in enumerate(config_obj.models):
|
||||||
@@ -415,11 +394,6 @@ def config(request: Request):
|
|||||||
model_dict["non_logo_attributes"] = model.non_logo_attributes
|
model_dict["non_logo_attributes"] = model.non_logo_attributes
|
||||||
model_dict["labelmap"] = model.merged_labelmap
|
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"]:
|
if not config["plus"]["enabled"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -427,33 +401,17 @@ def config(request: Request):
|
|||||||
model_dict["plus"] = None
|
model_dict["plus"] = None
|
||||||
|
|
||||||
if model.path:
|
if model.path:
|
||||||
model_dict["plus"] = load_plus_model_info(os.path.basename(model.path))
|
model_json_path = FilePath(model.path).with_suffix(".json")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(model_json_path) as f:
|
||||||
|
model_dict["plus"] = json.load(f)
|
||||||
|
except (FileNotFoundError, json.JSONDecodeError):
|
||||||
|
pass
|
||||||
|
|
||||||
return JSONResponse(content=config)
|
return JSONResponse(content=config)
|
||||||
|
|
||||||
|
|
||||||
def _redact_go2rtc_credentials(go2rtc: dict[str, Any]) -> None:
|
|
||||||
"""Redact go2rtc secrets outside of stream URLs, in place."""
|
|
||||||
for section in ("rtsp", "api", "mqtt"):
|
|
||||||
if isinstance(go2rtc.get(section), dict):
|
|
||||||
redact_credential(go2rtc[section], "username")
|
|
||||||
redact_credential(go2rtc[section], "password")
|
|
||||||
|
|
||||||
if isinstance(go2rtc.get("ngrok"), dict):
|
|
||||||
redact_credential(go2rtc["ngrok"], "command")
|
|
||||||
|
|
||||||
if isinstance(go2rtc.get("homekit"), dict):
|
|
||||||
for accessory in go2rtc["homekit"].values():
|
|
||||||
if isinstance(accessory, dict):
|
|
||||||
for key in ("pin", "device_private", "pairings"):
|
|
||||||
redact_credential(accessory, key)
|
|
||||||
|
|
||||||
# publish destinations carry stream keys in the URL path
|
|
||||||
if isinstance(go2rtc.get("publish"), dict):
|
|
||||||
for name in list(go2rtc["publish"]):
|
|
||||||
redact_credential(go2rtc["publish"], name)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profiles", dependencies=[Depends(allow_any_authenticated())])
|
@router.get("/profiles", dependencies=[Depends(allow_any_authenticated())])
|
||||||
def get_profiles(request: Request):
|
def get_profiles(request: Request):
|
||||||
"""List all available profiles and the currently active profile."""
|
"""List all available profiles and the currently active profile."""
|
||||||
@@ -478,8 +436,6 @@ def ffmpeg_presets():
|
|||||||
hwaccel_presets = [
|
hwaccel_presets = [
|
||||||
"preset-rpi-64-h264",
|
"preset-rpi-64-h264",
|
||||||
"preset-rpi-64-h265",
|
"preset-rpi-64-h265",
|
||||||
"preset-apple-silicon-h264",
|
|
||||||
"preset-apple-silicon-h265",
|
|
||||||
"preset-jetson-h264",
|
"preset-jetson-h264",
|
||||||
"preset-jetson-h265",
|
"preset-jetson-h265",
|
||||||
"preset-rkmpp",
|
"preset-rkmpp",
|
||||||
@@ -574,63 +530,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"]))])
|
@router.post("/config/save", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
||||||
new_config = body.decode()
|
new_config = body.decode()
|
||||||
@@ -707,22 +606,10 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
|||||||
status_code=400,
|
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
|
# Save the config to file
|
||||||
try:
|
try:
|
||||||
|
config_file = find_config_file()
|
||||||
|
|
||||||
with open(config_file, "w") as f:
|
with open(config_file, "w") as f:
|
||||||
f.write(new_config)
|
f.write(new_config)
|
||||||
f.close()
|
f.close()
|
||||||
@@ -768,24 +655,17 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _restore_masked_camera_paths(config_data: dict, saved: dict) -> str | None:
|
def _restore_masked_camera_paths(config_data: dict, config: FrigateConfig) -> None:
|
||||||
"""Substitute incoming masked camera paths with the saved ones.
|
"""Substitute incoming `*:*` masked credentials with the in-memory ones.
|
||||||
|
|
||||||
The /config response masks ffmpeg input credentials, so the settings UI
|
The /config response masks ffmpeg input credentials, so the settings UI
|
||||||
sends the masked path back when sibling fields (e.g. hwaccel_args) are
|
sends the masked path back when sibling fields (e.g. hwaccel_args) are
|
||||||
edited. Each masked path is matched to the saved input that masks to the
|
edited. Without this we'd write `rtsp://*:*@host` into YAML and lose
|
||||||
same value, with roles breaking a tie, so deleting or reordering inputs
|
the real credentials. Mutates `config_data` in place.
|
||||||
keeps each path's own credentials. The saved path is restored as written,
|
|
||||||
keeping any {FRIGATE_*} placeholder. Mutates `config_data` in place and
|
|
||||||
returns an error message when a masked path has no single match.
|
|
||||||
"""
|
"""
|
||||||
cameras = config_data.get("cameras")
|
cameras = config_data.get("cameras")
|
||||||
if not isinstance(cameras, dict):
|
if not isinstance(cameras, dict):
|
||||||
return None
|
return
|
||||||
|
|
||||||
saved_cameras = saved.get("cameras")
|
|
||||||
if not isinstance(saved_cameras, dict):
|
|
||||||
saved_cameras = {}
|
|
||||||
|
|
||||||
for camera_name, camera_data in cameras.items():
|
for camera_name, camera_data in cameras.items():
|
||||||
if not isinstance(camera_data, dict):
|
if not isinstance(camera_data, dict):
|
||||||
@@ -793,122 +673,20 @@ def _restore_masked_camera_paths(config_data: dict, saved: dict) -> str | None:
|
|||||||
inputs = camera_data.get("ffmpeg", {}).get("inputs")
|
inputs = camera_data.get("ffmpeg", {}).get("inputs")
|
||||||
if not isinstance(inputs, list):
|
if not isinstance(inputs, list):
|
||||||
continue
|
continue
|
||||||
|
existing = config.cameras.get(camera_name)
|
||||||
saved_camera = saved_cameras.get(camera_name)
|
if existing is None:
|
||||||
saved_ffmpeg = (
|
continue
|
||||||
saved_camera.get("ffmpeg") if isinstance(saved_camera, dict) else None
|
existing_paths = [inp.path for inp in existing.ffmpeg.inputs]
|
||||||
)
|
|
||||||
saved_inputs = [
|
|
||||||
inp
|
|
||||||
for inp in (
|
|
||||||
saved_ffmpeg.get("inputs") if isinstance(saved_ffmpeg, dict) else None
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
if isinstance(inp, dict) and isinstance(inp.get("path"), str)
|
|
||||||
]
|
|
||||||
|
|
||||||
for index, input_obj in enumerate(inputs):
|
for index, input_obj in enumerate(inputs):
|
||||||
if not isinstance(input_obj, dict):
|
if not isinstance(input_obj, dict):
|
||||||
continue
|
continue
|
||||||
path = input_obj.get("path")
|
path = input_obj.get("path")
|
||||||
if not isinstance(path, str) or not is_masked_camera_path(path):
|
if not isinstance(path, str):
|
||||||
continue
|
continue
|
||||||
candidates = [
|
if ("://*:*@" in path or "user=*&password=*" in path) and index < len(
|
||||||
inp
|
existing_paths
|
||||||
for inp in saved_inputs
|
):
|
||||||
if clean_camera_user_pass(_resolve_frigate_vars(inp["path"])) == path
|
input_obj["path"] = existing_paths[index]
|
||||||
]
|
|
||||||
if len({inp["path"] for inp in candidates}) > 1:
|
|
||||||
candidates = [
|
|
||||||
inp
|
|
||||||
for inp in candidates
|
|
||||||
if inp.get("roles") == input_obj.get("roles")
|
|
||||||
]
|
|
||||||
real_paths = {inp["path"] for inp in candidates}
|
|
||||||
if len(real_paths) != 1:
|
|
||||||
return (
|
|
||||||
f"Input {index} of camera {camera_name} has masked credentials "
|
|
||||||
"that could not be matched; resend it with real credentials"
|
|
||||||
)
|
|
||||||
input_obj["path"] = real_paths.pop()
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_frigate_vars(value: str) -> str:
|
|
||||||
"""Resolve {FRIGATE_*} placeholders the way /config shows them."""
|
|
||||||
try:
|
|
||||||
return substitute_frigate_vars(value)
|
|
||||||
except ValueError:
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
def _saved_config_from_memory(config: FrigateConfig) -> dict[str, Any]:
|
|
||||||
"""Shape the in-memory camera inputs like the saved YAML."""
|
|
||||||
return {
|
|
||||||
"cameras": {
|
|
||||||
name: {
|
|
||||||
"ffmpeg": {
|
|
||||||
"inputs": [
|
|
||||||
{"path": inp.path, "roles": [role.value for role in inp.roles]}
|
|
||||||
for inp in camera.ffmpeg.inputs
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for name, camera in config.cameras.items()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _config_data_to_updates(
|
|
||||||
config_data: dict, saved: dict[str, Any]
|
|
||||||
) -> tuple[dict[str, Any], str | None]:
|
|
||||||
"""Flatten a /config/set payload after restoring masked credentials.
|
|
||||||
|
|
||||||
`saved` is the config the masked values were taken from. Returns the flat
|
|
||||||
updates, or an error message when a masked value can't be restored.
|
|
||||||
"""
|
|
||||||
error = _restore_masked_camera_paths(config_data, saved)
|
|
||||||
if error:
|
|
||||||
return {}, error
|
|
||||||
|
|
||||||
updates = flatten_config_data(config_data)
|
|
||||||
# Convert None values to empty strings for deletion (e.g., when deleting masks)
|
|
||||||
updates = {k: ("" if v is None else v) for k, v in updates.items()}
|
|
||||||
# Drop sentinel-valued fields so untouched credential placeholders don't
|
|
||||||
# clobber the saved YAML value.
|
|
||||||
updates = {k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL}
|
|
||||||
|
|
||||||
error = _find_masked_update(updates)
|
|
||||||
if error:
|
|
||||||
return {}, error
|
|
||||||
|
|
||||||
return updates, None
|
|
||||||
|
|
||||||
|
|
||||||
def _holds_masked_value(value: Any) -> bool:
|
|
||||||
if isinstance(value, str):
|
|
||||||
return REDACTED_CREDENTIAL_SENTINEL in value or is_masked_camera_path(value)
|
|
||||||
if isinstance(value, dict):
|
|
||||||
return any(_holds_masked_value(v) for v in value.values())
|
|
||||||
if isinstance(value, list):
|
|
||||||
return any(_holds_masked_value(v) for v in value)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def _find_masked_update(updates: dict[str, Any]) -> str | None:
|
|
||||||
"""Return an error naming the first update that still holds a masked value.
|
|
||||||
|
|
||||||
Runs after the restores so a masked path or redaction sentinel that could
|
|
||||||
not be restored is never written over the real credential.
|
|
||||||
"""
|
|
||||||
for key, value in updates.items():
|
|
||||||
if _holds_masked_value(value):
|
|
||||||
return (
|
|
||||||
f"{key} contains masked or saved credentials; "
|
|
||||||
"resend it with real values"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONResponse:
|
def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONResponse:
|
||||||
@@ -921,13 +699,13 @@ def _config_set_in_memory(request: Request, body: AppConfigSetBody) -> JSONRespo
|
|||||||
try:
|
try:
|
||||||
updates = {}
|
updates = {}
|
||||||
if body.config_data:
|
if body.config_data:
|
||||||
updates, error = _config_data_to_updates(
|
_restore_masked_camera_paths(body.config_data, request.app.frigate_config)
|
||||||
body.config_data, _saved_config_from_memory(request.app.frigate_config)
|
updates = flatten_config_data(body.config_data)
|
||||||
)
|
updates = {k: ("" if v is None else v) for k, v in updates.items()}
|
||||||
if error:
|
# Drop any field whose value is still the redaction sentinel
|
||||||
return JSONResponse(
|
updates = {
|
||||||
content={"success": False, "message": error}, status_code=400
|
k: v for k, v in updates.items() if v != REDACTED_CREDENTIAL_SENTINEL
|
||||||
)
|
}
|
||||||
|
|
||||||
if not updates:
|
if not updates:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -1037,17 +815,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"]))])
|
@router.put("/config/set", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def config_set(request: Request, body: AppConfigSetBody):
|
def config_set(request: Request, body: AppConfigSetBody):
|
||||||
config_file = find_config_file()
|
config_file = find_config_file()
|
||||||
@@ -1074,26 +841,22 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
# Filter out empty keys but keep blank values for non-empty keys
|
# Filter out empty keys but keep blank values for non-empty keys
|
||||||
query_string = {k: v for k, v in query_string.items() if k}
|
query_string = {k: v for k, v in query_string.items() if k}
|
||||||
|
|
||||||
error = None
|
|
||||||
|
|
||||||
if query_string:
|
if query_string:
|
||||||
updates = process_config_query_string(query_string)
|
updates = process_config_query_string(query_string)
|
||||||
error = _find_masked_update(updates)
|
|
||||||
elif body.config_data:
|
elif body.config_data:
|
||||||
try:
|
_restore_masked_camera_paths(
|
||||||
saved_config = ruamel.yaml.YAML(typ="safe").load(old_raw_config)
|
body.config_data, request.app.frigate_config
|
||||||
except ruamel.yaml.YAMLError:
|
|
||||||
saved_config = None
|
|
||||||
updates, error = _config_data_to_updates(
|
|
||||||
body.config_data,
|
|
||||||
saved_config if isinstance(saved_config, dict) else {},
|
|
||||||
)
|
|
||||||
|
|
||||||
if error:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": error},
|
|
||||||
status_code=400,
|
|
||||||
)
|
)
|
||||||
|
updates = flatten_config_data(body.config_data)
|
||||||
|
# Convert None values to empty strings for deletion (e.g., when deleting masks)
|
||||||
|
updates = {k: ("" if v is None else v) for k, v in updates.items()}
|
||||||
|
# Drop sentinel-valued fields so untouched credential
|
||||||
|
# placeholders don't clobber the saved YAML value.
|
||||||
|
updates = {
|
||||||
|
k: v
|
||||||
|
for k, v in updates.items()
|
||||||
|
if v != REDACTED_CREDENTIAL_SENTINEL
|
||||||
|
}
|
||||||
|
|
||||||
if not updates:
|
if not updates:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -1106,19 +869,6 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
status_code=400,
|
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
|
# apply all updates in a single operation
|
||||||
update_yaml_file_bulk(config_file, updates)
|
update_yaml_file_bulk(config_file, updates)
|
||||||
|
|
||||||
@@ -1170,13 +920,6 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
),
|
),
|
||||||
status_code=400,
|
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:
|
except Exception as e:
|
||||||
logging.error(f"Error updating config: {e}")
|
logging.error(f"Error updating config: {e}")
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -1189,15 +932,9 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
if request.app.dispatcher is not None:
|
if request.app.dispatcher is not None:
|
||||||
request.app.dispatcher.clear_runtime_state_for_yaml_keys(updates.keys())
|
request.app.dispatcher.clear_runtime_state_for_yaml_keys(updates.keys())
|
||||||
|
|
||||||
go2rtc_synced = True
|
|
||||||
|
|
||||||
if body.requires_restart == 0 or body.update_topic:
|
if body.requires_restart == 0 or body.update_topic:
|
||||||
old_config: FrigateConfig = request.app.frigate_config
|
old_config: FrigateConfig = request.app.frigate_config
|
||||||
swap_runtime_config(request.app, 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:
|
||||||
if body.update_topic.startswith("config/cameras/"):
|
if body.update_topic.startswith("config/cameras/"):
|
||||||
@@ -1257,7 +994,6 @@ def config_set(request: Request, body: AppConfigSetBody):
|
|||||||
if body.requires_restart == 0
|
if body.requires_restart == 0
|
||||||
else "Config successfully updated, restart to apply"
|
else "Config successfully updated, restart to apply"
|
||||||
),
|
),
|
||||||
"go2rtc_synced": go2rtc_synced,
|
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
status_code=200,
|
status_code=200,
|
||||||
|
|||||||
+76
-154
@@ -12,9 +12,8 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
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 import APIRouter, Depends, HTTPException, Request, Response
|
||||||
from fastapi.responses import JSONResponse, RedirectResponse
|
from fastapi.responses import JSONResponse, RedirectResponse
|
||||||
from joserfc import jwt
|
from joserfc import jwt
|
||||||
@@ -32,8 +31,6 @@ from frigate.api.media_auth import (
|
|||||||
check_camera_access,
|
check_camera_access,
|
||||||
deny_response_for_media_uri,
|
deny_response_for_media_uri,
|
||||||
is_role_restricted,
|
is_role_restricted,
|
||||||
normalize_request_path,
|
|
||||||
split_request_uri,
|
|
||||||
)
|
)
|
||||||
from frigate.config import AuthConfig, ProxyConfig
|
from frigate.config import AuthConfig, ProxyConfig
|
||||||
from frigate.const import CONFIG_DIR, JWT_SECRET_ENV_VAR, PASSWORD_HASH_ALGORITHM
|
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):
|
if path.startswith(EXEMPT_PREFIXES):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Camera routes enforce per-camera access via route-level dependencies
|
# Dynamic camera path exemption:
|
||||||
# (e.g. require_camera_access). Match on the route template, not the raw
|
# Any path whose first segment matches a configured camera name should
|
||||||
# path, so a camera named like another namespace (e.g. "faces") can't
|
# bypass the global admin requirement. These endpoints enforce access
|
||||||
# waive the admin check for that namespace's routes.
|
# via route-level dependencies (e.g. require_camera_access) to ensure
|
||||||
route = request.scope.get("route")
|
# per-camera authorization. This allows non-admin authenticated users
|
||||||
if (
|
# (e.g. viewer role) to access camera-specific resources without
|
||||||
route is not None
|
# needing admin privileges.
|
||||||
and route.path.startswith("/{camera_name}")
|
try:
|
||||||
and request.path_params.get("camera_name")
|
if path.startswith("/"):
|
||||||
in request.app.frigate_config.cameras
|
first_segment = path.split("/", 2)[1]
|
||||||
):
|
if (
|
||||||
return
|
first_segment
|
||||||
|
and first_segment in request.app.frigate_config.cameras
|
||||||
|
):
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# For all other paths, require admin role
|
# For all other paths, require admin role
|
||||||
# Internal port requests have admin role set automatically
|
# Internal port requests have admin role set automatically
|
||||||
@@ -316,19 +318,17 @@ def get_remote_addr(request: Request):
|
|||||||
|
|
||||||
route = list(reversed(forwarded_for.split(",")))
|
route = list(reversed(forwarded_for.split(",")))
|
||||||
logger.debug(f"IP Route: {[r for r in route]}")
|
logger.debug(f"IP Route: {[r for r in route]}")
|
||||||
trusted_proxies = [
|
trusted_proxies = []
|
||||||
ipaddress.ip_network(proxy, strict=False)
|
for proxy in request.app.frigate_config.auth.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
|
# return the first remote address that is not trusted
|
||||||
for addr in route:
|
for addr in route:
|
||||||
try:
|
ip = ipaddress.ip_address(addr.strip())
|
||||||
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"
|
|
||||||
|
|
||||||
logger.debug(f"Checking {ip} (v{ip.version})")
|
logger.debug(f"Checking {ip} (v{ip.version})")
|
||||||
trusted = False
|
trusted = False
|
||||||
for trusted_proxy in trusted_proxies:
|
for trusted_proxy in trusted_proxies:
|
||||||
@@ -471,42 +471,14 @@ def create_encoded_jwt(user, role, expiration, secret):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _user_session(user: User) -> tuple[str, int | None]:
|
|
||||||
changed_at = user.password_changed_at
|
|
||||||
|
|
||||||
# floored like iat, so a login in the same second as the change stays valid
|
|
||||||
return user.role, int(changed_at.timestamp()) if changed_at else None
|
|
||||||
|
|
||||||
|
|
||||||
def load_user_sessions() -> dict[str, tuple[str, int | None]]:
|
|
||||||
"""Map each username to its role and password change time for /auth."""
|
|
||||||
return {user.username: _user_session(user) for user in User.select()}
|
|
||||||
|
|
||||||
|
|
||||||
def refresh_user_session(request: Request, username: str) -> None:
|
|
||||||
"""Sync the /auth session map after a user is created, changed or deleted."""
|
|
||||||
user = User.get_or_none(User.username == username)
|
|
||||||
if user is None:
|
|
||||||
request.app.user_sessions.pop(username, None)
|
|
||||||
else:
|
|
||||||
request.app.user_sessions[username] = _user_session(user)
|
|
||||||
|
|
||||||
|
|
||||||
def is_secure_request(request: Request) -> bool:
|
|
||||||
"""Check the cookie setting and nginx's forwarded scheme for HTTPS."""
|
|
||||||
# nginx sets X-Forwarded-Proto to its own scheme, replacing any client value
|
|
||||||
return (
|
|
||||||
request.app.frigate_config.auth.cookie_secure
|
|
||||||
or request.headers.get("x-forwarded-proto") == "https"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
|
def set_jwt_cookie(response: Response, cookie_name, encoded_jwt, max_age, secure):
|
||||||
# Starlette sets SameSite=Lax by default. The cookie is still sent to
|
# TODO: ideally this would set secure as well, but that requires TLS
|
||||||
# same-site iframes (e.g. Home Assistant on the same host or domain), but
|
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
||||||
# not to cross-site ones. CSRF is also mitigated by requiring a custom
|
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
||||||
# X-CSRF-TOKEN header, which cross-origin pages cannot set without a CORS
|
# iframes, breaking embedded views such as the Home Assistant Frigate card.
|
||||||
# preflight that Frigate never grants (see check_csrf in api/fastapi_app.py).
|
# 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(
|
response.set_cookie(
|
||||||
key=cookie_name,
|
key=cookie_name,
|
||||||
value=encoded_jwt,
|
value=encoded_jwt,
|
||||||
@@ -779,6 +751,7 @@ def auth(request: Request):
|
|||||||
fail_response.headers["location"] = "/login"
|
fail_response.headers["location"] = "/login"
|
||||||
|
|
||||||
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
||||||
|
JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure
|
||||||
JWT_REFRESH = request.app.frigate_config.auth.refresh_time
|
JWT_REFRESH = request.app.frigate_config.auth.refresh_time
|
||||||
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
||||||
|
|
||||||
@@ -804,29 +777,15 @@ def auth(request: Request):
|
|||||||
if "sub" not in token.claims:
|
if "sub" not in token.claims:
|
||||||
logger.debug("user not set in jwt token")
|
logger.debug("user not set in jwt token")
|
||||||
return fail_response
|
return fail_response
|
||||||
|
if "role" not in token.claims:
|
||||||
|
logger.debug("role not set in jwt token")
|
||||||
|
return fail_response
|
||||||
if "exp" not in token.claims:
|
if "exp" not in token.claims:
|
||||||
logger.debug("exp not set in jwt token")
|
logger.debug("exp not set in jwt token")
|
||||||
return fail_response
|
return fail_response
|
||||||
|
|
||||||
user = token.claims.get("sub")
|
user = token.claims.get("sub")
|
||||||
session = request.app.user_sessions.get(user)
|
role = token.claims.get("role")
|
||||||
if session is None:
|
|
||||||
logger.debug("jwt user %s no longer exists", user)
|
|
||||||
return fail_response
|
|
||||||
|
|
||||||
role, password_changed_at = session
|
|
||||||
if password_changed_at is not None and (
|
|
||||||
int(token.claims.get("iat", 0)) < password_changed_at
|
|
||||||
):
|
|
||||||
logger.debug("jwt token issued before password change")
|
|
||||||
return fail_response
|
|
||||||
|
|
||||||
if role not in auth_config.roles:
|
|
||||||
logger.debug(
|
|
||||||
"jwt user %s has role %s, which is not in the config", user, role
|
|
||||||
)
|
|
||||||
return fail_response
|
|
||||||
|
|
||||||
current_time = int(time.time())
|
current_time = int(time.time())
|
||||||
|
|
||||||
# if the jwt is expired
|
# if the jwt is expired
|
||||||
@@ -847,6 +806,25 @@ def auth(request: Request):
|
|||||||
# if the jwt cookie is expiring soon
|
# if the jwt cookie is expiring soon
|
||||||
if jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
if jwt_source == "cookie" and expiration - JWT_REFRESH <= current_time:
|
||||||
logger.debug("jwt token expiring soon, refreshing cookie")
|
logger.debug("jwt token expiring soon, refreshing cookie")
|
||||||
|
|
||||||
|
# Check if password has been changed since token was issued
|
||||||
|
# If so, force re-login by rejecting the refresh
|
||||||
|
try:
|
||||||
|
user_obj = User.get_by_id(user)
|
||||||
|
if user_obj.password_changed_at is not None:
|
||||||
|
token_iat = int(token.claims.get("iat", 0))
|
||||||
|
password_changed_timestamp = int(
|
||||||
|
user_obj.password_changed_at.timestamp()
|
||||||
|
)
|
||||||
|
if token_iat < password_changed_timestamp:
|
||||||
|
logger.debug(
|
||||||
|
"jwt token issued before password change, rejecting refresh"
|
||||||
|
)
|
||||||
|
return fail_response
|
||||||
|
except DoesNotExist:
|
||||||
|
logger.debug("user not found")
|
||||||
|
return fail_response
|
||||||
|
|
||||||
new_expiration = current_time + JWT_SESSION_LENGTH
|
new_expiration = current_time + JWT_SESSION_LENGTH
|
||||||
new_encoded_jwt = create_encoded_jwt(
|
new_encoded_jwt = create_encoded_jwt(
|
||||||
user, role, new_expiration, request.app.jwt_token
|
user, role, new_expiration, request.app.jwt_token
|
||||||
@@ -856,7 +834,7 @@ def auth(request: Request):
|
|||||||
JWT_COOKIE_NAME,
|
JWT_COOKIE_NAME,
|
||||||
new_encoded_jwt,
|
new_encoded_jwt,
|
||||||
JWT_SESSION_LENGTH,
|
JWT_SESSION_LENGTH,
|
||||||
is_secure_request(request),
|
JWT_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
success_response.headers["remote-user"] = user
|
success_response.headers["remote-user"] = user
|
||||||
@@ -945,6 +923,7 @@ def login(request: Request, body: AppPostLoginBody):
|
|||||||
)
|
)
|
||||||
|
|
||||||
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
||||||
|
JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure
|
||||||
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
||||||
user = body.user
|
user = body.user
|
||||||
password = body.password
|
password = body.password
|
||||||
@@ -960,16 +939,13 @@ def login(request: Request, body: AppPostLoginBody):
|
|||||||
|
|
||||||
password_hash = db_user.password_hash
|
password_hash = db_user.password_hash
|
||||||
if verify_password(password, password_hash):
|
if verify_password(password, password_hash):
|
||||||
role = db_user.role
|
role = getattr(db_user, "role", "viewer")
|
||||||
if role not in request.app.frigate_config.auth.roles:
|
config_roles_set = set(request.app.frigate_config.auth.roles.keys())
|
||||||
|
if role not in config_roles_set:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Login refused for user '%s' with role '%s', which is not in the config",
|
f"User {db_user.username} has an invalid role {role}, falling back to 'viewer'."
|
||||||
user,
|
|
||||||
role,
|
|
||||||
)
|
)
|
||||||
failed_logins.record(user, time.time(), known=True)
|
role = "viewer"
|
||||||
return JSONResponse(content={"message": "Login failed"}, status_code=401)
|
|
||||||
|
|
||||||
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
||||||
encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token)
|
encoded_jwt = create_encoded_jwt(user, role, expiration, request.app.jwt_token)
|
||||||
response = Response("", 200)
|
response = Response("", 200)
|
||||||
@@ -978,7 +954,7 @@ def login(request: Request, body: AppPostLoginBody):
|
|||||||
JWT_COOKIE_NAME,
|
JWT_COOKIE_NAME,
|
||||||
encoded_jwt,
|
encoded_jwt,
|
||||||
JWT_SESSION_LENGTH,
|
JWT_SESSION_LENGTH,
|
||||||
is_secure_request(request),
|
JWT_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
# Clear admin_first_time_login flag after successful admin login so the
|
# Clear admin_first_time_login flag after successful admin login so the
|
||||||
# UI stops showing the first-time login documentation link.
|
# UI stops showing the first-time login documentation link.
|
||||||
@@ -1020,7 +996,7 @@ def create_user(
|
|||||||
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
|
HASH_ITERATIONS = request.app.frigate_config.auth.hash_iterations
|
||||||
config_roles = list(request.app.frigate_config.auth.roles.keys())
|
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)
|
return JSONResponse(content={"message": "Invalid username"}, status_code=400)
|
||||||
|
|
||||||
if body.role not in config_roles:
|
if body.role not in config_roles:
|
||||||
@@ -1044,11 +1020,9 @@ def create_user(
|
|||||||
User.username: body.username,
|
User.username: body.username,
|
||||||
User.password_hash: password_hash,
|
User.password_hash: password_hash,
|
||||||
User.role: role,
|
User.role: role,
|
||||||
User.password_changed_at: datetime.now(),
|
|
||||||
User.notification_tokens: [],
|
User.notification_tokens: [],
|
||||||
}
|
}
|
||||||
).execute()
|
).execute()
|
||||||
refresh_user_session(request, body.username)
|
|
||||||
request.app.config_publisher.publisher.publish("config/auth", None)
|
request.app.config_publisher.publisher.publish("config/auth", None)
|
||||||
return JSONResponse(content={"username": body.username})
|
return JSONResponse(content={"username": body.username})
|
||||||
|
|
||||||
@@ -1067,7 +1041,6 @@ def delete_user(request: Request, username: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
User.delete_by_id(username)
|
User.delete_by_id(username)
|
||||||
refresh_user_session(request, username)
|
|
||||||
request.app.config_publisher.publisher.publish("config/auth", None)
|
request.app.config_publisher.publisher.publish("config/auth", None)
|
||||||
return JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
@@ -1135,13 +1108,13 @@ async def update_password(
|
|||||||
User.password_changed_at: datetime.now(),
|
User.password_changed_at: datetime.now(),
|
||||||
}
|
}
|
||||||
).where(User.username == username).execute()
|
).where(User.username == username).execute()
|
||||||
refresh_user_session(request, username)
|
|
||||||
|
|
||||||
response = JSONResponse(content={"success": True})
|
response = JSONResponse(content={"success": True})
|
||||||
|
|
||||||
# If user changed their own password, issue a new JWT to keep them logged in
|
# If user changed their own password, issue a new JWT to keep them logged in
|
||||||
if current_username == username:
|
if current_username == username:
|
||||||
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
JWT_COOKIE_NAME = request.app.frigate_config.auth.cookie_name
|
||||||
|
JWT_COOKIE_SECURE = request.app.frigate_config.auth.cookie_secure
|
||||||
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
JWT_SESSION_LENGTH = request.app.frigate_config.auth.session_length
|
||||||
|
|
||||||
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
expiration = int(time.time()) + JWT_SESSION_LENGTH
|
||||||
@@ -1154,7 +1127,7 @@ async def update_password(
|
|||||||
JWT_COOKIE_NAME,
|
JWT_COOKIE_NAME,
|
||||||
encoded_jwt,
|
encoded_jwt,
|
||||||
JWT_SESSION_LENGTH,
|
JWT_SESSION_LENGTH,
|
||||||
is_secure_request(request),
|
JWT_COOKIE_SECURE,
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
@@ -1194,7 +1167,6 @@ async def update_role(
|
|||||||
)
|
)
|
||||||
|
|
||||||
User.set_by_id(username, {User.role: body.role})
|
User.set_by_id(username, {User.role: body.role})
|
||||||
refresh_user_session(request, username)
|
|
||||||
request.app.config_publisher.publisher.publish("config/auth", None)
|
request.app.config_publisher.publisher.publish("config/auth", None)
|
||||||
return JSONResponse(content={"success": True})
|
return JSONResponse(content={"success": True})
|
||||||
|
|
||||||
@@ -1250,44 +1222,11 @@ def _get_stream_owner_cameras(request: Request, stream_name: str) -> set[str]:
|
|||||||
return owner_cameras
|
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
|
# nginx proxies these paths straight to go2rtc with authentication-only checks
|
||||||
# (see auth_request.conf). Each names the desired stream via the `src` query
|
# (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 —
|
# param, so the camera-level check must happen here in the `/auth` subrequest —
|
||||||
# `require_go2rtc_stream_access` only guards the REST `/go2rtc/streams/{name}`
|
# `require_go2rtc_stream_access` only guards the REST `/go2rtc/streams/{name}`
|
||||||
# endpoint, not these proxied live-stream paths. The nginx locations are prefix
|
# endpoint, not these proxied live-stream paths.
|
||||||
# matches, so anything under these paths is proxied too.
|
|
||||||
GO2RTC_STREAM_PROXY_PATHS = frozenset(
|
GO2RTC_STREAM_PROXY_PATHS = frozenset(
|
||||||
{
|
{
|
||||||
"/live/mse/api/ws",
|
"/live/mse/api/ws",
|
||||||
@@ -1297,48 +1236,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(
|
def deny_response_for_go2rtc_stream(
|
||||||
original_url: str | None, role: str | None, request: Request
|
original_url: str | None, role: str | None, request: Request
|
||||||
) -> int | None:
|
) -> 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
|
Returns 403 when any `src` stream named in `original_url` resolves to a
|
||||||
stream (for every role), or when it resolves to a camera outside a
|
camera outside the role's allow-list (or when no `src` is provided on a
|
||||||
restricted role's allow-list (or no `src` is provided for such a role),
|
stream-proxy path), otherwise None. Mirrors the resolution logic in
|
||||||
otherwise None. Mirrors the resolution logic in
|
|
||||||
`require_go2rtc_stream_access` so substream names map to their owning
|
`require_go2rtc_stream_access` so substream names map to their owning
|
||||||
camera correctly.
|
camera correctly.
|
||||||
"""
|
"""
|
||||||
if not original_url:
|
if not original_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
path, query = split_request_uri(original_url)
|
parsed = urlparse(original_url)
|
||||||
if not _is_go2rtc_stream_proxy_request(path, request):
|
if parsed.path not in GO2RTC_STREAM_PROXY_PATHS:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
frigate_config = request.app.frigate_config
|
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
|
# admin and full-access roles (no allow-list) bypass the camera check
|
||||||
if not role or not is_role_restricted(role, frigate_config):
|
if not role or not is_role_restricted(role, frigate_config):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
sources = parse_qs(parsed.query).get("src", [])
|
||||||
if not sources:
|
if not sources:
|
||||||
# a stream-proxy request naming no stream has nothing legitimate to
|
# a stream-proxy request naming no stream has nothing legitimate to
|
||||||
# show a restricted user
|
# show a restricted user
|
||||||
|
|||||||
+7
-51
@@ -33,25 +33,16 @@ from frigate.config.camera.updater import (
|
|||||||
CameraConfigUpdateEnum,
|
CameraConfigUpdateEnum,
|
||||||
CameraConfigUpdateTopic,
|
CameraConfigUpdateTopic,
|
||||||
)
|
)
|
||||||
|
from frigate.config.env import UnknownVariableError, substitute_frigate_vars
|
||||||
from frigate.models import User
|
from frigate.models import User
|
||||||
from frigate.util.builtin import (
|
from frigate.util.builtin import clean_camera_user_pass, get_record_segment_time
|
||||||
clean_camera_user_pass,
|
|
||||||
clear_orphaned_comments,
|
|
||||||
get_record_segment_time,
|
|
||||||
)
|
|
||||||
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
|
from frigate.util.camera_cleanup import cleanup_camera_db, cleanup_camera_files
|
||||||
from frigate.util.config import find_config_file
|
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.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 (
|
from frigate.util.services import (
|
||||||
analyze_record_keyframes,
|
analyze_record_keyframes,
|
||||||
ffprobe_stream,
|
ffprobe_stream,
|
||||||
is_restricted_go2rtc_api_source,
|
is_restricted_go2rtc_source,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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 = ""):
|
def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
|
||||||
"""Add or update a go2rtc stream configuration."""
|
"""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(
|
logger.warning(
|
||||||
"Rejected go2rtc stream '%s' with restricted source",
|
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
|
||||||
stream_name,
|
stream_name,
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -178,9 +169,9 @@ def go2rtc_add_stream(request: Request, stream_name: str, src: str = ""):
|
|||||||
except UnknownVariableError:
|
except UnknownVariableError:
|
||||||
resolved_src = src
|
resolved_src = src
|
||||||
|
|
||||||
if is_restricted_go2rtc_api_source(resolved_src):
|
if is_restricted_go2rtc_source(resolved_src):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Rejected go2rtc stream '%s' with restricted source",
|
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
|
||||||
stream_name,
|
stream_name,
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
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"]))])
|
@router.get("/ffprobe", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def ffprobe(request: Request, paths: str = "", detailed: bool = False):
|
def ffprobe(request: Request, paths: str = "", detailed: bool = False):
|
||||||
path_param = paths
|
path_param = paths
|
||||||
@@ -1277,7 +1240,6 @@ async def delete_camera(
|
|||||||
# Remove camera from config
|
# Remove camera from config
|
||||||
if "cameras" in data and camera_name in data["cameras"]:
|
if "cameras" in data and camera_name in data["cameras"]:
|
||||||
del data["cameras"][camera_name]
|
del data["cameras"][camera_name]
|
||||||
clear_orphaned_comments(data["cameras"], data, "cameras")
|
|
||||||
|
|
||||||
# Remove camera from auth roles
|
# Remove camera from auth roles
|
||||||
auth = data.get("auth", {})
|
auth = data.get("auth", {})
|
||||||
@@ -1380,12 +1342,6 @@ async def delete_camera(
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.debug("Failed to remove go2rtc stream for %s", camera_name)
|
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(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
"success": True,
|
"success": True,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Object classification APIs."""
|
"""Object classification APIs."""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -49,9 +48,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter(tags=[Tags.classification])
|
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:
|
def invalid_name_response(value: str) -> JSONResponse:
|
||||||
"""Response for a name that cannot be used as a path component."""
|
"""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:
|
if model_name is None:
|
||||||
return invalid_name_response(body.model_name)
|
return invalid_name_response(body.model_name)
|
||||||
|
|
||||||
async with example_collection_lock:
|
collect_object_classification_examples(model_name, body.label)
|
||||||
await asyncio.to_thread(
|
|
||||||
collect_object_classification_examples, model_name, body.label
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": True, "message": "Example generation completed"},
|
content={"success": True, "message": "Example generation completed"},
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ def swap_runtime_config(app: FastAPI, config: FrigateConfig) -> None:
|
|||||||
|
|
||||||
if app.stats_emitter is not None:
|
if app.stats_emitter is not None:
|
||||||
app.stats_emitter.config = config
|
app.stats_emitter.config = config
|
||||||
app.stats_emitter.hardware_stats.set_config(config)
|
|
||||||
|
|
||||||
if app.dispatcher is not None:
|
if app.dispatcher is not None:
|
||||||
app.dispatcher.config = config
|
app.dispatcher.config = config
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ class AppTimelineHourlyQueryParameters(BaseModel):
|
|||||||
after: float | None = None
|
after: float | None = None
|
||||||
before: float | None = None
|
before: float | None = None
|
||||||
limit: int | None = 200
|
limit: int | None = 200
|
||||||
timezone: str | None = "UTC"
|
timezone: str | None = "utc"
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ class EventsQueryParams(BaseModel):
|
|||||||
zone: str | None = "all"
|
zone: str | None = "all"
|
||||||
zones: str | None = "all"
|
zones: str | None = "all"
|
||||||
limit: int | None = 100
|
limit: int | None = 100
|
||||||
offset: int | None = Field(0, ge=0)
|
|
||||||
after: float | None = None
|
after: float | None = None
|
||||||
before: float | None = None
|
before: float | None = None
|
||||||
time_range: str | None = DEFAULT_TIME_RANGE
|
time_range: str | None = DEFAULT_TIME_RANGE
|
||||||
@@ -40,7 +39,7 @@ class EventsQueryParams(BaseModel):
|
|||||||
max_length: float | None = None
|
max_length: float | None = None
|
||||||
event_id: str | None = None
|
event_id: str | None = None
|
||||||
sort: str | None = None
|
sort: str | None = None
|
||||||
timezone: str | None = "UTC"
|
timezone: str | None = "utc"
|
||||||
|
|
||||||
|
|
||||||
class EventsSearchQueryParams(BaseModel):
|
class EventsSearchQueryParams(BaseModel):
|
||||||
@@ -56,7 +55,6 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
deprecated=True,
|
deprecated=True,
|
||||||
)
|
)
|
||||||
limit: int | None = 50
|
limit: int | None = 50
|
||||||
offset: int | None = Field(0, ge=0)
|
|
||||||
cameras: str | None = "all"
|
cameras: str | None = "all"
|
||||||
labels: str | None = "all"
|
labels: str | None = "all"
|
||||||
sub_labels: str | None = "all"
|
sub_labels: str | None = "all"
|
||||||
@@ -68,7 +66,7 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
has_clip: bool | None = None
|
has_clip: bool | None = None
|
||||||
has_snapshot: bool | None = None
|
has_snapshot: bool | None = None
|
||||||
is_submitted: bool | None = None
|
is_submitted: bool | None = None
|
||||||
timezone: str | None = "UTC"
|
timezone: str | None = "utc"
|
||||||
min_score: float | None = None
|
min_score: float | None = None
|
||||||
max_score: float | None = None
|
max_score: float | None = None
|
||||||
min_speed: float | None = None
|
min_speed: float | None = None
|
||||||
@@ -78,6 +76,6 @@ class EventsSearchQueryParams(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class EventsSummaryQueryParams(BaseModel):
|
class EventsSummaryQueryParams(BaseModel):
|
||||||
timezone: str | None = "UTC"
|
timezone: str | None = "utc"
|
||||||
has_clip: int | None = None
|
has_clip: int | None = None
|
||||||
has_snapshot: int | None = None
|
has_snapshot: int | None = None
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from pydantic.json_schema import SkipJsonSchema
|
|||||||
|
|
||||||
|
|
||||||
class MediaRecordingsSummaryQueryParams(BaseModel):
|
class MediaRecordingsSummaryQueryParams(BaseModel):
|
||||||
timezone: str = "UTC"
|
timezone: str = "utc"
|
||||||
cameras: str | None = "all"
|
cameras: str | None = "all"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class ReviewSummaryQueryParams(BaseModel):
|
|||||||
cameras: str = "all"
|
cameras: str = "all"
|
||||||
labels: str = "all"
|
labels: str = "all"
|
||||||
zones: str = "all"
|
zones: str = "all"
|
||||||
timezone: str = "UTC"
|
timezone: str = "utc"
|
||||||
|
|
||||||
|
|
||||||
class ReviewActivityMotionQueryParams(BaseModel):
|
class ReviewActivityMotionQueryParams(BaseModel):
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ class AppConfigSetBody(BaseModel):
|
|||||||
update_topic: str | None = None
|
update_topic: str | None = None
|
||||||
config_data: dict[str, Any] | None = None
|
config_data: dict[str, Any] | None = None
|
||||||
skip_save: bool = False
|
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):
|
class GenAIProbeBody(BaseModel):
|
||||||
|
|||||||
+2
-11
@@ -129,7 +129,6 @@ def events(
|
|||||||
zones = zone
|
zones = zone
|
||||||
|
|
||||||
limit = params.limit
|
limit = params.limit
|
||||||
offset = params.offset
|
|
||||||
after = params.after
|
after = params.after
|
||||||
before = params.before
|
before = params.before
|
||||||
time_range = params.time_range
|
time_range = params.time_range
|
||||||
@@ -362,15 +361,11 @@ def events(
|
|||||||
else:
|
else:
|
||||||
order_by = Event.start_time.desc()
|
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 = (
|
events = (
|
||||||
Event.select(*selected_columns)
|
Event.select(*selected_columns)
|
||||||
.where(reduce(operator.and_, clauses))
|
.where(reduce(operator.and_, clauses))
|
||||||
.order_by(order_by, *tiebreaker)
|
.order_by(order_by)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
)
|
)
|
||||||
@@ -539,7 +534,6 @@ def events_search(
|
|||||||
search_type = params.search_type
|
search_type = params.search_type
|
||||||
include_thumbnails = params.include_thumbnails
|
include_thumbnails = params.include_thumbnails
|
||||||
limit = params.limit
|
limit = params.limit
|
||||||
offset = params.offset
|
|
||||||
sort = params.sort
|
sort = params.sort
|
||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
@@ -846,9 +840,6 @@ def events_search(
|
|||||||
if search_results:
|
if search_results:
|
||||||
events_query = events_query.where(Event.id << list(search_results.keys()))
|
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
|
# Fetch events and process them in a single pass
|
||||||
processed_events = []
|
processed_events = []
|
||||||
for event in events_query.dicts():
|
for event in events_query.dicts():
|
||||||
@@ -906,7 +897,7 @@ def events_search(
|
|||||||
processed_events.sort(key=lambda x: x["start_time"], reverse=True)
|
processed_events.sort(key=lambda x: x["start_time"], reverse=True)
|
||||||
|
|
||||||
# Limit the number of events returned
|
# Limit the number of events returned
|
||||||
processed_events = processed_events[offset:][:limit]
|
processed_events = processed_events[:limit]
|
||||||
|
|
||||||
return JSONResponse(content=processed_events)
|
return JSONResponse(content=processed_events)
|
||||||
|
|
||||||
|
|||||||
+20
-37
@@ -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.response.generic_response import GenericResponse
|
||||||
from frigate.api.defs.tags import Tags
|
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.const import CLIPS_DIR, EXPORT_DIR
|
||||||
from frigate.jobs.export import (
|
from frigate.jobs.export import (
|
||||||
ExportJob,
|
ExportJob,
|
||||||
@@ -72,7 +71,6 @@ from frigate.jobs.export import (
|
|||||||
from frigate.models import Export, ExportCase, Previews, Recordings
|
from frigate.models import Export, ExportCase, Previews, Recordings
|
||||||
from frigate.record.export import (
|
from frigate.record.export import (
|
||||||
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
|
DEFAULT_TIME_LAPSE_FFMPEG_ARGS,
|
||||||
DEFAULT_TIME_LAPSE_FFMPEG_INPUT_ARGS,
|
|
||||||
ChaptersEnum,
|
ChaptersEnum,
|
||||||
ExportStreamEnum,
|
ExportStreamEnum,
|
||||||
PlaybackSourceEnum,
|
PlaybackSourceEnum,
|
||||||
@@ -80,7 +78,6 @@ from frigate.record.export import (
|
|||||||
validate_ffmpeg_args,
|
validate_ffmpeg_args,
|
||||||
)
|
)
|
||||||
from frigate.util.path import sanitize_contained_path
|
from frigate.util.path import sanitize_contained_path
|
||||||
from frigate.util.recording_coverage import recording_overlap
|
|
||||||
from frigate.util.time import is_current_hour
|
from frigate.util.time import is_current_hour
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -136,7 +133,6 @@ def _validate_export_case(export_case_id: str | None) -> JSONResponse | None:
|
|||||||
|
|
||||||
|
|
||||||
def _sanitize_existing_image(
|
def _sanitize_existing_image(
|
||||||
request: Request,
|
|
||||||
image_path: str | None,
|
image_path: str | None,
|
||||||
) -> tuple[str | None, JSONResponse | None]:
|
) -> tuple[str | None, JSONResponse | None]:
|
||||||
if not image_path:
|
if not image_path:
|
||||||
@@ -144,15 +140,6 @@ def _sanitize_existing_image(
|
|||||||
|
|
||||||
existing_image = sanitize_contained_path(image_path, CLIPS_DIR)
|
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:
|
if existing_image is None:
|
||||||
return None, JSONResponse(
|
return None, JSONResponse(
|
||||||
content={"success": False, "message": "Invalid image path"},
|
content={"success": False, "message": "Invalid image path"},
|
||||||
@@ -178,7 +165,15 @@ def _validate_export_source(
|
|||||||
) -> str | None:
|
) -> str | None:
|
||||||
if playback_source == PlaybackSourceEnum.recordings:
|
if playback_source == PlaybackSourceEnum.recordings:
|
||||||
query = Recordings.select().where(
|
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
|
# a pinned export reads only that stream, so the other stream's
|
||||||
@@ -243,7 +238,9 @@ def _get_item_recording_export_errors(
|
|||||||
|
|
||||||
query = Recordings.select(Recordings.start_time, Recordings.end_time).where(
|
query = Recordings.select(Recordings.start_time, Recordings.end_time).where(
|
||||||
Recordings.camera == camera_name,
|
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
|
# a pinned batch reads only that stream, so the other stream's
|
||||||
@@ -703,7 +700,7 @@ def export_recordings_batch(
|
|||||||
sanitized_images: list[str | None] = []
|
sanitized_images: list[str | None] = []
|
||||||
for item in body.items:
|
for item in body.items:
|
||||||
existing_image, image_validation_error = _sanitize_existing_image(
|
existing_image, image_validation_error = _sanitize_existing_image(
|
||||||
request, item.image_path
|
item.image_path
|
||||||
)
|
)
|
||||||
if image_validation_error is not None:
|
if image_validation_error is not None:
|
||||||
return image_validation_error
|
return image_validation_error
|
||||||
@@ -851,9 +848,7 @@ def export_recording(
|
|||||||
|
|
||||||
playback_source = body.source
|
playback_source = body.source
|
||||||
friendly_name = body.name
|
friendly_name = body.name
|
||||||
existing_image, image_validation_error = _sanitize_existing_image(
|
existing_image, image_validation_error = _sanitize_existing_image(body.image_path)
|
||||||
request, body.image_path
|
|
||||||
)
|
|
||||||
if image_validation_error is not None:
|
if image_validation_error is not None:
|
||||||
return image_validation_error
|
return image_validation_error
|
||||||
|
|
||||||
@@ -1042,15 +1037,9 @@ def export_recording_custom(
|
|||||||
if camera_validation_error is not None:
|
if camera_validation_error is not None:
|
||||||
return camera_validation_error
|
return camera_validation_error
|
||||||
|
|
||||||
# Validate user-provided ffmpeg args to prevent injection and add to cases.
|
|
||||||
# Admin users are trusted and skip validation.
|
|
||||||
is_admin = request.headers.get("remote-role", "") == "admin"
|
|
||||||
|
|
||||||
playback_source = body.source
|
playback_source = body.source
|
||||||
friendly_name = body.name
|
friendly_name = body.name
|
||||||
existing_image, image_validation_error = _sanitize_existing_image(
|
existing_image, image_validation_error = _sanitize_existing_image(body.image_path)
|
||||||
request, body.image_path
|
|
||||||
)
|
|
||||||
if image_validation_error is not None:
|
if image_validation_error is not None:
|
||||||
return image_validation_error
|
return image_validation_error
|
||||||
ffmpeg_input_args = body.ffmpeg_input_args
|
ffmpeg_input_args = body.ffmpeg_input_args
|
||||||
@@ -1058,16 +1047,6 @@ def export_recording_custom(
|
|||||||
cpu_fallback = body.cpu_fallback
|
cpu_fallback = body.cpu_fallback
|
||||||
|
|
||||||
export_case_id = body.export_case_id
|
export_case_id = body.export_case_id
|
||||||
|
|
||||||
if export_case_id is not None and not is_admin:
|
|
||||||
return JSONResponse(
|
|
||||||
content={
|
|
||||||
"success": False,
|
|
||||||
"message": "Only admins can attach exports to an existing case.",
|
|
||||||
},
|
|
||||||
status_code=403,
|
|
||||||
)
|
|
||||||
|
|
||||||
case_validation_error = _validate_export_case(export_case_id)
|
case_validation_error = _validate_export_case(export_case_id)
|
||||||
if case_validation_error is not None:
|
if case_validation_error is not None:
|
||||||
return case_validation_error
|
return case_validation_error
|
||||||
@@ -1084,6 +1063,10 @@ def export_recording_custom(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate user-provided ffmpeg args to prevent injection.
|
||||||
|
# Admin users are trusted and skip validation.
|
||||||
|
is_admin = request.headers.get("remote-role", "") == "admin"
|
||||||
|
|
||||||
if not is_admin:
|
if not is_admin:
|
||||||
for args_label, args_value in [
|
for args_label, args_value in [
|
||||||
("input", ffmpeg_input_args),
|
("input", ffmpeg_input_args),
|
||||||
@@ -1104,7 +1087,7 @@ def export_recording_custom(
|
|||||||
|
|
||||||
# Set default values if not provided (timelapse defaults)
|
# Set default values if not provided (timelapse defaults)
|
||||||
if ffmpeg_input_args is None:
|
if ffmpeg_input_args is None:
|
||||||
ffmpeg_input_args = DEFAULT_TIME_LAPSE_FFMPEG_INPUT_ARGS
|
ffmpeg_input_args = ""
|
||||||
|
|
||||||
if ffmpeg_output_args is None:
|
if ffmpeg_output_args is None:
|
||||||
ffmpeg_output_args = DEFAULT_TIME_LAPSE_FFMPEG_ARGS
|
ffmpeg_output_args = DEFAULT_TIME_LAPSE_FFMPEG_ARGS
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from contextlib import asynccontextmanager
|
|
||||||
|
|
||||||
from fastapi import Depends, FastAPI, Request
|
from fastapi import Depends, FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
@@ -82,20 +81,9 @@ def create_fastapi_app(
|
|||||||
config_holder: ConfigHolder | None = None,
|
config_holder: ConfigHolder | None = None,
|
||||||
notice_registry: NoticeRegistry | 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")
|
logger.info("Starting FastAPI app")
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
debug=False,
|
debug=False,
|
||||||
lifespan=lifespan,
|
|
||||||
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
swagger_ui_parameters={"apisSorter": "alpha", "operationsSorter": "alpha"},
|
||||||
dependencies=[Depends(require_admin_by_default())]
|
dependencies=[Depends(require_admin_by_default())]
|
||||||
if enforce_default_admin
|
if enforce_default_admin
|
||||||
@@ -131,6 +119,15 @@ def create_fastapi_app(
|
|||||||
database.close()
|
database.close()
|
||||||
return response
|
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)
|
# Rate limiter (used for login endpoint)
|
||||||
if frigate_config.auth.failed_login_rate_limit is None:
|
if frigate_config.auth.failed_login_rate_limit is None:
|
||||||
limiter.enabled = False
|
limiter.enabled = False
|
||||||
@@ -196,9 +193,7 @@ def create_fastapi_app(
|
|||||||
key_bytes = str(secret).encode("utf-8")
|
key_bytes = str(secret).encode("utf-8")
|
||||||
|
|
||||||
app.jwt_token = OctKey.import_key(key_bytes)
|
app.jwt_token = OctKey.import_key(key_bytes)
|
||||||
app.user_sessions = auth.load_user_sessions()
|
|
||||||
else:
|
else:
|
||||||
app.jwt_token = None
|
app.jwt_token = None
|
||||||
app.user_sessions = {}
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
+95
-116
@@ -14,10 +14,10 @@ from enum import Enum
|
|||||||
from pathlib import Path as FilePath
|
from pathlib import Path as FilePath
|
||||||
from typing import IO, Any
|
from typing import IO, Any
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pytz
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request, Response
|
from fastapi import APIRouter, Depends, Path, Query, Request, Response
|
||||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||||
from pathvalidate import sanitize_filename
|
from pathvalidate import sanitize_filename
|
||||||
@@ -62,10 +62,9 @@ from frigate.util.recording_coverage import (
|
|||||||
build_spans,
|
build_spans,
|
||||||
null_audio_glitches,
|
null_audio_glitches,
|
||||||
plan_clip,
|
plan_clip,
|
||||||
recording_overlap,
|
|
||||||
resolve_coverage,
|
resolve_coverage,
|
||||||
|
stream_has_audio,
|
||||||
)
|
)
|
||||||
from frigate.util.time import get_normalized_tz_name
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -190,7 +189,7 @@ async def camera_ptz_info(request: Request, camera_name: str):
|
|||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
request.app.onvif.get_camera_info(camera_name), request.app.onvif.loop
|
request.app.onvif.get_camera_info(camera_name), request.app.onvif.loop
|
||||||
)
|
)
|
||||||
result = await asyncio.wrap_future(future)
|
result = future.result()
|
||||||
return JSONResponse(content=result)
|
return JSONResponse(content=result)
|
||||||
else:
|
else:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -259,15 +258,15 @@ async def latest_frame(
|
|||||||
|
|
||||||
frame = request.app.camera_error_image
|
frame = request.app.camera_error_image
|
||||||
|
|
||||||
|
height = int(params.height or str(frame.shape[0]))
|
||||||
|
width = int(height * frame.shape[1] / frame.shape[0])
|
||||||
|
|
||||||
if frame is None:
|
if frame is None:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Unable to get valid frame"},
|
content={"success": False, "message": "Unable to get valid frame"},
|
||||||
status_code=500,
|
status_code=500,
|
||||||
)
|
)
|
||||||
|
|
||||||
height = int(params.height or str(frame.shape[0]))
|
|
||||||
width = int(height * frame.shape[1] / frame.shape[0])
|
|
||||||
|
|
||||||
if height < 1 or width < 1:
|
if height < 1 or width < 1:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content="Invalid height / width requested :: {} / {}".format(
|
content="Invalid height / width requested :: {} / {}".format(
|
||||||
@@ -297,15 +296,10 @@ async def latest_frame(
|
|||||||
and request.app.frigate_config.birdseye.enabled
|
and request.app.frigate_config.birdseye.enabled
|
||||||
and request.app.frigate_config.birdseye.restream
|
and request.app.frigate_config.birdseye.restream
|
||||||
):
|
):
|
||||||
frame = frame_processor.get_current_frame(camera_name)
|
frame = cv2.cvtColor(
|
||||||
|
frame_processor.get_current_frame(camera_name),
|
||||||
if frame is None:
|
cv2.COLOR_YUV2BGR_I420,
|
||||||
return JSONResponse(
|
)
|
||||||
content={"success": False, "message": "Unable to get valid frame"},
|
|
||||||
status_code=500,
|
|
||||||
)
|
|
||||||
|
|
||||||
frame = cv2.cvtColor(frame, cv2.COLOR_YUV2BGR_I420)
|
|
||||||
|
|
||||||
height = int(params.height or str(frame.shape[0]))
|
height = int(params.height or str(frame.shape[0]))
|
||||||
width = int(height * frame.shape[1] / frame.shape[0])
|
width = int(height * frame.shape[1] / frame.shape[0])
|
||||||
@@ -353,7 +347,10 @@ async def get_snapshot_from_recording(
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
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)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
|
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -369,7 +366,10 @@ async def get_snapshot_from_recording(
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
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)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
|
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -384,13 +384,8 @@ async def get_snapshot_from_recording(
|
|||||||
mime_type = "png" if format == "png" else "jpeg"
|
mime_type = "png" if format == "png" else "jpeg"
|
||||||
config: FrigateConfig = request.app.frigate_config
|
config: FrigateConfig = request.app.frigate_config
|
||||||
|
|
||||||
image_data = await asyncio.to_thread(
|
image_data = get_image_from_recording(
|
||||||
get_image_from_recording,
|
config.ffmpeg, recording.path, time_in_segment, codec, height
|
||||||
config.ffmpeg,
|
|
||||||
recording.path,
|
|
||||||
time_in_segment,
|
|
||||||
codec,
|
|
||||||
height,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
@@ -432,7 +427,9 @@ async def submit_recording_snapshot_to_plus(
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
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)
|
.where(Recordings.camera == camera_name)
|
||||||
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
|
.order_by(Recordings.stream_type.asc(), Recordings.start_time.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
@@ -442,12 +439,8 @@ async def submit_recording_snapshot_to_plus(
|
|||||||
config: FrigateConfig = request.app.frigate_config
|
config: FrigateConfig = request.app.frigate_config
|
||||||
recording: Recordings = recording_query.get()
|
recording: Recordings = recording_query.get()
|
||||||
time_in_segment = frame_time - recording.start_time
|
time_in_segment = frame_time - recording.start_time
|
||||||
image_data = await asyncio.to_thread(
|
image_data = get_image_from_recording(
|
||||||
get_image_from_recording,
|
config.ffmpeg, recording.path, time_in_segment, "png"
|
||||||
config.ffmpeg,
|
|
||||||
recording.path,
|
|
||||||
time_in_segment,
|
|
||||||
"png",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
@@ -546,7 +539,11 @@ async def recording_clip(
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
Recordings.end_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.camera == camera_name)
|
||||||
.where(Recordings.stream_type == stream_type)
|
.where(Recordings.stream_type == stream_type)
|
||||||
.order_by(Recordings.start_time.asc())
|
.order_by(Recordings.start_time.asc())
|
||||||
@@ -684,10 +681,15 @@ async def _vod_response(
|
|||||||
end_ts,
|
end_ts,
|
||||||
force_discontinuity,
|
force_discontinuity,
|
||||||
)
|
)
|
||||||
|
intervals = resolve_coverage(camera_name, start_ts, end_ts)
|
||||||
|
|
||||||
# rows contradicting their stream's audio composition are
|
# rows contradicting their stream's audio composition are
|
||||||
# truncated-shutdown glitches
|
# truncated-shutdown glitches
|
||||||
|
main_audio = stream_has_audio(intervals, main=True)
|
||||||
|
sub_audio = stream_has_audio(intervals, main=False)
|
||||||
|
|
||||||
spans = build_spans(
|
spans = build_spans(
|
||||||
null_audio_glitches(resolve_coverage(camera_name, start_ts, end_ts)),
|
null_audio_glitches(intervals, main_audio, sub_audio),
|
||||||
stream_preference,
|
stream_preference,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -841,9 +843,7 @@ async def vod_hour(
|
|||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC)
|
datetime(int(parts[0]), int(parts[1]), day, hour, tzinfo=UTC)
|
||||||
- datetime.now(
|
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
||||||
ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/")))
|
|
||||||
).utcoffset()
|
|
||||||
)
|
)
|
||||||
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||||
start_ts = start_date.timestamp()
|
start_ts = start_date.timestamp()
|
||||||
@@ -886,8 +886,9 @@ async def vod_event(
|
|||||||
# If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false
|
# If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false
|
||||||
if (
|
if (
|
||||||
event.start_time < datetime.now().timestamp() - 300
|
event.start_time < datetime.now().timestamp() - 300
|
||||||
and isinstance(vod_response, JSONResponse)
|
and type(vod_response) is tuple
|
||||||
and vod_response.status_code == 404
|
and len(vod_response) == 2
|
||||||
|
and vod_response[1] == 404
|
||||||
):
|
):
|
||||||
Event.update(has_clip=False).where(Event.id == event_id).execute()
|
Event.update(has_clip=False).where(Event.id == event_id).execute()
|
||||||
|
|
||||||
@@ -955,80 +956,64 @@ async def event_snapshot(
|
|||||||
event_complete = False
|
event_complete = False
|
||||||
jpg_bytes = None
|
jpg_bytes = None
|
||||||
frame_time = 0
|
frame_time = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
event = Event.get(Event.id == event_id, Event.end_time != None)
|
event = Event.get(Event.id == event_id, Event.end_time != None)
|
||||||
await require_camera_access(event.camera, request=request)
|
|
||||||
except DoesNotExist:
|
|
||||||
event = None
|
|
||||||
|
|
||||||
if event is not None:
|
|
||||||
event_complete = True
|
event_complete = True
|
||||||
|
await require_camera_access(event.camera, request=request)
|
||||||
if not event.has_snapshot:
|
if not event.has_snapshot:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Snapshot not available"},
|
content={"success": False, "message": "Snapshot not available"},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
snapshot_settings = _resolve_snapshot_settings(
|
||||||
|
request.app.frigate_config.cameras[event.camera].snapshots, params
|
||||||
|
)
|
||||||
|
jpg_bytes, frame_time = get_event_snapshot_bytes(
|
||||||
|
event,
|
||||||
|
ext="jpg",
|
||||||
|
timestamp=snapshot_settings["timestamp"],
|
||||||
|
bounding_box=snapshot_settings["bounding_box"],
|
||||||
|
crop=snapshot_settings["crop"],
|
||||||
|
height=snapshot_settings["height"],
|
||||||
|
quality=snapshot_settings["quality"],
|
||||||
|
timestamp_style=request.app.frigate_config.cameras[
|
||||||
|
event.camera
|
||||||
|
].timestamp_style,
|
||||||
|
colormap=request.app.frigate_config.model_for_camera(event.camera).colormap,
|
||||||
|
)
|
||||||
|
except DoesNotExist:
|
||||||
|
# see if the object is currently being tracked
|
||||||
try:
|
try:
|
||||||
snapshot_settings = _resolve_snapshot_settings(
|
camera_states: list[CameraState] = (
|
||||||
request.app.frigate_config.cameras[event.camera].snapshots, params
|
request.app.detected_frames_processor.get_camera_states()
|
||||||
)
|
|
||||||
jpg_bytes, frame_time = get_event_snapshot_bytes(
|
|
||||||
event,
|
|
||||||
ext="jpg",
|
|
||||||
timestamp=snapshot_settings["timestamp"],
|
|
||||||
bounding_box=snapshot_settings["bounding_box"],
|
|
||||||
crop=snapshot_settings["crop"],
|
|
||||||
height=snapshot_settings["height"],
|
|
||||||
quality=snapshot_settings["quality"],
|
|
||||||
timestamp_style=request.app.frigate_config.cameras[
|
|
||||||
event.camera
|
|
||||||
].timestamp_style,
|
|
||||||
colormap=request.app.frigate_config.model_for_camera(
|
|
||||||
event.camera
|
|
||||||
).colormap,
|
|
||||||
)
|
)
|
||||||
|
for camera_state in camera_states:
|
||||||
|
if event_id in camera_state.tracked_objects:
|
||||||
|
tracked_obj = camera_state.tracked_objects.get(event_id)
|
||||||
|
if tracked_obj is not None:
|
||||||
|
snapshot_settings = _resolve_snapshot_settings(
|
||||||
|
camera_state.camera_config.snapshots, params
|
||||||
|
)
|
||||||
|
jpg_bytes, frame_time = tracked_obj.get_img_bytes(
|
||||||
|
ext="jpg",
|
||||||
|
timestamp=snapshot_settings["timestamp"],
|
||||||
|
bounding_box=snapshot_settings["bounding_box"],
|
||||||
|
crop=snapshot_settings["crop"],
|
||||||
|
height=snapshot_settings["height"],
|
||||||
|
quality=snapshot_settings["quality"],
|
||||||
|
)
|
||||||
|
await require_camera_access(camera_state.name, request=request)
|
||||||
except Exception:
|
except Exception:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Unknown error occurred"},
|
content={"success": False, "message": "Ongoing event not found"},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
else:
|
except Exception:
|
||||||
# see if the object is currently being tracked
|
return JSONResponse(
|
||||||
camera_states: list[CameraState] = (
|
content={"success": False, "message": "Unknown error occurred"},
|
||||||
request.app.detected_frames_processor.get_camera_states()
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
for camera_state in camera_states:
|
|
||||||
tracked_obj = camera_state.tracked_objects.get(event_id)
|
|
||||||
|
|
||||||
if tracked_obj is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
await require_camera_access(camera_state.name, request=request)
|
|
||||||
|
|
||||||
try:
|
|
||||||
snapshot_settings = _resolve_snapshot_settings(
|
|
||||||
camera_state.camera_config.snapshots, params
|
|
||||||
)
|
|
||||||
jpg_bytes, frame_time = tracked_obj.get_img_bytes(
|
|
||||||
ext="jpg",
|
|
||||||
timestamp=snapshot_settings["timestamp"],
|
|
||||||
bounding_box=snapshot_settings["bounding_box"],
|
|
||||||
crop=snapshot_settings["crop"],
|
|
||||||
height=snapshot_settings["height"],
|
|
||||||
quality=snapshot_settings["quality"],
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": "Ongoing event not found"},
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
if jpg_bytes is None:
|
if jpg_bytes is None:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Live frame not available"},
|
content={"success": False, "message": "Live frame not available"},
|
||||||
@@ -1077,25 +1062,19 @@ async def event_thumbnail(
|
|||||||
|
|
||||||
if not thumbnail_bytes:
|
if not thumbnail_bytes:
|
||||||
# see if the object is currently being tracked
|
# see if the object is currently being tracked
|
||||||
camera_states = request.app.detected_frames_processor.get_camera_states()
|
try:
|
||||||
|
camera_states = request.app.detected_frames_processor.get_camera_states()
|
||||||
for camera_state in camera_states:
|
for camera_state in camera_states:
|
||||||
tracked_obj = camera_state.tracked_objects.get(event_id)
|
if event_id in camera_state.tracked_objects:
|
||||||
|
tracked_obj = camera_state.tracked_objects.get(event_id)
|
||||||
if tracked_obj is None:
|
if tracked_obj is not None:
|
||||||
continue
|
await require_camera_access(camera_state.name, request=request)
|
||||||
|
thumbnail_bytes = tracked_obj.get_thumbnail(extension.value)
|
||||||
await require_camera_access(camera_state.name, request=request)
|
except Exception:
|
||||||
|
return JSONResponse(
|
||||||
try:
|
content={"success": False, "message": "Event not found"},
|
||||||
thumbnail_bytes = tracked_obj.get_thumbnail(extension.value)
|
status_code=404,
|
||||||
except Exception:
|
)
|
||||||
return JSONResponse(
|
|
||||||
content={"success": False, "message": "Event not found"},
|
|
||||||
status_code=404,
|
|
||||||
)
|
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
if not thumbnail_bytes:
|
if not thumbnail_bytes:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
|
|||||||
+10
-42
@@ -13,9 +13,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote, urlparse
|
||||||
|
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
@@ -25,8 +24,6 @@ from frigate.models import Export, User
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_URI_SCHEME_AND_AUTHORITY = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?]*")
|
|
||||||
|
|
||||||
|
|
||||||
class MediaAuthResolution(str, Enum):
|
class MediaAuthResolution(str, Enum):
|
||||||
"""Classification of an `X-Original-URL` path for media-auth purposes."""
|
"""Classification of an `X-Original-URL` path for media-auth purposes."""
|
||||||
@@ -44,36 +41,6 @@ class MediaAuthResolution(str, Enum):
|
|||||||
UNKNOWN = "unknown"
|
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:
|
def extract_path(original_url: str | None) -> str | None:
|
||||||
"""Return the decoded path component of nginx's `X-Original-URL` header.
|
"""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`
|
request like `/recordings/.../allowed_cam/../forbidden_cam/file.mp4`
|
||||||
would (1) parse as the allowed camera in our auth check, (2) be served
|
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
|
as the forbidden camera by nginx. To close the bypass we reject any URI
|
||||||
whose path contains `.` or `..` segments outright. Repeated slashes are
|
whose path contains `.` or `..` segments outright.
|
||||||
merged, as nginx does before matching locations.
|
|
||||||
"""
|
"""
|
||||||
if not original_url:
|
if not original_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
path, _ = split_request_uri(original_url)
|
parsed = urlparse(original_url)
|
||||||
decoded = re.sub(r"/{2,}", "/", unquote(path))
|
raw_path = parsed.path or original_url
|
||||||
|
decoded = unquote(raw_path)
|
||||||
if not decoded:
|
if not decoded:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -285,11 +252,12 @@ def deny_response_for_media_uri(
|
|||||||
path = extract_path(original_url)
|
path = extract_path(original_url)
|
||||||
|
|
||||||
# `extract_path` returns None for URIs containing `.` or `..` segments.
|
# `extract_path` returns None for URIs containing `.` or `..` segments.
|
||||||
# Deny them when nginx would resolve them under a media root, and pass
|
# For media-root URIs that's a traversal attempt — deny outright. For
|
||||||
# the rest through to nginx and the backend.
|
# non-media URIs, pass through (nginx / the backend handle them).
|
||||||
if path is None:
|
if path is None:
|
||||||
normalized = normalize_request_path(split_request_uri(original_url)[0])
|
raw = urlparse(original_url).path or original_url
|
||||||
first = normalized.lstrip("/").split("/", 1)[0]
|
decoded = unquote(raw)
|
||||||
|
first = decoded.lstrip("/").split("/", 1)[0] if decoded else ""
|
||||||
if first in ("clips", "recordings", "exports"):
|
if first in ("clips", "recordings", "exports"):
|
||||||
return 403
|
return 403
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -9,10 +9,7 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from frigate.api.auth import require_camera_access
|
from frigate.api.auth import require_camera_access
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.config import FrigateConfig
|
|
||||||
from frigate.jobs.motion_search import (
|
from frigate.jobs.motion_search import (
|
||||||
MotionSearchCapacityError,
|
|
||||||
MotionSearchUserBusyError,
|
|
||||||
cancel_motion_search_job,
|
cancel_motion_search_job,
|
||||||
get_motion_search_job,
|
get_motion_search_job,
|
||||||
start_motion_search_job,
|
start_motion_search_job,
|
||||||
@@ -98,20 +95,6 @@ class MotionSearchStatusResponse(BaseModel):
|
|||||||
progress: float | None = None
|
progress: float | None = None
|
||||||
|
|
||||||
|
|
||||||
def _per_user_identity(request: Request, config: FrigateConfig) -> str | None:
|
|
||||||
"""Return the requesting user, or None when callers share one identity.
|
|
||||||
|
|
||||||
Internal-port requests are all "anonymous", and with auth disabled and no
|
|
||||||
proxy user header every browser is "viewer".
|
|
||||||
"""
|
|
||||||
username = request.headers.get("remote-user")
|
|
||||||
if not username or username == "anonymous":
|
|
||||||
return None
|
|
||||||
if config.auth.enabled or config.proxy.header_map.user:
|
|
||||||
return username
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/{camera_name}/search/motion",
|
"/{camera_name}/search/motion",
|
||||||
response_model=MotionSearchStartResponse,
|
response_model=MotionSearchStartResponse,
|
||||||
@@ -155,35 +138,18 @@ async def start_motion_search(
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
# Start the job using the jobs module
|
||||||
job_id = start_motion_search_job(
|
job_id = start_motion_search_job(
|
||||||
config=config,
|
config=config,
|
||||||
camera_name=camera_name,
|
camera_name=camera_name,
|
||||||
start_time=body.start_time,
|
start_time=body.start_time,
|
||||||
end_time=body.end_time,
|
end_time=body.end_time,
|
||||||
polygon_points=body.polygon_points,
|
polygon_points=body.polygon_points,
|
||||||
threshold=body.threshold,
|
threshold=body.threshold,
|
||||||
min_area=body.min_area,
|
min_area=body.min_area,
|
||||||
parallel=body.parallel,
|
parallel=body.parallel,
|
||||||
max_results=body.max_results,
|
max_results=body.max_results,
|
||||||
username=_per_user_identity(request, config),
|
)
|
||||||
)
|
|
||||||
except MotionSearchUserBusyError:
|
|
||||||
return JSONResponse(
|
|
||||||
content={
|
|
||||||
"success": False,
|
|
||||||
"message": "A motion search is already running for this user",
|
|
||||||
},
|
|
||||||
status_code=409,
|
|
||||||
)
|
|
||||||
except MotionSearchCapacityError:
|
|
||||||
return JSONResponse(
|
|
||||||
content={
|
|
||||||
"success": False,
|
|
||||||
"message": "Too many motion searches are running",
|
|
||||||
},
|
|
||||||
status_code=429,
|
|
||||||
)
|
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={
|
content={
|
||||||
|
|||||||
+22
-50
@@ -14,18 +14,17 @@ router = APIRouter(tags=[Tags.notices])
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/notices", dependencies=[Depends(require_role(["admin"]))])
|
@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.
|
"""Get notices, most severe first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
include_hidden: Also return acknowledged and muted notices, for the
|
include_dismissed: Also return dismissed notices, for the history view
|
||||||
hidden list
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The notices
|
The notices
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
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())
|
return JSONResponse(content=request.app.notice_registry.stats())
|
||||||
|
|
||||||
|
|
||||||
@router.get("/notices/muted_checks", dependencies=[Depends(require_role(["admin"]))])
|
@router.get(
|
||||||
def get_muted_checks(request: Request) -> JSONResponse:
|
"/notices/dismissed_checks", dependencies=[Depends(require_role(["admin"]))]
|
||||||
"""Get the muted config and stream check rows, newest first."""
|
)
|
||||||
return JSONResponse(content=request.app.notice_registry.muted_checks())
|
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"]))])
|
@router.delete("/notices/dismissed", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def unhide_all_notices(request: Request) -> JSONResponse:
|
def purge_dismissed(request: Request) -> JSONResponse:
|
||||||
"""Show every acknowledged and muted notice and check row again."""
|
"""Delete every dismissed notice and check row so each can show again."""
|
||||||
request.app.notice_registry.unhide_all()
|
request.app.notice_registry.purge_dismissed()
|
||||||
return JSONResponse(content={"success": True, "message": "Notices shown again"})
|
return JSONResponse(
|
||||||
|
content={"success": True, "message": "Dismissed notices cleared"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# model notice ids contain a slash, so the id is a path parameter
|
# model notice ids contain a slash, so the id is a path parameter
|
||||||
@router.post(
|
@router.post(
|
||||||
"/notices/{notice_id:path}/acknowledge",
|
"/notices/{notice_id:path}/dismiss",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
)
|
)
|
||||||
def acknowledge_notice(request: Request, notice_id: str) -> JSONResponse:
|
def dismiss_notice(request: Request, notice_id: str) -> JSONResponse:
|
||||||
"""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
|
It stays hidden if the same problem happens again.
|
||||||
can only be muted.
|
|
||||||
"""
|
"""
|
||||||
if not request.app.notice_registry.acknowledge(notice_id):
|
if not request.app.notice_registry.dismiss(notice_id):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content={"success": False, "message": "Notice not found"},
|
content={"success": False, "message": "Notice not found"},
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(content={"success": True, "message": "Notice acknowledged"})
|
return JSONResponse(content={"success": True, "message": "Notice dismissed"})
|
||||||
|
|
||||||
|
|
||||||
@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"})
|
|
||||||
|
|||||||
+75
-16
@@ -1,17 +1,19 @@
|
|||||||
"""Notification apis."""
|
"""Notification apis."""
|
||||||
|
|
||||||
|
import ipaddress
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from cryptography.hazmat.primitives import serialization
|
from cryptography.hazmat.primitives import serialization
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from peewee import DoesNotExist
|
||||||
from py_vapid import Vapid01, utils
|
from py_vapid import Vapid01, utils
|
||||||
|
|
||||||
from frigate.api.auth import allow_any_authenticated
|
from frigate.api.auth import allow_any_authenticated
|
||||||
from frigate.api.defs.tags import Tags
|
from frigate.api.defs.tags import Tags
|
||||||
from frigate.comms.webpush import validate_push_endpoint
|
|
||||||
from frigate.const import CONFIG_DIR
|
from frigate.const import CONFIG_DIR
|
||||||
from frigate.models import User
|
from frigate.models import User
|
||||||
|
|
||||||
@@ -19,13 +21,76 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
router = APIRouter(tags=[Tags.notifications])
|
router = APIRouter(tags=[Tags.notifications])
|
||||||
|
|
||||||
|
# Push endpoints are opaque URLs but stay well under this in practice
|
||||||
|
MAX_ENDPOINT_LENGTH = 2048
|
||||||
|
|
||||||
|
# Suffixes that only ever resolve on the local network
|
||||||
|
INTERNAL_HOST_SUFFIXES = (".local", ".localdomain", ".internal", ".home.arpa")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_push_endpoint(endpoint: Any) -> str | None:
|
||||||
|
"""Return a reason the endpoint is unusable, or None when it is valid.
|
||||||
|
|
||||||
|
Subscriptions are issued by the browser vendor's push service, so a valid
|
||||||
|
endpoint is always a public https URL. Anything else is either a broken
|
||||||
|
registration or an attempt to aim the notification sender somewhere it
|
||||||
|
should not reach.
|
||||||
|
"""
|
||||||
|
if not isinstance(endpoint, str) or not endpoint:
|
||||||
|
return "endpoint must be a url"
|
||||||
|
|
||||||
|
if len(endpoint) > MAX_ENDPOINT_LENGTH:
|
||||||
|
return "endpoint is too long"
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = urlparse(endpoint)
|
||||||
|
port = parsed.port
|
||||||
|
except ValueError:
|
||||||
|
return "endpoint is not a valid url"
|
||||||
|
|
||||||
|
if parsed.scheme != "https":
|
||||||
|
return "endpoint must use https"
|
||||||
|
|
||||||
|
if parsed.username or parsed.password:
|
||||||
|
return "endpoint must not include credentials"
|
||||||
|
|
||||||
|
if port is not None and port != 443:
|
||||||
|
return "endpoint must use the default https port"
|
||||||
|
|
||||||
|
hostname = parsed.hostname
|
||||||
|
|
||||||
|
if not hostname:
|
||||||
|
return "endpoint must include a hostname"
|
||||||
|
|
||||||
|
try:
|
||||||
|
address = ipaddress.ip_address(hostname)
|
||||||
|
except ValueError:
|
||||||
|
address = None
|
||||||
|
|
||||||
|
if address is not None:
|
||||||
|
# A push service is never reachable at an address only this network can
|
||||||
|
# route, so anything non-global is a misconfiguration at best
|
||||||
|
if not address.is_global:
|
||||||
|
return "endpoint must not use a private address"
|
||||||
|
elif hostname == "localhost" or "." not in hostname:
|
||||||
|
return "endpoint must use a fully qualified hostname"
|
||||||
|
elif hostname.endswith(INTERNAL_HOST_SUFFIXES):
|
||||||
|
return "endpoint must not use an internal hostname"
|
||||||
|
|
||||||
|
# The subscription token lives in the path, and webpush.py assumes there is
|
||||||
|
# a separator after the host when it builds the VAPID audience
|
||||||
|
if len(parsed.path) <= 1:
|
||||||
|
return "endpoint must include a subscription path"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _validate_subscription(sub: Any) -> str | None:
|
def _validate_subscription(sub: Any) -> str | None:
|
||||||
"""Return a reason the subscription is unusable, or None when it is valid."""
|
"""Return a reason the subscription is unusable, or None when it is valid."""
|
||||||
if not isinstance(sub, dict):
|
if not isinstance(sub, dict):
|
||||||
return "subscription must be an object"
|
return "subscription must be an object"
|
||||||
|
|
||||||
reason = validate_push_endpoint(sub.get("endpoint"))
|
reason = _validate_push_endpoint(sub.get("endpoint"))
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
return reason
|
return reason
|
||||||
@@ -108,22 +173,16 @@ def register_notifications(request: Request, body: dict = None):
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
updated = (
|
try:
|
||||||
User.update(notification_tokens=User.notification_tokens.append(sub))
|
User.update(notification_tokens=User.notification_tokens.append(sub)).where(
|
||||||
.where(User.username == username)
|
User.username == username
|
||||||
.execute()
|
).execute()
|
||||||
)
|
return JSONResponse(
|
||||||
|
content=({"success": True, "message": "Successfully saved token."}),
|
||||||
if not updated:
|
status_code=200,
|
||||||
logger.warning(
|
|
||||||
"Rejected notification registration for unknown user %s", username
|
|
||||||
)
|
)
|
||||||
|
except DoesNotExist:
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=({"success": False, "message": "Could not find user."}),
|
content=({"success": False, "message": "Could not find user."}),
|
||||||
status_code=404,
|
status_code=404,
|
||||||
)
|
)
|
||||||
|
|
||||||
return JSONResponse(
|
|
||||||
content=({"success": True, "message": "Successfully saved token."}),
|
|
||||||
status_code=200,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta
|
||||||
from zoneinfo import ZoneInfo
|
|
||||||
|
|
||||||
|
import pytz
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import JSONResponse
|
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.api.defs.tags import Tags
|
||||||
from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE
|
from frigate.const import BASE_DIR, CACHE_DIR, PREVIEW_FRAME_TYPE
|
||||||
from frigate.models import Previews
|
from frigate.models import Previews
|
||||||
from frigate.util.time import get_normalized_tz_name
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -127,9 +126,7 @@ def preview_hour(
|
|||||||
parts = year_month.split("-")
|
parts = year_month.split("-")
|
||||||
start_date = (
|
start_date = (
|
||||||
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC)
|
datetime(int(parts[0]), int(parts[1]), int(day), int(hour), tzinfo=UTC)
|
||||||
- datetime.now(
|
- datetime.now(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
||||||
ZoneInfo(get_normalized_tz_name(tz_name.replace(",", "/")))
|
|
||||||
).utcoffset()
|
|
||||||
)
|
)
|
||||||
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
end_date = start_date + timedelta(hours=1) - timedelta(milliseconds=1)
|
||||||
start_ts = start_date.timestamp()
|
start_ts = start_date.timestamp()
|
||||||
|
|||||||
+11
-5
@@ -36,7 +36,6 @@ from frigate.util.recording_coverage import (
|
|||||||
coverage_spans,
|
coverage_spans,
|
||||||
known_video_codecs,
|
known_video_codecs,
|
||||||
realized_timelines,
|
realized_timelines,
|
||||||
recording_overlap,
|
|
||||||
resolve_coverage,
|
resolve_coverage,
|
||||||
stream_media_summary,
|
stream_media_summary,
|
||||||
)
|
)
|
||||||
@@ -176,7 +175,7 @@ async def all_recordings_summary(
|
|||||||
@router.get(
|
@router.get(
|
||||||
"/{camera_name}/recordings/summary", dependencies=[Depends(require_camera_access)]
|
"/{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"""
|
"""Returns hourly summary for recordings of given camera"""
|
||||||
|
|
||||||
time_range_query = (
|
time_range_query = (
|
||||||
@@ -413,8 +412,11 @@ async def no_recordings(
|
|||||||
if not camera_list:
|
if not camera_list:
|
||||||
return JSONResponse(content=[])
|
return JSONResponse(content=[])
|
||||||
|
|
||||||
before = params.before or datetime.now().timestamp()
|
before = params.before or datetime.datetime.now().timestamp()
|
||||||
after = params.after or (datetime.now() - timedelta(hours=1)).timestamp()
|
after = (
|
||||||
|
params.after
|
||||||
|
or (datetime.datetime.now() - datetime.timedelta(hours=1)).timestamp()
|
||||||
|
)
|
||||||
scale = params.scale
|
scale = params.scale
|
||||||
|
|
||||||
recordings: list[tuple[float, float]] = []
|
recordings: list[tuple[float, float]] = []
|
||||||
@@ -538,7 +540,11 @@ async def delete_recordings(
|
|||||||
|
|
||||||
# Build query to find overlapping recordings
|
# Build query to find overlapping recordings
|
||||||
clauses = [
|
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),
|
(Recordings.camera << camera_list),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+20
-23
@@ -37,7 +37,6 @@ from frigate.const import STREAM_TYPE_MAIN
|
|||||||
from frigate.embeddings import EmbeddingsContext
|
from frigate.embeddings import EmbeddingsContext
|
||||||
from frigate.models import Recordings, ReviewSegment, UserReviewStatus
|
from frigate.models import Recordings, ReviewSegment, UserReviewStatus
|
||||||
from frigate.review.types import SeverityEnum
|
from frigate.review.types import SeverityEnum
|
||||||
from frigate.util.recording_coverage import recording_overlap
|
|
||||||
from frigate.util.time import get_dst_transitions
|
from frigate.util.time import get_dst_transitions
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -45,22 +44,6 @@ logger = logging.getLogger(__name__)
|
|||||||
router = APIRouter(tags=[Tags.review])
|
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(
|
@router.get(
|
||||||
"/review",
|
"/review",
|
||||||
response_model=list[ReviewSegmentResponse],
|
response_model=list[ReviewSegmentResponse],
|
||||||
@@ -110,7 +93,10 @@ async def review(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
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))
|
clauses.append(reduce(operator.or_, label_clauses))
|
||||||
|
|
||||||
if zones != "all":
|
if zones != "all":
|
||||||
@@ -253,7 +239,10 @@ async def review_summary(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
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))
|
clauses.append(reduce(operator.or_, label_clauses))
|
||||||
if zones != "all":
|
if zones != "all":
|
||||||
# use matching so segments with multiple zones
|
# use matching so segments with multiple zones
|
||||||
@@ -351,8 +340,9 @@ async def review_summary(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
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))
|
clauses.append(reduce(operator.or_, label_clauses))
|
||||||
|
|
||||||
# Find the time range of available data
|
# Find the time range of available data
|
||||||
@@ -586,7 +576,14 @@ def delete_reviews(body: ReviewModifyMultipleBody):
|
|||||||
camera_name = review["camera"]
|
camera_name = review["camera"]
|
||||||
recordings = (
|
recordings = (
|
||||||
Recordings.select(Recordings.id, Recordings.path)
|
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)
|
.where(Recordings.camera == camera_name)
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
@@ -696,7 +693,7 @@ def motion_activity(
|
|||||||
df = df[df["camera"] != ""]
|
df = df[df["camera"] != ""]
|
||||||
|
|
||||||
# change types for output
|
# 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")
|
normalized = df.reset_index().to_dict("records")
|
||||||
return JSONResponse(content=normalized)
|
return JSONResponse(content=normalized)
|
||||||
|
|
||||||
|
|||||||
+16
-43
@@ -12,8 +12,8 @@ from pathlib import Path
|
|||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from peewee import SqliteDatabase
|
|
||||||
from peewee_migrate import Router
|
from peewee_migrate import Router
|
||||||
|
from playhouse.sqlite_ext import SqliteExtDatabase
|
||||||
|
|
||||||
from frigate.api.auth import hash_password
|
from frigate.api.auth import hash_password
|
||||||
from frigate.api.fastapi_app import create_fastapi_app
|
from frigate.api.fastapi_app import create_fastapi_app
|
||||||
@@ -49,6 +49,7 @@ from frigate.debug_replay import (
|
|||||||
DebugReplayManager,
|
DebugReplayManager,
|
||||||
cleanup_replay_cameras,
|
cleanup_replay_cameras,
|
||||||
)
|
)
|
||||||
|
from frigate.detectors.detector_config import SceneEnum
|
||||||
from frigate.detectors.detector_types import api_types
|
from frigate.detectors.detector_types import api_types
|
||||||
from frigate.detectors.device import build_detector_config, runner_names
|
from frigate.detectors.device import build_detector_config, runner_names
|
||||||
from frigate.embeddings import EmbeddingProcess, EmbeddingsContext
|
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.base import ObjectDetectProcess
|
||||||
from frigate.object_detection.util import detection_frame_size
|
from frigate.object_detection.util import detection_frame_size
|
||||||
from frigate.output.output import OutputProcess
|
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.ptz.onvif import OnvifController
|
||||||
from frigate.record.cleanup import RecordingCleanup
|
from frigate.record.cleanup import RecordingCleanup
|
||||||
from frigate.record.export import migrate_exports
|
from frigate.record.export import migrate_exports
|
||||||
@@ -107,7 +108,7 @@ class FrigateApp:
|
|||||||
self.metrics_manager = manager
|
self.metrics_manager = manager
|
||||||
self.audio_process: mp.Process | None = None
|
self.audio_process: mp.Process | None = None
|
||||||
self.stop_event = stop_event
|
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
|
model.scene: mp.Queue() for model in config.models
|
||||||
}
|
}
|
||||||
self.detectors: dict[str, ObjectDetectProcess] = {}
|
self.detectors: dict[str, ObjectDetectProcess] = {}
|
||||||
@@ -166,7 +167,11 @@ class FrigateApp:
|
|||||||
# create camera_metrics
|
# create camera_metrics
|
||||||
for camera_name in self.config.cameras.keys():
|
for camera_name in self.config.cameras.keys():
|
||||||
self.camera_metrics[camera_name] = CameraMetrics(self.metrics_manager)
|
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:
|
def init_queues(self) -> None:
|
||||||
# Queue for cameras to push tracked objects to
|
# Queue for cameras to push tracked objects to
|
||||||
@@ -186,7 +191,7 @@ class FrigateApp:
|
|||||||
self.timeline_queue: Queue = mp.Queue()
|
self.timeline_queue: Queue = mp.Queue()
|
||||||
|
|
||||||
def init_database(self) -> None:
|
def init_database(self) -> None:
|
||||||
def vacuum_db(db: SqliteDatabase) -> None:
|
def vacuum_db(db: SqliteExtDatabase) -> None:
|
||||||
logger.info("Running database vacuum")
|
logger.info("Running database vacuum")
|
||||||
db.execute_sql("VACUUM;")
|
db.execute_sql("VACUUM;")
|
||||||
|
|
||||||
@@ -197,7 +202,7 @@ class FrigateApp:
|
|||||||
logger.error("Unable to write to /config to save DB state")
|
logger.error("Unable to write to /config to save DB state")
|
||||||
|
|
||||||
# Migrate DB schema
|
# Migrate DB schema
|
||||||
migrate_db = SqliteDatabase(self.config.database.path)
|
migrate_db = SqliteExtDatabase(self.config.database.path)
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
del logging.getLogger("peewee_migrate").handlers[:]
|
del logging.getLogger("peewee_migrate").handlers[:]
|
||||||
@@ -390,7 +395,7 @@ class FrigateApp:
|
|||||||
logger.error("Unable to prepare the %s runtime: %s", detector_type, err)
|
logger.error("Unable to prepare the %s runtime: %s", detector_type, err)
|
||||||
|
|
||||||
def start_detectors(self) -> None:
|
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
|
model.scene: [] for model in self.config.models
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -439,7 +444,7 @@ class FrigateApp:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def start_ptz_autotracker(self) -> None:
|
def start_ptz_autotracker(self) -> None:
|
||||||
self.ptz_autotracker_thread = PtzAutoTracker(
|
self.ptz_autotracker_thread = PtzAutoTrackerThread(
|
||||||
self.config,
|
self.config,
|
||||||
self.onvif_controller,
|
self.onvif_controller,
|
||||||
self.ptz_metrics,
|
self.ptz_metrics,
|
||||||
@@ -478,7 +483,7 @@ class FrigateApp:
|
|||||||
|
|
||||||
def start_audio_processor(self) -> None:
|
def start_audio_processor(self) -> None:
|
||||||
self.audio_process = AudioProcessor(
|
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.audio_process.start()
|
||||||
self.processes["audio_detector"] = self.audio_process.pid or 0
|
self.processes["audio_detector"] = self.audio_process.pid or 0
|
||||||
@@ -551,20 +556,10 @@ class FrigateApp:
|
|||||||
"output",
|
"output",
|
||||||
lambda: OutputProcess(self.config, self.stop_event),
|
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:
|
for attr, key, factory in specs:
|
||||||
if getattr(self, attr, None) is None:
|
if not hasattr(self, attr):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
def on_restart(
|
def on_restart(
|
||||||
@@ -581,12 +576,7 @@ class FrigateApp:
|
|||||||
|
|
||||||
def init_auth(self) -> None:
|
def init_auth(self) -> None:
|
||||||
if self.config.auth.enabled:
|
if self.config.auth.enabled:
|
||||||
if (
|
if User.select().count() == 0:
|
||||||
User.select().count() == 0
|
|
||||||
or User.select()
|
|
||||||
.where((User.username == "admin") & (User.password_hash == ""))
|
|
||||||
.exists()
|
|
||||||
):
|
|
||||||
password = secrets.token_hex(16)
|
password = secrets.token_hex(16)
|
||||||
password_hash = hash_password(
|
password_hash = hash_password(
|
||||||
password, iterations=self.config.auth.hash_iterations
|
password, iterations=self.config.auth.hash_iterations
|
||||||
@@ -598,9 +588,6 @@ class FrigateApp:
|
|||||||
User.password_hash: password_hash,
|
User.password_hash: password_hash,
|
||||||
User.notification_tokens: [],
|
User.notification_tokens: [],
|
||||||
}
|
}
|
||||||
).on_conflict(
|
|
||||||
conflict_target=[User.username],
|
|
||||||
update={User.password_hash: password_hash},
|
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
self.config.auth.admin_first_time_login = True
|
self.config.auth.admin_first_time_login = True
|
||||||
@@ -622,7 +609,6 @@ class FrigateApp:
|
|||||||
username="admin",
|
username="admin",
|
||||||
role="admin",
|
role="admin",
|
||||||
password_hash=password_hash,
|
password_hash=password_hash,
|
||||||
password_changed_at=datetime.datetime.now(),
|
|
||||||
notification_tokens=[],
|
notification_tokens=[],
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
@@ -632,19 +618,6 @@ class FrigateApp:
|
|||||||
logger.info(f"*** Password: {password} ***")
|
logger.info(f"*** Password: {password} ***")
|
||||||
logger.info("********************************************************")
|
logger.info("********************************************************")
|
||||||
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:
|
def start(self) -> None:
|
||||||
logger.info(f"Starting Frigate ({VERSION})")
|
logger.info(f"Starting Frigate ({VERSION})")
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ class CameraMetrics:
|
|||||||
|
|
||||||
|
|
||||||
class PTZMetrics:
|
class PTZMetrics:
|
||||||
|
autotracker_enabled: Synchronized
|
||||||
|
|
||||||
start_time: Synchronized
|
start_time: Synchronized
|
||||||
stop_time: Synchronized
|
stop_time: Synchronized
|
||||||
frame_time: Synchronized
|
frame_time: Synchronized
|
||||||
@@ -50,17 +52,21 @@ class PTZMetrics:
|
|||||||
max_zoom: Synchronized
|
max_zoom: Synchronized
|
||||||
min_zoom: Synchronized
|
min_zoom: Synchronized
|
||||||
|
|
||||||
|
tracking_active: Event
|
||||||
motor_stopped: Event
|
motor_stopped: Event
|
||||||
reset: Event
|
reset: Event
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self, *, autotracker_enabled: bool):
|
||||||
self.start_time = mp.Value("d", 0)
|
self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment]
|
||||||
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)
|
|
||||||
|
|
||||||
|
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.motor_stopped = mp.Event()
|
||||||
self.reset = mp.Event()
|
self.reset = mp.Event()
|
||||||
|
|
||||||
|
|||||||
@@ -104,13 +104,12 @@ class CameraActivityManager:
|
|||||||
all_objects: list[dict[str, Any]] = []
|
all_objects: list[dict[str, Any]] = []
|
||||||
|
|
||||||
for camera in new_activity.keys():
|
for camera in new_activity.keys():
|
||||||
camera_config = self.config.cameras.get(camera)
|
if camera not in self.config.cameras:
|
||||||
if camera_config is None:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# handle cameras that were added dynamically
|
# handle cameras that were added dynamically
|
||||||
if camera not in self.camera_all_object_counts:
|
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", [])
|
new_objects = new_activity[camera].get("objects", [])
|
||||||
all_objects.extend(new_objects)
|
all_objects.extend(new_objects)
|
||||||
@@ -235,13 +234,12 @@ class AudioActivityManager:
|
|||||||
now = datetime.datetime.now().timestamp()
|
now = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
for camera in new_activity.keys():
|
for camera in new_activity.keys():
|
||||||
camera_config = self.config.cameras.get(camera)
|
if camera not in self.config.cameras:
|
||||||
if camera_config is None:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# handle cameras that were added dynamically
|
# handle cameras that were added dynamically
|
||||||
if camera not in self.current_audio_detections:
|
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", [])
|
new_detections = new_activity[camera].get("detections", [])
|
||||||
if self.compare_audio_activity(camera, new_detections, now):
|
if self.compare_audio_activity(camera, new_detections, now):
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from frigate.config.camera.updater import (
|
|||||||
CameraConfigUpdateSubscriber,
|
CameraConfigUpdateSubscriber,
|
||||||
)
|
)
|
||||||
from frigate.const import REPLAY_CAMERA_PREFIX
|
from frigate.const import REPLAY_CAMERA_PREFIX
|
||||||
|
from frigate.detectors.detector_config import SceneEnum
|
||||||
from frigate.models import Regions
|
from frigate.models import Regions
|
||||||
from frigate.object_detection.util import detection_frame_size
|
from frigate.object_detection.util import detection_frame_size
|
||||||
from frigate.util.builtin import empty_and_close_queue
|
from frigate.util.builtin import empty_and_close_queue
|
||||||
@@ -30,7 +31,7 @@ class CameraMaintainer(threading.Thread):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
detection_queues: dict[str, Queue],
|
detection_queues: dict[SceneEnum, Queue],
|
||||||
detected_frames_queue: Queue,
|
detected_frames_queue: Queue,
|
||||||
camera_metrics: DictProxy,
|
camera_metrics: DictProxy,
|
||||||
ptz_metrics: dict[str, PTZMetrics],
|
ptz_metrics: dict[str, PTZMetrics],
|
||||||
@@ -120,7 +121,9 @@ class CameraMaintainer(threading.Thread):
|
|||||||
|
|
||||||
if runtime:
|
if runtime:
|
||||||
self.camera_metrics[name] = CameraMetrics(self.metrics_manager)
|
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(
|
self.region_grids[name] = get_camera_regions_grid(
|
||||||
name,
|
name,
|
||||||
config.detect,
|
config.detect,
|
||||||
@@ -169,10 +172,6 @@ class CameraMaintainer(threading.Thread):
|
|||||||
frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1]
|
frame_size = config.frame_shape_yuv[0] * config.frame_shape_yuv[1]
|
||||||
self.frame_manager.create(f"{config.name}_frame{i}", frame_size)
|
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(
|
capture_process = CameraCapture(
|
||||||
config,
|
config,
|
||||||
count,
|
count,
|
||||||
|
|||||||
+22
-14
@@ -16,7 +16,7 @@ from frigate.config import (
|
|||||||
ZoomingModeEnum,
|
ZoomingModeEnum,
|
||||||
)
|
)
|
||||||
from frigate.const import CLIPS_DIR, THUMB_DIR
|
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.track.tracked_object import TrackedObject
|
||||||
from frigate.util.image import (
|
from frigate.util.image import (
|
||||||
SharedMemoryFrameManager,
|
SharedMemoryFrameManager,
|
||||||
@@ -35,7 +35,7 @@ class CameraState:
|
|||||||
name: str,
|
name: str,
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
frame_manager: SharedMemoryFrameManager,
|
frame_manager: SharedMemoryFrameManager,
|
||||||
ptz_autotracker_thread: PtzAutoTracker,
|
ptz_autotracker_thread: PtzAutoTrackerThread,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -115,13 +115,17 @@ class CameraState:
|
|||||||
# draw thicker box around ptz autotracked object
|
# draw thicker box around ptz autotracked object
|
||||||
if (
|
if (
|
||||||
self.camera_config.onvif.autotracking.enabled
|
self.camera_config.onvif.autotracking.enabled
|
||||||
and self.ptz_autotracker_thread.autotracker_init.get(self.name)
|
and self.ptz_autotracker_thread.ptz_autotracker.autotracker_init.get(
|
||||||
and self.ptz_autotracker_thread.tracked_object[self.name]
|
self.name
|
||||||
|
)
|
||||||
|
and self.ptz_autotracker_thread.ptz_autotracker.tracked_object[
|
||||||
|
self.name
|
||||||
|
]
|
||||||
is not None
|
is not None
|
||||||
and obj["id"]
|
and obj["id"]
|
||||||
== self.ptz_autotracker_thread.tracked_object[ # type: ignore[union-attr]
|
== self.ptz_autotracker_thread.ptz_autotracker.tracked_object[
|
||||||
self.name
|
self.name
|
||||||
].obj_data["id"]
|
].obj_data["id"] # type: ignore[attr-defined]
|
||||||
and obj["frame_time"] == frame_time
|
and obj["frame_time"] == frame_time
|
||||||
):
|
):
|
||||||
thickness = 5
|
thickness = 5
|
||||||
@@ -134,9 +138,9 @@ class CameraState:
|
|||||||
and self.camera_config.detect.width is not None
|
and self.camera_config.detect.width is not None
|
||||||
and self.camera_config.detect.height is not None
|
and self.camera_config.detect.height is not None
|
||||||
):
|
):
|
||||||
max_target_box = calculate_max_target_box(
|
max_target_box = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[
|
||||||
self.camera_config.onvif.autotracking.zoom_factor
|
self.name
|
||||||
)
|
]["max_target_box"] # type: ignore[index]
|
||||||
side_length = max_target_box * (
|
side_length = max_target_box * (
|
||||||
max(
|
max(
|
||||||
self.camera_config.detect.width,
|
self.camera_config.detect.width,
|
||||||
@@ -349,7 +353,7 @@ class CameraState:
|
|||||||
return
|
return
|
||||||
|
|
||||||
current_frame = self.frame_manager.get(
|
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()
|
tracked_objects = self.tracked_objects.copy()
|
||||||
@@ -392,6 +396,7 @@ class CameraState:
|
|||||||
"attributes": new_obj.obj_data["attributes"],
|
"attributes": new_obj.obj_data["attributes"],
|
||||||
"current_estimated_speed": 0,
|
"current_estimated_speed": 0,
|
||||||
"velocity_angle": 0,
|
"velocity_angle": 0,
|
||||||
|
"path_data": [],
|
||||||
"recognized_license_plate": None,
|
"recognized_license_plate": None,
|
||||||
"recognized_license_plate_score": None,
|
"recognized_license_plate_score": None,
|
||||||
}
|
}
|
||||||
@@ -403,7 +408,7 @@ class CameraState:
|
|||||||
self.send_mqtt_snapshot(new_obj, object_type)
|
self.send_mqtt_snapshot(new_obj, object_type)
|
||||||
|
|
||||||
for c in self.callbacks["start"]:
|
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:
|
for id in updated_ids:
|
||||||
updated_obj = tracked_objects[id]
|
updated_obj = tracked_objects[id]
|
||||||
@@ -415,7 +420,7 @@ class CameraState:
|
|||||||
|
|
||||||
if autotracker_update or significant_update:
|
if autotracker_update or significant_update:
|
||||||
for c in self.callbacks["autotrack"]:
|
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:
|
if thumb_update and current_frame is not None:
|
||||||
# ensure this frame is stored in the cache
|
# ensure this frame is stored in the cache
|
||||||
@@ -465,7 +470,7 @@ class CameraState:
|
|||||||
):
|
):
|
||||||
# call event handlers
|
# call event handlers
|
||||||
for c in self.callbacks["update"]:
|
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
|
updated_obj.last_published = frame_time
|
||||||
|
|
||||||
# send MQTT snapshot when object first enters a required zone,
|
# send MQTT snapshot when object first enters a required zone,
|
||||||
@@ -487,7 +492,7 @@ class CameraState:
|
|||||||
removed_obj.obj_data["end_time"] = frame_time
|
removed_obj.obj_data["end_time"] = frame_time
|
||||||
logger.debug(f"{self.name}: end callback for object {id}")
|
logger.debug(f"{self.name}: end callback for object {id}")
|
||||||
for c in self.callbacks["end"]:
|
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?
|
# TODO: can i switch to looking this up and only changing when an event ends?
|
||||||
# maintain best objects
|
# maintain best objects
|
||||||
@@ -602,6 +607,9 @@ class CameraState:
|
|||||||
self.current_frame_time = frame_time
|
self.current_frame_time = frame_time
|
||||||
self._current_frame = np.copy(current_frame)
|
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
|
self.previous_frame_id = frame_name
|
||||||
|
|
||||||
def send_mqtt_snapshot(self, new_obj: TrackedObject, object_type: str) -> None:
|
def send_mqtt_snapshot(self, new_obj: TrackedObject, object_type: str) -> None:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user