From 6840415b6c813420ada40103a0a6a1d95e35b578 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 16 Aug 2025 22:20:21 -0500 Subject: [PATCH 01/33] Fix content type for latest image API endpoint (#19555) * Fix content type for latest image API endpoint Extension is an enum and .value needed to be appended. Additionally, fastapi's Response() automatically sets the content type when media_type is specified, so a Content-Type in the headers was redundant. * Remove another unneeded Content-Type --- frigate/api/media.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index b4db46d38..163e50518 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -142,15 +142,13 @@ def latest_frame( "regions": params.regions, } quality = params.quality - mime_type = extension - if extension == "png": + if extension == Extension.png: quality_params = None - elif extension == "webp": + elif extension == Extension.webp: quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), quality] - else: + else: # jpg or jpeg quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), quality] - mime_type = "jpeg" if camera_name in request.app.frigate_config.cameras: frame = frame_processor.get_current_frame(camera_name, draw_options) @@ -193,12 +191,11 @@ def latest_frame( frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) - _, img = cv2.imencode(f".{extension}", frame, quality_params) + _, img = cv2.imencode(f".{extension.value}", frame, quality_params) return Response( content=img.tobytes(), - media_type=f"image/{mime_type}", + media_type=f"image/{extension.value}", headers={ - "Content-Type": f"image/{mime_type}", "Cache-Control": "no-store" if not params.store else "private, max-age=60", @@ -215,12 +212,11 @@ def latest_frame( frame = cv2.resize(frame, dsize=(width, height), interpolation=cv2.INTER_AREA) - _, img = cv2.imencode(f".{extension}", frame, quality_params) + _, img = cv2.imencode(f".{extension.value}", frame, quality_params) return Response( content=img.tobytes(), - media_type=f"image/{mime_type}", + media_type=f"image/{extension.value}", headers={ - "Content-Type": f"image/{mime_type}", "Cache-Control": "no-store" if not params.store else "private, max-age=60", From 1db26cb41e27c59f1b18cf6205708b51f86a8b3a Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 17 Aug 2025 18:26:18 -0500 Subject: [PATCH 02/33] Ensure birdseye is enabled before trying to grab a frame from it (#19573) --- frigate/api/media.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index 163e50518..b32847ac7 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -201,7 +201,11 @@ def latest_frame( else "private, max-age=60", }, ) - elif camera_name == "birdseye" and request.app.frigate_config.birdseye.restream: + elif ( + camera_name == "birdseye" + and request.app.frigate_config.birdseye.enabled + and request.app.frigate_config.birdseye.restream + ): frame = cv2.cvtColor( frame_processor.get_current_frame(camera_name), cv2.COLOR_YUV2BGR_I420, From ceced7cc917b3cb9de8e819824bcfdf20be1b068 Mon Sep 17 00:00:00 2001 From: harry <103653962+harrydg1@users.noreply.github.com> Date: Mon, 18 Aug 2025 02:45:21 +0200 Subject: [PATCH 03/33] Install non-free i965 driver (#19571) --- docker/main/install_deps.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index 9684199f8..b60d78006 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -57,9 +57,17 @@ fi # arch specific packages if [[ "${TARGETARCH}" == "amd64" ]]; then + # Install non-free version of i965 driver + CODENAME=$(grep VERSION_CODENAME= /etc/os-release | cut -d= -f2) \ + && sed -i -E "s/^(deb http:\/\/deb\.debian\.org\/debian ${CODENAME} main)(.*)$/\1 contrib non-free non-free-firmware\2/" /etc/apt/sources.list \ + && apt-get -qq update \ + && apt-get install --no-install-recommends --no-install-suggests -y i965-va-driver-shaders \ + && sed -i -E "s/(deb http:\/\/deb\.debian\.org\/debian ${CODENAME} main) contrib non-free non-free-firmware/\1/" /etc/apt/sources.list \ + && apt-get update + # install amd / intel-i965 driver packages apt-get -qq install --no-install-recommends --no-install-suggests -y \ - i965-va-driver intel-gpu-tools onevpl-tools \ + intel-gpu-tools onevpl-tools \ libva-drm2 \ mesa-va-drivers radeontop From 9ed7ccab75ab53e1f75adaf2660bc9f2cef42c14 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:48:21 -0500 Subject: [PATCH 04/33] Embeddings maintainer should start if bird classification is enabled (#19576) --- frigate/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frigate/app.py b/frigate/app.py index cc596a98a..abcefdc56 100644 --- a/frigate/app.py +++ b/frigate/app.py @@ -250,6 +250,7 @@ class FrigateApp: and not genai_cameras and not self.config.lpr.enabled and not self.config.face_recognition.enabled + and not self.config.classification.bird.enabled ): return From b45f64286815ba4059c8acac776de8a5f95046da Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 18 Aug 2025 07:21:42 -0600 Subject: [PATCH 05/33] Use sed on correct file (#19590) --- docker/main/install_deps.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index b60d78006..5dea3c874 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -58,11 +58,10 @@ fi # arch specific packages if [[ "${TARGETARCH}" == "amd64" ]]; then # Install non-free version of i965 driver - CODENAME=$(grep VERSION_CODENAME= /etc/os-release | cut -d= -f2) \ - && sed -i -E "s/^(deb http:\/\/deb\.debian\.org\/debian ${CODENAME} main)(.*)$/\1 contrib non-free non-free-firmware\2/" /etc/apt/sources.list \ + sed -i -E "/^Components: main$/s/main/main contrib non-free non-free-firmware/" "/etc/apt/sources.list.d/debian.sources" \ && apt-get -qq update \ && apt-get install --no-install-recommends --no-install-suggests -y i965-va-driver-shaders \ - && sed -i -E "s/(deb http:\/\/deb\.debian\.org\/debian ${CODENAME} main) contrib non-free non-free-firmware/\1/" /etc/apt/sources.list \ + && sed -i -E "/^Components: main contrib non-free non-free-firmware$/s/main contrib non-free non-free-firmware/main/" "/etc/apt/sources.list.d/debian.sources" \ && apt-get update # install amd / intel-i965 driver packages From ba20b61c43736623dd621acb93e92c874c5b3ee8 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 18 Aug 2025 08:26:02 -0500 Subject: [PATCH 06/33] Deprecate API field include_thumbnails (#19584) * Add deprecation note to API docs for include_thumbnails * for search query params as well --- .../api/defs/query/events_query_parameters.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/frigate/api/defs/query/events_query_parameters.py b/frigate/api/defs/query/events_query_parameters.py index d707ba8cc..187dd3f91 100644 --- a/frigate/api/defs/query/events_query_parameters.py +++ b/frigate/api/defs/query/events_query_parameters.py @@ -1,6 +1,6 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field DEFAULT_TIME_RANGE = "00:00,24:00" @@ -21,7 +21,14 @@ class EventsQueryParams(BaseModel): has_clip: Optional[int] = None has_snapshot: Optional[int] = None in_progress: Optional[int] = None - include_thumbnails: Optional[int] = 1 + include_thumbnails: Optional[int] = Field( + 1, + description=( + "Deprecated. Thumbnail data is no longer included in the response. " + "Use the /api/events/:event_id/thumbnail.:extension endpoint instead." + ), + deprecated=True, + ) favorites: Optional[int] = None min_score: Optional[float] = None max_score: Optional[float] = None @@ -40,7 +47,14 @@ class EventsSearchQueryParams(BaseModel): query: Optional[str] = None event_id: Optional[str] = None search_type: Optional[str] = "thumbnail" - include_thumbnails: Optional[int] = 1 + include_thumbnails: Optional[int] = Field( + 1, + description=( + "Deprecated. Thumbnail data is no longer included in the response. " + "Use the /api/events/:event_id/thumbnail.:extension endpoint instead." + ), + deprecated=True, + ) limit: Optional[int] = 50 cameras: Optional[str] = "all" labels: Optional[str] = "all" From 353ee1228cbd3d73ebfa7288eaaf79020e715f5e Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 18 Aug 2025 15:49:50 -0500 Subject: [PATCH 07/33] Return 500 from the face registration endpoint if Frigate has not yet been restarted (#19601) --- frigate/api/classification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate/api/classification.py b/frigate/api/classification.py index e33d81e81..21ee59fb6 100644 --- a/frigate/api/classification.py +++ b/frigate/api/classification.py @@ -214,7 +214,7 @@ async def register_face(request: Request, name: str, file: UploadFile): ) context: EmbeddingsContext = request.app.embeddings - result = context.register_face(name, await file.read()) + result = None if context is None else context.register_face(name, await file.read()) if not isinstance(result, dict): return JSONResponse( From d27e8c1bbf53cd952dd9f4617bc8ee02c00c6719 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:07:24 -0500 Subject: [PATCH 08/33] run autotracking setup method in asyncio coroutine (#19614) --- frigate/ptz/autotrack.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index f38bf1f5f..662ce63d3 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -1329,7 +1329,11 @@ class PtzAutoTracker: if camera_config.onvif.autotracking.enabled: if not self.autotracker_init[camera]: - self._autotracker_setup(camera_config, camera) + future = asyncio.run_coroutine_threadsafe( + self._autotracker_setup(camera_config, camera), self.onvif.loop + ) + # Wait for the coroutine to complete + future.result() if self.calibrating[camera]: logger.debug(f"{camera}: Calibrating camera") From ec2543c23f8169d073a1f6b79fd905efbeb38651 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 19 Aug 2025 12:14:14 -0600 Subject: [PATCH 09/33] Fix hls not loading video in explore (#19625) --- .../components/player/GenericVideoPlayer.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/web/src/components/player/GenericVideoPlayer.tsx b/web/src/components/player/GenericVideoPlayer.tsx index d64d9a736..4d6cb4ee5 100644 --- a/web/src/components/player/GenericVideoPlayer.tsx +++ b/web/src/components/player/GenericVideoPlayer.tsx @@ -1,4 +1,10 @@ -import React, { useState, useRef, useEffect, useCallback } from "react"; +import React, { + useState, + useRef, + useEffect, + useCallback, + useMemo, +} from "react"; import { useVideoDimensions } from "@/hooks/use-video-dimensions"; import HlsVideoPlayer from "./HlsVideoPlayer"; import ActivityIndicator from "../indicators/activity-indicator"; @@ -89,6 +95,12 @@ export function GenericVideoPlayer({ }, ); + const hlsSource = useMemo(() => { + return { + playlist: source, + }; + }, [source]); + return (
@@ -107,9 +119,7 @@ export function GenericVideoPlayer({ > Date: Tue, 19 Aug 2025 14:42:20 -0500 Subject: [PATCH 10/33] Revert video dimension layout fix for chrome (#19636) originally introduced in https://github.com/blakeblackshear/frigate/pull/19414 --- web/src/hooks/use-video-dimensions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/hooks/use-video-dimensions.ts b/web/src/hooks/use-video-dimensions.ts index 1fad71dc8..448dd5078 100644 --- a/web/src/hooks/use-video-dimensions.ts +++ b/web/src/hooks/use-video-dimensions.ts @@ -17,7 +17,7 @@ export function useVideoDimensions( }); const videoAspectRatio = useMemo(() => { - return videoResolution.width / videoResolution.height; + return videoResolution.width / videoResolution.height || 16 / 9; }, [videoResolution]); const containerAspectRatio = useMemo(() => { @@ -25,7 +25,7 @@ export function useVideoDimensions( }, [containerWidth, containerHeight]); const videoDimensions = useMemo(() => { - if (!containerWidth || !containerHeight || !videoAspectRatio) + if (!containerWidth || !containerHeight) return { width: "100%", height: "100%" }; if (containerAspectRatio > videoAspectRatio) { const height = containerHeight; From 8f4b5b4bdb25ab432f18ae80378686d64888341c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0uklje?= <129388921+JanSuklje@users.noreply.github.com> Date: Wed, 20 Aug 2025 02:29:11 +0200 Subject: [PATCH 11/33] Refactored Viewer role Notifications settings (#19640) - now each individual element is shown if allowed by role, instead of having multiple return statement for each role --- web/src/pages/Settings.tsx | 10 +- .../settings/NotificationsSettingsView.tsx | 263 ++++++++++-------- 2 files changed, 150 insertions(+), 123 deletions(-) diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 368d4dea0..06bf65fd5 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -73,7 +73,11 @@ export default function Settings() { const isAdmin = useIsAdmin(); - const allowedViewsForViewer: SettingsType[] = ["ui", "debug"]; + const allowedViewsForViewer: SettingsType[] = [ + "ui", + "debug", + "notifications", + ]; const visibleSettingsViews = !isAdmin ? allowedViewsForViewer : allSettingsViews; @@ -164,7 +168,7 @@ export default function Settings() { useSearchEffect("page", (page: string) => { if (allSettingsViews.includes(page as SettingsType)) { // Restrict viewer to UI settings - if (!isAdmin && !["ui", "debug"].includes(page)) { + if (!isAdmin && !allowedViewsForViewer.includes(page as SettingsType)) { setPage("ui"); } else { setPage(page as SettingsType); @@ -200,7 +204,7 @@ export default function Settings() { onValueChange={(value: SettingsType) => { if (value) { // Restrict viewer navigation - if (!isAdmin && !["ui", "debug"].includes(value)) { + if (!isAdmin && !allowedViewsForViewer.includes(value)) { setPageToggle("ui"); } else { setPageToggle(value); diff --git a/web/src/views/settings/NotificationsSettingsView.tsx b/web/src/views/settings/NotificationsSettingsView.tsx index ab0241e24..7381c20dc 100644 --- a/web/src/views/settings/NotificationsSettingsView.tsx +++ b/web/src/views/settings/NotificationsSettingsView.tsx @@ -46,6 +46,8 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Trans, useTranslation } from "react-i18next"; import { useDateLocale } from "@/hooks/use-date-locale"; import { useDocDomain } from "@/hooks/use-doc-domain"; +import { useIsAdmin } from "@/hooks/use-is-admin"; +import { cn } from "@/lib/utils"; const NOTIFICATION_SERVICE_WORKER = "notifications-worker.js"; @@ -64,6 +66,10 @@ export default function NotificationView({ const { t } = useTranslation(["views/settings"]); const { getLocaleDocUrl } = useDocDomain(); + // roles + + const isAdmin = useIsAdmin(); + const { data: config, mutate: updateConfig } = useSWR( "config", { @@ -380,7 +386,11 @@ export default function NotificationView({
-
+
{t("notification.notificationSettings.title")} @@ -403,138 +413,151 @@ export default function NotificationView({
-
- - ( - - {t("notification.email.title")} - - - - - {t("notification.email.desc")} - - - - )} - /> + {isAdmin && ( + + + ( + + {t("notification.email.title")} + + + + + {t("notification.email.desc")} + + + + )} + /> - ( - - {allCameras && allCameras?.length > 0 ? ( - <> -
- - {t("notification.cameras.title")} - -
-
- ( + ( + + {allCameras && allCameras?.length > 0 ? ( + <> +
+ + {t("notification.cameras.title")} + +
+
+ ( + { + setChangedValue(true); + if (checked) { + form.setValue("cameras", []); + } + field.onChange(checked); + }} + /> + )} + /> + {allCameras?.map((camera) => ( { setChangedValue(true); + let newCameras; if (checked) { - form.setValue("cameras", []); + newCameras = [ + ...field.value, + camera.name, + ]; + } else { + newCameras = field.value?.filter( + (value) => value !== camera.name, + ); } - field.onChange(checked); + field.onChange(newCameras); + form.setValue("allEnabled", false); }} /> - )} - /> - {allCameras?.map((camera) => ( - { - setChangedValue(true); - let newCameras; - if (checked) { - newCameras = [ - ...field.value, - camera.name, - ]; - } else { - newCameras = field.value?.filter( - (value) => value !== camera.name, - ); - } - field.onChange(newCameras); - form.setValue("allEnabled", false); - }} - /> - ))} + ))} +
+ + ) : ( +
+ {t("notification.cameras.noCameras")}
- - ) : ( -
- {t("notification.cameras.noCameras")} -
- )} + )} - - - {t("notification.cameras.desc")} - -
- )} - /> - -
- - -
- - + /> + +
+ + +
+ + + )}
-
- - +
+ + {t("notification.deviceSpecific")}
- {notificationCameras.length > 0 && ( + {isAdmin && notificationCameras.length > 0 && (
From 75e33d8a566b128fc5802d6dd6875f077071f36a Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 Aug 2025 08:03:50 -0500 Subject: [PATCH 12/33] Catch invalid key in genai prompt (#19657) --- frigate/genai/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/frigate/genai/__init__.py b/frigate/genai/__init__.py index 2c0aadbd9..a3fc7a09c 100644 --- a/frigate/genai/__init__.py +++ b/frigate/genai/__init__.py @@ -40,10 +40,15 @@ class GenAIClient: event: Event, ) -> Optional[str]: """Generate a description for the frame.""" - prompt = camera_config.genai.object_prompts.get( - event.label, - camera_config.genai.prompt, - ).format(**model_to_dict(event)) + try: + prompt = camera_config.genai.object_prompts.get( + event.label, + camera_config.genai.prompt, + ).format(**model_to_dict(event)) + except KeyError as e: + logger.error(f"Invalid key in GenAI prompt: {e}") + return None + logger.debug(f"Sending images to genai provider with prompt: {prompt}") return self._send(prompt, thumbnails) From 2b185a1105300ee26a938a27507b5868b559b5fe Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 Aug 2025 13:57:24 -0500 Subject: [PATCH 13/33] Update bug report template (#19664) * update bug report template * remove additional field --- .github/DISCUSSION_TEMPLATE/report-a-bug.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml index 21e4f746f..a32ee5938 100644 --- a/.github/DISCUSSION_TEMPLATE/report-a-bug.yml +++ b/.github/DISCUSSION_TEMPLATE/report-a-bug.yml @@ -6,7 +6,7 @@ body: value: | Use this form to submit a reproducible bug in Frigate or Frigate's UI. - Before submitting your bug report, please [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. + Before submitting your bug report, please ask the AI with the "Ask AI" button on the [official documentation site][ai] about your issue, [search the discussions][discussions], look at recent open and closed [pull requests][prs], read the [official Frigate documentation][docs], and read the [Frigate FAQ][faq] pinned at the Discussion page to see if your bug has already been fixed by the developers or reported by the community. **If you are unsure if your issue is actually a bug or not, please submit a support request first.** @@ -14,6 +14,7 @@ body: [prs]: https://www.github.com/blakeblackshear/frigate/pulls [docs]: https://docs.frigate.video [faq]: https://github.com/blakeblackshear/frigate/discussions/12724 + [ai]: https://docs.frigate.video - type: checkboxes attributes: label: Checklist @@ -26,6 +27,8 @@ body: - label: I have tried a different browser to see if it is related to my browser. required: true - label: I have tried reproducing the issue in [incognito mode](https://www.computerworld.com/article/1719851/how-to-go-incognito-in-chrome-firefox-safari-and-edge.html) to rule out problems with any third party extensions or plugins I have installed. + - label: I have asked the AI at https://docs.frigate.video about my issue. + required: true - type: textarea id: description attributes: From 664a6fd0cb65587fafc4972e97984cdfa0bbb623 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 20 Aug 2025 15:19:55 -0500 Subject: [PATCH 14/33] remove newlines (#19671) let mermaid format the text directly --- docs/docs/frigate/video_pipeline.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/docs/frigate/video_pipeline.md b/docs/docs/frigate/video_pipeline.md index 313e27ed5..ba9365650 100644 --- a/docs/docs/frigate/video_pipeline.md +++ b/docs/docs/frigate/video_pipeline.md @@ -15,10 +15,10 @@ At a high level, there are five processing steps that could be applied to a came %%{init: {"themeVariables": {"edgeLabelBackground": "transparent"}}}%% flowchart LR - Feed(Feed\nacquisition) --> Decode(Video\ndecoding) - Decode --> Motion(Motion\ndetection) - Motion --> Object(Object\ndetection) - Feed --> Recording(Recording\nand\nvisualization) + Feed(Feed acquisition) --> Decode(Video decoding) + Decode --> Motion(Motion detection) + Motion --> Object(Object detection) + Feed --> Recording(Recording and visualization) Motion --> Recording Object --> Recording ``` From 8a01643acf70441c8a14ef87e9edd22df79e3a69 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 21 Aug 2025 05:43:07 -0500 Subject: [PATCH 15/33] clarify webrtc for two way talk (#19683) --- docs/docs/frigate/installation.md | 2 +- docs/docs/guides/configuring_go2rtc.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index c94daba45..88decf7c9 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -43,7 +43,7 @@ The following ports are used by Frigate and can be mapped via docker as required | `8971` | Authenticated UI and API access without TLS. Reverse proxies should use this port. | | `5000` | Internal unauthenticated UI and API access. Access to this port should be limited. Intended to be used within the docker network for services that integrate with Frigate. | | `8554` | RTSP restreaming. By default, these streams are unauthenticated. Authentication can be configured in go2rtc section of config. | -| `8555` | WebRTC connections for low latency live views. | +| `8555` | WebRTC connections for cameras with two-way talk support. | #### Common Docker Compose storage configurations diff --git a/docs/docs/guides/configuring_go2rtc.md b/docs/docs/guides/configuring_go2rtc.md index 652aa3b26..49af31c1a 100644 --- a/docs/docs/guides/configuring_go2rtc.md +++ b/docs/docs/guides/configuring_go2rtc.md @@ -114,7 +114,7 @@ section. ## Next steps 1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera). -2. You may also prefer to [setup WebRTC](/configuration/live#webrtc-extra-configuration) for slightly lower latency than MSE. Note that WebRTC only supports h264 and specific audio formats and may require opening ports on your router. +2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports h264 and specific audio formats and may require opening ports on your router. ## Important considerations From 7cf439e010145c05535971662952e6bb9992acdb Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 21 Aug 2025 09:21:18 -0500 Subject: [PATCH 16/33] remove h264 reference for webrtc (#19688) --- docs/docs/guides/configuring_go2rtc.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/configuring_go2rtc.md b/docs/docs/guides/configuring_go2rtc.md index 49af31c1a..474dde0a2 100644 --- a/docs/docs/guides/configuring_go2rtc.md +++ b/docs/docs/guides/configuring_go2rtc.md @@ -114,7 +114,7 @@ section. ## Next steps 1. If the stream you added to go2rtc is also used by Frigate for the `record` or `detect` role, you can migrate your config to pull from the RTSP restream to reduce the number of connections to your camera as shown [here](/configuration/restream#reduce-connections-to-camera). -2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports h264 and specific audio formats and may require opening ports on your router. +2. You can [set up WebRTC](/configuration/live#webrtc-extra-configuration) if your camera supports two-way talk. Note that WebRTC only supports specific audio formats and may require opening ports on your router. ## Important considerations From 4fe246f472697731654dc35cdcf482887bc05e09 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Fri, 22 Aug 2025 07:04:30 -0500 Subject: [PATCH 17/33] Fixes (#19708) * use custom swr fetcher to check for audio support The go2rtc API doesn't always return stream data for anything not being actively consumed, so audio support was not always being correctly deduced. So we can use a custom swr fetcher to call the endpoint that probes the streams, which returns the correct producers data. * return correct mime type for thumbnail and latest frame endpoints follow up to https://github.com/blakeblackshear/frigate/pull/19555 --- .../api/defs/query/media_query_parameters.py | 5 ++ frigate/api/media.py | 18 +++---- web/src/hooks/use-camera-live-mode.ts | 51 +++++++++++++++++-- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/frigate/api/defs/query/media_query_parameters.py b/frigate/api/defs/query/media_query_parameters.py index 4750d3277..8ab799a56 100644 --- a/frigate/api/defs/query/media_query_parameters.py +++ b/frigate/api/defs/query/media_query_parameters.py @@ -10,6 +10,11 @@ class Extension(str, Enum): jpg = "jpg" jpeg = "jpeg" + def get_mime_type(self) -> str: + if self in (Extension.jpg, Extension.jpeg): + return "image/jpeg" + return f"image/{self.value}" + class MediaLatestFrameQueryParams(BaseModel): bbox: Optional[int] = None diff --git a/frigate/api/media.py b/frigate/api/media.py index b32847ac7..1b9b98a16 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -194,7 +194,7 @@ def latest_frame( _, img = cv2.imencode(f".{extension.value}", frame, quality_params) return Response( content=img.tobytes(), - media_type=f"image/{extension.value}", + media_type=extension.get_mime_type(), headers={ "Cache-Control": "no-store" if not params.store @@ -219,7 +219,7 @@ def latest_frame( _, img = cv2.imencode(f".{extension.value}", frame, quality_params) return Response( content=img.tobytes(), - media_type=f"image/{extension.value}", + media_type=extension.get_mime_type(), headers={ "Cache-Control": "no-store" if not params.store @@ -878,7 +878,7 @@ def event_snapshot( def event_thumbnail( request: Request, event_id: str, - extension: str, + extension: Extension, max_cache_age: int = Query( 2592000, description="Max cache age in seconds. Default 30 days in seconds." ), @@ -903,7 +903,7 @@ def event_thumbnail( if event_id in camera_state.tracked_objects: tracked_obj = camera_state.tracked_objects.get(event_id) if tracked_obj is not None: - thumbnail_bytes = tracked_obj.get_thumbnail(extension) + thumbnail_bytes = tracked_obj.get_thumbnail(extension.value) except Exception: return JSONResponse( content={"success": False, "message": "Event not found"}, @@ -931,23 +931,21 @@ def event_thumbnail( ) quality_params = None - - if extension == "jpg" or extension == "jpeg": + if extension in (Extension.jpg, Extension.jpeg): quality_params = [int(cv2.IMWRITE_JPEG_QUALITY), 70] - elif extension == "webp": + elif extension == Extension.webp: quality_params = [int(cv2.IMWRITE_WEBP_QUALITY), 60] - _, img = cv2.imencode(f".{extension}", thumbnail, quality_params) + _, img = cv2.imencode(f".{extension.value}", thumbnail, quality_params) thumbnail_bytes = img.tobytes() return Response( thumbnail_bytes, - media_type=f"image/{extension}", + media_type=extension.get_mime_type(), headers={ "Cache-Control": f"private, max-age={max_cache_age}" if event_complete else "no-store", - "Content-Type": f"image/{extension}", }, ) diff --git a/web/src/hooks/use-camera-live-mode.ts b/web/src/hooks/use-camera-live-mode.ts index 238ac70cc..76689b9bc 100644 --- a/web/src/hooks/use-camera-live-mode.ts +++ b/web/src/hooks/use-camera-live-mode.ts @@ -1,5 +1,5 @@ import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useState, useMemo } from "react"; import useSWR from "swr"; import { LivePlayerMode, LiveStreamMetadata } from "@/types/live"; @@ -8,9 +8,54 @@ export default function useCameraLiveMode( windowVisible: boolean, ) { const { data: config } = useSWR("config"); - const { data: allStreamMetadata } = useSWR<{ + + // Get comma-separated list of restreamed stream names for SWR key + const restreamedStreamsKey = useMemo(() => { + if (!cameras || !config) return null; + + const streamNames = new Set(); + cameras.forEach((camera) => { + const isRestreamed = Object.keys(config.go2rtc.streams || {}).includes( + Object.values(camera.live.streams)[0], + ); + + if (isRestreamed) { + Object.values(camera.live.streams).forEach((streamName) => { + streamNames.add(streamName); + }); + } + }); + + return streamNames.size > 0 + ? Array.from(streamNames).sort().join(",") + : null; + }, [cameras, config]); + + const streamsFetcher = useCallback(async (key: string) => { + const streamNames = key.split(","); + const metadata: { [key: string]: LiveStreamMetadata } = {}; + + await Promise.all( + streamNames.map(async (streamName) => { + try { + const response = await fetch(`/api/go2rtc/streams/${streamName}`); + if (response.ok) { + const data = await response.json(); + metadata[streamName] = data; + } + } catch (error) { + // eslint-disable-next-line no-console + console.error(`Failed to fetch metadata for ${streamName}:`, error); + } + }), + ); + + return metadata; + }, []); + + const { data: allStreamMetadata = {} } = useSWR<{ [key: string]: LiveStreamMetadata; - }>(config ? "go2rtc/streams" : null, { revalidateOnFocus: false }); + }>(restreamedStreamsKey, streamsFetcher, { revalidateOnFocus: false }); const [preferredLiveModes, setPreferredLiveModes] = useState<{ [key: string]: LivePlayerMode; From 4347402fcc1e6f4b9b35553bc8c2bf0b141c253b Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 25 Aug 2025 06:32:50 -0600 Subject: [PATCH 18/33] Don't mention 9.0.0 GFX version (#19742) --- docs/docs/configuration/object_detectors.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/docs/configuration/object_detectors.md b/docs/docs/configuration/object_detectors.md index e048e0ec5..18dc683a9 100644 --- a/docs/docs/configuration/object_detectors.md +++ b/docs/docs/configuration/object_detectors.md @@ -440,14 +440,13 @@ Also AMD/ROCm does not "officially" support integrated GPUs. It still does work For the rocm frigate build there is some automatic detection: -- gfx90c -> 9.0.0 - 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 `9.0.0`, then you should configure it from command line as: +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 -$ docker run -e HSA_OVERRIDE_GFX_VERSION=9.0.0 \ +$ docker run -e HSA_OVERRIDE_GFX_VERSION=10.0.0 \ ... ``` @@ -458,7 +457,7 @@ services: frigate: environment: - HSA_OVERRIDE_GFX_VERSION: "9.0.0" + HSA_OVERRIDE_GFX_VERSION: "10.0.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. From 4fcb1ea7ac5504b661fb64d814ae17b833f9023a Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Mon, 25 Aug 2025 12:33:17 -0600 Subject: [PATCH 19/33] Unload HLS on unmount (#19747) * Unload HLS player on unmount so segments don't continue to load * Add query arg for event padding --- frigate/api/media.py | 14 +++++++++++--- web/src/components/player/HlsVideoPlayer.tsx | 16 +++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index 1b9b98a16..60110345f 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -1156,7 +1156,11 @@ def event_snapshot_clean(request: Request, event_id: str, download: bool = False @router.get("/events/{event_id}/clip.mp4") -def event_clip(request: Request, event_id: str): +def event_clip( + request: Request, + event_id: str, + padding: int = Query(0, description="Padding to apply to clip."), +): try: event: Event = Event.get(Event.id == event_id) except DoesNotExist: @@ -1169,8 +1173,12 @@ def event_clip(request: Request, event_id: str): content={"success": False, "message": "Clip not available"}, status_code=404 ) - end_ts = datetime.now().timestamp() if event.end_time is None else event.end_time - return recording_clip(request, event.camera, event.start_time, end_ts) + end_ts = ( + datetime.now().timestamp() + if event.end_time is None + else event.end_time + padding + ) + return recording_clip(request, event.camera, event.start_time - padding, end_ts) @router.get("/events/{event_id}/preview.gif") diff --git a/web/src/components/player/HlsVideoPlayer.tsx b/web/src/components/player/HlsVideoPlayer.tsx index 7fde436ad..93f1da702 100644 --- a/web/src/components/player/HlsVideoPlayer.tsx +++ b/web/src/components/player/HlsVideoPlayer.tsx @@ -123,13 +123,6 @@ export default function HlsVideoPlayer({ return; } - // we must destroy the hlsRef every time the source changes - // so that we can create a new HLS instance with startPosition - // set at the optimal point in time - if (hlsRef.current) { - hlsRef.current.destroy(); - } - hlsRef.current = new Hls({ maxBufferLength: 10, maxBufferSize: 20 * 1000 * 1000, @@ -138,6 +131,15 @@ export default function HlsVideoPlayer({ hlsRef.current.attachMedia(videoRef.current); hlsRef.current.loadSource(currentSource.playlist); videoRef.current.playbackRate = currentPlaybackRate; + + return () => { + // we must destroy the hlsRef every time the source changes + // so that we can create a new HLS instance with startPosition + // set at the optimal point in time + if (hlsRef.current) { + hlsRef.current.destroy(); + } + } }, [videoRef, hlsRef, useHlsCompat, currentSource]); // state handling From 0dda37ac439963ab89f89c1340ce01d0a10d66b2 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 25 Aug 2025 18:11:42 -0500 Subject: [PATCH 20/33] fix export dialog overflowing due to i18n time lengths (#19736) wrap the pair of custom time pickers in a flex-wrap --- web/src/components/overlay/ExportDialog.tsx | 258 ++++++++++---------- 1 file changed, 130 insertions(+), 128 deletions(-) diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 44b55bfe3..c0c0e4538 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -433,137 +433,139 @@ function CustomTimeSelector({ className={`mt-3 flex items-center rounded-lg bg-secondary text-secondary-foreground ${isDesktop ? "mx-8 gap-2 px-2" : "pl-2"}`} > - { - if (!open) { - setStartOpen(false); - } - }} - > - - - - - { - if (!day) { - return; - } - - setRange({ - before: endTime, - after: day.getTime() / 1000 + 1, - }); - }} - /> - - { - const clock = e.target.value; - const [hour, minute, second] = isIOS - ? [...clock.split(":"), "00"] - : clock.split(":"); - - const start = new Date(startTime * 1000); - start.setHours( - parseInt(hour), - parseInt(minute), - parseInt(second ?? 0), - 0, - ); - setRange({ - before: endTime, - after: start.getTime() / 1000, - }); - }} - /> - - - - { - if (!open) { - setEndOpen(false); - } - }} - > - - - - - { - if (!day) { - return; - } + } + }} + > + + + + + { + if (!day) { + return; + } - setRange({ - after: startTime, - before: day.getTime() / 1000, - }); - }} - /> - - { - const clock = e.target.value; - const [hour, minute, second] = isIOS - ? [...clock.split(":"), "00"] - : clock.split(":"); + setRange({ + before: endTime, + after: day.getTime() / 1000 + 1, + }); + }} + /> + + { + const clock = e.target.value; + const [hour, minute, second] = isIOS + ? [...clock.split(":"), "00"] + : clock.split(":"); - const end = new Date(endTime * 1000); - end.setHours( - parseInt(hour), - parseInt(minute), - parseInt(second ?? 0), - 0, - ); - setRange({ - before: end.getTime() / 1000, - after: startTime, - }); - }} - /> - - + const start = new Date(startTime * 1000); + start.setHours( + parseInt(hour), + parseInt(minute), + parseInt(second ?? 0), + 0, + ); + setRange({ + before: endTime, + after: start.getTime() / 1000, + }); + }} + /> + + + + { + if (!open) { + setEndOpen(false); + } + }} + > + + + + + { + if (!day) { + return; + } + + setRange({ + after: startTime, + before: day.getTime() / 1000, + }); + }} + /> + + { + const clock = e.target.value; + const [hour, minute, second] = isIOS + ? [...clock.split(":"), "00"] + : clock.split(":"); + + const end = new Date(endTime * 1000); + end.setHours( + parseInt(hour), + parseInt(minute), + parseInt(second ?? 0), + 0, + ); + setRange({ + before: end.getTime() / 1000, + after: startTime, + }); + }} + /> + + +
); } From f62feeb50c5ad63cf36626c147b12884c835fa96 Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Tue, 26 Aug 2025 20:01:34 +0800 Subject: [PATCH 21/33] docs: update hardware acceleation video page nvidia docker compose image (#19762) --- docs/docs/configuration/hardware_acceleration_video.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md index 057ae223d..43f293309 100644 --- a/docs/docs/configuration/hardware_acceleration_video.md +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -229,7 +229,7 @@ Additional configuration is needed for the Docker container to be able to access services: frigate: ... - image: ghcr.io/blakeblackshear/frigate:stable + image: ghcr.io/blakeblackshear/frigate:stable-tensorrt deploy: # <------------- Add this section resources: reservations: @@ -247,7 +247,7 @@ docker run -d \ --name frigate \ ... --gpus=all \ - ghcr.io/blakeblackshear/frigate:stable + ghcr.io/blakeblackshear/frigate:stable-tensorrt ``` ### Setup Decoder From b5aa1b2c212d4e08e5ecc99826c889faf8c4fc8c Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:39:23 -0500 Subject: [PATCH 22/33] Fix autotracking calibration crash when zooming is disabled (#19776) --- frigate/ptz/autotrack.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 662ce63d3..922c0a1d9 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -369,12 +369,13 @@ class PtzAutoTracker: logger.info(f"Camera calibration for {camera} in progress") # zoom levels test + self.zoom_time[camera] = 0 + if ( self.config.cameras[camera].onvif.autotracking.zooming != ZoomingModeEnum.disabled ): logger.info(f"Calibration for {camera} in progress: 0% complete") - self.zoom_time[camera] = 0 for i in range(2): # absolute move to 0 - fully zoomed out From fa6956c46e6a8c27679f4b08550dca3835411338 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:24:43 -0500 Subject: [PATCH 23/33] Update openapi schema with include_thumbnails deprecation comment (#19777) --- docs/static/frigate-api.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/static/frigate-api.yaml b/docs/static/frigate-api.yaml index 3df025d9f..ca53bdcf7 100644 --- a/docs/static/frigate-api.yaml +++ b/docs/static/frigate-api.yaml @@ -1759,6 +1759,10 @@ paths: - name: include_thumbnails in: query required: false + description: > + Deprecated. Thumbnail data is no longer included in the response. + Use the /api/events/:event_id/thumbnail.:extension endpoint instead. + deprecated: true schema: anyOf: - type: integer @@ -1973,6 +1977,10 @@ paths: - name: include_thumbnails in: query required: false + description: > + Deprecated. Thumbnail data is no longer included in the response. + Use the /api/events/:event_id/thumbnail.:extension endpoint instead. + deprecated: true schema: anyOf: - type: integer From ad694f55110913fc3488b87692a308ba655f4d77 Mon Sep 17 00:00:00 2001 From: GuoQing Liu <842607283@qq.com> Date: Wed, 27 Aug 2025 05:24:58 +0800 Subject: [PATCH 24/33] docs: rk ffmpeg preset is outdated (#19780) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: rk ffmpeg preset is outdated * 更新 hardware_acceleration_video.md docs: remove video decoding page redundant titles --- docs/docs/configuration/ffmpeg_presets.md | 3 +-- docs/docs/configuration/hardware_acceleration_video.md | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/docs/configuration/ffmpeg_presets.md b/docs/docs/configuration/ffmpeg_presets.md index 8fd0fd811..8bba62e36 100644 --- a/docs/docs/configuration/ffmpeg_presets.md +++ b/docs/docs/configuration/ffmpeg_presets.md @@ -21,8 +21,7 @@ See [the hwaccel docs](/configuration/hardware_acceleration_video.md) for more i | preset-nvidia | Nvidia GPU | | | preset-jetson-h264 | Nvidia Jetson with h264 stream | | | preset-jetson-h265 | Nvidia Jetson with h265 stream | | -| preset-rk-h264 | Rockchip MPP with h264 stream | Use image with \*-rk suffix and privileged mode | -| preset-rk-h265 | Rockchip MPP with h265 stream | Use image with \*-rk suffix and privileged mode | +| preset-rkmpp | Rockchip MPP | Use image with \*-rk suffix and privileged mode | ### Input Args Presets diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md index 43f293309..cb8d7007b 100644 --- a/docs/docs/configuration/hardware_acceleration_video.md +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -9,7 +9,6 @@ It is highly recommended to use a GPU for hardware acceleration video decoding i Depending on your system, these parameters may not be compatible. More information on hardware accelerated decoding for ffmpeg can be found here: https://trac.ffmpeg.org/wiki/HWAccelIntro -# Object Detection ## Raspberry Pi 3/4 From 667c302a7d14d13840db89298cc90884cf956319 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 27 Aug 2025 06:44:10 -0500 Subject: [PATCH 25/33] Allow scrolling on languages menu on mobile devices (#19797) --- web/src/components/menu/GeneralSettings.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/components/menu/GeneralSettings.tsx b/web/src/components/menu/GeneralSettings.tsx index c4ccdff84..efc636a91 100644 --- a/web/src/components/menu/GeneralSettings.tsx +++ b/web/src/components/menu/GeneralSettings.tsx @@ -332,7 +332,9 @@ export default function GeneralSettings({ className }: GeneralSettingsProps) { From 281c461647e5a0c98f4223e0b310889849a66cfb Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Wed, 27 Aug 2025 06:27:08 -0600 Subject: [PATCH 26/33] Add support for Frigate+ input data type (#19799) --- frigate/detectors/detector_config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frigate/detectors/detector_config.py b/frigate/detectors/detector_config.py index 3893908d2..d7883523d 100644 --- a/frigate/detectors/detector_config.py +++ b/frigate/detectors/detector_config.py @@ -158,6 +158,9 @@ class ModelConfig(BaseModel): self.input_pixel_format = model_info["pixelFormat"] self.model_type = model_info["type"] + if model_info.get("inputDataType"): + self.input_dtype = model_info["inputDataType"] + # generate list of attribute labels self.attributes_map = { **model_info.get("attributes", DEFAULT_ATTRIBUTE_LABEL_MAP), From 16b7f7f6e7926db61a07102c0793a37a4e8d1762 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:27:18 -0500 Subject: [PATCH 27/33] Fix HLS video initial aspect on Chrome (#19805) Explore videos are very small on Chrome specifically, this has something to do with how the latest version of Chrome loads video metadata. This change provides a default aspect ratio instead of a default height when the container ref is not defined yet --- web/src/hooks/use-video-dimensions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/hooks/use-video-dimensions.ts b/web/src/hooks/use-video-dimensions.ts index 448dd5078..25b8af350 100644 --- a/web/src/hooks/use-video-dimensions.ts +++ b/web/src/hooks/use-video-dimensions.ts @@ -26,7 +26,7 @@ export function useVideoDimensions( const videoDimensions = useMemo(() => { if (!containerWidth || !containerHeight) - return { width: "100%", height: "100%" }; + return { aspectRatio: "16 / 9", width: "100%" }; if (containerAspectRatio > videoAspectRatio) { const height = containerHeight; const width = height * videoAspectRatio; From e9dc30235ba1804c80e80c71983844cf41c32a32 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Thu, 28 Aug 2025 06:09:23 -0600 Subject: [PATCH 28/33] Cleanup vod clip handling and add padding arg (#19813) --- frigate/api/media.py | 46 +++++++++++++++++++------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/frigate/api/media.py b/frigate/api/media.py index 60110345f..971475cba 100644 --- a/frigate/api/media.py +++ b/frigate/api/media.py @@ -749,7 +749,10 @@ def vod_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: st "/vod/event/{event_id}", description="Returns an HLS playlist for the specified object. Append /master.m3u8 or /index.m3u8 for HLS playback.", ) -def vod_event(event_id: str): +def vod_event( + event_id: str, + padding: int = Query(0, description="Padding to apply to the vod."), +): try: event: Event = Event.get(Event.id == event_id) except DoesNotExist: @@ -772,32 +775,23 @@ def vod_event(event_id: str): status_code=404, ) - clip_path = os.path.join(CLIPS_DIR, f"{event.camera}-{event.id}.mp4") - - if not os.path.isfile(clip_path): - end_ts = ( - datetime.now().timestamp() if event.end_time is None else event.end_time - ) - vod_response = vod_ts(event.camera, event.start_time, end_ts) - # If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false - if ( - event.start_time < datetime.now().timestamp() - 300 - and type(vod_response) is tuple - and len(vod_response) == 2 - and vod_response[1] == 404 - ): - Event.update(has_clip=False).where(Event.id == event_id).execute() - return vod_response - - duration = int((event.end_time - event.start_time) * 1000) - return JSONResponse( - content={ - "cache": True, - "discontinuity": False, - "durations": [duration], - "sequences": [{"clips": [{"type": "source", "path": clip_path}]}], - } + end_ts = ( + datetime.now().timestamp() + if event.end_time is None + else (event.end_time + padding) ) + vod_response = vod_ts(event.camera, event.start_time - padding, end_ts) + + # If the recordings are not found and the event started more than 5 minutes ago, set has_clip to false + if ( + event.start_time < datetime.now().timestamp() - 300 + and type(vod_response) is tuple + and len(vod_response) == 2 + and vod_response[1] == 404 + ): + Event.update(has_clip=False).where(Event.id == event_id).execute() + + return vod_response @router.get( From f7ed8b4cab8f43639a6e71c4d7fbac5731ab9d06 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:18:50 -0500 Subject: [PATCH 29/33] Autotracking improvements (#19873) * Use asyncio lock when checking camera status get_camera_status() can be called during normal autotracking movement and from routine camera_maintenance(). Some cameras cause one of the status calls to hang, which then subsequently hangs autotracking. A lock serializes access and prevents the hang. * use while loop in camera_maintenance for status check some cameras seem to take a little bit to update their status, don't assume the first call shows the motor has stopped --- frigate/ptz/autotrack.py | 4 +- frigate/ptz/onvif.py | 172 ++++++++++++++++++++------------------- 2 files changed, 92 insertions(+), 84 deletions(-) diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index 922c0a1d9..a75ea13ae 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -1462,7 +1462,7 @@ class PtzAutoTracker: if not self.autotracker_init[camera]: self._autotracker_setup(self.config.cameras[camera], camera) # regularly update camera status - if not self.ptz_metrics[camera].motor_stopped.is_set(): + while not self.ptz_metrics[camera].motor_stopped.is_set(): await self.onvif.get_camera_status(camera) # return to preset if tracking is over @@ -1491,7 +1491,7 @@ class PtzAutoTracker: ) # update stored zoom level from preset - if not self.ptz_metrics[camera].motor_stopped.is_set(): + while not self.ptz_metrics[camera].motor_stopped.is_set(): await self.onvif.get_camera_status(camera) self.ptz_metrics[camera].tracking_active.clear() diff --git a/frigate/ptz/onvif.py b/frigate/ptz/onvif.py index 81c8b9852..424c4c0dd 100644 --- a/frigate/ptz/onvif.py +++ b/frigate/ptz/onvif.py @@ -48,6 +48,8 @@ class OnvifController: self.config = config self.ptz_metrics = ptz_metrics + self.status_locks: dict[str, asyncio.Lock] = {} + # Create a dedicated event loop and run it in a separate thread self.loop = asyncio.new_event_loop() self.loop_thread = threading.Thread(target=self._run_event_loop, daemon=True) @@ -59,6 +61,7 @@ class OnvifController: continue if cam.onvif.host: self.camera_configs[cam_name] = cam + self.status_locks[cam_name] = asyncio.Lock() asyncio.run_coroutine_threadsafe(self._init_cameras(), self.loop) @@ -764,105 +767,110 @@ class OnvifController: return False async def get_camera_status(self, camera_name: str) -> None: - if camera_name not in self.cams.keys(): - logger.error(f"ONVIF is not configured for {camera_name}") - return - - if not self.cams[camera_name]["init"]: - if not await self._init_onvif(camera_name): + async with self.status_locks[camera_name]: + if camera_name not in self.cams.keys(): + logger.error(f"ONVIF is not configured for {camera_name}") return - status_request = self.cams[camera_name]["status_request"] - try: - status = await self.cams[camera_name]["ptz"].GetStatus(status_request) - except Exception: - pass # We're unsupported, that'll be reported in the next check. + if not self.cams[camera_name]["init"]: + if not await self._init_onvif(camera_name): + return - try: - pan_tilt_status = getattr(status.MoveStatus, "PanTilt", None) - zoom_status = getattr(status.MoveStatus, "Zoom", None) + status_request = self.cams[camera_name]["status_request"] + try: + status = await self.cams[camera_name]["ptz"].GetStatus(status_request) + except Exception: + pass # We're unsupported, that'll be reported in the next check. - # if it's not an attribute, see if MoveStatus even exists in the status result - if pan_tilt_status is None: - pan_tilt_status = getattr(status, "MoveStatus", None) + try: + pan_tilt_status = getattr(status.MoveStatus, "PanTilt", None) + zoom_status = getattr(status.MoveStatus, "Zoom", None) - # we're unsupported - if pan_tilt_status is None or pan_tilt_status not in [ - "IDLE", - "MOVING", - ]: - raise Exception - except Exception: - logger.warning( - f"Camera {camera_name} does not support the ONVIF GetStatus method. Autotracking will not function correctly and must be disabled in your config." + # if it's not an attribute, see if MoveStatus even exists in the status result + if pan_tilt_status is None: + pan_tilt_status = getattr(status, "MoveStatus", None) + + # we're unsupported + if pan_tilt_status is None or pan_tilt_status not in [ + "IDLE", + "MOVING", + ]: + raise Exception + except Exception: + logger.warning( + f"Camera {camera_name} does not support the ONVIF GetStatus method. Autotracking will not function correctly and must be disabled in your config." + ) + return + + logger.debug( + f"{camera_name}: Pan/tilt status: {pan_tilt_status}, Zoom status: {zoom_status}" ) - return - logger.debug( - f"{camera_name}: Pan/tilt status: {pan_tilt_status}, Zoom status: {zoom_status}" - ) + if pan_tilt_status == "IDLE" and ( + zoom_status is None or zoom_status == "IDLE" + ): + self.cams[camera_name]["active"] = False + if not self.ptz_metrics[camera_name].motor_stopped.is_set(): + self.ptz_metrics[camera_name].motor_stopped.set() - if pan_tilt_status == "IDLE" and (zoom_status is None or zoom_status == "IDLE"): - self.cams[camera_name]["active"] = False - if not self.ptz_metrics[camera_name].motor_stopped.is_set(): - self.ptz_metrics[camera_name].motor_stopped.set() + logger.debug( + f"{camera_name}: PTZ stop time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + else: + self.cams[camera_name]["active"] = True + if self.ptz_metrics[camera_name].motor_stopped.is_set(): + self.ptz_metrics[camera_name].motor_stopped.clear() + + logger.debug( + f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + + self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ + camera_name + ].frame_time.value + self.ptz_metrics[camera_name].stop_time.value = 0 + + if ( + self.config.cameras[camera_name].onvif.autotracking.zooming + != ZoomingModeEnum.disabled + ): + # store absolute zoom level as 0 to 1 interpolated from the values of the camera + self.ptz_metrics[camera_name].zoom_level.value = numpy.interp( + round(status.Position.Zoom.x, 2), + [ + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Min"], + self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Max"], + ], + [0, 1], + ) logger.debug( - f"{camera_name}: PTZ stop time: {self.ptz_metrics[camera_name].frame_time.value}" + f"{camera_name}: Camera zoom level: {self.ptz_metrics[camera_name].zoom_level.value}" ) + # some hikvision cams won't update MoveStatus, so warn if it hasn't changed + if ( + not self.ptz_metrics[camera_name].motor_stopped.is_set() + and not self.ptz_metrics[camera_name].reset.is_set() + and self.ptz_metrics[camera_name].start_time.value != 0 + and self.ptz_metrics[camera_name].frame_time.value + > (self.ptz_metrics[camera_name].start_time.value + 10) + and self.ptz_metrics[camera_name].stop_time.value == 0 + ): + logger.debug( + f"Start time: {self.ptz_metrics[camera_name].start_time.value}, Stop time: {self.ptz_metrics[camera_name].stop_time.value}, Frame time: {self.ptz_metrics[camera_name].frame_time.value}" + ) + # set the stop time so we don't come back into this again and spam the logs self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ camera_name ].frame_time.value - else: - self.cams[camera_name]["active"] = True - if self.ptz_metrics[camera_name].motor_stopped.is_set(): - self.ptz_metrics[camera_name].motor_stopped.clear() - - logger.debug( - f"{camera_name}: PTZ start time: {self.ptz_metrics[camera_name].frame_time.value}" + logger.warning( + f"Camera {camera_name} is still in ONVIF 'MOVING' status." ) - self.ptz_metrics[camera_name].start_time.value = self.ptz_metrics[ - camera_name - ].frame_time.value - self.ptz_metrics[camera_name].stop_time.value = 0 - - if ( - self.config.cameras[camera_name].onvif.autotracking.zooming - != ZoomingModeEnum.disabled - ): - # store absolute zoom level as 0 to 1 interpolated from the values of the camera - self.ptz_metrics[camera_name].zoom_level.value = numpy.interp( - round(status.Position.Zoom.x, 2), - [ - self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Min"], - self.cams[camera_name]["absolute_zoom_range"]["XRange"]["Max"], - ], - [0, 1], - ) - logger.debug( - f"{camera_name}: Camera zoom level: {self.ptz_metrics[camera_name].zoom_level.value}" - ) - - # some hikvision cams won't update MoveStatus, so warn if it hasn't changed - if ( - not self.ptz_metrics[camera_name].motor_stopped.is_set() - and not self.ptz_metrics[camera_name].reset.is_set() - and self.ptz_metrics[camera_name].start_time.value != 0 - and self.ptz_metrics[camera_name].frame_time.value - > (self.ptz_metrics[camera_name].start_time.value + 10) - and self.ptz_metrics[camera_name].stop_time.value == 0 - ): - logger.debug( - f"Start time: {self.ptz_metrics[camera_name].start_time.value}, Stop time: {self.ptz_metrics[camera_name].stop_time.value}, Frame time: {self.ptz_metrics[camera_name].frame_time.value}" - ) - # set the stop time so we don't come back into this again and spam the logs - self.ptz_metrics[camera_name].stop_time.value = self.ptz_metrics[ - camera_name - ].frame_time.value - logger.warning(f"Camera {camera_name} is still in ONVIF 'MOVING' status.") - def close(self) -> None: """Gracefully shut down the ONVIF controller.""" if not hasattr(self, "loop") or self.loop.is_closed(): From 198e53bd42fff1086a38fc2d42254a7ee7f5598b Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:23:44 -0500 Subject: [PATCH 30/33] Fix stream stats display (#19874) * Fix stats calculations and labels * fix linter from complaining * fix mse calc * label --- web/src/components/player/JSMpegPlayer.tsx | 2 +- web/src/components/player/LivePlayer.tsx | 2 +- web/src/components/player/MsePlayer.tsx | 4 ++-- web/src/components/player/PlayerStats.tsx | 4 ++-- web/src/components/player/WebRTCPlayer.tsx | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/components/player/JSMpegPlayer.tsx b/web/src/components/player/JSMpegPlayer.tsx index 3753a9e46..f85535013 100644 --- a/web/src/components/player/JSMpegPlayer.tsx +++ b/web/src/components/player/JSMpegPlayer.tsx @@ -164,7 +164,7 @@ export default function JSMpegPlayer({ statsIntervalRef.current = setInterval(() => { const currentTimestamp = Date.now(); const timeDiff = (currentTimestamp - lastTimestampRef.current) / 1000; // in seconds - const bitrate = (bytesReceivedRef.current * 8) / timeDiff / 1000; // in kbps + const bitrate = bytesReceivedRef.current / timeDiff / 1000; // in kBps setStats?.({ streamType: "jsmpeg", diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index 3d8df2b34..d6f751499 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -80,7 +80,7 @@ export default function LivePlayer({ const [stats, setStats] = useState({ streamType: "-", - bandwidth: 0, // in kbps + bandwidth: 0, // in kBps latency: undefined, // in seconds totalFrames: 0, droppedFrames: undefined, diff --git a/web/src/components/player/MsePlayer.tsx b/web/src/components/player/MsePlayer.tsx index f3ef17a24..7c831e596 100644 --- a/web/src/components/player/MsePlayer.tsx +++ b/web/src/components/player/MsePlayer.tsx @@ -338,7 +338,7 @@ function MSEPlayer({ // console.debug("VideoRTC.buffer", b.byteLength, bufLen); } else { try { - sb?.appendBuffer(data); + sb?.appendBuffer(data as ArrayBuffer); } catch (e) { // no-op } @@ -592,7 +592,7 @@ function MSEPlayer({ const now = Date.now(); const bytesLoaded = totalBytesLoaded.current; const timeElapsed = (now - lastTimestamp) / 1000; // seconds - const bandwidth = (bytesLoaded - lastLoadedBytes) / timeElapsed / 1024; // kbps + const bandwidth = (bytesLoaded - lastLoadedBytes) / timeElapsed / 1000; // kBps lastLoadedBytes = bytesLoaded; lastTimestamp = now; diff --git a/web/src/components/player/PlayerStats.tsx b/web/src/components/player/PlayerStats.tsx index baea08b35..6d7e19f5e 100644 --- a/web/src/components/player/PlayerStats.tsx +++ b/web/src/components/player/PlayerStats.tsx @@ -17,7 +17,7 @@ export function PlayerStats({ stats, minimal }: PlayerStatsProps) {

{t("stats.bandwidth.title")}{" "} - {stats.bandwidth.toFixed(2)} kbps + {stats.bandwidth.toFixed(2)} kBps

{stats.latency != undefined && (

@@ -66,7 +66,7 @@ export function PlayerStats({ stats, minimal }: PlayerStatsProps) {

{t("stats.bandwidth.short")}{" "} - {stats.bandwidth.toFixed(2)} kbps + {stats.bandwidth.toFixed(2)} kBps
{stats.latency != undefined && (
diff --git a/web/src/components/player/WebRTCPlayer.tsx b/web/src/components/player/WebRTCPlayer.tsx index b4c9ea6b2..81d6a72dd 100644 --- a/web/src/components/player/WebRTCPlayer.tsx +++ b/web/src/components/player/WebRTCPlayer.tsx @@ -266,7 +266,7 @@ export default function WebRtcPlayer({ const bitrate = timeDiff > 0 ? (bytesReceived - lastBytesReceived) / timeDiff / 1000 - : 0; // in kbps + : 0; // in kBps setStats?.({ streamType: "WebRTC", From 62047c80d569fc33c060f265c701c0fcd2cce819 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 2 Sep 2025 07:17:01 -0500 Subject: [PATCH 31/33] Poll for camera status on tracking end instead of waiting (#19879) --- frigate/ptz/autotrack.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frigate/ptz/autotrack.py b/frigate/ptz/autotrack.py index a75ea13ae..c6d43bbba 100644 --- a/frigate/ptz/autotrack.py +++ b/frigate/ptz/autotrack.py @@ -1462,7 +1462,7 @@ class PtzAutoTracker: if not self.autotracker_init[camera]: self._autotracker_setup(self.config.cameras[camera], camera) # regularly update camera status - while not self.ptz_metrics[camera].motor_stopped.is_set(): + if not self.ptz_metrics[camera].motor_stopped.is_set(): await self.onvif.get_camera_status(camera) # return to preset if tracking is over @@ -1481,7 +1481,8 @@ class PtzAutoTracker: self.tracked_object[camera] = None self.tracked_object_history[camera].clear() - self.ptz_metrics[camera].motor_stopped.wait() + while not self.ptz_metrics[camera].motor_stopped.is_set(): + await self.onvif.get_camera_status(camera) logger.debug( f"{camera}: Time is {self.ptz_metrics[camera].frame_time.value}, returning to preset: {autotracker_config.return_preset}" ) From e664cb228509c82dc7482317ae54dc1e706ea064 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 2 Sep 2025 10:24:25 -0600 Subject: [PATCH 32/33] Set lower bound on retry interval (#19883) --- frigate/config/camera/ffmpeg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frigate/config/camera/ffmpeg.py b/frigate/config/camera/ffmpeg.py index 04bbfac7b..dd65fdcd4 100644 --- a/frigate/config/camera/ffmpeg.py +++ b/frigate/config/camera/ffmpeg.py @@ -61,6 +61,7 @@ class FfmpegConfig(FrigateBaseModel): retry_interval: float = Field( default=10.0, title="Time in seconds to wait before FFmpeg retries connecting to the camera.", + gt=0.0, ) apple_compatibility: bool = Field( default=False, From fe3eb24dfe46e894861c2a2152648c241167fb76 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Tue, 2 Sep 2025 14:21:18 -0600 Subject: [PATCH 33/33] Update Reolink support docs (#19887) --- docs/docs/configuration/camera_specific.md | 16 ++++++++-------- docs/docs/configuration/cameras.md | 5 +---- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md index 644054d7a..98bb02c17 100644 --- a/docs/docs/configuration/camera_specific.md +++ b/docs/docs/configuration/camera_specific.md @@ -144,7 +144,13 @@ WEB Digest Algorithm - MD5 ### Reolink Cameras -Reolink has older cameras (ex: 410 & 520) as well as newer camera (ex: 520a & 511wa) which support different subsets of options. In both cases using the http stream is recommended. +Reolink has many different camera models with inconsistently supported features and behavior. The below table shows a summary of various features and recommendations. + +| Camera Resolution | Camera Generation | Recommended Stream Type | Additional Notes | +| 5MP or lower | All | http-flv | Stream is h264 | +| 6MP or higher | Latest (ex: Duo3, CX-8##) | http-flv with ffmpeg 8.0, or rtsp | This uses the new http-flv-enhanced over H265 which requires ffmpeg 8.0 | +| 6MP or higher | Older (ex: RLC-8##) | rtsp | | + Frigate works much better with newer reolink cameras that are setup with the below options: If available, recommended settings are: @@ -157,12 +163,6 @@ According to [this discussion](https://github.com/blakeblackshear/frigate/issues Cameras connected via a Reolink NVR can be connected with the http stream, use `channel[0..15]` in the stream url for the additional channels. The setup of main stream can be also done via RTSP, but isn't always reliable on all hardware versions. The example configuration is working with the oldest HW version RLN16-410 device with multiple types of cameras. -:::warning - -The below configuration only works for reolink cameras with stream resolution of 5MP or lower, 8MP+ cameras need to use RTSP as http-flv is not supported in this case. - -::: - ```yaml go2rtc: streams: @@ -259,7 +259,7 @@ To use a USB camera (webcam) with Frigate, the recommendation is to use go2rtc's go2rtc: streams: usb_camera: - - "ffmpeg:device?video=0&video_size=1024x576#video=h264" + - "ffmpeg:device?video=0&video_size=1024x576#video=h264" cameras: usb_camera: diff --git a/docs/docs/configuration/cameras.md b/docs/docs/configuration/cameras.md index 41b15d637..d6a8915c3 100644 --- a/docs/docs/configuration/cameras.md +++ b/docs/docs/configuration/cameras.md @@ -107,10 +107,7 @@ This list of working and non-working PTZ cameras is based on user feedback. | Hanwha XNP-6550RH | ✅ | ❌ | | | Hikvision | ✅ | ❌ | Incomplete ONVIF support (MoveStatus won't update even on latest firmware) - reported with HWP-N4215IH-DE and DS-2DE3304W-DE, but likely others | | Hikvision DS-2DE3A404IWG-E/W | ✅ | ✅ | | -| Reolink 511WA | ✅ | ❌ | Zoom only | -| Reolink E1 Pro | ✅ | ❌ | | -| Reolink E1 Zoom | ✅ | ❌ | | -| Reolink RLC-823A 16x | ✅ | ❌ | | +| Reolink | ✅ | ❌ | | | Speco O8P32X | ✅ | ❌ | | | Sunba 405-D20X | ✅ | ❌ | Incomplete ONVIF support reported on original, and 4k models. All models are suspected incompatable. | | Tapo | ✅ | ❌ | Many models supported, ONVIF Service Port: 2020 |