Compare commits

...
4 Commits
Author SHA1 Message Date
Rami MouroandGitHub f930321a5b Merge 8a40ea5c87 into 729ee86043 2026-07-04 20:26:57 +12:00
Josh HawkinsandGitHub 729ee86043 Miscellaneous fixes (#23619)
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
CI / Jetson Jetpack 6 (push) Waiting to run
* fix stale active object indicators on the live dashboard

The camera_activity/<camera> snapshot cache is only written when a client sends onConnect, and object "end" events only update the local state of mounted useCameraActivity hooks, never the cache. As a result, a hook that seeded from a stale cache or missed an "end" event while disconnected showed objects that had already left, with no path to correct itself short of a full page reload.

This change will re-request the snapshot on hook mount (collapsed to one onConnect per task across camera cards), and always re-notify camera_activity topics so hooks reconcile against their own local state instead of relying on snapshot-vs-snapshot comparison, and clear the payload dedup cache on reconnect and resync so byte-identical snapshots still apply.

* docs tweaks

* fix mqtt log message

* use consistent values for lpr debug frame filenames

with millisecond resolution

* apply object events through a functional updater to prevent lost updates

The events effect derived a new objects list from the value captured at render time and wrote the whole list back. When events arrived close
together, a run derived from a stale list erased a concurrent run's removal; the resurrected object then had no remaining "end" event to clear it, and the add branch could mint a duplicate entry that no splice could ever remove, leaving the live dashboard showing active objects the backend had already cleared, until a page reload.

The fix is to apply each event inside setObjects so it operates on the true current list exactly once. Unchanged results return the same reference so React bails out of re-rendering, and the label rewrite is hoisted so added objects get the sub_label/verified label directly instead of relying on the effect re-running against its own state update.
2026-07-04 00:25:22 -06:00
Rami MouroandClaude Opus 4.8 8a40ea5c87 fix(qualcomm): make install_cdi resolve QNN skels across BSP and Ubuntu/QAIRT layouts
The bundled CDI descriptors assume the Qualcomm Linux BSP filesystem layout
(/usr/lib/rfsa/adsp). On the equally-official Ubuntu image for RB3 Gen 2 /
Rubik Pi 3, the QAIRT apt packages board-select the Hexagon skels into
/usr/lib/dsp/cdsp and leave /usr/lib/rfsa/adsp as self-referential symlink
loops, so install_cdi silently dropped the skel mounts and NPU offload failed
silently at runtime (or a host-symlink workaround broke on reboot with an
ELOOP CDI mount error).

install_cdi.py now resolves each missing/broken mount from a fallback library
dir and rewrites it to the real file, injects ADSP_LIBRARY_PATH so the DSP
loader finds the skels, and warns loudly (with an optional --strict) when a
critical QNN/HTP library cannot be found instead of failing silently. Docs
updated to cover both images and a stronger verify step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:48:44 -06:00
Rami Mouro e769ba7a8c feat(detectors): add Qualcomm Hexagon NPU support (community board)
Add a community-supported board build for Qualcomm SoCs with a Hexagon
NPU, accelerating TFLite object detection through the QNN TFLite delegate
on the Hexagon Tensor Processor (HTP).

Supported boards:
- IQ9100 (IQ-9075 EVK)
- QCS6490 (RB3 Gen 2 Vision Kit / Rubik Pi 3)

- detector: new `qualcomm_tfl` plugin loading libQnnTFLiteDelegate.so on
  the HTP backend, reusing the shared TFLite delegate helpers
- docker: `docker/qualcomm` board build (Dockerfile, qualcomm.hcl,
  qualcomm.mk) producing the arm64 `-qualcomm` image
- cdi: per-board Container Device Interface descriptors and an install
  helper exposing the NPU device nodes and QNN libraries to the container
- ci/codeowners: register the qualcomm build target and code owner
- docs: installation, hardware, and object detector documentation, using
  the shared model config dropdown component
- i18n: add the Qualcomm detector label and description to the generated
  en config locale

Signed-off-by: Rami Mouro <rmouro@qti.qualcomm.com>
2026-06-29 19:44:04 -06:00
24 changed files with 3973 additions and 119 deletions
+25
View File
@@ -197,6 +197,31 @@ jobs:
set: |
synaptics.tags=${{ steps.setup.outputs.image-name }}-synaptics
*.cache-from=type=gha
qualcomm_build:
runs-on: ubuntu-22.04-arm
name: Qualcomm Build
needs:
- arm64_build
steps:
- name: Check out code
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Set up QEMU and Buildx
id: setup
uses: ./.github/actions/setup
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Qualcomm build
uses: docker/bake-action@v7
with:
source: .
push: true
targets: qualcomm
files: docker/qualcomm/qualcomm.hcl
set: |
qualcomm.tags=${{ steps.setup.outputs.image-name }}-qualcomm
*.cache-from=type=gha
# The majority of users running arm64 are rpi users, so the rpi
# build should be the primary arm64 image
assemble_default_build:
+1
View File
@@ -5,3 +5,4 @@
/docker/rockchip/ @MarcA711
/docker/rocm/ @harakas
/docker/hailo8l/ @spanner3003
/docker/qualcomm/ @ramalamadingdong
+21
View File
@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1.6
# https://askubuntu.com/questions/972516/debian-frontend-environment-variable
ARG DEBIAN_FRONTEND=noninteractive
# Globally set pip break-system-packages option to avoid having to specify it every time
ARG PIP_BREAK_SYSTEM_PACKAGES=1
FROM wheels AS qualcomm-wheels
ARG TARGETARCH
# No extra wheels needed — ai-edge-litert is already in the base image
# and the QNN delegate library (libQnnTFLiteDelegate.so) is provided
# by the host via CDI bind mounts at runtime.
FROM deps AS qualcomm-deps
ARG TARGETARCH
ARG PIP_BREAK_SYSTEM_PACKAGES
WORKDIR /opt/frigate/
COPY --from=rootfs / /
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
import argparse
import json
import os
# The CDI descriptor is authored against the Qualcomm Linux BSP filesystem layout,
# where the QNN/HTP runtime libraries and the Hexagon "skel" files live under
# /usr/lib and /usr/lib/rfsa/adsp. On other officially-supported images for the
# same boards -- notably Ubuntu for RB3 Gen 2 / Rubik Pi 3 with the QAIRT apt
# packages (qairt-libs/qairt-tools) -- the same files are board-selected into a
# different directory, and /usr/lib/rfsa/adsp may even contain self-referential
# symlink loops. When a mount's authored hostPath is missing (or a broken/looping
# symlink), look the file up by name in these fallback directories and rewrite the
# mount to the real, resolved file so NPU offload works on both layouts.
FALLBACK_LIB_DIRS = [
"/usr/lib/dsp/cdsp", # QAIRT apt packages (board-selected from /usr/share/qcom/.../dsp/cdsp)
]
# Libraries without which HTP/NPU offload cannot work. If any of these cannot be
# resolved we warn prominently (and abort under --strict) instead of silently
# producing a CDI that starts fine but never offloads to the NPU.
CRITICAL_BASENAMES = {
"libQnnTFLiteDelegate.so",
"libQnnHtp.so",
"libQnnHtpV68Skel.so",
"libQnnHtpV73Skel.so",
"libQnnHtpV75Skel.so",
"libQnnSystem.so",
}
def resolve_host_path(host_path):
"""Resolve a mount's hostPath to a usable, real file/dir on this host.
Returns the original path when it already resolves (the Qualcomm Linux BSP
layout). If it is missing or a broken/looping symlink, search FALLBACK_LIB_DIRS
by basename and return the real (symlink-resolved) path, so the rewritten mount
is stable across reboots / apt reconfigures. Returns None if not found anywhere.
"""
# os.path.exists() follows symlinks and returns False for a broken link or an
# ELOOP ("too many levels of symbolic links") -- exactly the treat-as-missing
# behavior we want before falling back.
if os.path.exists(host_path):
return host_path
basename = os.path.basename(host_path)
for fallback_dir in FALLBACK_LIB_DIRS:
candidate = os.path.join(fallback_dir, basename)
if candidate != host_path and os.path.exists(candidate):
return os.path.realpath(candidate)
return None
parser = argparse.ArgumentParser(description="Install CDI on Dragonwing IoT boards")
parser.add_argument("--file", type=str, required=True, help="CDI input file")
parser.add_argument(
"--strict",
action="store_true",
help="Exit non-zero if any critical QNN/HTP library cannot be resolved",
)
args, unknown = parser.parse_known_args()
if os.path.exists("/etc/cdi/cdi-hw-acc.json"):
print("/etc/cdi/cdi-hw-acc.json already exists. Remove it first before continuing.")
exit(1)
if not os.path.exists(args.file):
print(f"{args.file} (via --file) does not exist")
exit(1)
# Check if directory exists
if not os.path.exists("/etc/cdi"):
# Check if we can create it in /etc
if not os.access(os.path.dirname("/etc/cdi"), os.W_OK):
print(
f"{os.path.dirname('/etc/cdi')} is not writable. Re-run this script with sudo."
)
exit(1)
os.mkdir("/etc/cdi")
else:
# Directory exists → check if writable
if not os.access("/etc/cdi", os.W_OK):
print("/etc/cdi is not writable. Re-run this script with sudo.")
exit(1)
with open(args.file, "r") as f:
cdi = json.loads(f.read())
print("Resolving mount paths...")
relocated = []
missing = []
for device in cdi["devices"]:
new_mounts = []
for mount in device["containerEdits"].get("mounts", []):
resolved = resolve_host_path(mount["hostPath"])
if resolved is None:
missing.append(mount["hostPath"])
print(f" Missing {mount['hostPath']}")
continue
if resolved != mount["hostPath"]:
relocated.append((mount["hostPath"], resolved))
print(f" Resolved {mount['hostPath']} -> {resolved}")
# Keep containerPath as authored; only the host source moves.
mount["hostPath"] = resolved
new_mounts.append(mount)
device["containerEdits"]["mounts"] = new_mounts
# The QNN HTP backend (libcdsprpc) locates the Hexagon skel purely via
# ADSP_LIBRARY_PATH. Point it at the in-container directory where the skels are
# mounted so offload works regardless of the host's on-disk layout.
container_edits = device["containerEdits"]
env = container_edits.setdefault("env", [])
if not any(e.startswith("ADSP_LIBRARY_PATH=") for e in env):
env.append("ADSP_LIBRARY_PATH=/usr/lib/rfsa/adsp")
print("")
if relocated:
print(
f"Resolved {len(relocated)} mount(s) from a fallback library directory "
"(non-BSP image layout)."
)
critical_missing = [m for m in missing if os.path.basename(m) in CRITICAL_BASENAMES]
if missing:
print(f"WARNING: {len(missing)} expected host path(s) were missing and skipped.")
if critical_missing:
print("")
print(
"WARNING: critical QNN/HTP libraries could not be found on this host -- NPU "
"offload will NOT work and detection would fall back to CPU:"
)
for path in critical_missing:
print(f" {path}")
print(
"Confirm the QAIRT runtime is installed (e.g. the qairt-libs/qairt-tools "
"packages) and that the Hexagon skels exist as real files."
)
if args.strict:
print("Aborting due to --strict.")
exit(1)
print("")
print("Writing to /etc/cdi/cdi-hw-acc.json...")
with open("/etc/cdi/cdi-hw-acc.json", "w") as f:
f.write(json.dumps(cdi, indent=4))
print("Writing to /etc/cdi/cdi-hw-acc.json OK")
+27
View File
@@ -0,0 +1,27 @@
target wheels {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "wheels"
}
target deps {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "deps"
}
target rootfs {
dockerfile = "docker/main/Dockerfile"
platforms = ["linux/arm64"]
target = "rootfs"
}
target qualcomm {
dockerfile = "docker/qualcomm/Dockerfile"
contexts = {
wheels = "target:wheels",
deps = "target:deps",
rootfs = "target:rootfs"
}
platforms = ["linux/arm64"]
}
+15
View File
@@ -0,0 +1,15 @@
BOARDS += qualcomm
local-qualcomm: version
docker buildx bake --file=docker/qualcomm/qualcomm.hcl qualcomm \
--set qualcomm.tags=frigate:latest-qualcomm \
--load
build-qualcomm: version
docker buildx bake --file=docker/qualcomm/qualcomm.hcl qualcomm \
--set qualcomm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-qualcomm
push-qualcomm: build-qualcomm
docker buildx bake --file=docker/qualcomm/qualcomm.hcl qualcomm \
--set qualcomm.tags=$(IMAGE_REPO):${GITHUB_REF_NAME}-$(COMMIT_HASH)-qualcomm \
--push
+13
View File
@@ -265,6 +265,19 @@
}
]
},
"qualcomm": {
"title": "Qualcomm",
"models": [
{
"key": "ssd_mobiledet",
"label": "SSD MobileDet",
"recommended": true,
"download": "The default SSD MobileDet TFLite model (`/cpu_model.tflite`) bundled with the Frigate container is used automatically. This model is INT8 quantized and compatible with the QNN HTP backend.\n\nOnly `.tflite` models are supported with this detector. A custom model must be INT8 quantized for the Hexagon NPU.",
"ui": "Navigate to **Settings > System > Detectors and model** and select **Qualcomm** from the detector type dropdown and click **Add**. The bundled SSD MobileDet model is used automatically.\n\nTo use a custom model, configure in the **Custom Model** tab:\n\n| Field | Value |\n| ---------------------------------------- | ---------------------------------- |\n| **Custom object detector model path** | `/config/your_custom_model.tflite` |\n| **Object detection model input width** | `320` |\n| **Object detection model input height** | `320` |\n| **Label map for custom object detector** | `/labelmap/coco-80.txt` |",
"yaml": "detectors:\n qualcomm_npu:\n type: qualcomm_tfl"
}
]
},
"rknn": {
"title": "RKNN",
"models": [
+2 -2
View File
@@ -126,12 +126,12 @@ birdseye:
### Sorting cameras in the Birdseye view
It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level.
It is possible to override the order of cameras that are being shown in the Birdseye view. The order is set at the camera level (when using YAML).
<ConfigTabs>
<TabItem value="ui">
Navigate to <NavPath path="Settings > Camera configuration > Birdseye" /> for each camera and set the **Position** field to control the display order.
Navigate to <NavPath path="Settings > System > Birdseye" /> and in the **Camera order** field, use the drag handle next to each camera name to control the display order.
</TabItem>
<TabItem value="yaml">
+1 -1
View File
@@ -194,7 +194,7 @@ Camera groups let you organize cameras together with a shared name and icon, mak
<ConfigTabs>
<TabItem value="ui">
On the Live dashboard, press the **+** icon in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order.
On the Live dashboard, press the **pencil icon** in the main navigation to add a new camera group. Configure the group name, select which cameras to include, choose an icon, and set the display order.
</TabItem>
<TabItem value="yaml">
+39 -39
View File
@@ -7,27 +7,27 @@ import ConfigTabs from "@site/src/components/ConfigTabs";
import TabItem from "@theme/TabItem";
import NavPath from "@site/src/components/NavPath";
Some presets of FFmpeg args are provided by default to make the configuration easier. All presets can be seen in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py).
Frigate ships with a set of FFmpeg presets to keep your configuration short and readable. Each preset expands to a longer list of FFmpeg arguments at runtime. You can see exactly what every preset expands to in [this file](https://github.com/blakeblackshear/frigate/blob/master/frigate/ffmpeg_presets.py).
### Hwaccel Presets
In the config file you reference a preset by its name (for example, `preset-vaapi`). In the UI, the same preset is shown with a friendly label (for example, **VAAPI (Intel/AMD GPU)**). Both refer to the same thing — the tables below list the config name alongside the label you'll see in the UI.
It is highly recommended to use hwaccel presets in the config. These presets not only replace the longer args, but they also give Frigate hints of what hardware is available and allows Frigate to make other optimizations using the GPU such as when encoding the birdseye restream or when scaling a stream that has a size different than the native stream size.
### Hwaccel (Hardware Acceleration) Presets
See [the hwaccel docs](/configuration/hardware_acceleration_video.md) for more info on how to setup hwaccel for your GPU / iGPU.
Hardware acceleration arguments tell FFmpeg to decode your camera's video stream on a GPU or integrated graphics chip instead of the CPU, which dramatically lowers CPU usage. Using a preset is highly recommended. Beyond replacing a long list of arguments, each preset also tells Frigate what hardware is available so it can offload additional work to the GPU — for example, encoding the Birdseye restream or scaling a stream whose resolution differs from the camera's native size.
| Preset | Usage | Other Notes |
| --------------------- | ------------------------------ | ----------------------------------------------------- |
| preset-rpi-64-h264 | 64 bit Rpi with h264 stream | |
| preset-rpi-64-h265 | 64 bit Rpi with h265 stream | |
| preset-vaapi | Intel & AMD VAAPI | Check hwaccel docs to ensure correct driver is chosen |
| preset-intel-qsv-h264 | Intel QSV with h264 stream | If issues occur recommend using vaapi preset instead |
| preset-intel-qsv-h265 | Intel QSV with h265 stream | If issues occur recommend using vaapi preset instead |
| preset-nvidia | Nvidia GPU | |
| preset-jetson-h264 | Nvidia Jetson with h264 stream | |
| preset-jetson-h265 | Nvidia Jetson with h265 stream | |
| preset-rkmpp | Rockchip MPP | Use image with \*-rk suffix and privileged mode |
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.
Select the appropriate hwaccel preset for your hardware.
| Preset (YAML config) | UI Label | Usage | Notes |
| --------------------- | ----------------------- | --------------------------------- | --------------------------------------------------------------- |
| preset-rpi-64-h264 | Raspberry Pi (H.264) | 64-bit Raspberry Pi, H.264 stream | |
| preset-rpi-64-h265 | Raspberry Pi (H.265) | 64-bit Raspberry Pi, H.265 stream | |
| preset-vaapi | VAAPI (Intel/AMD GPU) | Intel or AMD GPU via VAAPI | Check the hwaccel docs to ensure the correct driver is selected |
| preset-intel-qsv-h264 | Intel QuickSync (H.264) | Intel QuickSync, H.264 stream | If you have issues, use the VAAPI preset instead |
| preset-intel-qsv-h265 | Intel QuickSync (H.265) | Intel QuickSync, H.265 stream | If you have issues, use the VAAPI preset instead |
| preset-nvidia | NVIDIA GPU | NVIDIA GPU | |
| preset-jetson-h264 | NVIDIA Jetson (H.264) | NVIDIA Jetson, H.264 stream | |
| preset-jetson-h265 | NVIDIA Jetson (H.265) | NVIDIA Jetson, H.265 stream | |
| preset-rkmpp | Rockchip RKMPP | Rockchip MPP | Use an image with the `-rk` suffix and run in privileged mode |
<ConfigTabs>
<TabItem value="ui">
@@ -53,25 +53,25 @@ cameras:
### Input Args Presets
Input args presets help make the config more readable and handle use cases for different types of streams to ensure maximum compatibility.
Input arguments are passed to FFmpeg before your camera source and control how Frigate connects to and reads the stream — the transport protocol, timeouts, reconnection behavior, and how the stream is probed. The right input args ensure a reliable connection and maximum compatibility for each type of stream.
See [the camera specific docs](/configuration/camera_specific.md) for more info on non-standard cameras and recommendations for using them in Frigate.
See [the camera-specific docs](/configuration/camera_specific.md) for more on non-standard cameras and recommendations for using them in Frigate.
| Preset | Usage | Other Notes |
| -------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ |
| preset-http-jpeg-generic | HTTP Live Jpeg | Recommend restreaming live jpeg instead |
| preset-http-mjpeg-generic | HTTP Mjpeg Stream | Recommend restreaming mjpeg stream instead |
| preset-http-reolink | Reolink HTTP-FLV Stream | Only for reolink http, not when restreaming as rtsp |
| preset-rtmp-generic | RTMP Stream | |
| preset-rtsp-generic | RTSP Stream | This is the default when nothing is specified |
| preset-rtsp-restream | RTSP Stream from restream | Use for rtsp restream as source for frigate |
| preset-rtsp-restream-low-latency | RTSP Stream from restream | Use for rtsp restream as source for frigate to lower latency, may cause issues with some cameras |
| preset-rtsp-udp | RTSP Stream via UDP | Use when camera is UDP only |
| preset-rtsp-blue-iris | Blue Iris RTSP Stream | Use when consuming a stream from Blue Iris |
| Preset (config) | UI Label | Usage | Notes |
| -------------------------------- | ----------------------------------------- | --------------------------- | ------------------------------------------------------------------------------- |
| preset-http-jpeg-generic | HTTP JPEG (Generic) | HTTP live JPEG | Restreaming the live JPEG is recommended instead |
| preset-http-mjpeg-generic | HTTP MJPEG (Generic) | HTTP MJPEG stream | Restreaming the MJPEG stream is recommended instead |
| preset-http-reolink | HTTP - Reolink Cameras | Reolink HTTP-FLV stream | Only for Reolink HTTP, not when restreaming as RTSP |
| preset-rtmp-generic | RTMP (Generic) | RTMP stream | |
| preset-rtsp-generic | RTSP (Generic) | RTSP stream | The default when no input args are specified |
| preset-rtsp-restream | RTSP - Restream from go2rtc | RTSP stream from a restream | Use when a go2rtc restream is the source for Frigate |
| preset-rtsp-restream-low-latency | RTSP - Restream from go2rtc (Low Latency) | RTSP stream from a restream | Lowers latency for a go2rtc restream source; may cause issues with some cameras |
| preset-rtsp-udp | RTSP - UDP | RTSP stream over UDP | Use when the camera only supports UDP |
| preset-rtsp-blue-iris | RTSP - Blue Iris | Blue Iris RTSP stream | Use when consuming a stream from Blue Iris |
:::warning
It is important to be mindful of input args when using restream because you can have a mix of protocols. `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, when using a reolink cam with the rtsp restream as a source for record the preset-http-reolink will cause a crash. In this case presets will need to be set at the stream level. See the example below.
Be mindful of input arguments when restreaming, because you can end up with a mix of protocols. The `http` and `rtmp` presets cannot be used with `rtsp` streams. For example, using a Reolink camera with an RTSP restream as the recording source while `preset-http-reolink` is applied will cause a crash. In cases like this, set the preset at the stream level instead. See the example below.
:::
@@ -96,13 +96,13 @@ cameras:
### Output Args Presets
Output args presets help make the config more readable and handle use cases for different types of streams to ensure consistent recordings.
Output arguments are passed to FFmpeg after your camera source and control how recordings are written — which codecs are used and whether audio and video are copied as-is or re-encoded. The right output args ensure consistent, playable recordings for each type of stream.
| Preset | Usage | Other Notes |
| -------------------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| preset-record-generic | Record WITHOUT audio | If your camera doesn't have audio, or if you don't want to record audio, use this option |
| preset-record-generic-audio-copy | Record WITH original audio | Use this to enable audio in recordings |
| preset-record-generic-audio-aac | Record WITH transcoded aac audio | This is the default when no option is specified. Use it to transcode audio to AAC. If the source is already in AAC format, use preset-record-generic-audio-copy instead to avoid unnecessary re-encoding |
| preset-record-mjpeg | Record an mjpeg stream | Recommend restreaming mjpeg stream instead |
| preset-record-jpeg | Record live jpeg | Recommend restreaming live jpeg instead |
| preset-record-ubiquiti | Record ubiquiti stream with audio | Recordings with ubiquiti non-standard audio |
| Preset (config) | UI Label | Usage | Notes |
| -------------------------------- | ------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| preset-record-generic | Record (Generic, no audio) | Record without audio | Use this if your camera has no audio, or if you don't want to record audio |
| preset-record-generic-audio-copy | Record (Generic + Copy Audio) | Record with the original audio | Use this to keep the camera's audio in recordings without re-encoding |
| preset-record-generic-audio-aac | Record (Generic + Audio to AAC) | Record with audio transcoded to AAC | The default when no output args are specified. Transcodes audio to AAC. If the source is already AAC, use `preset-record-generic-audio-copy` to avoid re-encoding |
| preset-record-mjpeg | Record - MJPEG Cameras | Record an MJPEG stream | Restreaming the MJPEG stream is recommended instead |
| preset-record-jpeg | Record - JPEG Cameras | Record a live JPEG | Restreaming the live JPEG is recommended instead |
| preset-record-ubiquiti | Record - Ubiquiti Cameras | Record a Ubiquiti stream with audio | Handles Ubiquiti's non-standard audio format |
+7 -8
View File
@@ -27,13 +27,12 @@ Running Generative AI models on CPU is not recommended, as high inference times
You must use a vision-capable model with Frigate. The following models are recommended for local deployment:
| Model | Notes |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. |
| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. |
| `Intern3.5VL` | Relatively fast with good vision comprehension |
| `gemma3` | Slower model with good vision and temporal understanding |
| Model | Notes |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `qwen3-vl` | Strong visual and situational understanding, enhanced ability to identify smaller objects and interactions with object. |
| `qwen3.5` | Strong situational understanding, but missing DeepStack from qwen3-vl leading to worse performance for identifying objects in people's hand and other small details. |
| `qwen3.6` | Strong situational understanding, similar to qwen3-vl |
| `gemma4` | Strong situational understanding, sometimes resorts to more vague terms like 'interacts' instead of assigning a specific action. |
:::info
@@ -294,7 +293,7 @@ Other HTTP options are available, see the [python-genai documentation](https://g
### OpenAI
OpenAI does not have a free tier for their API. With the release of gpt-4o, pricing has been reduced and each generation should cost fractions of a cent if you choose to go this route.
OpenAI does not have a free tier for their API.
#### Supported Models
@@ -53,6 +53,10 @@ Frigate supports multiple different detectors that work on different types of ha
- [RKNN](#rockchip-platform): RKNN models can run on Rockchip devices with included NPUs.
**Qualcomm** <CommunityBadge />
- [Qualcomm](#qualcomm): TFLite models can run on Qualcomm SoCs with a Hexagon NPU (e.g. IQ9100, QCS6490) via the QNN TFLite delegate.
**Synaptics** <CommunityBadge />
- [Synaptics](#synaptics): synap models can run on Synaptics devices(e.g astra machina) with included NPUs.
@@ -652,6 +656,21 @@ When configuring the Synap detector, you have to specify the model: a local **pa
<ModelConfigDropdown detectorTitle="Synaptics" models={objectDetectorsModels.synaptics.models} />
## Qualcomm
Hardware accelerated object detection is supported on the following Qualcomm SoCs with a Hexagon NPU:
- IQ9100 / IQ-9075 EVK
- QCS6490 / RB3 Gen 2 Vision Kit / Rubik Pi 3
This implementation uses the QNN TFLite delegate (`libQnnTFLiteDelegate.so`) to accelerate TFLite model inference on the Qualcomm Hexagon Tensor Processor (HTP / NPU). The delegate library and device drivers are provided by the host operating system and made available to the container via [CDI (Container Device Interface)](https://docs.docker.com/build/building/cdi/).
See the [installation docs](../frigate/installation.md#qualcomm) for information on setting up CDI and configuring the hardware.
### Configuration
<ModelConfigDropdown detectorTitle="Qualcomm" models={objectDetectorsModels.qualcomm.models} />
## Rockchip platform
Hardware accelerated object detection is supported on the following SoCs:
+15
View File
@@ -107,6 +107,10 @@ Frigate supports multiple different detectors that work on different types of ha
- [AXEngine](#axera): axera models can run on AXERA NPUs via AXEngine, delivering highly efficient object detection.
**Qualcomm** <CommunityBadge />
- [Qualcomm](#qualcomm): TFLite models can run on Qualcomm SoCs with a Hexagon NPU (e.g. IQ9100, QCS6490) via the QNN TFLite delegate.
:::
### Hailo-8
@@ -295,6 +299,17 @@ The inference time of a rk3588 with all 3 cores enabled is typically 25-30 ms fo
| Name | AXERA AX650N/AX8850N Inference Time |
| ---------------- | ----------------------------------- |
| yolov9-tiny | ~ 4 ms |
### Qualcomm
Frigate supports hardware object detection on Qualcomm SoCs with a Hexagon NPU, including the IQ9100 and QCS6490 (RB3 Gen 2). The QNN TFLite delegate is used to accelerate inference on the HTP (Hexagon Tensor Processor).
A single detector is typically sufficient for multiple camera streams. The default model is **SSD MobileDet** (INT8 quantized).
| Name | IQ9100 Inference Time | QCS6490 Inference Time |
| ------------- | --------------------- | ---------------------- |
| ssd_mobiledet | ~ 0.8 ms | ~ 5.55 ms |
Detailed information is available [in the detector docs](/configuration/object_detectors#qualcomm).
## What does Frigate use the CPU for and what does it use a detector for? (ELI5 Version)
+115
View File
@@ -472,6 +472,120 @@ If you are using `docker run`, add this option to your command `--device /dev/ax
#### Configuration
Finally, configure [hardware object detection](/configuration/object_detectors#axera) to complete the setup.
### Qualcomm
Hardware accelerated object detection is supported on the following Qualcomm SoCs:
| Board | SoC | CDI Config File |
| ----- | --- | --------------- |
| IQ-9075 EVK | IQ9100 | `cdi-hw-acc-9100.json` |
| RB3 Gen 2 Vision Kit / Rubik Pi 3 | QCS6490 | `cdi-hw-acc-6490.json` |
The Qualcomm integration uses [CDI (Container Device Interface)](https://docs.docker.com/build/building/cdi/) to provide the container with access to the NPU device nodes and the QNN delegate libraries from the host.
#### Prerequisites
- **QNN runtime on the host**: The QNN runtime libraries (including `libQnnTFLiteDelegate.so` and `libQnnHtp.so`), the Hexagon "skel" files, and the NPU device nodes (`/dev/fastrpc-cdsp`) must be present on the host; they are bind-mounted into the container via CDI. Both officially-supported images for these boards provide them:
- **Qualcomm Linux BSP** — ships them under the layout the bundled CDI descriptors target (`/usr/lib`, `/usr/lib/rfsa/adsp`).
- **Ubuntu for RB3 Gen 2 / Rubik Pi 3** — install the `qairt-libs` and `qairt-tools` packages from the Qualcomm IoT PPA. These board-select the Hexagon skels into a different directory (`/usr/lib/dsp/cdsp`), and `/usr/lib/rfsa/adsp` may contain self-referential symlink loops; `install_cdi.py` detects this and rewrites the affected mounts to the real files automatically (see below).
- **Docker 25.0+**: CDI device support requires Docker 25.0 or later. Check with `docker --version`.
- **arm64 architecture**: The Qualcomm Docker image is built for `linux/arm64` only. If building locally, you must run the build on the target board itself.
#### CDI Installation
1. Download or copy the CDI setup files from the [Frigate repository](https://github.com/blakeblackshear/frigate/tree/dev/docker/qualcomm/cdi).
2. Install the CDI configuration for your board:
```bash
# IQ9100 / IQ-9075 EVK
sudo python3 install_cdi.py --file cdi-hw-acc-9100.json
# QCS6490 / RB3 Gen 2 Vision Kit / Rubik Pi 3
sudo python3 install_cdi.py --file cdi-hw-acc-6490.json
```
This writes a CDI descriptor to `/etc/cdi/cdi-hw-acc.json`. For any mount whose default (BSP) path is absent or a broken symlink, the script first tries to resolve the file from the QAIRT layout (e.g. `/usr/lib/dsp/cdsp`) and rewrites the mount to the real file; paths it cannot resolve anywhere are skipped with a warning. If a **critical** QNN/HTP library is missing it prints a prominent warning — and, with `--strict`, exits non-zero — so a misconfigured host fails at install time instead of silently falling back to CPU at runtime.
3. Verify the descriptor was written and that it contains the QNN delegate and Hexagon skel mounts (their absence means the QNN runtime is not installed on the host):
```bash
ls -l /etc/cdi/cdi-hw-acc.json
grep -oE 'libQnn(TFLiteDelegate|HtpV[0-9]+Skel|System)\.so' /etc/cdi/cdi-hw-acc.json | sort -u
```
#### Setup
Follow Frigate's default installation instructions, but use a docker image with `-qualcomm` suffix, for example `ghcr.io/blakeblackshear/frigate:stable-qualcomm`.
:::note
The pre-built `stable-qualcomm` image is not yet published. Until it is available, you must build the image locally:
```bash
git clone https://github.com/blakeblackshear/frigate.git
cd frigate
make local-qualcomm
```
This builds `frigate:latest-qualcomm` on your device. Use `frigate:latest-qualcomm` as the image name in the examples below instead of the `ghcr.io` URL.
:::
Grant Docker access to your Qualcomm hardware by passing the CDI device. In your `docker-compose.yml`:
```yaml
services:
frigate:
container_name: frigate
restart: unless-stopped
image: ghcr.io/blakeblackshear/frigate:stable-qualcomm
devices:
- qualcomm.com/device=cdi-hw-acc
volumes:
- /etc/localtime:/etc/localtime:ro
- /path/to/your/config:/config
- /path/to/your/storage:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs:
size: 1000000000
ports:
- "8971:8971"
- "8554:8554"
- "8555:8555/tcp"
- "8555:8555/udp"
```
If using `docker run`, pass the CDI device with:
```bash
docker run -d \
--name frigate \
--restart=unless-stopped \
--device qualcomm.com/device=cdi-hw-acc \
--mount type=tmpfs,target=/tmp/cache,tmpfs-size=1000000000 \
--shm-size=256m \
-v /path/to/your/storage:/media/frigate \
-v /path/to/your/config:/config \
-v /etc/localtime:/etc/localtime:ro \
-e FRIGATE_RTSP_PASSWORD='password' \
-p 8971:8971 \
-p 8554:8554 \
-p 8555:8555/tcp \
-p 8555:8555/udp \
ghcr.io/blakeblackshear/frigate:stable-qualcomm
```
:::tip
Unlike other hardware integrations that pass individual `/dev/` device nodes, the Qualcomm CDI approach bundles all required device nodes, library bind mounts, and environment variables into a single `--device qualcomm.com/device=cdi-hw-acc` flag.
:::
#### Configuration
Next, you should configure [hardware object detection](/configuration/object_detectors#qualcomm).
## Docker
@@ -558,6 +672,7 @@ The community supported docker image tags for the current stable version are:
- `stable-tensorrt-jp6` - Frigate build optimized for Nvidia Jetson devices running Jetpack 6
- `stable-rk` - Frigate build for SBCs with Rockchip SoC
- `stable-qualcomm` - Frigate build for Qualcomm SoCs with Hexagon NPU (IQ9100, QCS6490)
## Home Assistant App
+2 -2
View File
@@ -49,9 +49,9 @@ This almost always means that the width/height defined for your camera are not c
These messages in the logs are expected in certain situations. Frigate checks the integrity of the recordings before storing. Occasionally these cached files will be invalid and cleaned up automatically.
### "On connect called"
### "MQTT connected" repeats in the logs
If you see repeated "On connect called" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`.
If you see repeated "MQTT connected" messages in your logs, check for another instance of Frigate. This happens when multiple Frigate containers are trying to connect to MQTT with the same `client_id`.
### Error: Database Is Locked
+44 -1
View File
@@ -10,4 +10,47 @@ title: GPU Errors
Some users have reported issues using some Intel iGPUs with OpenVINO, where the GPU would not be detected. This error can be caused by various problems, so it is important to ensure the configuration is setup correctly. Some solutions users have noted:
- In some cases users have noted that an HDMI dummy plug was necessary to be plugged into the motherboard's HDMI port.
- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container.
- When mixing an Intel iGPU with Nvidia GPU, the devices can be mixed up between `/dev/dri/renderD128` and `/dev/dri/renderD129` so it is important to confirm the correct device, or map the entire `/dev/dri` directory into the Frigate container.
## Intel/AMD GPU
### Hardware acceleration is not being used
For VAAPI or QSV to work, the GPU's render device must be passed through to the Frigate container. Intel and AMD GPUs expose this as a render node under `/dev/dri`, usually `/dev/dri/renderD128`. If it is not passed through, hardware acceleration is unavailable — ffmpeg fails to initialize it (for example `Failed to open the drm device` or `No VA display found for device`) and GPU usage stays at zero while CPU usage remains high.
Pass the render device through when starting the container. With `docker compose`:
```yaml
services:
frigate:
devices:
- /dev/dri/renderD128:/dev/dri/renderD128 # Intel / AMD GPU, update for your hardware
```
Or with `docker run`, add `--device /dev/dri/renderD128`. See the [installation docs](/frigate/installation) for a complete example.
If it still isn't working after passing the device through:
- **Confirm the render node exists and is the correct one.** Run `ls /dev/dri` on the host — you should see one or more `renderD12X` entries. Systems with more than one GPU (an Intel iGPU plus a discrete GPU) can expose both `/dev/dri/renderD128` and `/dev/dri/renderD129`, and the numbering is not guaranteed. Pass through the correct node, or map the entire directory (`/dev/dri:/dev/dri`, or `--device /dev/dri`) so all render nodes are available.
- **Check device permissions.** The Frigate process must be able to access the render node. This is usually automatic when the container runs as root (the default), but nested setups such as an unprivileged Proxmox/LXC container often require making the device accessible on the host (for example, a world-readable render node) or running the container privileged. Note that running Frigate inside an LXC is not officially supported — see the [installation docs](/frigate/installation#proxmox) for details.
### Failed to download frame: -5
When using VAAPI or QSV hardware acceleration, ffmpeg may crash and restart periodically with a signature like this in the `ffmpeg.<camera>.detect` log:
```
[AVHWFramesContext @ 0x...] Failed to sync surface ... (operation failed).
[hwdownload @ 0x...] Failed to download frame: -5.
[vf#0:0 @ 0x...] Error while filtering: Input/output error
[vf#0:0 @ 0x...] Task finished with error code: -5 (Input/output error)
[frigate.video] <camera>: Unable to read frames from ffmpeg process.
```
This is a hardware frame synchronization failure between ffmpeg and the GPU driver, not a Frigate bug. It comes from how a specific camera stream interacts with the GPU's decode and scaling path, so it is highly dependent on your hardware, driver, and stream. Frigate's automatic hardware acceleration detection is a best-guess effort, so the fix is usually to tune the configuration for your specific hardware and camera. The solutions below are ordered from most to least likely to help:
- **Switch between the VAAPI and QSV presets.** On Intel Gen 12 and newer iGPUs, `preset-intel-qsv-h264` / `preset-intel-qsv-h265` is often more stable than the auto-detected `preset-vaapi`. See the [hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for the recommended preset for your Intel generation.
- **Try a different VAAPI driver.** The default driver is `iHD`. On older Intel CPUs, `LIBVA_DRIVER_NAME=i965` can be more stable; on AMD GPUs use `LIBVA_DRIVER_NAME=radeonsi`. See [the hardware acceleration docs](/configuration/hardware_acceleration_video.md#intel-based-cpus) for how to set the driver.
- **Use a codec that decodes more reliably.** H.265/HEVC streams may trigger this error far more often than H.264 depending on your CPU generation. If your camera exposes a separate sub-stream, assign an H.264 stream to the `detect` role. Cameras that output full-range YUV (for example some Hikvision models) are especially prone to it.
- **Match the detect resolution to the stream resolution.** When the `detect` resolution differs from the stream, Frigate inserts a GPU scaling filter (`scale_vaapi`), which is where these surface-sync failures can often originate. Set the `detect` `width` and `height` to match the exact resolution of the stream assigned the `detect` role.
- **Match the detect `fps` to the camera stream.** Aggressively dropping frames (for example `detect` `fps: 1` on a stream that runs at 15 fps) can cause timing mismatches in the GPU's frame buffer. Lower the sub-stream's frame rate on the camera itself instead of dropping most frames in Frigate.
- **Fall back to software decoding.** If none of the above resolve it, remove the preset for that camera (`hwaccel_args: []`). Hardware decoding is only an optimization — on a capable CPU, software-decoding a low-resolution sub-stream is inexpensive and gives a stable detect pipeline.
@@ -86,13 +86,15 @@ class LicensePlateProcessingMixin:
self.similarity_threshold = 0.8
self.cluster_threshold = 0.85
def _detect(self, image: np.ndarray) -> List[np.ndarray]:
def _detect(self, image: np.ndarray, debug_frame_id: int) -> List[np.ndarray]:
"""
Detect possible areas of text in the input image by first resizing and normalizing it,
running a detection model, and filtering out low-probability regions.
Args:
image (np.ndarray): The input image in which license plates will be detected.
debug_frame_id (int): Shared id used to name debug images so all artifacts
from a single LPR pass share the same filename suffix.
Returns:
List[np.ndarray]: A list of bounding box coordinates representing detected license plates.
@@ -106,9 +108,8 @@ class LicensePlateProcessingMixin:
normalized_image = self._normalize_image(resized_image)
if WRITE_DEBUG_IMAGES:
current_time = int(datetime.datetime.now().timestamp())
cv2.imwrite(
f"debug/frames/license_plate_resized_{current_time}.jpg",
f"debug/frames/license_plate_resized_{debug_frame_id}.jpg",
resized_image,
)
@@ -203,7 +204,7 @@ class LicensePlateProcessingMixin:
return self.ctc_decoder(outputs)
def _process_license_plate(
self, camera: str, id: str, image: np.ndarray
self, camera: str, id: str, image: np.ndarray, debug_frame_id: int
) -> Tuple[List[str], List[List[float]], List[int]]:
"""
Complete pipeline for detecting, classifying, and recognizing license plates in the input image.
@@ -214,6 +215,8 @@ class LicensePlateProcessingMixin:
camera (str): Camera identifier.
id (str): Event identifier.
image (np.ndarray): The input image in which to detect, classify, and recognize license plates.
debug_frame_id (int): Shared id used to name debug images so all artifacts
from a single LPR pass share the same filename suffix.
Returns:
Tuple[List[str], List[List[float]], List[int]]: Detected license plate texts, character-level confidence scores for each plate (flattened into a single list per plate), and areas of the plates.
@@ -227,7 +230,7 @@ class LicensePlateProcessingMixin:
logger.debug("Model runners not loaded")
return [], [], []
boxes = self._detect(image)
boxes = self._detect(image, debug_frame_id)
if len(boxes) == 0:
logger.debug(f"{camera}: No boxes found by OCR detector model")
return [], [], []
@@ -243,7 +246,6 @@ class LicensePlateProcessingMixin:
boxes, plate_width=plate_width, gap_fraction=0.1
)
current_time = int(datetime.datetime.now().timestamp())
if WRITE_DEBUG_IMAGES:
debug_image = image.copy()
for box in boxes:
@@ -259,7 +261,7 @@ class LicensePlateProcessingMixin:
)
cv2.imwrite(
f"debug/frames/license_plate_boxes_{current_time}.jpg", debug_image
f"debug/frames/license_plate_boxes_{debug_frame_id}.jpg", debug_image
)
boxes = self._sort_boxes(list(boxes))
@@ -322,7 +324,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
for i, img in enumerate(group_plate_images):
cv2.imwrite(
f"debug/frames/license_plate_cropped_{current_time}_{group_indices[i] + 1}.jpg",
f"debug/frames/license_plate_cropped_{debug_frame_id}_{group_indices[i] + 1}.jpg",
img,
)
@@ -335,7 +337,7 @@ class LicensePlateProcessingMixin:
cv2.imwrite(
os.path.join(
CLIPS_DIR,
f"lpr/{camera}/{id}/{current_time}_{group_indices[i] + 1}.jpg",
f"lpr/{camera}/{id}/{debug_frame_id}_{group_indices[i] + 1}.jpg",
),
img,
)
@@ -1199,6 +1201,7 @@ class LicensePlateProcessingMixin:
self.metrics.yolov9_lpr_pps.value = self.plates_det_second.eps()
camera = obj_data if dedicated_lpr else obj_data["camera"]
current_time = int(datetime.datetime.now().timestamp())
debug_frame_id = int(datetime.datetime.now().timestamp() * 1000)
if not self.config.cameras[camera].lpr.enabled:
return
@@ -1214,7 +1217,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
cv2.imwrite(
f"debug/frames/dedicated_lpr_masked_{current_time}.jpg",
f"debug/frames/dedicated_lpr_masked_{debug_frame_id}.jpg",
rgb,
)
@@ -1326,7 +1329,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
cv2.imwrite(
f"debug/frames/car_frame_{current_time}.jpg",
f"debug/frames/car_frame_{debug_frame_id}.jpg",
car,
)
@@ -1454,7 +1457,7 @@ class LicensePlateProcessingMixin:
if WRITE_DEBUG_IMAGES:
cv2.imwrite(
f"debug/frames/license_plate_frame_{current_time}.jpg",
f"debug/frames/license_plate_frame_{debug_frame_id}.jpg",
license_plate_frame,
)
@@ -1464,7 +1467,7 @@ class LicensePlateProcessingMixin:
# run detection, returns results sorted by confidence, best first
start = datetime.datetime.now().timestamp()
license_plates, confidences, areas = self._process_license_plate(
camera, id, license_plate_frame
camera, id, license_plate_frame, debug_frame_id
)
self.plates_rec_second.update()
self.plate_rec_speed.update(datetime.datetime.now().timestamp() - start)
+48
View File
@@ -0,0 +1,48 @@
"""Qualcomm QNN TFLite delegate detector for Qualcomm NPU acceleration."""
import logging
from pydantic import ConfigDict
from typing_extensions import Literal
from frigate.detectors.detection_api import DetectionApi
from frigate.detectors.detector_config import BaseDetectorConfig
from ..detector_utils import (
tflite_detect_raw,
tflite_init,
tflite_load_delegate_interpreter,
)
logger = logging.getLogger(__name__)
# Use _tfl suffix to default to the bundled tflite model
DETECTOR_KEY = "qualcomm_tfl"
class QualcommDetectorConfig(BaseDetectorConfig):
"""Qualcomm NPU detector using the QNN TFLite delegate to accelerate inference on Qualcomm SoCs with a Hexagon NPU (e.g. IQ9100, QCS6490)."""
model_config = ConfigDict(
title="Qualcomm",
)
type: Literal[DETECTOR_KEY]
class QualcommTfl(DetectionApi):
type_key = DETECTOR_KEY
def __init__(self, detector_config: QualcommDetectorConfig):
# The QNN TFLite delegate library is provided by the host via CDI bind mounts
delegate_library = "libQnnTFLiteDelegate.so"
# Use the Hexagon Tensor Processor (HTP / NPU) backend
device_config = {"backend_type": "htp"}
interpreter = tflite_load_delegate_interpreter(
delegate_library, detector_config, device_config
)
tflite_init(self, interpreter)
def detect_raw(self, tensor_input):
return tflite_detect_raw(self, tensor_input)
+4
View File
@@ -413,6 +413,10 @@
"description": "The device to use for OpenVINO inference (e.g. 'CPU', 'GPU', 'NPU')."
}
},
"qualcomm_tfl": {
"label": "Qualcomm",
"description": "Qualcomm NPU detector using the QNN TFLite delegate to accelerate inference on Qualcomm SoCs with a Hexagon NPU (e.g. IQ9100, QCS6490)."
},
"rknn": {
"label": "RKNN",
"description": "RKNN detector for Rockchip NPUs; runs compiled RKNN models on Rockchip hardware.",
+8 -1
View File
@@ -2,7 +2,11 @@ import { baseUrl } from "./baseUrl";
import { ReactNode, useCallback, useEffect, useRef } from "react";
import { WsSendContext } from "./wsContext";
import type { Update } from "./wsContext";
import { processWsMessage, resetWsStore } from "./ws";
import {
invalidateCameraActivityCache,
processWsMessage,
resetWsStore,
} from "./ws";
export function WsProvider({ children }: { children: ReactNode }) {
const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`;
@@ -34,6 +38,9 @@ export function WsProvider({ children }: { children: ReactNode }) {
ws.onopen = () => {
reconnectAttempt.current = 0;
// events may have been missed while disconnected — the snapshot
// requested below must fully apply even if byte-identical
invalidateCameraActivityCache();
ws.send(
JSON.stringify({ topic: "onConnect", message: "", retain: false }),
);
+28 -5
View File
@@ -82,12 +82,15 @@ export function processWsMessage(raw: string) {
function applyTopicUpdate(topic: string, newVal: unknown) {
const oldVal = wsState[topic];
// camera_activity snapshots always re-notify: consumers reconcile local
// state that may have diverged from an unchanged snapshot
const isActivitySnapshot = topic.startsWith("camera_activity/");
// Fast path: === for primitives ("ON"/"OFF", numbers).
// Fall back to isEqual for objects/arrays.
const unchanged =
oldVal === newVal ||
(typeof newVal === "object" && newVal !== null && isEqual(oldVal, newVal));
if (unchanged) return;
if (unchanged && !isActivitySnapshot) return;
wsState[topic] = newVal;
// Snapshot the Set — a listener may trigger unmount that modifies it.
@@ -131,6 +134,25 @@ let wsMessageIdCounter = 0;
// traversals) on every flush — critical with many cameras.
let lastCameraActivityPayload: string | null = null;
// Make the next camera_activity snapshot fully apply even when byte-identical
// to the previous one — local state may have diverged while no messages flowed
export function invalidateCameraActivityCache() {
lastCameraActivityPayload = null;
}
// Collapse same-task resync requests (one hook per camera card) into a
// single onConnect round-trip
let resyncScheduled = false;
function requestCameraActivityResync(sendOnConnect: () => void) {
if (resyncScheduled) return;
resyncScheduled = true;
queueMicrotask(() => {
resyncScheduled = false;
invalidateCameraActivityCache();
sendOnConnect();
});
}
function applyCameraActivity(payload: string) {
// Fast path: if the raw JSON string is identical, nothing changed.
if (payload === lastCameraActivityPayload) return;
@@ -509,15 +531,16 @@ export function useInitialCameraState(
// camera_activity sub-topic payload is already parsed by expandCameraActivity
const data = payload as FrigateCameraState | undefined;
// onConnect is sent once in WsProvider.onopen — no need to re-request on
// every component mount. Components read cached wsState immediately via
// useSyncExternalStore. Only re-request when the user tabs back in.
// the cached snapshot is only written on onConnect and can be stale by the
// time this hook mounts — re-request on mount and when the user tabs back in
useEffect(() => {
if (!revalidateOnFocus) return;
requestCameraActivityResync(() => sendCommand("onConnect"));
const listener = () => {
if (document.visibilityState === "visible") {
sendCommand("onConnect");
requestCameraActivityResync(() => sendCommand("onConnect"));
}
};
addEventListener("visibilitychange", listener);
+62 -47
View File
@@ -7,7 +7,7 @@ import {
} from "@/api/ws";
import { CameraConfig, FrigateConfig } from "@/types/frigateConfig";
import { MotionData, ReviewSegment } from "@/types/review";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { AudioDetection, ObjectType } from "@/types/ws";
import { useTimelineUtils } from "./use-timeline-utils";
import useDeepMemo from "./use-deep-memo";
@@ -57,7 +57,13 @@ export function useCameraActivity(
);
useEffect(() => {
if (updatedCameraState) {
setObjects(updatedCameraState.objects);
// functional updater keeps `objects` out of the deps: this effect must
// only run on snapshot arrival, or it would re-assert a stale snapshot
// over newer event-driven state
const newObjects = updatedCameraState.objects ?? [];
setObjects((currentObjects) =>
isEqual(currentObjects, newObjects) ? currentObjects : newObjects,
);
}
}, [updatedCameraState, camera]);
@@ -82,15 +88,6 @@ export function useCameraActivity(
const { payload: event } = useFrigateEvents();
const updatedEvent = useDeepMemo(event);
const handleSetObjects = useCallback(
(newObjects: ObjectType[]) => {
if (!isEqual(objects, newObjects)) {
setObjects(newObjects);
}
},
[objects],
);
useEffect(() => {
if (!updatedEvent) {
return;
@@ -100,53 +97,71 @@ export function useCameraActivity(
return;
}
const updatedEventIndex =
objects?.findIndex((obj) => obj.id === updatedEvent.after.id) ?? -1;
// functional updater keeps `objects` out of the deps: each event applies
// exactly once against the true current list, so events arriving close
// together cannot clobber each other's changes with a stale write-back
setObjects((currentObjects) => {
const existingObjects = currentObjects ?? [];
const updatedEventIndex = existingObjects.findIndex(
(obj) => obj.id === updatedEvent.after.id,
);
let newObjects: ObjectType[] = [...(objects ?? [])];
if (updatedEvent.type === "end") {
if (updatedEventIndex !== -1) {
if (updatedEvent.type === "end") {
if (updatedEventIndex === -1) {
return currentObjects;
}
const newObjects = [...existingObjects];
newObjects.splice(updatedEventIndex, 1);
return newObjects;
}
} else {
let label = updatedEvent.after.label;
if (updatedEvent.after.sub_label) {
const sub_label = updatedEvent.after.sub_label[0];
if (attributeLabels.includes(sub_label)) {
label = sub_label;
} else {
label = `${label}-verified`;
}
}
if (updatedEventIndex === -1) {
// add unknown updatedEvent to list if not stationary
if (!updatedEvent.after.stationary) {
const newActiveObject: ObjectType = {
id: updatedEvent.after.id,
label: updatedEvent.after.label,
stationary: updatedEvent.after.stationary,
area: updatedEvent.after.area,
ratio: updatedEvent.after.ratio,
score: updatedEvent.after.score,
sub_label: updatedEvent.after.sub_label?.[0] ?? "",
};
newObjects = [...(objects ?? []), newActiveObject];
if (updatedEvent.after.stationary) {
return currentObjects;
}
} else {
let label = updatedEvent.after.label;
if (updatedEvent.after.sub_label) {
const sub_label = updatedEvent.after.sub_label[0];
if (attributeLabels.includes(sub_label)) {
label = sub_label;
} else {
label = `${label}-verified`;
}
}
newObjects[updatedEventIndex] = {
...newObjects[updatedEventIndex],
const newActiveObject: ObjectType = {
id: updatedEvent.after.id,
label,
stationary: updatedEvent.after.stationary,
area: updatedEvent.after.area,
ratio: updatedEvent.after.ratio,
score: updatedEvent.after.score,
sub_label: updatedEvent.after.sub_label?.[0] ?? "",
};
return [...existingObjects, newActiveObject];
}
}
handleSetObjects(newObjects);
}, [attributeLabels, camera, updatedEvent, objects, handleSetObjects]);
const existing = existingObjects[updatedEventIndex];
if (
existing.label === label &&
existing.stationary === updatedEvent.after.stationary
) {
return currentObjects;
}
const newObjects = [...existingObjects];
newObjects[updatedEventIndex] = {
...existing,
label,
stationary: updatedEvent.after.stationary,
};
return newObjects;
});
}, [attributeLabels, camera, updatedEvent]);
// determine if camera is offline