mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-19 21:22:50 +03:00
Compare commits
167
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a5eacda8 | ||
|
|
ee2ae1a957 | ||
|
|
ac77d06d83 | ||
|
|
d7bb780ff8 | ||
|
|
893542f4ca | ||
|
|
ecffe9ce25 | ||
|
|
303c6663c5 | ||
|
|
275e18403e | ||
|
|
1a8644fed5 | ||
|
|
add310d63d | ||
|
|
c177849f57 | ||
|
|
c9f7cc8165 | ||
|
|
c42fb58391 | ||
|
|
62ca5b25ea | ||
|
|
698b1ce628 | ||
|
|
16a377233e | ||
|
|
6ff8bbd014 | ||
|
|
b22ef39aad | ||
|
|
7620ea61a8 | ||
|
|
754a4a9888 | ||
|
|
58d6454537 | ||
|
|
9c399fc519 | ||
|
|
48470c4537 | ||
|
|
8fd57d81fc | ||
|
|
99134ed4cd | ||
|
|
822c8e2bda | ||
|
|
df92f07a79 | ||
|
|
7227929a9d | ||
|
|
03ff04311b | ||
|
|
5eb2b6f04d | ||
|
|
ba5330f8cf | ||
|
|
7c6c529314 | ||
|
|
15da89e56c | ||
|
|
64b07889af | ||
|
|
cfa08c56b2 | ||
|
|
2b0a69c028 | ||
|
|
a3ca8742b0 | ||
|
|
d3b2af932c | ||
|
|
b79e043976 | ||
|
|
e61ad52b67 | ||
|
|
41d9577d72 | ||
|
|
cf67f08913 | ||
|
|
ff9eb8f184 | ||
|
|
bc06b8c753 | ||
|
|
e7164882f5 | ||
|
|
135a95d8f2 | ||
|
|
7e402d70df | ||
|
|
186a730790 | ||
|
|
5c32af3c7f | ||
|
|
acc84d7658 | ||
|
|
8e569ff4f9 | ||
|
|
2cb03f442c | ||
|
|
79177db969 | ||
|
|
9d9de6263e | ||
|
|
e8e6326d37 | ||
|
|
c75f7c3241 | ||
|
|
2e92251aa5 | ||
|
|
cc86519603 | ||
|
|
01f3369659 | ||
|
|
7786da465f | ||
|
|
e66da62db3 | ||
|
|
c06a97378c | ||
|
|
f1d1748fb3 | ||
|
|
33cb8d987d | ||
|
|
2273cf2d50 | ||
|
|
f084e6acc7 | ||
|
|
a3ea529eb5 | ||
|
|
c53adf1f72 | ||
|
|
528967a783 | ||
|
|
02dd4df3d9 | ||
|
|
fb67d735b6 | ||
|
|
f929134a1b | ||
|
|
25f8568c66 | ||
|
|
f1c4a9f769 | ||
|
|
28d8f5bbfa | ||
|
|
8b89b79ef5 | ||
|
|
6b1e084fdc | ||
|
|
cb745d0a50 | ||
|
|
9ab52c2be6 | ||
|
|
dab0d85830 | ||
|
|
9b2839f4fb | ||
|
|
c06bf97b5e | ||
|
|
f3723698cd | ||
|
|
e912d0852b | ||
|
|
38b87feece | ||
|
|
9b02a077e9 | ||
|
|
d1cb8395ea | ||
|
|
3d0d12366f | ||
|
|
321e78e65b | ||
|
|
17a8efa09c | ||
|
|
0936f4b4ef | ||
|
|
160d213025 | ||
|
|
24a236a152 | ||
|
|
16eac66463 | ||
|
|
3c57ba5020 | ||
|
|
be497bdb28 | ||
|
|
068068ce19 | ||
|
|
2754f788cc | ||
|
|
b749b0bbc5 | ||
|
|
e37041c879 | ||
|
|
1bb61eb808 | ||
|
|
5e87d101da | ||
|
|
d092189b8e | ||
|
|
6d377e56e4 | ||
|
|
b13164cd3d | ||
|
|
2cdb10a96a | ||
|
|
6051374852 | ||
|
|
abe99e5838 | ||
|
|
698f3c1904 | ||
|
|
22a21c761d | ||
|
|
b5613ca06c | ||
|
|
dfd425655e | ||
|
|
19a7f35c00 | ||
|
|
8266a3d9dd | ||
|
|
2755f7c8c3 | ||
|
|
6259872314 | ||
|
|
bd4f7a3656 | ||
|
|
0ea571abfe | ||
|
|
bf032bc92a | ||
|
|
8b035e36d3 | ||
|
|
b7fae3bafd | ||
|
|
ae88705b88 | ||
|
|
c5f979d522 | ||
|
|
0ea9c96f89 | ||
|
|
ca6a85be29 | ||
|
|
6637103b58 | ||
|
|
19a378af43 | ||
|
|
3e075885f7 | ||
|
|
8858f06624 | ||
|
|
f6e119c89e | ||
|
|
32d15f2cbd | ||
|
|
94146e2051 | ||
|
|
f08e880099 | ||
|
|
e155f67f22 | ||
|
|
c26eb023fc | ||
|
|
bd75d4bba0 | ||
|
|
c95d7f1808 | ||
|
|
e36c7ae118 | ||
|
|
008a02eca8 | ||
|
|
61e50a366f | ||
|
|
dc97f294a0 | ||
|
|
a1c8bf99a7 | ||
|
|
f9a37d4f4f | ||
|
|
0977fa5e15 | ||
|
|
4e196516dd | ||
|
|
6791df7971 | ||
|
|
9664d9ceae | ||
|
|
c959df32c9 | ||
|
|
c5889ef35f | ||
|
|
9443289112 | ||
|
|
40f8ba1f7f | ||
|
|
3941355051 | ||
|
|
397f5253a5 | ||
|
|
1a278630da | ||
|
|
9d0d8a99bb | ||
|
|
bbc412763d | ||
|
|
ac9ac50df5 | ||
|
|
93aa6c4174 | ||
|
|
26e6adee88 | ||
|
|
de416b7ae7 | ||
|
|
06967fec91 | ||
|
|
d69107de33 | ||
|
|
04480a18b6 | ||
|
|
b02aea03cd | ||
|
|
51171319a4 | ||
|
|
b1b725b80a | ||
|
|
50a2b6729e |
@@ -17,9 +17,14 @@ 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'
|
||||||
@@ -29,15 +34,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@v5
|
uses: ASzc/change-string-case-action@v8
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository }}
|
string: ${{ github.repository }}
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v4
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc
|
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
+141
-22
@@ -1,40 +1,159 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
- package-ecosystem: "github-actions"
|
- package-ecosystem: "github-actions"
|
||||||
directory: "/"
|
# Local composite actions are only scanned when their directory is listed
|
||||||
|
directories:
|
||||||
|
- "/"
|
||||||
|
- "/.github/actions/setup"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
cooldown:
|
||||||
target-branch: dev
|
default-days: 3
|
||||||
- package-ecosystem: "docker"
|
open-pull-requests-limit: 5
|
||||||
directory: "/docker"
|
groups:
|
||||||
schedule:
|
actions:
|
||||||
interval: daily
|
patterns: ["*"]
|
||||||
open-pull-requests-limit: 10
|
actions-security:
|
||||||
target-branch: dev
|
applies-to: security-updates
|
||||||
|
patterns: ["*"]
|
||||||
|
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
directory: "/docker/main"
|
directory: "/docker/main"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
cooldown:
|
||||||
target-branch: dev
|
default-days: 3
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
# Sets that fail pip resolution unless they move together
|
||||||
|
peewee:
|
||||||
|
patterns: ["peewee", "peewee-migrate"]
|
||||||
|
crypto:
|
||||||
|
patterns: ["cryptography", "joserfc", "pywebpush", "py-vapid"]
|
||||||
|
pydantic-genai:
|
||||||
|
patterns: ["pydantic", "google-genai"]
|
||||||
|
pip-main-minor-patch:
|
||||||
|
patterns: ["*"]
|
||||||
|
update-types: ["minor", "patch"]
|
||||||
|
pip-main-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns: ["*"]
|
||||||
|
ignore:
|
||||||
|
# numpy 2.5 needs Python 3.12; norfair, tflite_runtime and rknn-toolkit2
|
||||||
|
# cap numpy below 2
|
||||||
|
- dependency-name: "numpy"
|
||||||
|
versions: [">=2"]
|
||||||
|
# Every OpenCV wheel from 4.12 requires numpy 2
|
||||||
|
- dependency-name: "opencv-python-headless"
|
||||||
|
versions: [">=4.12"]
|
||||||
|
- dependency-name: "opencv-contrib-python"
|
||||||
|
versions: [">=4.12"]
|
||||||
|
# aarch64 only. Rockchip rknn-toolkit2 caps protobuf at 4.25.4 and
|
||||||
|
# TensorFlow 2.21 needs protobuf 6.31 or newer.
|
||||||
|
- dependency-name: "tensorflow"
|
||||||
|
versions: [">=2.20"]
|
||||||
|
# Needs Python 3.12 and numpy 2
|
||||||
|
- dependency-name: "librosa"
|
||||||
|
versions: [">=1.0"]
|
||||||
|
# Needs Python 3.12
|
||||||
|
- dependency-name: "scipy"
|
||||||
|
versions: [">=1.18"]
|
||||||
|
# Newer stubs report false errors against peewee 4
|
||||||
|
- dependency-name: "types-peewee"
|
||||||
|
versions: [">=4.1"]
|
||||||
|
# sdist only and imported by HailoRT; needs a Hailo device to test
|
||||||
|
- dependency-name: "netifaces"
|
||||||
|
versions: [">=0.11"]
|
||||||
|
|
||||||
- package-ecosystem: "pip"
|
- package-ecosystem: "pip"
|
||||||
directory: "/docker/tensorrt"
|
directory: "/docker/tensorrt"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
# No pull request check builds these images, so version updates are off.
|
||||||
target-branch: dev
|
# Security updates still open.
|
||||||
|
open-pull-requests-limit: 0
|
||||||
|
groups:
|
||||||
|
pip-trt-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns: ["*"]
|
||||||
|
ignore:
|
||||||
|
# onnx 1.17 and newer write a newer IR and opset than TensorRT 8.6 on
|
||||||
|
# JetPack 6 is known to parse
|
||||||
|
- dependency-name: "onnx"
|
||||||
|
versions: [">=1.17"]
|
||||||
|
# 1.27 moved to CUDA 13, and the image ships the CUDA 12.8 wheel set
|
||||||
|
- dependency-name: "onnxruntime-gpu"
|
||||||
|
versions: [">=1.27"]
|
||||||
|
# Hand-matched CUDA 12.8 set (2c9a25e67); move as a block
|
||||||
|
- dependency-name: "nvidia-*"
|
||||||
|
# Must match the JetPack CUDA runtime
|
||||||
|
- dependency-name: "cuda-python"
|
||||||
|
versions: [">=12.9"]
|
||||||
|
|
||||||
- package-ecosystem: "npm"
|
- package-ecosystem: "npm"
|
||||||
directory: "/web"
|
directory: "/web"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: weekly
|
||||||
open-pull-requests-limit: 10
|
cooldown:
|
||||||
target-branch: dev
|
default-days: 3
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
groups:
|
||||||
|
radix:
|
||||||
|
patterns: ["@radix-ui/*"]
|
||||||
|
rjsf:
|
||||||
|
patterns: ["@rjsf/*"]
|
||||||
|
web-minor-patch:
|
||||||
|
patterns: ["*"]
|
||||||
|
update-types: ["minor", "patch"]
|
||||||
|
web-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns: ["*"]
|
||||||
|
ignore:
|
||||||
|
# typescript-eslint peers typescript below 6.1
|
||||||
|
- dependency-name: "typescript"
|
||||||
|
versions: [">=6.1"]
|
||||||
|
# Tailwind 4 is a config rewrite; scrollbar 4 and merge 3 depend on it
|
||||||
|
- dependency-name: "tailwindcss"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
- dependency-name: "tailwind-scrollbar"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
- dependency-name: "tailwind-merge"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
# Intermittent failure in the UI settings import spec, cause not found
|
||||||
|
- dependency-name: "zod"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
# Every later version clamps the pan when the player hides or drops
|
||||||
|
# pinch gestures near the edges, 4.2.0 included
|
||||||
|
- dependency-name: "react-zoom-pan-pinch"
|
||||||
|
versions: [">3.4.4"]
|
||||||
|
# 0.54 pins a vulnerable dompurify; 0.55+ breaks YAML validation until
|
||||||
|
# monaco-worker-manager releases its PR 4
|
||||||
|
- dependency-name: "monaco-editor"
|
||||||
|
versions: [">=0.54.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: daily
|
interval: weekly
|
||||||
|
cooldown:
|
||||||
|
default-days: 3
|
||||||
|
open-pull-requests-limit: 5
|
||||||
allow:
|
allow:
|
||||||
- dependency-name: "@docusaurus/*"
|
- dependency-name: "@docusaurus/*"
|
||||||
open-pull-requests-limit: 10
|
groups:
|
||||||
target-branch: dev
|
docusaurus:
|
||||||
|
patterns: ["@docusaurus/*"]
|
||||||
|
docs-security:
|
||||||
|
applies-to: security-updates
|
||||||
|
patterns: ["*"]
|
||||||
|
ignore:
|
||||||
|
# A Docusaurus major needs the openapi plugin and theme moved in the
|
||||||
|
# same commit
|
||||||
|
- dependency-name: "@docusaurus/*"
|
||||||
|
update-types: ["version-update:semver-major"]
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ jobs:
|
|||||||
- arm64_build
|
- arm64_build
|
||||||
steps:
|
steps:
|
||||||
- id: lowercaseRepo
|
- id: lowercaseRepo
|
||||||
uses: ASzc/change-string-case-action@v6
|
uses: ASzc/change-string-case-action@v8
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository }}
|
string: ${{ github.repository }}
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: lowercaseRepo
|
- id: lowercaseRepo
|
||||||
uses: ASzc/change-string-case-action@v6
|
uses: ASzc/change-string-case-action@v8
|
||||||
with:
|
with:
|
||||||
string: ${{ github.repository }}
|
string: ${{ github.repository }}
|
||||||
- name: Log in to the Container registry
|
- name: Log in to the Container registry
|
||||||
|
|||||||
@@ -217,6 +217,8 @@ 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.15.20
|
ruff == 0.17.0
|
||||||
|
|
||||||
# types
|
# types
|
||||||
types-peewee == 4.0.*
|
types-peewee == 4.0.*
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
numpy
|
numpy
|
||||||
openvino >= 2026.2.0
|
openvino >= 2026.4.1
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
aiofiles == 25.1.*
|
aiofiles == 25.1.*
|
||||||
click == 8.5.*
|
click == 8.5.*
|
||||||
# FastAPI
|
# FastAPI
|
||||||
aiohttp == 3.12.*
|
aiohttp == 3.14.*
|
||||||
starlette == 0.47.*
|
starlette == 1.7.*
|
||||||
starlette-context == 0.5.*
|
starlette-context == 0.5.*
|
||||||
fastapi[standard-no-fastapi-cloud-cli] == 0.116.*
|
fastapi[standard-no-fastapi-cloud-cli] == 0.142.*
|
||||||
uvicorn == 0.52.*
|
uvicorn == 0.54.*
|
||||||
slowapi == 0.1.*
|
slowapi == 0.1.*
|
||||||
joserfc == 1.6.*
|
joserfc == 1.7.*
|
||||||
cryptography == 46.0.*
|
cryptography == 50.0.*
|
||||||
pathvalidate == 3.3.*
|
pathvalidate == 3.3.*
|
||||||
markupsafe == 3.0.*
|
markupsafe == 3.0.*
|
||||||
python-multipart == 0.0.31
|
python-multipart == 0.0.32
|
||||||
# Classification Model Training
|
# Classification Model Training
|
||||||
tensorflow == 2.19.* ; platform_machine == 'aarch64'
|
tensorflow == 2.19.* ; platform_machine == 'aarch64'
|
||||||
tensorflow-cpu == 2.19.* ; platform_machine == 'x86_64'
|
tensorflow-cpu == 2.21.* ; platform_machine == 'x86_64'
|
||||||
# General
|
# General
|
||||||
mypy == 1.6.1
|
mypy == 2.4.0
|
||||||
onvif-zeep-async == 4.0.*
|
onvif-zeep-async == 4.3.*
|
||||||
paho-mqtt == 2.1.*
|
paho-mqtt == 2.1.*
|
||||||
pandas == 2.2.*
|
pandas == 3.0.*
|
||||||
peewee == 3.17.*
|
peewee == 4.5.*
|
||||||
peewee_migrate == 1.14.*
|
peewee_migrate == 2.3.*
|
||||||
psutil == 7.1.*
|
psutil == 7.2.*
|
||||||
pydantic == 2.10.*
|
pydantic == 2.14.*
|
||||||
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
git+https://github.com/fbcotter/py3nvml#egg=py3nvml
|
||||||
pytz == 2025.*
|
tzdata == 2026.5
|
||||||
pyzmq == 27.1.*
|
pyzmq == 27.2.*
|
||||||
ruamel.yaml == 0.18.*
|
ruamel.yaml == 0.19.*
|
||||||
tzlocal == 5.2
|
tzlocal == 5.4.4
|
||||||
requests == 2.33.*
|
requests == 2.34.*
|
||||||
types-requests == 2.32.*
|
types-requests == 2.33.*
|
||||||
norfair == 2.3.*
|
norfair == 2.3.*
|
||||||
setproctitle == 1.3.*
|
setproctitle == 1.3.*
|
||||||
ws4py == 0.5.*
|
ws4py == 0.6.*
|
||||||
unidecode == 1.4.*
|
unidecode == 1.4.*
|
||||||
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.16.*
|
scipy == 1.17.*
|
||||||
# OpenVino & ONNX
|
# OpenVino & ONNX
|
||||||
openvino == 2025.4.*
|
openvino == 2026.4.*
|
||||||
onnxruntime == 1.22.*
|
onnxruntime == 1.30.*
|
||||||
# Embeddings
|
# Embeddings
|
||||||
transformers == 4.45.*
|
transformers == 5.19.*
|
||||||
# Generative AI
|
# Generative AI
|
||||||
google-genai == 1.58.*
|
google-genai == 2.31.*
|
||||||
ollama == 0.6.*
|
ollama == 0.6.*
|
||||||
openai == 1.65.*
|
openai == 3.26.*
|
||||||
# push notifications
|
# push notifications
|
||||||
py-vapid == 1.9.4
|
py-vapid == 1.9.6
|
||||||
pywebpush == 2.0.*
|
pywebpush == 2.5.*
|
||||||
# alpr
|
# alpr
|
||||||
pyclipper == 1.4.*
|
pyclipper == 1.4.*
|
||||||
shapely == 2.0.*
|
shapely == 2.2.*
|
||||||
rapidfuzz==3.12.*
|
rapidfuzz==3.14.*
|
||||||
# HailoRT
|
# HailoRT
|
||||||
argcomplete==2.0.*
|
argcomplete==3.7.*
|
||||||
contextlib2==0.6.*
|
contextlib2==21.6.*
|
||||||
future==0.18.*
|
future==1.0.*
|
||||||
netaddr==1.3.*
|
netaddr==1.3.*
|
||||||
netifaces==0.10.*
|
netifaces==0.10.*
|
||||||
prometheus-client == 0.26.*
|
prometheus-client == 0.26.*
|
||||||
@@ -71,6 +71,6 @@ tflite_runtime @ https://github.com/feranick/TFlite-builds/releases/download/v2.
|
|||||||
sherpa-onnx==1.13.*
|
sherpa-onnx==1.13.*
|
||||||
faster-whisper==1.2.*
|
faster-whisper==1.2.*
|
||||||
librosa==0.11.*
|
librosa==0.11.*
|
||||||
soundfile==0.13.*
|
soundfile==0.14.*
|
||||||
# Memory profiling
|
# Memory profiling
|
||||||
memray == 1.20.*
|
memray == 1.20.*
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
scikit-build == 0.18.*
|
scikit-build == 0.19.*
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ do
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
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'`
|
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'`
|
||||||
|
|
||||||
case "$liveprint" in
|
case "$liveprint" in
|
||||||
*Fingerprint*)
|
*Fingerprint*)
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ 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,
|
||||||
@@ -174,6 +175,17 @@ 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")
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ 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 $scheme://$http_host$request_uri;
|
proxy_set_header X-Original-URL $request_uri;
|
||||||
|
# set by the go2rtc stream locations; client headers are not forwarded, so
|
||||||
|
# this cannot be spoofed
|
||||||
|
proxy_set_header X-Go2rtc-Stream-Proxy $go2rtc_stream_proxy;
|
||||||
proxy_set_header X-Server-Port $server_port;
|
proxy_set_header X-Server-Port $server_port;
|
||||||
proxy_set_header Content-Length "";
|
proxy_set_header Content-Length "";
|
||||||
# Pass along auth related info
|
# Pass along auth related info
|
||||||
|
|||||||
@@ -219,6 +219,8 @@ 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;
|
||||||
@@ -228,6 +230,8 @@ 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;
|
||||||
@@ -248,6 +252,8 @@ 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;
|
||||||
|
|||||||
@@ -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 $scheme://$http_host$request_uri;
|
proxy_set_header X-Original-URL $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;
|
||||||
|
|||||||
+11
-50
@@ -2,32 +2,9 @@
|
|||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
@@ -42,51 +19,35 @@ 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 pip3 uninstall -y onnxruntime \
|
RUN --mount=type=bind,source=docker/rocm/prune_rocm.sh,target=/deps/prune_rocm.sh \
|
||||||
&& pip3 install -r /requirements.txt
|
pip3 uninstall -y onnxruntime \
|
||||||
|
&& 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
|
||||||
FROM scratch AS rocm-dist
|
COPY --from=rootfs / /
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
Executable
+63
@@ -0,0 +1,63 @@
|
|||||||
|
#!/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 +1,19 @@
|
|||||||
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
|
# ROCm 10.1.0, MIGraphX, and its ONNX Runtime plugin provider from AMD's wheels.
|
||||||
|
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,6 +1,3 @@
|
|||||||
variable "ROCM" {
|
|
||||||
default = "7.2.3"
|
|
||||||
}
|
|
||||||
variable "HSA_OVERRIDE_GFX_VERSION" {
|
variable "HSA_OVERRIDE_GFX_VERSION" {
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
@@ -8,12 +5,6 @@ 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"]
|
||||||
@@ -30,12 +21,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,4 @@ nvidia-nccl-cu12==2.26.2.post1; platform_machine == 'x86_64'
|
|||||||
nvidia-nvjitlink-cu12==12.8.93; platform_machine == 'x86_64'
|
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==3.20.3; platform_machine == 'x86_64'
|
protobuf==7.36.*; platform_machine == 'x86_64'
|
||||||
|
|||||||
@@ -824,6 +824,38 @@ cpu:
|
|||||||
models:
|
models:
|
||||||
- devices:
|
- devices:
|
||||||
- cpu:3
|
- cpu:3
|
||||||
|
xdna2:
|
||||||
|
title: AMD XDNA2
|
||||||
|
models:
|
||||||
|
- key: yolov9
|
||||||
|
label: YOLOv9
|
||||||
|
recommended: true
|
||||||
|
download: |-
|
||||||
|
Prepare the model using the frigate-xdna setup instructions linked above. For local YOLO models, Frigate must have access to the same ONNX file bytes as the sidecar. The example below uses YOLOv9-C at 320x320. Frigate+ models may instead use the same `plus://MODEL_ID` in Frigate and the sidecar.
|
||||||
|
ui: |-
|
||||||
|
Navigate to **Settings > System > Detection models** and add a model. The ZMQ endpoint is not reported by the hardware probe, so set `devices` to `zmq:tcp://xdna:5555` in YAML. Then, on the same model, open the **Custom Model** tab and configure:
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| ---------------------------------------- | ------------------------------------------ |
|
||||||
|
| **Custom object detector model path** | `/config/models/yolov9-c-320.onnx` |
|
||||||
|
| **Label map for custom object detector** | `/labelmap/coco-80.txt` |
|
||||||
|
| **Object detection model input width** | `320` |
|
||||||
|
| **Object detection model input height** | `320` |
|
||||||
|
| **Model Input Pixel Color Format** | `rgb` (Frigate's default value) |
|
||||||
|
| **Model Input Tensor Shape** | `nchw` |
|
||||||
|
| **Model Input D Type** | `float` |
|
||||||
|
| **Object Detection Model Type** | `yolo-generic` |
|
||||||
|
yaml: |-
|
||||||
|
models:
|
||||||
|
- devices:
|
||||||
|
- zmq:tcp://xdna:5555
|
||||||
|
model_type: yolo-generic
|
||||||
|
width: 320
|
||||||
|
height: 320
|
||||||
|
input_tensor: nchw
|
||||||
|
input_dtype: float
|
||||||
|
path: /config/models/yolov9-c-320.onnx
|
||||||
|
labelmap_path: /labelmap/coco-80.txt
|
||||||
memryx:
|
memryx:
|
||||||
title: MemryX
|
title: MemryX
|
||||||
models:
|
models:
|
||||||
|
|||||||
@@ -152,9 +152,10 @@ 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
|
||||||
# a model with a scene of all is used by any camera that does not set one.
|
# the model with a scene of default is used by any camera that does not set one.
|
||||||
# Valid values are all, indoor, outdoor, indoor_thermal, outdoor_thermal
|
# Any name made up of letters, numbers, _ and - is valid, such as thermal.
|
||||||
- scene: all
|
# Models that use the same model file are combined into one model.
|
||||||
|
- 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
|
||||||
@@ -293,9 +294,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
|
record: preset-record-generic-audio-aac
|
||||||
# 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
|
# record_sub: preset-record-generic-audio-aac
|
||||||
# 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
|
||||||
@@ -316,9 +317,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 all)
|
# (default: the model with a scene of default)
|
||||||
# Valid values are all, indoor, outdoor, indoor_thermal, outdoor_thermal
|
# Must match the scene of a configured model
|
||||||
scene: outdoor
|
scene: thermal
|
||||||
# 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
|
||||||
@@ -555,7 +556,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.000,0.469,1.000,1.000,0.000,1.000"
|
coordinates: "0.000,0.469,1.0,0.469,1.0,1.0,0.000,1.0"
|
||||||
# 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.
|
||||||
@@ -570,6 +571,8 @@ 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
|
||||||
@@ -900,6 +903,21 @@ 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.
|
||||||
|
|||||||
@@ -289,7 +289,7 @@ The only field that is valid at the camera level is `enabled`. In particular `mo
|
|||||||
|
|
||||||
#### GenAI Provider
|
#### 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. Supported providers are **OpenAI**, **Azure OpenAI**, **Gemini**, and **llama.cpp** with an audio-capable model (a dedicated ASR model such as Qwen3-ASR, or a general multimodal model that accepts audio). Ollama is not supported as it has no audio input.
|
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:
|
To use a GenAI provider for audio transcription:
|
||||||
|
|
||||||
|
|||||||
@@ -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, or an RTSP stream for higher resolution cameras.
|
3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower. For higher resolution cameras, it tries the http-flv stream first and falls back to RTSP when the camera does not support it.
|
||||||
4. In the validation step, enable **Use stream compatibility mode** for http-flv streams when the wizard recommends it.
|
4. The wizard turns on **Use stream compatibility mode** for the http-flv stream it selects. Enable it for any other http-flv stream you add, such as the sub stream.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
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 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" |
|
| Annke CZ504 | ✅ | ❌ | |
|
||||||
| 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,6 +85,14 @@ 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:
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ 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-apple-silicon-h265 | Apple Silicon (H.265) | Apple Silicon Mac under lighter, H.265 stream | Needs the `lighter.sh/video` device |
|
||||||
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
|
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
|
||||||
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
|
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
|
||||||
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
|
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
|
||||||
|
|||||||
@@ -43,7 +43,19 @@ 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, and is not available on Ollama, which has no audio input. Each role may be assigned to exactly one provider. Define a single provider if you want it to do everything, or split the roles across several providers using the `roles` option.
|
Each provider handles one or more **roles**: `chat`, `descriptions`, `embeddings`, and `transcribe`. A provider handles the first three by default; `transcribe` must always be listed explicitly. Each role may be assigned to exactly one provider. Define a single provider if you want it to do everything, or split the roles across several providers using the `roles` option. Not every provider supports every role; see [Provider support](#provider-support).
|
||||||
|
|
||||||
|
### Provider support
|
||||||
|
|
||||||
|
| Provider | Descriptions | Chat | Embeddings | Transcription |
|
||||||
|
| ----------------------------- | :----------: | :--: | :--------: | :-----------: |
|
||||||
|
| llama.cpp (`llamacpp`) | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Ollama (`ollama`) | ✅ | ✅ | ✅ | ❌ |
|
||||||
|
| OpenAI (`openai`) | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Azure OpenAI (`azure_openai`) | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
| Google Gemini (`gemini`) | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
|
||||||
|
A ✅ means Frigate can use the provider for that feature. The configured model must also support it: a vision model for descriptions and chat, a multimodal embedding model for embeddings (see [Embedding models](#embedding-models)), and an audio-capable model for transcription. Some features also need extra provider setup, covered in each provider's section below. OpenAI-compatible servers use the `openai` provider, so they follow the OpenAI row.
|
||||||
|
|
||||||
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_`.
|
||||||
|
|
||||||
@@ -74,8 +86,9 @@ You must use a vision-capable model with Frigate. The following models are recom
|
|||||||
The `embeddings` role needs a different kind of model. Text queries are matched against the stored image embeddings, so the model must be trained to place images and text into the same vector space. A chat or description model will still return vectors when asked, but those vectors are not trained for retrieval and text searches will return poor matches with no error to indicate why.
|
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 |
|
||||||
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `qwen3-vl-embedding` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Must be served by llama.cpp started with `--embeddings` and `--mmproj`. |
|
| `embeddinggemma-2` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Strong semantic search accuracy with efficient inference on a small model. |
|
||||||
|
| `qwen3-vl-embedding` | Multimodal embeddings for [Semantic Search](/configuration/semantic_search#genai-provider). Good performance, large model that requires strong hardware for inference. |
|
||||||
|
|
||||||
#### Transcription models
|
#### Transcription models
|
||||||
|
|
||||||
@@ -152,6 +165,10 @@ 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.
|
||||||
@@ -197,6 +214,10 @@ 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.
|
||||||
|
|||||||
@@ -201,6 +201,8 @@ Review items are sent to the model as a sequence of still frames. Some models fo
|
|||||||
|
|
||||||
The notes come from tracking data rather than from the images, so they describe activity the model may not have picked up on its own. In testing with a person carrying three waste bins to the curb one at a time, `gemma4` described a single trip on every attempt with `frames`, and consistently described multiple trips with `annotated_frames`. Models that already handle these sequences well, such as the `qwen3-vl` family, gain little and should stay on `frames`.
|
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.
|
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
|
:::note
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ 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
|
||||||
@@ -132,7 +136,8 @@ 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:
|
||||||
|
|
||||||
- Linux kernel **5.19 or newer** for the `i915` driver, or any release of the `xe` driver.
|
- An Intel GPU from Broadwell (5th gen) or newer. Older GPUs do not expose these counters on any kernel.
|
||||||
|
- Linux kernel **5.19 or newer** for the `i915` driver (**6.5 or newer** on 12th gen and newer), or any release of the `xe` driver.
|
||||||
- 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.
|
||||||
@@ -533,3 +538,35 @@ 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).
|
||||||
|
|||||||
@@ -379,9 +379,9 @@ 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 > Threshold** | Set to `0.7` |
|
| **Object filters > License Plate > Confidence threshold** | Set to `0.7` |
|
||||||
|
|
||||||
Navigate to <NavPath path="Settings > Camera configuration > Motion detection" />.
|
Navigate to <NavPath path="Settings > Camera configuration > Motion detection" />.
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Frigate [defaults to MSE](#why-does-frigate-prefer-mse-over-webrtc-for-live-view
|
|||||||
|
|
||||||
- The choices are saved **per device, per camera** in your browser's local storage.
|
- 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:
|
- **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` (see [WebRTC extra configuration](#webrtc-extra-configuration)).
|
- **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.
|
- **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 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 audio codec**: WebRTC needs opus or G.711 audio, so a stream whose playback audio is only AAC (without an added opus/G.711 track) can't carry audio over WebRTC. See [Audio Support](#audio-support) for how to add one.
|
||||||
@@ -92,7 +92,7 @@ go2rtc:
|
|||||||
|
|
||||||
### Setting Streams For Live UI
|
### 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.
|
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.
|
||||||
|
|
||||||
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,6 +158,26 @@ 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:
|
||||||
@@ -363,6 +383,13 @@ 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.000,0.000,1.000,0.400,0.000,0.400"
|
coordinates: "0.000,0.000,1.0,0.000,1.0,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.000,0.700,1.000,1.000,0.000,1.000"
|
coordinates: "0.000,0.700,1.0,0.700,1.0,1.0,0.000,1.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
</TabItem>
|
</TabItem>
|
||||||
|
|||||||
@@ -30,10 +30,12 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
- [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**
|
||||||
|
|
||||||
@@ -102,32 +104,36 @@ 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 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`:
|
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`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
models:
|
models:
|
||||||
- scene: outdoor
|
- scene: default
|
||||||
path: plus://your-outdoor-model
|
path: plus://your-model
|
||||||
devices:
|
devices:
|
||||||
- edgetpu:pci:0
|
- edgetpu:pci:0
|
||||||
- scene: indoor
|
- scene: thermal
|
||||||
path: /config/model_cache/indoor.onnx
|
path: /config/model_cache/thermal.onnx
|
||||||
model_type: yolo-generic
|
model_type: yolo-generic
|
||||||
devices:
|
devices:
|
||||||
- openvino:GPU
|
- openvino:GPU
|
||||||
|
|
||||||
cameras:
|
cameras:
|
||||||
driveway:
|
driveway:
|
||||||
detect:
|
|
||||||
scene: outdoor
|
|
||||||
...
|
...
|
||||||
hallway:
|
backyard_thermal:
|
||||||
detect:
|
detect:
|
||||||
scene: indoor
|
scene: thermal
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
A scene is any name made up of letters, numbers, `_`, and `-`. The model with a scene of `default` is used by every camera that does not set one (or sets a scene that no model is configured for), and `default` is used when a model does not declare a scene. Changing a camera's scene requires a restart.
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
|
||||||
|
Scenes are for running **different** models. Do not configure the same model under several scenes to dedicate a detector to specific cameras: every detector of a model already serves every camera using it, and splitting them only leaves some detectors idle while others fall behind. Frigate detects models that use the same model file, even under a different path or file name, combines them into one model with all of their hardware, and logs a warning.
|
||||||
|
|
||||||
|
:::
|
||||||
|
|
||||||
### Choosing a model size
|
### Choosing a model size
|
||||||
|
|
||||||
@@ -388,6 +394,22 @@ 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.
|
||||||
@@ -414,19 +436,10 @@ 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
|
||||||
|
|
||||||
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.
|
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`:
|
||||||
|
|
||||||
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.0.0 \
|
$ docker run -e HSA_OVERRIDE_GFX_VERSION=10.3.0 \
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -437,16 +450,17 @@ services:
|
|||||||
frigate:
|
frigate:
|
||||||
...
|
...
|
||||||
environment:
|
environment:
|
||||||
HSA_OVERRIDE_GFX_VERSION: "10.0.0"
|
HSA_OVERRIDE_GFX_VERSION: "10.3.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
Figuring out what version you need can be complicated as you can't tell the chipset name and driver from the AMD brand name.
|
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.
|
||||||
|
|
||||||
- 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
|
You can't tell the chipset from the AMD brand name, so check it in the container:
|
||||||
- find the chipset version you have (gfxNNN) from the output of the `rocminfo` (see below)
|
|
||||||
- use a search engine to query what `HSA_OVERRIDE_GFX_VERSION` you need for the given gfx name ("gfxNNN ROCm HSA_OVERRIDE_GFX_VERSION")
|
- 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
|
||||||
- override the `HSA_OVERRIDE_GFX_VERSION` with relevant value
|
- find the chipset version you have (gfxNNN) from the output of `rocminfo` (see below)
|
||||||
- if things are not working check the frigate docker logs
|
- 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
|
||||||
|
|
||||||
#### Figuring out if AMD/ROCm is working and found your GPU
|
#### Figuring out if AMD/ROCm is working and found your GPU
|
||||||
|
|
||||||
@@ -484,7 +498,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, and TensorRT. 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, TensorRT, and a Mac's Neural Engine. On startup Frigate will automatically try to use a GPU if one is available.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
@@ -500,6 +514,9 @@ 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
|
||||||
@@ -515,6 +532,22 @@ 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} />
|
||||||
@@ -543,6 +576,28 @@ When using CPU detectors, you can add one CPU detector per camera. Adding more d
|
|||||||
|
|
||||||
# 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.
|
||||||
|
|||||||
@@ -46,9 +46,9 @@ 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 > Min Score** | Minimum score for a single detection to initiate tracking |
|
| **Object filters > Person > Minimum confidence** | Minimum score for a single detection to initiate tracking |
|
||||||
| **Object filters > Person > Threshold** | Minimum computed (median) score to be considered a true positive |
|
| **Object filters > Person > Confidence 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.
|
||||||
|
|
||||||
@@ -104,11 +104,11 @@ 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 > Min Area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
|
| **Object filters > Person > Minimum object area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
|
||||||
| **Object filters > Person > Max Area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
|
| **Object filters > Person > Maximum object area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
|
||||||
| **Object filters > Person > Min Ratio** | Minimum width/height ratio of the bounding box |
|
| **Object filters > Person > Minimum aspect ratio** | Minimum width/height ratio of the bounding box |
|
||||||
| **Object filters > Person > Max Ratio** | Maximum width/height ratio of the bounding box |
|
| **Object filters > Person > Maximum aspect 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.
|
||||||
|
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ 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 > Min Area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
|
| **Object filters > Person > Minimum object area** | Minimum bounding box area in pixels (or decimal for percentage of frame) |
|
||||||
| **Object filters > Person > Max Area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
|
| **Object filters > Person > Maximum object area** | Maximum bounding box area in pixels (or decimal for percentage of frame) |
|
||||||
| **Object filters > Person > Min Ratio** | Minimum width/height ratio of the bounding box |
|
| **Object filters > Person > Minimum aspect ratio** | Minimum width/height ratio of the bounding box |
|
||||||
| **Object filters > Person > Max Ratio** | Maximum width/height ratio of the bounding box |
|
| **Object filters > Person > Maximum aspect ratio** | Maximum width/height ratio of the bounding box |
|
||||||
| **Object filters > Person > Min Score** | Minimum score for the object to initiate tracking |
|
| **Object filters > Person > Minimum confidence** | Minimum score for the object to initiate tracking |
|
||||||
| **Object filters > Person > Threshold** | Minimum computed score to be considered a true positive |
|
| **Object filters > Person > Confidence 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,14 +191,12 @@ cameras:
|
|||||||
detect:
|
detect:
|
||||||
enabled: false
|
enabled: false
|
||||||
record:
|
record:
|
||||||
enabled: false
|
enabled: true
|
||||||
profiles:
|
profiles:
|
||||||
away:
|
away:
|
||||||
enabled: true
|
enabled: true
|
||||||
detect:
|
detect:
|
||||||
enabled: true
|
enabled: true
|
||||||
record:
|
|
||||||
enabled: true
|
|
||||||
home:
|
home:
|
||||||
enabled: false
|
enabled: false
|
||||||
```
|
```
|
||||||
@@ -251,6 +249,12 @@ 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.
|
||||||
|
|||||||
@@ -133,13 +133,12 @@ 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. Currently, only **llama.cpp** supports multimodal embeddings (both text and images).
|
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.
|
||||||
|
|
||||||
To use llama.cpp for semantic search:
|
To use a GenAI provider for semantic search:
|
||||||
|
|
||||||
1. Configure a GenAI provider with `embeddings` in its `roles`.
|
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.
|
||||||
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">
|
||||||
@@ -174,8 +173,6 @@ 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.
|
||||||
@@ -245,8 +242,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").
|
- Enter a **Name** for the trigger (e.g., "Red Car Alert"). Frigate derives the trigger's
|
||||||
- Enter a descriptive **Friendly Name** for the trigger (e.g., "Red car on the driveway camera").
|
internal **ID** from this name, which can be revealed and edited with the show/hide toggle.
|
||||||
- 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 **Friendly name**, **Objects**, **Loitering time**, and **Inertia** in the zone editor.
|
4. Configure zone options such as **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, enter the real-world **Distances** between each pair of consecutive points.
|
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.
|
||||||
- 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>
|
||||||
|
|||||||
@@ -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.
|
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.
|
||||||
|
|
||||||
## 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.
|
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.
|
||||||
|
|
||||||
## Top Score
|
## Top Score
|
||||||
|
|
||||||
|
|||||||
@@ -72,12 +72,19 @@ Frigate supports multiple different detectors that work on different types of ha
|
|||||||
|
|
||||||
**AMD**
|
**AMD**
|
||||||
|
|
||||||
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD Discrete GPUs to provide efficient object detection
|
- [ROCm](#rocm---amd-gpu): ROCm can run on AMD discrete and recent integrated 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
|
||||||
@@ -182,7 +189,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`. 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.
|
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.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -211,7 +218,13 @@ Inference is done with the `onnx` detector type. Speeds will vary greatly depend
|
|||||||
|
|
||||||
### Apple Silicon
|
### Apple Silicon
|
||||||
|
|
||||||
With the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon.
|
Frigate on a Mac is best run in the [lighter](https://github.com/fieldwork-ai/lighter) container runtime, where the [ONNX detector](../configuration/object_detectors.md#apple-neural-engine-lighter) runs on the Neural Engine of M1 and newer Macs from inside the Frigate container. There is no separate detector process to install or keep running, and the same container can decode video on the Mac's media engine.
|
||||||
|
|
||||||
|
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
|
||||||
|
| ---- | -------------------------------------- | ----------------------- | ---------------------- |
|
||||||
|
| M1 | t-320: 3.3 ms s-320: 7 ms s-640: 13 ms | 320: 6.6 ms | Nano-320: 38 ms |
|
||||||
|
|
||||||
|
Alternatively, with the [Apple Silicon](../configuration/object_detectors.md#apple-silicon-detector) detector Frigate can take advantage of the NPU in M1 and newer Apple Silicon.
|
||||||
|
|
||||||
:::warning
|
:::warning
|
||||||
|
|
||||||
@@ -227,7 +240,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 discrete AMD GPUs.
|
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).
|
||||||
|
|
||||||
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
|
| Name | YOLOv9 Inference Time | YOLO-NAS Inference Time | RF-DETR Inference Time |
|
||||||
| -------------- | --------------------------- | ------------------------- | ---------------------- |
|
| -------------- | --------------------------- | ------------------------- | ---------------------- |
|
||||||
@@ -328,6 +341,32 @@ 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**.
|
||||||
|
|
||||||
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.
|
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 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.
|
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.
|
||||||
|
|
||||||
<ShmCalculator/>
|
<ShmCalculator/>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ title: Updating
|
|||||||
|
|
||||||
# Updating Frigate
|
# Updating Frigate
|
||||||
|
|
||||||
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).
|
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).
|
||||||
|
|
||||||
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.0` instead of `0.17.1`). For example:
|
- Edit your `docker-compose.yml` file to specify the desired version tag (e.g., `0.18.2` instead of `0.17.1`). For example:
|
||||||
```yaml
|
```yaml
|
||||||
services:
|
services:
|
||||||
frigate:
|
frigate:
|
||||||
image: ghcr.io/blakeblackshear/frigate:0.18.0
|
image: ghcr.io/blakeblackshear/frigate:0.18.2
|
||||||
```
|
```
|
||||||
- Then pull the image:
|
- Then pull the image:
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/blakeblackshear/frigate:0.18.0
|
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
|
||||||
```
|
```
|
||||||
- **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.0`, `0.18.0-tensorrt`, or `stable`):
|
- Pull the image with the appropriate tag (e.g., `0.18.2`, `0.18.2-tensorrt`, or `stable`):
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/blakeblackshear/frigate:0.18.0
|
docker pull ghcr.io/blakeblackshear/frigate:0.18.2
|
||||||
```
|
```
|
||||||
|
|
||||||
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.17.1`) in your `docker run` command.
|
- For Docker: Specify an older tag (e.g., `ghcr.io/blakeblackshear/frigate:0.18.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,6 +45,8 @@ 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
|
||||||
|
|
||||||
@@ -218,3 +220,68 @@ 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,6 +11,12 @@ 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`
|
||||||
@@ -212,6 +218,7 @@ 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.
|
||||||
|
|
||||||
@@ -235,7 +242,8 @@ 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": {
|
||||||
@@ -254,7 +262,16 @@ 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,6 +27,10 @@ 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,7 +21,13 @@ 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 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.
|
Yes. Subscriptions to Frigate+ provide access to the infrastructure used to train the models. Models you train during an active subscription remain licensed for your continued use even after your subscription ends — models already in your model cache will keep working indefinitely. An active subscription is required to train new models and download new versions.
|
||||||
|
|
||||||
|
### Can I use Frigate+ models commercially?
|
||||||
|
|
||||||
|
A standard subscription covers use on camera systems you own or operate, including for your business. A shop, restaurant, warehouse, or office running Frigate+ at its own locations (including multiple locations) is exactly the kind of use the subscription is for.
|
||||||
|
What the standard subscription does not cover is using Frigate+ models to provide a product or service to others. If you're deploying models at your customers' sites, bundling them with hardware you sell, or running them as part of a hosted or managed service, even if your customers never receive the model files themselves, you'll need a commercial license.
|
||||||
|
Note that professional installers are fine under standard subscriptions when each customer holds their own Frigate+ subscription. The commercial license is for cases where your license powers your customers' sites.
|
||||||
|
|
||||||
### Why can't I submit images to Frigate+?
|
### Why can't I submit images to Frigate+?
|
||||||
|
|
||||||
|
|||||||
@@ -63,10 +63,10 @@ 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 **Min Score** and **Threshold** for each object type, then click **Save**.
|
Navigate to <NavPath path="Settings > Global configuration > Objects" />. Under **Object filters**, set **Minimum confidence** and **Confidence threshold** for each object type, then click **Save**.
|
||||||
|
|
||||||
| Object | Min Score | Threshold |
|
| Object | Minimum confidence | Confidence threshold |
|
||||||
| ----------------- | --------- | --------- |
|
| ----------------- | ------------------ | -------------------- |
|
||||||
| **dog** | .7 | .9 |
|
| **dog** | .7 | .9 |
|
||||||
| **cat** | .65 | .8 |
|
| **cat** | .65 | .8 |
|
||||||
| **face** | .7 | |
|
| **face** | .7 | |
|
||||||
|
|||||||
@@ -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`
|
- **People**: `person`, `face`, `baby`
|
||||||
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `license_plate`
|
- **Vehicles**: `car`, `motorcycle`, `bicycle`, `boat`, `school_bus`, `garbage truck`, `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`
|
- **Animals**: `dog`, `cat`, `deer`, `horse`, `bird`, `raccoon`, `fox`, `bear`, `cow`, `squirrel`, `goat`, `rabbit`, `skunk`, `kangaroo`, `possum`, `rodent`
|
||||||
- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella`
|
- **Other**: `package`, `waste_bin`, `bbq_grill`, `robot_lawnmower`, `umbrella`, `baby_stroller`
|
||||||
|
|
||||||
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,9 +77,12 @@ 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 `baby` labels are mapped to `person` until support for new labels is added.
|
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.
|
||||||
|
|
||||||
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`
|
- **Vehicles**: `tractor`, `golf_cart`, `bus`, `airplane`, `helicopter`, `rickshaw`, `scooter`
|
||||||
|
- **Delivery Logos**: `bpost`, `auspost`, `aramex`, `transoflex`, `parcelforce`, `hermes`, `cargus`, `fan_courier`, `sameday`, `la_poste`
|
||||||
|
- **Animals**: `badger`, `chicken`, `duck`, `turkey`, `groundhog`, `boar`, `hedgehog`, `wombat`, `bobcat`, `mustelid`, `mountain_lion`, `crocodile`, `monkey`, `coyote`, `porcupine`, `sheep`, `snake`, `lizard`, `heron`, `elk`, `moose`, `pig`, `donkey`, `civet`
|
||||||
|
- **Other**: `sports_ball`, `drone`, `lawnmower`
|
||||||
|
|
||||||
Candidate labels are not available for automatic suggestions.
|
Candidate labels are not available for automatic suggestions.
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ go2rtc:
|
|||||||
|
|
||||||
Point the camera's inputs at the restream as described in the [restream docs](/configuration/restream.md), and swap `detect -> width` and `detect -> height` to match the rotated resolution.
|
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.
|
||||||
@@ -184,6 +192,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, 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.
|
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.
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -397,19 +397,11 @@ 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 hardware acceleration configuration
|
#### Step 6: Verify go2rtc stream 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 8: Check system resources
|
#### Step 7: 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:
|
||||||
|
|
||||||
|
|||||||
+29
-21
@@ -3,6 +3,9 @@ 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",
|
||||||
@@ -23,17 +26,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.
|
||||||
@@ -45,8 +48,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: {
|
||||||
@@ -85,13 +88,13 @@ 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"],
|
||||||
@@ -131,6 +134,11 @@ 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",
|
||||||
@@ -148,19 +156,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
+34
-25
@@ -7030,9 +7030,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.18",
|
"version": "1.1.21",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz",
|
||||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
"integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
@@ -7698,14 +7698,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/compression": {
|
"node_modules/compression": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz",
|
||||||
"integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
|
"integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "3.1.2",
|
"bytes": "3.1.2",
|
||||||
"compressible": "~2.0.18",
|
"compressible": "~2.0.18",
|
||||||
"debug": "2.6.9",
|
"debug": "2.6.9",
|
||||||
|
"destroy": "1.2.0",
|
||||||
"negotiator": "~0.6.4",
|
"negotiator": "~0.6.4",
|
||||||
"on-headers": "~1.1.0",
|
"on-headers": "~1.1.0",
|
||||||
"safe-buffer": "5.2.1",
|
"safe-buffer": "5.2.1",
|
||||||
@@ -7713,6 +7714,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/compression/node_modules/bytes": {
|
"node_modules/compression/node_modules/bytes": {
|
||||||
@@ -9460,9 +9465,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dompurify": {
|
"node_modules/dompurify": {
|
||||||
"version": "3.4.13",
|
"version": "3.4.16",
|
||||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz",
|
||||||
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
|
"integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==",
|
||||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@types/trusted-types": "^2.0.7"
|
"@types/trusted-types": "^2.0.7"
|
||||||
@@ -10125,9 +10130,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
||||||
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
|
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -11375,15 +11380,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/image-size": {
|
"node_modules/image-size": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz",
|
||||||
"integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==",
|
"integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"image-size": "bin/image-size.js"
|
"image-size": "bin/image-size.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.x"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/immer": {
|
"node_modules/immer": {
|
||||||
@@ -17448,9 +17453,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/proxy-addr": {
|
"node_modules/proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
|
||||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
"integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"forwarded": "0.2.0",
|
"forwarded": "0.2.0",
|
||||||
@@ -17458,6 +17463,10 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/proxy-addr/node_modules/ipaddr.js": {
|
"node_modules/proxy-addr/node_modules/ipaddr.js": {
|
||||||
@@ -19229,9 +19238,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.10.0",
|
"version": "1.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.12.0.tgz",
|
||||||
"integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==",
|
"integrity": "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
@@ -19513,9 +19522,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
|||||||
@@ -2,42 +2,53 @@ 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("26.32MB");
|
const [result, setResult] = useState("-");
|
||||||
const [singleCameraShm, setSingleCameraShm] = useState("26.32MB");
|
const [singleCameraShm, setSingleCameraShm] = useState("-");
|
||||||
const [totalShm, setTotalShm] = useState("26.32MB");
|
const [addedCamerasShm, setAddedCamerasShm] = useState("-");
|
||||||
|
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("-");
|
||||||
setTotalShm("-");
|
setAddedCamerasShm("-");
|
||||||
|
setFullRingShm("-");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single camera base SHM calculation (excluding logs)
|
const cameraFramesMb = cameraCount * frameMb(width, height) + addedCamerasMb;
|
||||||
// Formula: (width * height * 1.5 * 20 + 270480) / 1048576
|
|
||||||
const singleCameraBase =
|
|
||||||
(width * height * 1.5 * 20 + 270480) / 1048576;
|
|
||||||
setSingleCameraShm(`${singleCameraBase.toFixed(2)}mb`);
|
|
||||||
|
|
||||||
// Total SHM calculation (multiple cameras, including logs)
|
setSingleCameraShm(formatMb(frameMb(width, height) * TARGET_FRAMES));
|
||||||
const totalBase = singleCameraBase * cameraCount;
|
setAddedCamerasShm(formatMb(addedCamerasMb * TARGET_FRAMES));
|
||||||
const finalResult = totalBase + 40; // Default includes logs +40mb
|
setResult(formatMb(Math.round(RESERVED_MB + cameraFramesMb * TARGET_FRAMES)));
|
||||||
|
setFullRingShm(
|
||||||
|
formatMb(Math.round(RESERVED_MB + cameraFramesMb * DEFAULT_FRAMES)),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
setTotalShm(`${(totalBase + 40).toFixed(2)}mb`);
|
const formatMb = (value) => {
|
||||||
|
if (value >= 1024) {
|
||||||
// Format result
|
return `${(value / 1024).toFixed(2)}gb`;
|
||||||
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) => {
|
||||||
@@ -147,19 +158,27 @@ const ShmCalculator = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.formulaDisplay}>
|
<div className={styles.formulaDisplay}>
|
||||||
<p>
|
<p>
|
||||||
<strong>Single Camera:</strong> {formatWithUnit(singleCameraShm)}
|
<strong>Per Camera:</strong> {formatWithUnit(singleCameraShm)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Formula:</strong> (width × height × 1.5 × 20 + 270480) ÷
|
<strong>Formula:</strong> (width × height × 1.5 + 270480) ÷
|
||||||
1048576
|
1048576 × {TARGET_FRAMES} frames
|
||||||
</p>
|
</p>
|
||||||
{cameraCount > 1 && (
|
|
||||||
<p>
|
<p>
|
||||||
<strong>Total ({cameraCount} cameras):</strong> {formatWithUnit(totalShm)}
|
<strong>Room for 2 Added Cameras:</strong>{" "}
|
||||||
|
{formatWithUnit(addedCamerasShm)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
<p>
|
<p>
|
||||||
<strong>With Logs:</strong> + 40<span className={styles.unit}>mb</span>
|
<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
+178
-28
@@ -412,6 +412,39 @@ 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:
|
||||||
@@ -1055,6 +1088,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1098,6 +1132,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1252,6 +1287,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1611,6 +1647,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1658,6 +1695,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1705,6 +1743,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1746,6 +1785,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -1829,6 +1869,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -2090,7 +2131,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -3225,7 +3266,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -3450,6 +3491,7 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
|
additionalProperties: true
|
||||||
title: Body
|
title: Body
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -4174,19 +4216,20 @@ paths:
|
|||||||
Get notices, most severe first.
|
Get notices, most severe first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
include_dismissed: Also return dismissed notices, for the history view
|
include_hidden: Also return acknowledged and muted notices, for the
|
||||||
|
hidden list
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The notices
|
The notices
|
||||||
operationId: get_notices_notices_get
|
operationId: get_notices_notices_get
|
||||||
parameters:
|
parameters:
|
||||||
- name: include_dismissed
|
- name: include_hidden
|
||||||
in: query
|
in: query
|
||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
title: Include Dismissed
|
title: Include Hidden
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful Response
|
description: Successful Response
|
||||||
@@ -4221,16 +4264,16 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
x-required-role: admin
|
||||||
/notices/dismissed_checks:
|
/notices/muted_checks:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Notices
|
- Notices
|
||||||
summary: Get Dismissed Checks
|
summary: Get Muted Checks
|
||||||
description: |-
|
description: |-
|
||||||
**Access:** Admin role required.
|
**Access:** Admin role required.
|
||||||
|
|
||||||
Get the dismissed config and stream check rows, newest first.
|
Get the muted config and stream check rows, newest first.
|
||||||
operationId: get_dismissed_checks_notices_dismissed_checks_get
|
operationId: get_muted_checks_notices_muted_checks_get
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful Response
|
description: Successful Response
|
||||||
@@ -4240,16 +4283,16 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
x-required-role: admin
|
||||||
/notices/dismissed:
|
/notices/hidden:
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- Notices
|
- Notices
|
||||||
summary: Purge Dismissed
|
summary: Unhide All Notices
|
||||||
description: |-
|
description: |-
|
||||||
**Access:** Admin role required.
|
**Access:** Admin role required.
|
||||||
|
|
||||||
Delete every dismissed notice and check row so each can show again.
|
Show every acknowledged and muted notice and check row again.
|
||||||
operationId: purge_dismissed_notices_dismissed_delete
|
operationId: unhide_all_notices_notices_hidden_delete
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Successful Response
|
description: Successful Response
|
||||||
@@ -4259,18 +4302,83 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- frigateAdminAuth: []
|
- frigateAdminAuth: []
|
||||||
x-required-role: admin
|
x-required-role: admin
|
||||||
/notices/{notice_id}/dismiss:
|
/notices/{notice_id}/acknowledge:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- Notices
|
- Notices
|
||||||
summary: Dismiss Notice
|
summary: Acknowledge Notice
|
||||||
description: |-
|
description: |-
|
||||||
**Access:** Admin role required.
|
**Access:** Admin role required.
|
||||||
|
|
||||||
Hide a notice or a config or stream check row.
|
Hide a notice until it happens again.
|
||||||
|
|
||||||
It stays hidden if the same problem happens again.
|
Config and stream check rows and the update notice never repeat, so they
|
||||||
operationId: dismiss_notice_notices__notice_id__dismiss_post
|
can only be muted.
|
||||||
|
operationId: acknowledge_notice_notices__notice_id__acknowledge_post
|
||||||
|
parameters:
|
||||||
|
- name: notice_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
title: Notice Id
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful Response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
'422':
|
||||||
|
description: Validation Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HTTPValidationError'
|
||||||
|
security:
|
||||||
|
- frigateAdminAuth: []
|
||||||
|
x-required-role: admin
|
||||||
|
/notices/{notice_id}/mute:
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- Notices
|
||||||
|
summary: Mute Notice
|
||||||
|
description: |-
|
||||||
|
**Access:** Admin role required.
|
||||||
|
|
||||||
|
Hide a notice or a config or stream check row for good.
|
||||||
|
operationId: mute_notice_notices__notice_id__mute_post
|
||||||
|
parameters:
|
||||||
|
- name: notice_id
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
title: Notice Id
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful Response
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
'422':
|
||||||
|
description: Validation Error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HTTPValidationError'
|
||||||
|
security:
|
||||||
|
- frigateAdminAuth: []
|
||||||
|
x-required-role: admin
|
||||||
|
/notices/{notice_id}/hidden:
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- Notices
|
||||||
|
summary: Unhide Notice
|
||||||
|
description: |-
|
||||||
|
**Access:** Admin role required.
|
||||||
|
|
||||||
|
Show an acknowledged or muted notice or check row again.
|
||||||
|
operationId: unhide_notice_notices__notice_id__hidden_delete
|
||||||
parameters:
|
parameters:
|
||||||
- name: notice_id
|
- name: notice_id
|
||||||
in: path
|
in: path
|
||||||
@@ -4394,6 +4502,16 @@ 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
|
||||||
@@ -4548,7 +4666,7 @@ paths:
|
|||||||
anyOf:
|
anyOf:
|
||||||
- type: string
|
- type: string
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -4699,6 +4817,16 @@ 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
|
||||||
@@ -4800,7 +4928,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
|
||||||
@@ -4880,7 +5008,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
|
||||||
@@ -5533,6 +5661,7 @@ 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
|
||||||
@@ -5581,6 +5710,7 @@ 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':
|
||||||
@@ -5623,6 +5753,7 @@ 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':
|
||||||
@@ -5659,6 +5790,7 @@ 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':
|
||||||
@@ -7235,7 +7367,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
|
||||||
@@ -7285,7 +7417,7 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
default: utc
|
default: UTC
|
||||||
title: Timezone
|
title: Timezone
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
@@ -7455,6 +7587,7 @@ 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
|
||||||
@@ -7658,13 +7791,19 @@ components:
|
|||||||
title: Update Topic
|
title: Update Topic
|
||||||
config_data:
|
config_data:
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: object
|
- additionalProperties: true
|
||||||
|
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:
|
||||||
@@ -7893,7 +8032,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
contentMediaType: application/octet-stream
|
||||||
title: File
|
title: File
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -7903,7 +8042,7 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
file:
|
file:
|
||||||
type: string
|
type: string
|
||||||
format: binary
|
contentMediaType: application/octet-stream
|
||||||
title: File
|
title: File
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
@@ -8001,6 +8140,7 @@ components:
|
|||||||
tool_calls:
|
tool_calls:
|
||||||
anyOf:
|
anyOf:
|
||||||
- items:
|
- items:
|
||||||
|
additionalProperties: true
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
@@ -8296,6 +8436,7 @@ 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
|
||||||
@@ -8367,7 +8508,8 @@ components:
|
|||||||
default: true
|
default: true
|
||||||
draw:
|
draw:
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: object
|
- additionalProperties: true
|
||||||
|
type: object
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Draw
|
title: Draw
|
||||||
default: {}
|
default: {}
|
||||||
@@ -8610,7 +8752,8 @@ components:
|
|||||||
description: Error message for failed jobs
|
description: Error message for failed jobs
|
||||||
results:
|
results:
|
||||||
anyOf:
|
anyOf:
|
||||||
- type: object
|
- additionalProperties: true
|
||||||
|
type: object
|
||||||
- type: 'null'
|
- type: 'null'
|
||||||
title: Results
|
title: Results
|
||||||
description: Result metadata for completed jobs
|
description: Result metadata for completed jobs
|
||||||
@@ -8860,6 +9003,7 @@ 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
|
||||||
@@ -9400,6 +9544,7 @@ 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
|
||||||
@@ -9477,6 +9622,11 @@ 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
|
||||||
|
|||||||
+3
-1
@@ -15,7 +15,9 @@ from frigate.util.config import find_config_file
|
|||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
manager = mp.Manager()
|
# Fork this manager so the forkserver does not start until the first
|
||||||
|
# worker process, after the config has applied environment_vars.
|
||||||
|
manager = mp.get_context("fork").Manager()
|
||||||
faulthandler.enable()
|
faulthandler.enable()
|
||||||
|
|
||||||
# Setup the logging thread
|
# Setup the logging thread
|
||||||
|
|||||||
+137
-5
@@ -63,6 +63,7 @@ from frigate.util.builtin import (
|
|||||||
flatten_config_data,
|
flatten_config_data,
|
||||||
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 (
|
||||||
@@ -70,9 +71,15 @@ from frigate.util.config import (
|
|||||||
find_config_file,
|
find_config_file,
|
||||||
redact_credential,
|
redact_credential,
|
||||||
)
|
)
|
||||||
|
from frigate.util.live_streams import (
|
||||||
|
generated_transcode_streams,
|
||||||
|
sync_transcode_streams,
|
||||||
|
)
|
||||||
from frigate.util.object_names import get_categorized_object_names
|
from frigate.util.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,
|
||||||
@@ -306,9 +313,12 @@ def config(request: Request):
|
|||||||
mode="json", warnings="none", exclude_none=True
|
mode="json", warnings="none", exclude_none=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# remove environment_vars for non-admin users
|
is_admin = request.headers.get("remote-role") == "admin"
|
||||||
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")
|
||||||
@@ -365,7 +375,15 @@ def config(request: Request):
|
|||||||
camera_name
|
camera_name
|
||||||
)
|
)
|
||||||
if base_sections:
|
if base_sections:
|
||||||
camera_dict["base_config"] = base_sections
|
# copy so redaction below can't alter the profile manager's cache
|
||||||
|
camera_dict["base_config"] = copy.deepcopy(base_sections)
|
||||||
|
|
||||||
|
# cameras inherit the global notification email
|
||||||
|
if not is_admin:
|
||||||
|
redact_credential(camera_dict["notifications"], "email")
|
||||||
|
redact_credential(
|
||||||
|
camera_dict.get("base_config", {}).get("notifications", {}), "email"
|
||||||
|
)
|
||||||
|
|
||||||
# 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(
|
||||||
@@ -393,6 +411,11 @@ 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
|
||||||
|
|
||||||
@@ -429,6 +452,8 @@ 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",
|
||||||
@@ -523,6 +548,63 @@ 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()
|
||||||
@@ -599,10 +681,22 @@ def config_save(save_option: str, body: Any = Body(media_type="text/plain")):
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save the config to file
|
|
||||||
try:
|
|
||||||
config_file = find_config_file()
|
config_file = find_config_file()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_file) as f:
|
||||||
|
old_raw_config = f.read()
|
||||||
|
except OSError:
|
||||||
|
old_raw_config = ""
|
||||||
|
|
||||||
|
restricted_response = _restricted_go2rtc_streams_response(
|
||||||
|
old_raw_config, new_config
|
||||||
|
)
|
||||||
|
if restricted_response is not None:
|
||||||
|
return restricted_response
|
||||||
|
|
||||||
|
# Save the config to file
|
||||||
|
try:
|
||||||
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()
|
||||||
@@ -808,6 +902,17 @@ 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()
|
||||||
@@ -862,6 +967,19 @@ 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)
|
||||||
|
|
||||||
@@ -913,6 +1031,13 @@ 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(
|
||||||
@@ -925,9 +1050,15 @@ 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/"):
|
||||||
@@ -987,6 +1118,7 @@ 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,
|
||||||
|
|||||||
+86
-37
@@ -12,8 +12,9 @@ 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, urlparse
|
from urllib.parse import parse_qs
|
||||||
|
|
||||||
|
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
|
||||||
@@ -31,6 +32,8 @@ 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
|
||||||
@@ -130,23 +133,18 @@ def require_admin_by_default():
|
|||||||
if path.startswith(EXEMPT_PREFIXES):
|
if path.startswith(EXEMPT_PREFIXES):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Dynamic camera path exemption:
|
# Camera routes enforce per-camera access via route-level dependencies
|
||||||
# Any path whose first segment matches a configured camera name should
|
# (e.g. require_camera_access). Match on the route template, not the raw
|
||||||
# bypass the global admin requirement. These endpoints enforce access
|
# path, so a camera named like another namespace (e.g. "faces") can't
|
||||||
# via route-level dependencies (e.g. require_camera_access) to ensure
|
# waive the admin check for that namespace's routes.
|
||||||
# per-camera authorization. This allows non-admin authenticated users
|
route = request.scope.get("route")
|
||||||
# (e.g. viewer role) to access camera-specific resources without
|
|
||||||
# needing admin privileges.
|
|
||||||
try:
|
|
||||||
if path.startswith("/"):
|
|
||||||
first_segment = path.split("/", 2)[1]
|
|
||||||
if (
|
if (
|
||||||
first_segment
|
route is not None
|
||||||
and first_segment in request.app.frigate_config.cameras
|
and route.path.startswith("/{camera_name}")
|
||||||
|
and request.path_params.get("camera_name")
|
||||||
|
in request.app.frigate_config.cameras
|
||||||
):
|
):
|
||||||
return
|
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
|
||||||
@@ -318,17 +316,19 @@ 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 = [
|
||||||
for proxy in request.app.frigate_config.auth.trusted_proxies:
|
ipaddress.ip_network(proxy, strict=False)
|
||||||
try:
|
for proxy in request.app.frigate_config.auth.trusted_proxies
|
||||||
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:
|
||||||
@@ -473,12 +473,11 @@ def create_encoded_jwt(user, role, expiration, secret):
|
|||||||
|
|
||||||
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):
|
||||||
# TODO: ideally this would set secure as well, but that requires TLS
|
# TODO: ideally this would set secure as well, but that requires TLS
|
||||||
# SameSite is intentionally left unset (browsers default to Lax). Setting
|
# Starlette sets SameSite=Lax by default. The cookie is still sent to
|
||||||
# SameSite=Lax/Strict would stop the cookie from being sent in cross-origin
|
# same-site iframes (e.g. Home Assistant on the same host or domain), but
|
||||||
# iframes, breaking embedded views such as the Home Assistant Frigate card.
|
# not to cross-site ones. CSRF is also mitigated by requiring a custom
|
||||||
# CSRF is instead mitigated by requiring a custom X-CSRF-TOKEN header, which
|
# X-CSRF-TOKEN header, which cross-origin pages cannot set without a CORS
|
||||||
# cross-origin pages cannot set without a CORS preflight that Frigate never
|
# preflight that Frigate never grants (see check_csrf in api/fastapi_app.py).
|
||||||
# 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,
|
||||||
@@ -1003,7 +1002,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("^[A-Za-z0-9._]+$", body.username):
|
if not re.match(r"^(?!\.+$)[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:
|
||||||
@@ -1229,11 +1228,44 @@ 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.
|
# endpoint, not these proxied live-stream paths. The nginx locations are prefix
|
||||||
|
# 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",
|
||||||
@@ -1243,31 +1275,48 @@ 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 role-restricted users from go2rtc live streams they cannot access.
|
"""Block go2rtc live-stream requests the current role may not make.
|
||||||
|
|
||||||
Returns 403 when any `src` stream named in `original_url` resolves to a
|
Returns 403 when any `src` named in `original_url` is not an existing go2rtc
|
||||||
camera outside the role's allow-list (or when no `src` is provided on a
|
stream (for every role), or when it resolves to a camera outside a
|
||||||
stream-proxy path), otherwise None. Mirrors the resolution logic in
|
restricted role's allow-list (or no `src` is provided for such a role),
|
||||||
|
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
|
||||||
|
|
||||||
parsed = urlparse(original_url)
|
path, query = split_request_uri(original_url)
|
||||||
if parsed.path not in GO2RTC_STREAM_PROXY_PATHS:
|
if not _is_go2rtc_stream_proxy_request(path, request):
|
||||||
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
|
||||||
|
|||||||
+51
-7
@@ -33,16 +33,25 @@ 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 clean_camera_user_pass, get_record_segment_time
|
from frigate.util.builtin import (
|
||||||
|
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_source,
|
is_restricted_go2rtc_api_source,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -148,9 +157,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_source(src):
|
if src and is_restricted_go2rtc_api_source(src):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
|
"Rejected go2rtc stream '%s' with restricted source",
|
||||||
stream_name,
|
stream_name,
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -169,9 +178,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_source(resolved_src):
|
if is_restricted_go2rtc_api_source(resolved_src):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Rejected go2rtc stream '%s' with restricted source type (echo/expr/exec)",
|
"Rejected go2rtc stream '%s' with restricted source",
|
||||||
stream_name,
|
stream_name,
|
||||||
)
|
)
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
@@ -248,6 +257,34 @@ 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
|
||||||
@@ -1240,6 +1277,7 @@ 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", {})
|
||||||
@@ -1342,6 +1380,12 @@ 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,5 +1,6 @@
|
|||||||
"""Object classification APIs."""
|
"""Object classification APIs."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
@@ -48,6 +49,9 @@ 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."""
|
||||||
@@ -1290,7 +1294,10 @@ 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)
|
||||||
|
|
||||||
collect_object_classification_examples(model_name, body.label)
|
async with example_collection_lock:
|
||||||
|
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,6 +51,7 @@ 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,6 +14,7 @@ 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
|
||||||
@@ -39,7 +40,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):
|
||||||
@@ -55,6 +56,7 @@ 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"
|
||||||
@@ -66,7 +68,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
|
||||||
@@ -76,6 +78,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,6 +10,8 @@ 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):
|
||||||
|
|||||||
+11
-2
@@ -129,6 +129,7 @@ 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
|
||||||
@@ -361,11 +362,15 @@ 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)
|
.order_by(order_by, *tiebreaker)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
.offset(offset)
|
||||||
.dicts()
|
.dicts()
|
||||||
.iterator()
|
.iterator()
|
||||||
)
|
)
|
||||||
@@ -534,6 +539,7 @@ 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
|
||||||
@@ -840,6 +846,9 @@ 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():
|
||||||
@@ -897,7 +906,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[:limit]
|
processed_events = processed_events[offset:][:limit]
|
||||||
|
|
||||||
return JSONResponse(content=processed_events)
|
return JSONResponse(content=processed_events)
|
||||||
|
|
||||||
|
|||||||
+21
-15
@@ -58,6 +58,7 @@ 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,
|
||||||
@@ -79,6 +80,7 @@ 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__)
|
||||||
@@ -134,6 +136,7 @@ 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:
|
||||||
@@ -141,6 +144,15 @@ 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"},
|
||||||
@@ -166,15 +178,7 @@ 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
|
||||||
@@ -239,9 +243,7 @@ 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,
|
||||||
Recordings.start_time.between(min_start, max_end)
|
recording_overlap(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
|
||||||
@@ -701,7 +703,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(
|
||||||
item.image_path
|
request, 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
|
||||||
@@ -849,7 +851,9 @@ 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(body.image_path)
|
existing_image, image_validation_error = _sanitize_existing_image(
|
||||||
|
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
|
||||||
|
|
||||||
@@ -1044,7 +1048,9 @@ def export_recording_custom(
|
|||||||
|
|
||||||
playback_source = body.source
|
playback_source = body.source
|
||||||
friendly_name = body.name
|
friendly_name = body.name
|
||||||
existing_image, image_validation_error = _sanitize_existing_image(body.image_path)
|
existing_image, image_validation_error = _sanitize_existing_image(
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
@@ -81,9 +82,20 @@ 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
|
||||||
@@ -119,15 +131,6 @@ 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
|
||||||
|
|||||||
+32
-32
@@ -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,9 +62,10 @@ 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__)
|
||||||
|
|
||||||
@@ -296,11 +297,16 @@ 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 = cv2.cvtColor(
|
frame = frame_processor.get_current_frame(camera_name)
|
||||||
frame_processor.get_current_frame(camera_name),
|
|
||||||
cv2.COLOR_YUV2BGR_I420,
|
if frame is None:
|
||||||
|
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])
|
||||||
|
|
||||||
@@ -347,10 +353,7 @@ async def get_snapshot_from_recording(
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(recording_overlap(frame_time, frame_time))
|
||||||
(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)
|
||||||
@@ -366,10 +369,7 @@ async def get_snapshot_from_recording(
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(recording_overlap(frame_time, frame_time))
|
||||||
(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,8 +384,13 @@ 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 = get_image_from_recording(
|
image_data = await asyncio.to_thread(
|
||||||
config.ffmpeg, recording.path, time_in_segment, codec, height
|
get_image_from_recording,
|
||||||
|
config.ffmpeg,
|
||||||
|
recording.path,
|
||||||
|
time_in_segment,
|
||||||
|
codec,
|
||||||
|
height,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
@@ -427,9 +432,7 @@ async def submit_recording_snapshot_to_plus(
|
|||||||
Recordings.path,
|
Recordings.path,
|
||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(recording_overlap(frame_time, frame_time))
|
||||||
(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)
|
||||||
@@ -439,8 +442,12 @@ 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 = get_image_from_recording(
|
image_data = await asyncio.to_thread(
|
||||||
config.ffmpeg, recording.path, time_in_segment, "png"
|
get_image_from_recording,
|
||||||
|
config.ffmpeg,
|
||||||
|
recording.path,
|
||||||
|
time_in_segment,
|
||||||
|
"png",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not image_data:
|
if not image_data:
|
||||||
@@ -539,11 +546,7 @@ async def recording_clip(
|
|||||||
Recordings.start_time,
|
Recordings.start_time,
|
||||||
Recordings.end_time,
|
Recordings.end_time,
|
||||||
)
|
)
|
||||||
.where(
|
.where(recording_overlap(start_ts, end_ts))
|
||||||
(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())
|
||||||
@@ -681,15 +684,10 @@ 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(intervals, main_audio, sub_audio),
|
null_audio_glitches(resolve_coverage(camera_name, start_ts, end_ts)),
|
||||||
stream_preference,
|
stream_preference,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -843,7 +841,9 @@ 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(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
- datetime.now(
|
||||||
|
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()
|
||||||
|
|||||||
+42
-10
@@ -13,8 +13,9 @@ 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, urlparse
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ 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."""
|
||||||
@@ -41,6 +44,36 @@ 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.
|
||||||
|
|
||||||
@@ -49,14 +82,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.
|
whose path contains `.` or `..` segments outright. Repeated slashes are
|
||||||
|
merged, as nginx does before matching locations.
|
||||||
"""
|
"""
|
||||||
if not original_url:
|
if not original_url:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
parsed = urlparse(original_url)
|
path, _ = split_request_uri(original_url)
|
||||||
raw_path = parsed.path or original_url
|
decoded = re.sub(r"/{2,}", "/", unquote(path))
|
||||||
decoded = unquote(raw_path)
|
|
||||||
if not decoded:
|
if not decoded:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -252,12 +285,11 @@ 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.
|
||||||
# For media-root URIs that's a traversal attempt — deny outright. For
|
# Deny them when nginx would resolve them under a media root, and pass
|
||||||
# non-media URIs, pass through (nginx / the backend handle them).
|
# the rest through to nginx and the backend.
|
||||||
if path is None:
|
if path is None:
|
||||||
raw = urlparse(original_url).path or original_url
|
normalized = normalize_request_path(split_request_uri(original_url)[0])
|
||||||
decoded = unquote(raw)
|
first = normalized.lstrip("/").split("/", 1)[0]
|
||||||
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
|
||||||
|
|||||||
+50
-22
@@ -14,17 +14,18 @@ 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_dismissed: bool = False) -> JSONResponse:
|
def get_notices(request: Request, include_hidden: bool = False) -> JSONResponse:
|
||||||
"""Get notices, most severe first.
|
"""Get notices, most severe first.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
include_dismissed: Also return dismissed notices, for the history view
|
include_hidden: Also return acknowledged and muted notices, for the
|
||||||
|
hidden list
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The notices
|
The notices
|
||||||
"""
|
"""
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=request.app.notice_registry.active(include_dismissed=include_dismissed)
|
content=request.app.notice_registry.active(include_hidden=include_hidden)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -34,37 +35,64 @@ 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(
|
@router.get("/notices/muted_checks", dependencies=[Depends(require_role(["admin"]))])
|
||||||
"/notices/dismissed_checks", dependencies=[Depends(require_role(["admin"]))]
|
def get_muted_checks(request: Request) -> JSONResponse:
|
||||||
)
|
"""Get the muted config and stream check rows, newest first."""
|
||||||
def get_dismissed_checks(request: Request) -> JSONResponse:
|
return JSONResponse(content=request.app.notice_registry.muted_checks())
|
||||||
"""Get the dismissed config and stream check rows, newest first."""
|
|
||||||
return JSONResponse(content=request.app.notice_registry.dismissed_checks())
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/notices/dismissed", dependencies=[Depends(require_role(["admin"]))])
|
@router.delete("/notices/hidden", dependencies=[Depends(require_role(["admin"]))])
|
||||||
def purge_dismissed(request: Request) -> JSONResponse:
|
def unhide_all_notices(request: Request) -> JSONResponse:
|
||||||
"""Delete every dismissed notice and check row so each can show again."""
|
"""Show every acknowledged and muted notice and check row again."""
|
||||||
request.app.notice_registry.purge_dismissed()
|
request.app.notice_registry.unhide_all()
|
||||||
return JSONResponse(
|
return JSONResponse(content={"success": True, "message": "Notices shown again"})
|
||||||
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}/dismiss",
|
"/notices/{notice_id:path}/acknowledge",
|
||||||
dependencies=[Depends(require_role(["admin"]))],
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
)
|
)
|
||||||
def dismiss_notice(request: Request, notice_id: str) -> JSONResponse:
|
def acknowledge_notice(request: Request, notice_id: str) -> JSONResponse:
|
||||||
"""Hide a notice or a config or stream check row.
|
"""Hide a notice until it happens again.
|
||||||
|
|
||||||
It stays hidden if the same problem happens again.
|
Config and stream check rows and the update notice never repeat, so they
|
||||||
|
can only be muted.
|
||||||
"""
|
"""
|
||||||
if not request.app.notice_registry.dismiss(notice_id):
|
if not request.app.notice_registry.acknowledge(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 dismissed"})
|
return JSONResponse(content={"success": True, "message": "Notice acknowledged"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/notices/{notice_id:path}/mute",
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
)
|
||||||
|
def mute_notice(request: Request, notice_id: str) -> JSONResponse:
|
||||||
|
"""Hide a notice or a config or stream check row for good."""
|
||||||
|
if not request.app.notice_registry.mute(notice_id):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Notice not found"},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(content={"success": True, "message": "Notice muted"})
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/notices/{notice_id:path}/hidden",
|
||||||
|
dependencies=[Depends(require_role(["admin"]))],
|
||||||
|
)
|
||||||
|
def unhide_notice(request: Request, notice_id: str) -> JSONResponse:
|
||||||
|
"""Show an acknowledged or muted notice or check row again."""
|
||||||
|
if not request.app.notice_registry.unhide(notice_id):
|
||||||
|
return JSONResponse(
|
||||||
|
content={"success": False, "message": "Notice not found"},
|
||||||
|
status_code=404,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JSONResponse(content={"success": True, "message": "Notice shown again"})
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ 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
|
||||||
@@ -173,16 +172,22 @@ def register_notifications(request: Request, body: dict = None):
|
|||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
updated = (
|
||||||
User.update(notification_tokens=User.notification_tokens.append(sub)).where(
|
User.update(notification_tokens=User.notification_tokens.append(sub))
|
||||||
User.username == username
|
.where(User.username == username)
|
||||||
).execute()
|
.execute()
|
||||||
return JSONResponse(
|
)
|
||||||
content=({"success": True, "message": "Successfully saved token."}),
|
|
||||||
status_code=200,
|
if not updated:
|
||||||
|
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,6 +22,7 @@ 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__)
|
||||||
|
|
||||||
@@ -126,7 +127,9 @@ 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(pytz.timezone(tz_name.replace(",", "/"))).utcoffset()
|
- datetime.now(
|
||||||
|
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()
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -175,7 +176,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 = (
|
||||||
@@ -537,11 +538,7 @@ 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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+23
-20
@@ -37,6 +37,7 @@ 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__)
|
||||||
@@ -44,6 +45,22 @@ 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],
|
||||||
@@ -93,10 +110,7 @@ async def review(
|
|||||||
filtered_labels = labels.split(",")
|
filtered_labels = labels.split(",")
|
||||||
|
|
||||||
for label in filtered_labels:
|
for label in filtered_labels:
|
||||||
label_clauses.append(
|
label_clauses.append(get_label_clause(label))
|
||||||
(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":
|
||||||
@@ -239,10 +253,7 @@ 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(
|
label_clauses.append(get_label_clause(label))
|
||||||
(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
|
||||||
@@ -340,9 +351,8 @@ 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(
|
label_clauses.append(get_label_clause(label, include_audio=False))
|
||||||
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
|
||||||
@@ -576,14 +586,7 @@ 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(
|
.where(recording_overlap(start_time, end_time))
|
||||||
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()
|
||||||
@@ -693,7 +696,7 @@ def motion_activity(
|
|||||||
df = df[df["camera"] != ""]
|
df = df[df["camera"] != ""]
|
||||||
|
|
||||||
# change types for output
|
# change types for output
|
||||||
df.index = df.index.astype(int) // (10**9)
|
df.index = df.index.as_unit("s").astype(int)
|
||||||
normalized = df.reset_index().to_dict("records")
|
normalized = df.reset_index().to_dict("records")
|
||||||
return JSONResponse(content=normalized)
|
return JSONResponse(content=normalized)
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -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,7 +49,6 @@ 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
|
||||||
@@ -78,7 +77,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 PtzAutoTrackerThread
|
from frigate.ptz.autotrack import PtzAutoTracker
|
||||||
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
|
||||||
@@ -108,7 +107,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[SceneEnum, Queue] = {
|
self.detection_queues: dict[str, 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] = {}
|
||||||
@@ -167,11 +166,7 @@ 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
|
||||||
@@ -191,7 +186,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: SqliteExtDatabase) -> None:
|
def vacuum_db(db: SqliteDatabase) -> None:
|
||||||
logger.info("Running database vacuum")
|
logger.info("Running database vacuum")
|
||||||
db.execute_sql("VACUUM;")
|
db.execute_sql("VACUUM;")
|
||||||
|
|
||||||
@@ -202,7 +197,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 = SqliteExtDatabase(self.config.database.path)
|
migrate_db = SqliteDatabase(self.config.database.path)
|
||||||
|
|
||||||
# Run migrations
|
# Run migrations
|
||||||
del logging.getLogger("peewee_migrate").handlers[:]
|
del logging.getLogger("peewee_migrate").handlers[:]
|
||||||
@@ -395,7 +390,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[SceneEnum, list[str]] = {
|
model_cameras: dict[str, list[str]] = {
|
||||||
model.scene: [] for model in self.config.models
|
model.scene: [] for model in self.config.models
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,7 +439,7 @@ class FrigateApp:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def start_ptz_autotracker(self) -> None:
|
def start_ptz_autotracker(self) -> None:
|
||||||
self.ptz_autotracker_thread = PtzAutoTrackerThread(
|
self.ptz_autotracker_thread = PtzAutoTracker(
|
||||||
self.config,
|
self.config,
|
||||||
self.onvif_controller,
|
self.onvif_controller,
|
||||||
self.ptz_metrics,
|
self.ptz_metrics,
|
||||||
@@ -586,7 +581,12 @@ class FrigateApp:
|
|||||||
|
|
||||||
def init_auth(self) -> None:
|
def init_auth(self) -> None:
|
||||||
if self.config.auth.enabled:
|
if self.config.auth.enabled:
|
||||||
if User.select().count() == 0:
|
if (
|
||||||
|
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,6 +598,9 @@ 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
|
||||||
@@ -628,6 +631,19 @@ 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,8 +43,6 @@ 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
|
||||||
@@ -52,21 +50,17 @@ 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, *, autotracker_enabled: bool):
|
def __init__(self) -> None:
|
||||||
self.autotracker_enabled = mp.Value("i", autotracker_enabled) # type: ignore[assignment]
|
self.start_time = mp.Value("d", 0)
|
||||||
|
self.stop_time = mp.Value("d", 0)
|
||||||
|
self.frame_time = mp.Value("d", 0)
|
||||||
|
self.zoom_level = mp.Value("d", 0)
|
||||||
|
self.max_zoom = mp.Value("d", 0)
|
||||||
|
self.min_zoom = mp.Value("d", 0)
|
||||||
|
|
||||||
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,12 +104,13 @@ 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():
|
||||||
if camera not in self.config.cameras:
|
camera_config = self.config.cameras.get(camera)
|
||||||
|
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(self.config.cameras[camera])
|
self.__init_camera(camera_config)
|
||||||
|
|
||||||
new_objects = new_activity[camera].get("objects", [])
|
new_objects = new_activity[camera].get("objects", [])
|
||||||
all_objects.extend(new_objects)
|
all_objects.extend(new_objects)
|
||||||
@@ -234,12 +235,13 @@ class AudioActivityManager:
|
|||||||
now = datetime.datetime.now().timestamp()
|
now = datetime.datetime.now().timestamp()
|
||||||
|
|
||||||
for camera in new_activity.keys():
|
for camera in new_activity.keys():
|
||||||
if camera not in self.config.cameras:
|
camera_config = self.config.cameras.get(camera)
|
||||||
|
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(self.config.cameras[camera])
|
self.__init_camera(camera_config)
|
||||||
|
|
||||||
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,7 +15,6 @@ 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
|
||||||
@@ -31,7 +30,7 @@ class CameraMaintainer(threading.Thread):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: FrigateConfig,
|
config: FrigateConfig,
|
||||||
detection_queues: dict[SceneEnum, Queue],
|
detection_queues: dict[str, 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],
|
||||||
@@ -121,9 +120,7 @@ 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,
|
||||||
@@ -172,6 +169,10 @@ 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,
|
||||||
|
|||||||
+14
-22
@@ -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 PtzAutoTrackerThread
|
from frigate.ptz.autotrack import PtzAutoTracker, calculate_max_target_box
|
||||||
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: PtzAutoTrackerThread,
|
ptz_autotracker_thread: PtzAutoTracker,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.name = name
|
self.name = name
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -115,17 +115,13 @@ 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.ptz_autotracker.autotracker_init.get(
|
and self.ptz_autotracker_thread.autotracker_init.get(self.name)
|
||||||
self.name
|
and self.ptz_autotracker_thread.tracked_object[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.ptz_autotracker.tracked_object[
|
== self.ptz_autotracker_thread.tracked_object[ # type: ignore[union-attr]
|
||||||
self.name
|
self.name
|
||||||
].obj_data["id"] # type: ignore[attr-defined]
|
].obj_data["id"]
|
||||||
and obj["frame_time"] == frame_time
|
and obj["frame_time"] == frame_time
|
||||||
):
|
):
|
||||||
thickness = 5
|
thickness = 5
|
||||||
@@ -138,9 +134,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 = self.ptz_autotracker_thread.ptz_autotracker.tracked_object_metrics[
|
max_target_box = calculate_max_target_box(
|
||||||
self.name
|
self.camera_config.onvif.autotracking.zoom_factor
|
||||||
]["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,
|
||||||
@@ -353,7 +349,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_name, self.camera_config.frame_shape_yuv, frame_time
|
||||||
)
|
)
|
||||||
|
|
||||||
tracked_objects = self.tracked_objects.copy()
|
tracked_objects = self.tracked_objects.copy()
|
||||||
@@ -396,7 +392,6 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -408,7 +403,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)
|
c(self.name, new_obj, frame_name, frame_time)
|
||||||
|
|
||||||
for id in updated_ids:
|
for id in updated_ids:
|
||||||
updated_obj = tracked_objects[id]
|
updated_obj = tracked_objects[id]
|
||||||
@@ -420,7 +415,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)
|
c(self.name, updated_obj, frame_name, frame_time)
|
||||||
|
|
||||||
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
|
||||||
@@ -470,7 +465,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)
|
c(self.name, updated_obj, frame_name, frame_time)
|
||||||
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,
|
||||||
@@ -492,7 +487,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)
|
c(self.name, removed_obj, frame_name, frame_time)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -607,9 +602,6 @@ 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:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Facilitates communication between processes."""
|
"""Facilitates communication between processes."""
|
||||||
|
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
import threading
|
||||||
from _pickle import UnpicklingError
|
from _pickle import UnpicklingError
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -18,9 +19,11 @@ class ConfigPublisher:
|
|||||||
self.socket = self.context.socket(zmq.PUB)
|
self.socket = self.context.socket(zmq.PUB)
|
||||||
self.socket.bind(SOCKET_PUB_SUB)
|
self.socket.bind(SOCKET_PUB_SUB)
|
||||||
self.stop_event: MpEvent = mp.Event()
|
self.stop_event: MpEvent = mp.Event()
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def publish(self, topic: str, payload: Any) -> None:
|
def publish(self, topic: str, payload: Any) -> None:
|
||||||
"""There is no communication back to the processes."""
|
"""There is no communication back to the processes."""
|
||||||
|
with self.lock:
|
||||||
self.socket.send_string(topic, flags=zmq.SNDMORE)
|
self.socket.send_string(topic, flags=zmq.SNDMORE)
|
||||||
self.socket.send_pyobj(payload)
|
self.socket.send_pyobj(payload)
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class DetectionTypeEnum(str, Enum):
|
|||||||
video = "video"
|
video = "video"
|
||||||
audio = "audio"
|
audio = "audio"
|
||||||
lpr = "lpr"
|
lpr = "lpr"
|
||||||
|
classification_state = "classification_state"
|
||||||
|
|
||||||
|
|
||||||
class DetectionPublisher(Publisher):
|
class DetectionPublisher(Publisher):
|
||||||
|
|||||||
@@ -322,7 +322,11 @@ class Dispatcher:
|
|||||||
self.audio_activity.expire_all(payload)
|
self.audio_activity.expire_all(payload)
|
||||||
|
|
||||||
def handle_update_event_description() -> None:
|
def handle_update_event_description() -> None:
|
||||||
event: Event = Event.get(Event.id == payload["id"])
|
event: Event | None = Event.get_or_none(Event.id == payload["id"])
|
||||||
|
|
||||||
|
if event is None:
|
||||||
|
return
|
||||||
|
|
||||||
cast(dict, event.data)["description"] = payload["description"]
|
cast(dict, event.data)["description"] = payload["description"]
|
||||||
event.save()
|
event.save()
|
||||||
self.publish(
|
self.publish(
|
||||||
@@ -759,15 +763,13 @@ class Dispatcher:
|
|||||||
"Autotracking must be enabled in the config to be turned on via MQTT."
|
"Autotracking must be enabled in the config to be turned on via MQTT."
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not self.ptz_metrics[camera_name].autotracker_enabled.value:
|
if not ptz_autotracker_settings.enabled:
|
||||||
logger.info(f"Turning on ptz autotracker for {camera_name}")
|
logger.info(f"Turning on ptz autotracker for {camera_name}")
|
||||||
self.ptz_metrics[camera_name].autotracker_enabled.value = True
|
|
||||||
self.ptz_metrics[camera_name].start_time.value = 0
|
self.ptz_metrics[camera_name].start_time.value = 0
|
||||||
ptz_autotracker_settings.enabled = True
|
ptz_autotracker_settings.enabled = True
|
||||||
elif payload == "OFF":
|
elif payload == "OFF":
|
||||||
if self.ptz_metrics[camera_name].autotracker_enabled.value:
|
if ptz_autotracker_settings.enabled:
|
||||||
logger.info(f"Turning off ptz autotracker for {camera_name}")
|
logger.info(f"Turning off ptz autotracker for {camera_name}")
|
||||||
self.ptz_metrics[camera_name].autotracker_enabled.value = False
|
|
||||||
self.ptz_metrics[camera_name].start_time.value = 0
|
self.ptz_metrics[camera_name].start_time.value = 0
|
||||||
ptz_autotracker_settings.enabled = False
|
ptz_autotracker_settings.enabled = False
|
||||||
|
|
||||||
@@ -782,7 +784,9 @@ class Dispatcher:
|
|||||||
try:
|
try:
|
||||||
payload = int(payload)
|
payload = int(payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
f"Received unsupported value for motion contour area: {payload}"
|
f"Received unsupported value for motion contour area: {payload}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
motion_settings = self.config.cameras[camera_name].motion
|
motion_settings = self.config.cameras[camera_name].motion
|
||||||
@@ -799,7 +803,9 @@ class Dispatcher:
|
|||||||
try:
|
try:
|
||||||
payload = int(payload)
|
payload = int(payload)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
logger.warning(
|
||||||
f"Received unsupported value for motion threshold: {payload}"
|
f"Received unsupported value for motion threshold: {payload}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
motion_settings = self.config.cameras[camera_name].motion
|
motion_settings = self.config.cameras[camera_name].motion
|
||||||
@@ -814,7 +820,9 @@ class Dispatcher:
|
|||||||
def _on_global_notification_command(self, payload: str) -> None:
|
def _on_global_notification_command(self, payload: str) -> None:
|
||||||
"""Callback for global notification topic."""
|
"""Callback for global notification topic."""
|
||||||
if payload != "ON" and payload != "OFF":
|
if payload != "ON" and payload != "OFF":
|
||||||
|
logger.warning(
|
||||||
f"Received unsupported value for all notification: {payload}"
|
f"Received unsupported value for all notification: {payload}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
notification_settings = self.config.notifications
|
notification_settings = self.config.notifications
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"""Facilitates communication between processes."""
|
"""Facilitates communication between processes."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -79,14 +80,21 @@ class EmbeddingsRequestor:
|
|||||||
self.context = zmq.Context()
|
self.context = zmq.Context()
|
||||||
self.socket = self.context.socket(zmq.REQ)
|
self.socket = self.context.socket(zmq.REQ)
|
||||||
self.socket.connect(SOCKET_REP_REQ)
|
self.socket.connect(SOCKET_REP_REQ)
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def send_data(self, topic: str, data: Any) -> Any:
|
def send_data(self, topic: str, data: Any) -> Any:
|
||||||
"""Sends data and then waits for reply."""
|
"""Sends data and then waits for reply."""
|
||||||
|
# an overlapping call fails fast so a slow reply can't stall the API
|
||||||
|
if not self.lock.acquire(blocking=False):
|
||||||
|
return ""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.socket.send_json((topic, data))
|
self.socket.send_json((topic, data))
|
||||||
return self.socket.recv_json()
|
return self.socket.recv_json()
|
||||||
except zmq.ZMQError:
|
except zmq.ZMQError:
|
||||||
return ""
|
return ""
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
self.socket.close()
|
self.socket.close()
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ from .zmq_proxy import Publisher, Subscriber
|
|||||||
|
|
||||||
|
|
||||||
class EventUpdatePublisher(
|
class EventUpdatePublisher(
|
||||||
Publisher[tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]]]
|
Publisher[
|
||||||
|
tuple[
|
||||||
|
EventTypeEnum,
|
||||||
|
EventStateEnum,
|
||||||
|
str | None,
|
||||||
|
str,
|
||||||
|
dict[str, Any],
|
||||||
|
float | None,
|
||||||
|
]
|
||||||
|
]
|
||||||
):
|
):
|
||||||
"""Publishes events (objects, audio, manual)."""
|
"""Publishes events (objects, audio, manual)."""
|
||||||
|
|
||||||
@@ -19,7 +28,14 @@ class EventUpdatePublisher(
|
|||||||
|
|
||||||
def publish(
|
def publish(
|
||||||
self,
|
self,
|
||||||
payload: tuple[EventTypeEnum, EventStateEnum, str | None, str, dict[str, Any]],
|
payload: tuple[
|
||||||
|
EventTypeEnum,
|
||||||
|
EventStateEnum,
|
||||||
|
str | None,
|
||||||
|
str,
|
||||||
|
dict[str, Any],
|
||||||
|
float | None,
|
||||||
|
],
|
||||||
sub_topic: str = "",
|
sub_topic: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
super().publish(payload, sub_topic)
|
super().publish(payload, sub_topic)
|
||||||
|
|||||||
@@ -79,9 +79,11 @@ class InterProcessRequestor:
|
|||||||
self.context = zmq.Context()
|
self.context = zmq.Context()
|
||||||
self.socket = self.context.socket(zmq.REQ)
|
self.socket = self.context.socket(zmq.REQ)
|
||||||
self.socket.connect(SOCKET_REP_REQ)
|
self.socket.connect(SOCKET_REP_REQ)
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
|
||||||
def send_data(self, topic: str, data: Any) -> Any:
|
def send_data(self, topic: str, data: Any) -> Any:
|
||||||
"""Sends data and then waits for reply."""
|
"""Sends data and then waits for reply."""
|
||||||
|
with self.lock:
|
||||||
try:
|
try:
|
||||||
self.socket.send_json((topic, data))
|
self.socket.send_json((topic, data))
|
||||||
return self.socket.recv_json()
|
return self.socket.recv_json()
|
||||||
|
|||||||
+58
-3
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import queue
|
import queue
|
||||||
|
import selectors
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
@@ -56,6 +58,11 @@ class MqttClient(Communicator):
|
|||||||
self._next_connect_time = 0.0
|
self._next_connect_time = 0.0
|
||||||
self._last_on_connect_dispatch = 0.0
|
self._last_on_connect_dispatch = 0.0
|
||||||
|
|
||||||
|
# lets other threads interrupt the worker's socket wait
|
||||||
|
self._wake_recv, self._wake_send = socket.socketpair()
|
||||||
|
self._wake_recv.setblocking(False)
|
||||||
|
self._wake_send.setblocking(False)
|
||||||
|
|
||||||
def subscribe(self, receiver: Callable) -> None:
|
def subscribe(self, receiver: Callable) -> None:
|
||||||
"""Wrapper for allowing dispatcher to subscribe."""
|
"""Wrapper for allowing dispatcher to subscribe."""
|
||||||
self._dispatcher = receiver
|
self._dispatcher = receiver
|
||||||
@@ -85,6 +92,7 @@ class MqttClient(Communicator):
|
|||||||
return
|
return
|
||||||
|
|
||||||
self._publish_queue.put(QueuedPublish(full_topic, payload, retain))
|
self._publish_queue.put(QueuedPublish(full_topic, payload, retain))
|
||||||
|
self._wake_worker()
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
if self._worker is None:
|
if self._worker is None:
|
||||||
@@ -100,9 +108,11 @@ class MqttClient(Communicator):
|
|||||||
publish_done,
|
publish_done,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self._wake_worker()
|
||||||
publish_done.wait(MQTT_SHUTDOWN_FLUSH_TIMEOUT)
|
publish_done.wait(MQTT_SHUTDOWN_FLUSH_TIMEOUT)
|
||||||
|
|
||||||
self._stop_event.set()
|
self._stop_event.set()
|
||||||
|
self._wake_worker()
|
||||||
|
|
||||||
if self.client is not None:
|
if self.client is not None:
|
||||||
try:
|
try:
|
||||||
@@ -358,7 +368,7 @@ class MqttClient(Communicator):
|
|||||||
deadline = time.monotonic() + MQTT_SHUTDOWN_FLUSH_TIMEOUT
|
deadline = time.monotonic() + MQTT_SHUTDOWN_FLUSH_TIMEOUT
|
||||||
while not message_info.is_published() and time.monotonic() < deadline:
|
while not message_info.is_published() and time.monotonic() < deadline:
|
||||||
if (
|
if (
|
||||||
self.client.loop(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
|
self._loop_client(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
|
||||||
!= mqtt.MQTT_ERR_SUCCESS
|
!= mqtt.MQTT_ERR_SUCCESS
|
||||||
):
|
):
|
||||||
break
|
break
|
||||||
@@ -368,6 +378,51 @@ class MqttClient(Communicator):
|
|||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _wake_worker(self) -> None:
|
||||||
|
try:
|
||||||
|
self._wake_send.send(b"\0")
|
||||||
|
except BlockingIOError:
|
||||||
|
# the buffer is full, so a wake is already pending
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _loop_client(self, timeout: float) -> int:
|
||||||
|
"""Drive Paho without select()'s limit on socket file descriptors."""
|
||||||
|
assert self.client is not None
|
||||||
|
client = self.client
|
||||||
|
sock = client.socket()
|
||||||
|
if sock is None:
|
||||||
|
return mqtt.MQTT_ERR_NO_CONN
|
||||||
|
|
||||||
|
events = selectors.EVENT_READ
|
||||||
|
if client.want_write():
|
||||||
|
events |= selectors.EVENT_WRITE
|
||||||
|
# TLS can have decrypted bytes buffered even when the socket is not ready.
|
||||||
|
pending = hasattr(sock, "pending") and sock.pending() > 0
|
||||||
|
with selectors.DefaultSelector() as selector:
|
||||||
|
selector.register(sock, events)
|
||||||
|
selector.register(self._wake_recv, selectors.EVENT_READ)
|
||||||
|
ready = {
|
||||||
|
key.fileobj: mask
|
||||||
|
for key, mask in selector.select(0.0 if pending else timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._wake_recv in ready:
|
||||||
|
try:
|
||||||
|
self._wake_recv.recv(4096)
|
||||||
|
except BlockingIOError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
ready_events = ready.get(sock, 0)
|
||||||
|
if pending or ready_events & selectors.EVENT_READ:
|
||||||
|
result = client.loop_read()
|
||||||
|
if result != mqtt.MQTT_ERR_SUCCESS or client.socket() is None:
|
||||||
|
return result
|
||||||
|
if ready_events & selectors.EVENT_WRITE:
|
||||||
|
result = client.loop_write()
|
||||||
|
if result != mqtt.MQTT_ERR_SUCCESS or client.socket() is None:
|
||||||
|
return result
|
||||||
|
return client.loop_misc()
|
||||||
|
|
||||||
def _mqtt_loop_worker(self) -> None:
|
def _mqtt_loop_worker(self) -> None:
|
||||||
# The worker owns all socket I/O so reconnect, subscribe, and publish
|
# The worker owns all socket I/O so reconnect, subscribe, and publish
|
||||||
# ordering stays serialized in one place.
|
# ordering stays serialized in one place.
|
||||||
@@ -384,7 +439,7 @@ class MqttClient(Communicator):
|
|||||||
|
|
||||||
assert self.client is not None
|
assert self.client is not None
|
||||||
try:
|
try:
|
||||||
result = self.client.loop(timeout=MQTT_LOOP_TIMEOUT)
|
result = self._loop_client(timeout=MQTT_LOOP_TIMEOUT)
|
||||||
except (OSError, mqtt.WebsocketConnectionError) as err:
|
except (OSError, mqtt.WebsocketConnectionError) as err:
|
||||||
logger.warning("MQTT loop error: %s", err)
|
logger.warning("MQTT loop error: %s", err)
|
||||||
self._schedule_reconnect()
|
self._schedule_reconnect()
|
||||||
@@ -617,7 +672,7 @@ class MqttClient(Communicator):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.client.loop(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
|
result = self._loop_client(timeout=MQTT_PUBLISH_WAIT_INTERVAL)
|
||||||
except (OSError, mqtt.WebsocketConnectionError) as err:
|
except (OSError, mqtt.WebsocketConnectionError) as err:
|
||||||
logger.warning("MQTT publish wait failed: %s", err)
|
logger.warning("MQTT publish wait failed: %s", err)
|
||||||
self._schedule_reconnect()
|
self._schedule_reconnect()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import threading
|
import threading
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable, Iterator
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from multiprocessing.synchronize import Event as MpEvent
|
from multiprocessing.synchronize import Event as MpEvent
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -41,6 +41,20 @@ class PushNotification:
|
|||||||
ttl: int = 0
|
ttl: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def _build_web_pushers(user: str, subs: list[dict[str, Any]]) -> list[WebPusher]:
|
||||||
|
"""Build pushers for a user's stored subscriptions, skipping unusable ones."""
|
||||||
|
pushers: list[WebPusher] = []
|
||||||
|
|
||||||
|
for sub in subs:
|
||||||
|
# WebPusher decodes the stored keys and raises on malformed ones
|
||||||
|
try:
|
||||||
|
pushers.append(WebPusher(sub))
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Skipping invalid notification subscription for %s", user)
|
||||||
|
|
||||||
|
return pushers
|
||||||
|
|
||||||
|
|
||||||
class WebPushClient(Communicator):
|
class WebPushClient(Communicator):
|
||||||
"""Frigate wrapper for webpush client."""
|
"""Frigate wrapper for webpush client."""
|
||||||
|
|
||||||
@@ -56,6 +70,7 @@ class WebPushClient(Communicator):
|
|||||||
for c in self.config.cameras.values()
|
for c in self.config.cameras.values()
|
||||||
}
|
}
|
||||||
self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None
|
self.suspension_broadcaster: Callable[[str, Any, bool], None] | None = None
|
||||||
|
self.config_lock = threading.Lock()
|
||||||
self.last_camera_notification_time: dict[str, float] = {
|
self.last_camera_notification_time: dict[str, float] = {
|
||||||
c.name: 0 # type: ignore[misc]
|
c.name: 0 # type: ignore[misc]
|
||||||
for c in self.config.cameras.values()
|
for c in self.config.cameras.values()
|
||||||
@@ -72,13 +87,13 @@ class WebPushClient(Communicator):
|
|||||||
# Pull keys from PEM or generate if they do not exist
|
# Pull keys from PEM or generate if they do not exist
|
||||||
self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem"))
|
self.vapid = Vapid01.from_file(os.path.join(CONFIG_DIR, "notifications.pem"))
|
||||||
|
|
||||||
users: list[dict[str, Any]] = (
|
users: Iterator[dict[str, Any]] = (
|
||||||
User.select(User.username, User.notification_tokens).dicts().iterator()
|
User.select(User.username, User.notification_tokens).dicts().iterator()
|
||||||
)
|
)
|
||||||
for user in users:
|
for user in users:
|
||||||
self.web_pushers[user["username"]] = []
|
self.web_pushers[user["username"]] = _build_web_pushers(
|
||||||
for sub in user["notification_tokens"]:
|
user["username"], user["notification_tokens"]
|
||||||
self.web_pushers[user["username"]].append(WebPusher(sub))
|
)
|
||||||
|
|
||||||
# notification and auth config updater
|
# notification and auth config updater
|
||||||
self.global_config_subscriber = ConfigSubscriber("config/")
|
self.global_config_subscriber = ConfigSubscriber("config/")
|
||||||
@@ -146,10 +161,7 @@ class WebPushClient(Communicator):
|
|||||||
User.username == user
|
User.username == user
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
self.web_pushers[user] = []
|
self.web_pushers[user] = _build_web_pushers(user, user_subs)
|
||||||
|
|
||||||
for sub in user_subs:
|
|
||||||
self.web_pushers[user].append(WebPusher(sub))
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Cleaned up {len(expired)} notification subscriptions for {user}"
|
f"Cleaned up {len(expired)} notification subscriptions for {user}"
|
||||||
@@ -198,6 +210,7 @@ class WebPushClient(Communicator):
|
|||||||
|
|
||||||
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
def publish(self, topic: str, payload: Any, retain: bool = False) -> None:
|
||||||
"""Wrapper for publishing when client is in valid state."""
|
"""Wrapper for publishing when client is in valid state."""
|
||||||
|
with self.config_lock:
|
||||||
# check for updated global config (notifications, auth)
|
# check for updated global config (notifications, auth)
|
||||||
while True:
|
while True:
|
||||||
config_topic, config_payload = (
|
config_topic, config_payload = (
|
||||||
|
|||||||
+81
-3
@@ -3,6 +3,9 @@
|
|||||||
import errno
|
import errno
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import queue
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
import threading
|
import threading
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -76,6 +79,12 @@ _WS_VIEWER_TOPICS = frozenset(
|
|||||||
# Camera-scoped command topics a camera-authorized (non-admin) user may send.
|
# Camera-scoped command topics a camera-authorized (non-admin) user may send.
|
||||||
_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
|
_WS_CAMERA_COMMAND_TOPICS = frozenset({"ptz"})
|
||||||
|
|
||||||
|
# Max outbound messages waiting on a client's writer thread.
|
||||||
|
WS_MAX_PENDING_MESSAGES = 256
|
||||||
|
|
||||||
|
# Seconds a single send() may wait for a client to free socket buffer space.
|
||||||
|
WS_SEND_TIMEOUT = 10
|
||||||
|
|
||||||
|
|
||||||
def _check_ws_authorization(
|
def _check_ws_authorization(
|
||||||
topic: str,
|
topic: str,
|
||||||
@@ -464,6 +473,78 @@ def _materialize_for_ws(
|
|||||||
|
|
||||||
|
|
||||||
class WebSocket(WebSocket_): # type: ignore[misc]
|
class WebSocket(WebSocket_): # type: ignore[misc]
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._send_queue: queue.Queue[tuple[Any, bool] | None] = queue.Queue(
|
||||||
|
maxsize=WS_MAX_PENDING_MESSAGES
|
||||||
|
)
|
||||||
|
self._writer: threading.Thread | None = None
|
||||||
|
self._aborted = False
|
||||||
|
|
||||||
|
def opened(self) -> None:
|
||||||
|
"""Set a send timeout on the socket and start this client's writer thread."""
|
||||||
|
|
||||||
|
# ws4py writes close and pong frames on the shared manager thread, so a
|
||||||
|
# stalled client must not block sendall forever. SO_SNDTIMEO leaves recv
|
||||||
|
# (and idle clients) untouched, unlike settimeout().
|
||||||
|
self.sock.setsockopt(
|
||||||
|
socket.SOL_SOCKET, socket.SO_SNDTIMEO, struct.pack("ll", WS_SEND_TIMEOUT, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
# every client gets its own writer so a client that stops reading only
|
||||||
|
# blocks itself, never the thread that called publish()
|
||||||
|
self._writer = threading.Thread(
|
||||||
|
target=self._drain_send_queue, name="ws_writer", daemon=True
|
||||||
|
)
|
||||||
|
self._writer.start()
|
||||||
|
|
||||||
|
def send(self, payload: Any, binary: bool = False) -> None:
|
||||||
|
"""Queue a message for this client's writer thread.
|
||||||
|
|
||||||
|
Returns without waiting for delivery. Disconnects the client if its queue
|
||||||
|
is full.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self._send_queue.put_nowait((payload, binary))
|
||||||
|
except queue.Full:
|
||||||
|
self._abort("Websocket client is not keeping up, disconnecting it")
|
||||||
|
|
||||||
|
def _drain_send_queue(self) -> None:
|
||||||
|
while True:
|
||||||
|
item = self._send_queue.get()
|
||||||
|
if item is None or self.terminated or self.sock is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
super().send(*item)
|
||||||
|
except Exception:
|
||||||
|
self._abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
def _abort(self, reason: str | None = None) -> None:
|
||||||
|
# publish() keeps hitting a full queue until the manager thread removes
|
||||||
|
# the connection, so only act (and log) the first time
|
||||||
|
if self._aborted:
|
||||||
|
return
|
||||||
|
self._aborted = True
|
||||||
|
if reason:
|
||||||
|
logger.warning(reason)
|
||||||
|
|
||||||
|
# shutdown rather than close so the ws4py manager thread sees EOF and
|
||||||
|
# runs its normal unregister/terminate; this also unblocks a stuck sendall
|
||||||
|
sock = self.sock
|
||||||
|
if sock is not None:
|
||||||
|
try:
|
||||||
|
sock.shutdown(socket.SHUT_RDWR)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def closed(self, code: int, reason: str | None = None) -> None:
|
||||||
|
"""Signal the writer thread to exit."""
|
||||||
|
try:
|
||||||
|
self._send_queue.put_nowait(None)
|
||||||
|
except queue.Full:
|
||||||
|
pass
|
||||||
|
|
||||||
def unhandled_error(self, error: Any) -> None:
|
def unhandled_error(self, error: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Handles the unfriendly socket closures on the server side
|
Handles the unfriendly socket closures on the server side
|
||||||
@@ -597,10 +678,7 @@ class WebSocketClient(Communicator):
|
|||||||
)
|
)
|
||||||
if message is None:
|
if message is None:
|
||||||
continue
|
continue
|
||||||
try:
|
|
||||||
ws.send(message)
|
ws.send(message)
|
||||||
except (ConnectionResetError, BrokenPipeError, ValueError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
if self.websocket_server is not None:
|
if self.websocket_server is not None:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import ipaddress
|
||||||
|
|
||||||
from pydantic import Field, field_validator, model_validator
|
from pydantic import Field, field_validator, model_validator
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
@@ -68,6 +70,14 @@ class AuthConfig(FrigateBaseModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@field_validator("trusted_proxies")
|
||||||
|
@classmethod
|
||||||
|
def validate_trusted_proxies(cls, v: list[str]) -> list[str]:
|
||||||
|
for proxy in v:
|
||||||
|
ipaddress.ip_network(proxy, strict=False)
|
||||||
|
|
||||||
|
return v
|
||||||
|
|
||||||
@field_validator("roles")
|
@field_validator("roles")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]:
|
def validate_roles(cls, v: dict[str, list[str]]) -> dict[str, list[str]]:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from pydantic import Field, model_validator
|
from pydantic import Field, model_validator
|
||||||
|
|
||||||
from frigate.detectors.detector_config import SceneEnum
|
from frigate.detectors.detector_config import DEFAULT_SCENE, SCENE_PATTERN
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
|
|
||||||
@@ -62,10 +62,11 @@ class DetectConfig(FrigateBaseModel):
|
|||||||
title="Detect width",
|
title="Detect width",
|
||||||
description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
|
description="Width (pixels) of frames used for the detect stream; leave empty to use the native stream resolution.",
|
||||||
)
|
)
|
||||||
scene: SceneEnum = Field(
|
scene: str = Field(
|
||||||
default=SceneEnum.all,
|
default=DEFAULT_SCENE,
|
||||||
|
pattern=SCENE_PATTERN,
|
||||||
title="Detect scene",
|
title="Detect scene",
|
||||||
description="The environment this camera looks at, used to pick which of the configured models runs on it. Cameras left on 'all' run the model configured with a scene of 'all'.",
|
description="The environment this camera looks at, used to pick which of the configured models runs on it. Cameras left on 'default' run the model configured with a scene of 'default'.",
|
||||||
)
|
)
|
||||||
fps: int = Field(
|
fps: int = Field(
|
||||||
default=5,
|
default=5,
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ from enum import Enum
|
|||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
from frigate.util.config import resolve_ffmpeg_path
|
from frigate.util.config import resolve_ffmpeg_path
|
||||||
|
from frigate.util.env import EnvString
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
from ..env import EnvString
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"CameraFfmpegConfig",
|
"CameraFfmpegConfig",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ from typing import Any, Self
|
|||||||
|
|
||||||
from pydantic import Field, model_validator
|
from pydantic import Field, model_validator
|
||||||
|
|
||||||
|
from frigate.util.env import EnvString
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
from ..env import EnvString
|
|
||||||
|
|
||||||
__all__ = ["GenAIConfig", "GenAIProviderEnum", "GenAIRoleEnum"]
|
__all__ = ["GenAIConfig", "GenAIProviderEnum", "GenAIRoleEnum"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,57 @@
|
|||||||
from pydantic import Field
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
|
from frigate.util.live_streams import DEFAULT_TRANSCODE_QUALITIES
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
|
|
||||||
__all__ = ["CameraLiveConfig"]
|
__all__ = ["CameraLiveConfig", "LiveTranscodeConfig", "LiveTranscodeQualityConfig"]
|
||||||
|
|
||||||
|
|
||||||
|
class LiveTranscodeQualityConfig(FrigateBaseModel):
|
||||||
|
height: int = Field(
|
||||||
|
ge=144,
|
||||||
|
le=2160,
|
||||||
|
title="Height",
|
||||||
|
description="Output height in pixels; width follows the source aspect ratio.",
|
||||||
|
)
|
||||||
|
bitrate: int = Field(
|
||||||
|
ge=64,
|
||||||
|
title="Bitrate",
|
||||||
|
description="Target and maximum video bitrate in kbps.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LiveTranscodeConfig(FrigateBaseModel):
|
||||||
|
enabled: bool = Field(
|
||||||
|
default=False,
|
||||||
|
title="Enable transcoded streams",
|
||||||
|
description="Add lower-quality live streams that go2rtc transcodes in real time while someone is watching.",
|
||||||
|
)
|
||||||
|
source: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
title="Source stream",
|
||||||
|
description="go2rtc stream to transcode. Defaults to the first live stream.",
|
||||||
|
)
|
||||||
|
qualities: list[LiveTranscodeQualityConfig] = Field(
|
||||||
|
default_factory=lambda: [
|
||||||
|
LiveTranscodeQualityConfig(**quality)
|
||||||
|
for quality in DEFAULT_TRANSCODE_QUALITIES
|
||||||
|
],
|
||||||
|
title="Qualities",
|
||||||
|
description="One transcoded stream is added per quality.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@field_validator("qualities")
|
||||||
|
@classmethod
|
||||||
|
def validate_unique_heights(
|
||||||
|
cls, qualities: list[LiveTranscodeQualityConfig]
|
||||||
|
) -> list[LiveTranscodeQualityConfig]:
|
||||||
|
heights = [quality.height for quality in qualities]
|
||||||
|
|
||||||
|
if len(heights) != len(set(heights)):
|
||||||
|
raise ValueError("Transcoded stream heights must be unique.")
|
||||||
|
|
||||||
|
return qualities
|
||||||
|
|
||||||
|
|
||||||
class CameraLiveConfig(FrigateBaseModel):
|
class CameraLiveConfig(FrigateBaseModel):
|
||||||
@@ -11,6 +60,11 @@ class CameraLiveConfig(FrigateBaseModel):
|
|||||||
title="Live stream names",
|
title="Live stream names",
|
||||||
description="Mapping of configured stream names to restream/go2rtc names used for live playback.",
|
description="Mapping of configured stream names to restream/go2rtc names used for live playback.",
|
||||||
)
|
)
|
||||||
|
transcode: LiveTranscodeConfig = Field(
|
||||||
|
default_factory=LiveTranscodeConfig,
|
||||||
|
title="Transcoded streams",
|
||||||
|
description="Lower-quality live streams transcoded on demand by go2rtc.",
|
||||||
|
)
|
||||||
height: int = Field(
|
height: int = Field(
|
||||||
default=720,
|
default=720,
|
||||||
title="Live height",
|
title="Live height",
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from frigate.util.env import EnvString
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
|
|
||||||
__all__ = ["NotificationConfig"]
|
__all__ = ["NotificationConfig"]
|
||||||
@@ -11,7 +13,7 @@ class NotificationConfig(FrigateBaseModel):
|
|||||||
title="Enable notifications",
|
title="Enable notifications",
|
||||||
description="Enable or disable notifications for all cameras; can be overridden per-camera.",
|
description="Enable or disable notifications for all cameras; can be overridden per-camera.",
|
||||||
)
|
)
|
||||||
email: str | None = Field(
|
email: EnvString | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
title="Notification email",
|
title="Notification email",
|
||||||
description="Email address used for push notifications or required by certain notification providers.",
|
description="Email address used for push notifications or required by certain notification providers.",
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ from enum import Enum
|
|||||||
|
|
||||||
from pydantic import Field, field_validator
|
from pydantic import Field, field_validator
|
||||||
|
|
||||||
|
from frigate.util.env import EnvString
|
||||||
|
|
||||||
from ..base import FrigateBaseModel
|
from ..base import FrigateBaseModel
|
||||||
from ..env import EnvString
|
|
||||||
from .objects import DEFAULT_TRACKED_OBJECTS
|
from .objects import DEFAULT_TRACKED_OBJECTS
|
||||||
|
|
||||||
__all__ = ["OnvifConfig", "PtzAutotrackConfig", "ZoomingModeEnum"]
|
__all__ = ["OnvifConfig", "PtzAutotrackConfig", "ZoomingModeEnum"]
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ class RecordConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
expire_interval: int = Field(
|
expire_interval: int = Field(
|
||||||
default=60,
|
default=60,
|
||||||
|
ge=1,
|
||||||
title="Record cleanup interval",
|
title="Record cleanup interval",
|
||||||
description="Minutes between cleanup passes that remove expired recording segments.",
|
description="Minutes between cleanup passes that remove expired recording segments.",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -128,8 +128,8 @@ class ZoneConfig(BaseModel):
|
|||||||
# we know if any points are > 1 then it is using the
|
# we know if any points are > 1 then it is using the
|
||||||
# old native resolution coordinates
|
# old native resolution coordinates
|
||||||
if isinstance(coordinates, list):
|
if isinstance(coordinates, list):
|
||||||
explicit = any(p.split(",")[0] > "1.0" for p in coordinates)
|
|
||||||
try:
|
try:
|
||||||
|
explicit = any(float(p.split(",")[0]) > 1.0 for p in coordinates)
|
||||||
self._contour = np.array(
|
self._contour = np.array(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
@@ -157,8 +157,8 @@ class ZoneConfig(BaseModel):
|
|||||||
)
|
)
|
||||||
elif isinstance(coordinates, str):
|
elif isinstance(coordinates, str):
|
||||||
points = coordinates.split(",")
|
points = coordinates.split(",")
|
||||||
explicit = any(p > "1.0" for p in points)
|
|
||||||
try:
|
try:
|
||||||
|
explicit = any(float(p) > 1.0 for p in points)
|
||||||
self._contour = np.array(
|
self._contour = np.array(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
|
|||||||
+181
-23
@@ -19,7 +19,7 @@ from ruamel.yaml import YAML
|
|||||||
|
|
||||||
from frigate.const import REGEX_JSON
|
from frigate.const import REGEX_JSON
|
||||||
from frigate.detectors import ModelConfig
|
from frigate.detectors import ModelConfig
|
||||||
from frigate.detectors.detector_config import SceneEnum
|
from frigate.detectors.detector_config import DEFAULT_SCENE
|
||||||
from frigate.detectors.device import DeviceParseError, DeviceSpec, parse_device
|
from frigate.detectors.device import DeviceParseError, DeviceSpec, parse_device
|
||||||
from frigate.plus import PlusApi
|
from frigate.plus import PlusApi
|
||||||
from frigate.util.builtin import (
|
from frigate.util.builtin import (
|
||||||
@@ -34,7 +34,15 @@ from frigate.util.config import (
|
|||||||
get_relative_coordinates,
|
get_relative_coordinates,
|
||||||
migrate_frigate_config,
|
migrate_frigate_config,
|
||||||
)
|
)
|
||||||
|
from frigate.util.env import EnvVars, reload_sources
|
||||||
from frigate.util.image import create_mask
|
from frigate.util.image import create_mask
|
||||||
|
from frigate.util.live_streams import (
|
||||||
|
default_transcode_source,
|
||||||
|
is_transcode_stream_name,
|
||||||
|
transcode_stream_name,
|
||||||
|
transcode_streams,
|
||||||
|
)
|
||||||
|
from frigate.util.runtime_deps import sha256_of
|
||||||
from frigate.util.services import auto_detect_hwaccel
|
from frigate.util.services import auto_detect_hwaccel
|
||||||
|
|
||||||
from .auth import AuthConfig
|
from .auth import AuthConfig
|
||||||
@@ -64,7 +72,6 @@ from .classification import (
|
|||||||
SemanticSearchModelEnum,
|
SemanticSearchModelEnum,
|
||||||
)
|
)
|
||||||
from .database import DatabaseConfig
|
from .database import DatabaseConfig
|
||||||
from .env import EnvVars, reload_sources
|
|
||||||
from .logger import LoggerConfig
|
from .logger import LoggerConfig
|
||||||
from .mqtt import MqttConfig
|
from .mqtt import MqttConfig
|
||||||
from .network import NetworkingConfig
|
from .network import NetworkingConfig
|
||||||
@@ -282,11 +289,78 @@ def verify_config_roles(camera_config: CameraConfig) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_live_transcode_streams(
|
||||||
|
frigate_config: FrigateConfig, camera_config: CameraConfig
|
||||||
|
) -> None:
|
||||||
|
"""Fold a camera's transcoded streams into its live stream list.
|
||||||
|
|
||||||
|
Enabled qualities missing from live.streams are appended, and entries the
|
||||||
|
user placed keep their position. Transcoded names that are no longer
|
||||||
|
generated are dropped unless they name a real go2rtc stream.
|
||||||
|
"""
|
||||||
|
live = camera_config.live
|
||||||
|
transcode = live.transcode
|
||||||
|
go2rtc_streams = frigate_config.go2rtc.model_dump().get("streams") or {}
|
||||||
|
generated: dict[str, str] = {}
|
||||||
|
|
||||||
|
if transcode.enabled:
|
||||||
|
if transcode.source is None:
|
||||||
|
transcode.source = default_transcode_source(
|
||||||
|
camera_config.name, live.streams
|
||||||
|
)
|
||||||
|
|
||||||
|
if transcode.source not in go2rtc_streams:
|
||||||
|
raise ValueError(
|
||||||
|
f"Camera {camera_config.name} has transcoded streams enabled, but its source {transcode.source} is not a go2rtc stream."
|
||||||
|
)
|
||||||
|
|
||||||
|
generated = transcode_streams(
|
||||||
|
camera_config.name,
|
||||||
|
transcode.source,
|
||||||
|
[quality.model_dump() for quality in transcode.qualities],
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in generated:
|
||||||
|
if name in go2rtc_streams:
|
||||||
|
raise ValueError(
|
||||||
|
f"Camera {camera_config.name} generates transcoded stream {name}, which collides with a go2rtc stream of the same name."
|
||||||
|
)
|
||||||
|
|
||||||
|
streams = {
|
||||||
|
label: name
|
||||||
|
for label, name in live.streams.items()
|
||||||
|
if name in generated
|
||||||
|
or name in go2rtc_streams
|
||||||
|
or not is_transcode_stream_name(camera_config.name, name)
|
||||||
|
}
|
||||||
|
placed = set(streams.values())
|
||||||
|
|
||||||
|
for quality in transcode.qualities if transcode.enabled else []:
|
||||||
|
name = transcode_stream_name(camera_config.name, quality.height)
|
||||||
|
|
||||||
|
if name in placed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = f"{quality.height}p"
|
||||||
|
|
||||||
|
if label in streams:
|
||||||
|
raise ValueError(
|
||||||
|
f"Camera {camera_config.name} already has a live stream named {label}; rename it or place the transcoded stream under another name."
|
||||||
|
)
|
||||||
|
|
||||||
|
streams[label] = name
|
||||||
|
|
||||||
|
live.streams = streams
|
||||||
|
|
||||||
|
|
||||||
def verify_valid_live_stream_names(
|
def verify_valid_live_stream_names(
|
||||||
frigate_config: FrigateConfig, camera_config: CameraConfig
|
frigate_config: FrigateConfig, camera_config: CameraConfig
|
||||||
) -> ValueError | None:
|
) -> ValueError | None:
|
||||||
"""Verify that a restream exists to use for live view."""
|
"""Verify that a restream exists to use for live view."""
|
||||||
for _, stream_name in camera_config.live.streams.items():
|
for _, stream_name in camera_config.live.streams.items():
|
||||||
|
if is_transcode_stream_name(camera_config.name, stream_name):
|
||||||
|
continue
|
||||||
|
|
||||||
if (
|
if (
|
||||||
stream_name
|
stream_name
|
||||||
not in frigate_config.go2rtc.model_dump().get("streams", {}).keys()
|
not in frigate_config.go2rtc.model_dump().get("streams", {}).keys()
|
||||||
@@ -536,7 +610,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
models: list[ModelConfig] = Field(
|
models: list[ModelConfig] = Field(
|
||||||
default_factory=_default_models,
|
default_factory=_default_models,
|
||||||
title="Detection models",
|
title="Detection models",
|
||||||
description="Object detection models and the hardware each one runs on. Cameras pick a model by matching their detect.scene against a model's scene.",
|
description="Object detection models and the hardware each one runs on. Cameras pick a model by matching their detect.scene against a model's scene, falling back to the 'default' model.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# GenAI config (named provider configs: name -> GenAIConfig)
|
# GenAI config (named provider configs: name -> GenAIConfig)
|
||||||
@@ -651,7 +725,9 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
_plus_api: PlusApi
|
_plus_api: PlusApi
|
||||||
_model_devices: dict[SceneEnum, list[DeviceSpec]]
|
_model_devices: dict[str, list[DeviceSpec]]
|
||||||
|
# scene -> model, including the scenes of duplicate models folded into another
|
||||||
|
_scene_models: dict[str, ModelConfig]
|
||||||
_camera_models: dict[str, ModelConfig]
|
_camera_models: dict[str, ModelConfig]
|
||||||
_all_attributes: list[str]
|
_all_attributes: list[str]
|
||||||
_all_attribute_logos: list[str]
|
_all_attribute_logos: list[str]
|
||||||
@@ -686,7 +762,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
def primary_model(self) -> ModelConfig:
|
def primary_model(self) -> ModelConfig:
|
||||||
"""The model used when no specific camera is in play."""
|
"""The model used when no specific camera is in play."""
|
||||||
for model in self.models:
|
for model in self.models:
|
||||||
if model.scene == SceneEnum.all:
|
if model.scene == DEFAULT_SCENE:
|
||||||
return model
|
return model
|
||||||
|
|
||||||
return self.models[0]
|
return self.models[0]
|
||||||
@@ -708,7 +784,7 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
|
|
||||||
if model is None:
|
if model is None:
|
||||||
camera = self.cameras.get(camera_name)
|
camera = self.cameras.get(camera_name)
|
||||||
scene = camera.detect.scene if camera is not None else SceneEnum.all
|
scene = camera.detect.scene if camera is not None else DEFAULT_SCENE
|
||||||
model = self._resolve_camera_model(camera_name, scene)
|
model = self._resolve_camera_model(camera_name, scene)
|
||||||
self._camera_models[camera_name] = model
|
self._camera_models[camera_name] = model
|
||||||
|
|
||||||
@@ -768,14 +844,14 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
if not self.models:
|
if not self.models:
|
||||||
raise ValueError("At least one model must be configured under models")
|
raise ValueError("At least one model must be configured under models")
|
||||||
|
|
||||||
model_devices: dict[SceneEnum, list[DeviceSpec]] = {}
|
model_devices: dict[str, list[DeviceSpec]] = {}
|
||||||
# device string -> the scene of the model that already claimed it
|
# device string -> the scene of the model that already claimed it
|
||||||
claimed_devices: dict[str, SceneEnum] = {}
|
claimed_devices: dict[str, str] = {}
|
||||||
|
|
||||||
for index, model in enumerate(self.models):
|
for index, model in enumerate(self.models):
|
||||||
scene = model.scene.value
|
scene = model.scene
|
||||||
|
|
||||||
if model.scene in model_devices:
|
if scene in model_devices:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Multiple models are configured with a scene of '{scene}'. Each model must use a different scene."
|
f"Multiple models are configured with a scene of '{scene}'. Each model must use a different scene."
|
||||||
)
|
)
|
||||||
@@ -804,17 +880,20 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
other = claimed_devices[device.raw]
|
other = claimed_devices[device.raw]
|
||||||
where = (
|
where = (
|
||||||
f"twice by model '{scene}'"
|
f"twice by model '{scene}'"
|
||||||
if other == model.scene
|
if other == scene
|
||||||
else f"by both the '{other.value}' and '{scene}' models"
|
else f"by both the '{other}' and '{scene}' models"
|
||||||
)
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Device '{device.raw}' is used {where}, but it can only run one detection process."
|
f"Device '{device.raw}' is used {where}, but it can only run one detection process."
|
||||||
)
|
)
|
||||||
|
|
||||||
claimed_devices[device.raw] = model.scene
|
claimed_devices[device.raw] = scene
|
||||||
|
|
||||||
self.models[index] = self._load_model(model, devices[0].detector)
|
self.models[index] = self._load_model(model, devices[0].detector)
|
||||||
model_devices[model.scene] = devices
|
model_devices[scene] = devices
|
||||||
|
|
||||||
|
self._scene_models = {model.scene: model for model in self.models}
|
||||||
|
self._consolidate_duplicate_models(model_devices)
|
||||||
|
|
||||||
attributes: set[str] = set()
|
attributes: set[str] = set()
|
||||||
attribute_logos: set[str] = set()
|
attribute_logos: set[str] = set()
|
||||||
@@ -838,36 +917,107 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
}
|
}
|
||||||
self._all_labels = labels
|
self._all_labels = labels
|
||||||
|
|
||||||
def _resolve_camera_model(self, name: str, scene: SceneEnum) -> ModelConfig:
|
def _consolidate_duplicate_models(
|
||||||
|
self, model_devices: dict[str, list[DeviceSpec]]
|
||||||
|
) -> None:
|
||||||
|
"""Fold models that load the same model file into a single model.
|
||||||
|
|
||||||
|
Separate scenes for one model only split the same work across separate
|
||||||
|
detection queues, so each device serves fewer cameras and is slower
|
||||||
|
overall than one shared model. The duplicate's devices are moved to the
|
||||||
|
model it duplicates and its scene resolves to that model.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_devices: Scene to parsed devices, updated in place
|
||||||
|
"""
|
||||||
|
kept: list[ModelConfig] = []
|
||||||
|
hashes: dict[str, str | None] = {}
|
||||||
|
|
||||||
|
def file_hash(path: str) -> str | None:
|
||||||
|
if path not in hashes:
|
||||||
|
hashes[path] = sha256_of(path) if os.path.isfile(path) else None
|
||||||
|
|
||||||
|
return hashes[path]
|
||||||
|
|
||||||
|
def same_model(a: ModelConfig, b: ModelConfig) -> bool:
|
||||||
|
# a model's devices all share a detector, so folding across
|
||||||
|
# detectors would produce an invalid model
|
||||||
|
if model_devices[a.scene][0].detector != model_devices[b.scene][0].detector:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not a.path or not b.path:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if os.path.realpath(a.path) == os.path.realpath(b.path):
|
||||||
|
return True
|
||||||
|
|
||||||
|
a_hash = file_hash(a.path)
|
||||||
|
return a_hash is not None and a_hash == file_hash(b.path)
|
||||||
|
|
||||||
|
for model in self.models:
|
||||||
|
original = next((other for other in kept if same_model(other, model)), None)
|
||||||
|
|
||||||
|
if original is None:
|
||||||
|
kept.append(model)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# keep the default model so cameras without a scene still find it
|
||||||
|
if model.scene == DEFAULT_SCENE:
|
||||||
|
kept[kept.index(original)] = model
|
||||||
|
original, model = model, original
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
"Models '%s' and '%s' use the same model file, so they have been combined into the '%s' model. Defining one model under several scenes to assign detectors to specific cameras is slower and less efficient than letting every detector serve every camera. Remove the '%s' model and list its devices under the '%s' model instead",
|
||||||
|
original.scene,
|
||||||
|
model.scene,
|
||||||
|
original.scene,
|
||||||
|
model.scene,
|
||||||
|
original.scene,
|
||||||
|
)
|
||||||
|
original.devices = [*original.devices, *model.devices]
|
||||||
|
model_devices[original.scene] = [
|
||||||
|
*model_devices[original.scene],
|
||||||
|
*model_devices.pop(model.scene),
|
||||||
|
]
|
||||||
|
self._scene_models[model.scene] = original
|
||||||
|
|
||||||
|
# anything already folded into the duplicate follows it
|
||||||
|
for scene, target in self._scene_models.items():
|
||||||
|
if target is model:
|
||||||
|
self._scene_models[scene] = original
|
||||||
|
|
||||||
|
self.models = kept
|
||||||
|
|
||||||
|
def _resolve_camera_model(self, name: str, scene: str) -> ModelConfig:
|
||||||
"""Resolve which model a camera runs on.
|
"""Resolve which model a camera runs on.
|
||||||
|
|
||||||
A camera may name a scene no model is configured for, which is valid as
|
A camera may name a scene no model is configured for, which is valid as
|
||||||
long as an 'all' model is there to fall back to.
|
long as a 'default' model is there to fall back to.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Name of the camera
|
name: Name of the camera
|
||||||
scene: The camera's detect scene, which defaults to 'all'
|
scene: The camera's detect scene, which defaults to 'default'
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The model the camera runs on
|
The model the camera runs on
|
||||||
"""
|
"""
|
||||||
by_scene = {model.scene: model for model in self.models}
|
model = self._scene_models.get(scene)
|
||||||
model = by_scene.get(scene)
|
|
||||||
|
|
||||||
if model is not None:
|
if model is not None:
|
||||||
return model
|
return model
|
||||||
|
|
||||||
default = by_scene.get(SceneEnum.all)
|
default = self._scene_models.get(DEFAULT_SCENE)
|
||||||
|
|
||||||
if default is None:
|
if default is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Camera '{name}' has a detect scene of '{scene.value}', but no model is configured for that scene or for 'all'."
|
f"Camera '{name}' has a detect scene of '{scene}', but no model is configured for that scene or for '{DEFAULT_SCENE}'."
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Camera '%s' has a detect scene of '%s', but no model is configured for that scene, so the 'all' model is used",
|
"Camera '%s' has a detect scene of '%s', but no model is configured for that scene, so the '%s' model is used",
|
||||||
name,
|
name,
|
||||||
scene.value,
|
scene,
|
||||||
|
DEFAULT_SCENE,
|
||||||
)
|
)
|
||||||
return default
|
return default
|
||||||
|
|
||||||
@@ -978,6 +1128,8 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
"face_recognition": ["enabled", "min_area"],
|
"face_recognition": ["enabled", "min_area"],
|
||||||
"lpr": ["enabled", "expire_time", "min_area", "enhancement"],
|
"lpr": ["enabled", "expire_time", "min_area", "enhancement"],
|
||||||
"audio_transcription": ["enabled", "live_enabled"],
|
"audio_transcription": ["enabled", "live_enabled"],
|
||||||
|
# transcode is camera-level only
|
||||||
|
"live": ["streams", "height", "quality"],
|
||||||
}
|
}
|
||||||
|
|
||||||
for section in allowed_fields_map:
|
for section in allowed_fields_map:
|
||||||
@@ -998,6 +1150,10 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
camera_model = self._resolve_camera_model(name, camera_config.detect.scene)
|
camera_model = self._resolve_camera_model(name, camera_config.detect.scene)
|
||||||
self._camera_models[name] = camera_model
|
self._camera_models[name] = camera_model
|
||||||
|
|
||||||
|
# point cameras at the model their duplicate scene was folded into
|
||||||
|
if camera_config.detect.scene in self._scene_models:
|
||||||
|
camera_config.detect.scene = camera_model.scene
|
||||||
|
|
||||||
if camera_config.ffmpeg.hwaccel_args == "auto":
|
if camera_config.ffmpeg.hwaccel_args == "auto":
|
||||||
camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args
|
camera_config.ffmpeg.hwaccel_args = self.ffmpeg.hwaccel_args
|
||||||
|
|
||||||
@@ -1206,6 +1362,8 @@ class FrigateConfig(FrigateBaseModel):
|
|||||||
if not camera_config.live.streams:
|
if not camera_config.live.streams:
|
||||||
camera_config.live.streams = {name: name}
|
camera_config.live.streams = {name: name}
|
||||||
|
|
||||||
|
apply_live_transcode_streams(self, camera_config)
|
||||||
|
|
||||||
# generate the ffmpeg commands
|
# generate the ffmpeg commands
|
||||||
camera_config.create_ffmpeg_cmds()
|
camera_config.create_ffmpeg_cmds()
|
||||||
self.cameras[name] = camera_config
|
self.cameras[name] = camera_config
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ from typing import Self
|
|||||||
from pydantic import Field, ValidationInfo, model_validator
|
from pydantic import Field, ValidationInfo, model_validator
|
||||||
|
|
||||||
from frigate.const import FREQUENCY_STATS_POINTS
|
from frigate.const import FREQUENCY_STATS_POINTS
|
||||||
|
from frigate.util.env import EnvString
|
||||||
|
|
||||||
from .base import FrigateBaseModel
|
from .base import FrigateBaseModel
|
||||||
from .env import EnvString
|
|
||||||
|
|
||||||
__all__ = ["MqttConfig"]
|
__all__ = ["MqttConfig"]
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user