Compare commits

...
9 Commits
14 changed files with 1924 additions and 1456 deletions
+1
View File
@@ -22,6 +22,7 @@ autotrack
autotracked autotracked
autotracker autotracker
autotracking autotracking
backchannel
balena balena
Beelink Beelink
BGRA BGRA
+2
View File
@@ -178,6 +178,8 @@ To use the Reolink Doorbell with two way talk, you should use the [recommended R
As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability. As a starting point to check compatibility for your camera, view the list of cameras supported for two-way talk on the [go2rtc repository](https://github.com/AlexxIT/go2rtc?tab=readme-ov-file#two-way-audio). For cameras in the category `ONVIF Profile T`, you can use the [ONVIF Conformant Products Database](https://www.onvif.org/conformant-products/)'s FeatureList to check for the presence of `AudioOutput`. A camera that supports `ONVIF Profile T` _usually_ supports this, but due to inconsistent support, a camera that explicitly lists this feature may still not work. If no entry for your camera exists on the database, it is recommended not to buy it or to consult with the manufacturer's support on the feature availability.
To prevent go2rtc from blocking other applications from accessing your camera's two-way audio, you must configure your stream with `#backchannel=0`. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
### Streaming options on camera group dashboards ### Streaming options on camera group dashboards
Frigate provides a dialog in the Camera Group Edit pane with several options for streaming on a camera group's dashboard. These settings are _per device_ and are saved in your device's local storage. Frigate provides a dialog in the Camera Group Edit pane with several options for streaming on a camera group's dashboard. These settings are _per device_ and are saved in your device's local storage.
+27 -1
View File
@@ -26,9 +26,10 @@ birdseye:
:::tip :::tip
To improve connection speed when using Birdseye via restream you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps` to a low value (e.g. `12`). This makes Frigate periodically push the last frame even when no motion is detected, reducing initial connection latency. To improve connection speed when using Birdseye via restream you can enable a small idle heartbeat by setting `birdseye.idle_heartbeat_fps` to a low value (e.g. `12`). This makes Frigate periodically push the last frame even when no motion is detected, reducing initial connection latency.
::: :::
### Securing Restream With Authentication ### Securing Restream With Authentication
The go2rtc restream can be secured with RTSP based username / password authentication. Ex: The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
@@ -159,6 +160,31 @@ go2rtc:
See [this comment](https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-2242296489) for more information. See [this comment](https://github.com/AlexxIT/go2rtc/issues/1217#issuecomment-2242296489) for more information.
## Preventing go2rtc from blocking two-way audio {#two-way-talk-restream}
For cameras that support two-way talk, go2rtc will automatically establish an audio output backchannel when connecting to an RTSP stream. This backchannel blocks access to the camera's audio output for two-way talk functionality, preventing both Frigate and other applications from using it.
To prevent this, you must configure two separate stream instances:
1. One stream instance with `#backchannel=0` for Frigate's viewing, recording, and detection (prevents go2rtc from establishing the blocking backchannel)
2. A second stream instance without `#backchannel=0` for two-way talk functionality (can be used by Frigate's WebRTC viewer or other applications)
Configuration example:
```yaml
go2rtc:
streams:
front_door:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2#backchannel=0
front_door_twoway:
- rtsp://user:password@10.0.10.10:554/cam/realmonitor?channel=1&subtype=2
```
In this configuration:
- `front_door` stream is used by Frigate for viewing, recording, and detection. The `#backchannel=0` parameter prevents go2rtc from establishing the audio output backchannel, so it won't block two-way talk access.
- `front_door_twoway` stream is used for two-way talk functionality. This stream can be used by Frigate's WebRTC viewer when two-way talk is enabled, or by other applications (like Home Assistant Advanced Camera Card) that need access to the camera's audio output channel.
## Advanced Restream Configurations ## Advanced Restream Configurations
The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below: The [exec](https://github.com/AlexxIT/go2rtc/tree/v1.9.10#source-exec) source in go2rtc can be used for custom ffmpeg commands. An example is below:
+1
View File
@@ -113,6 +113,7 @@ section.
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). 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 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.
3. If your camera supports two-way talk, you must configure your stream with `#backchannel=0` to prevent go2rtc from blocking other applications from accessing the camera's audio output. See [preventing go2rtc from blocking two-way audio](/configuration/restream#two-way-talk-restream) in the restream documentation.
## Homekit Configuration ## Homekit Configuration
+1790 -1409
View File
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -18,14 +18,14 @@
}, },
"dependencies": { "dependencies": {
"@docusaurus/core": "^3.7.0", "@docusaurus/core": "^3.7.0",
"@docusaurus/plugin-content-docs": "^3.6.3", "@docusaurus/plugin-content-docs": "^3.7.0",
"@docusaurus/preset-classic": "^3.7.0", "@docusaurus/preset-classic": "^3.7.0",
"@docusaurus/theme-mermaid": "^3.6.3", "@docusaurus/theme-mermaid": "^3.7.0",
"@inkeep/docusaurus": "^2.0.16", "@inkeep/docusaurus": "^2.0.16",
"@mdx-js/react": "^3.1.0", "@mdx-js/react": "^3.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"docusaurus-plugin-openapi-docs": "^4.3.1", "docusaurus-plugin-openapi-docs": "^4.5.1",
"docusaurus-theme-openapi-docs": "^4.3.1", "docusaurus-theme-openapi-docs": "^4.5.1",
"prism-react-renderer": "^2.4.1", "prism-react-renderer": "^2.4.1",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"react": "^18.3.1", "react": "^18.3.1",
@@ -44,9 +44,9 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "^3.4.0", "@docusaurus/module-type-aliases": "^3.7.0",
"@docusaurus/types": "^3.4.0", "@docusaurus/types": "^3.7.0",
"@types/react": "^18.3.7" "@types/react": "^18.3.27"
}, },
"engines": { "engines": {
"node": ">=18.0" "node": ">=18.0"
+34 -10
View File
@@ -5,10 +5,14 @@ import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import pytz import pytz
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from frigate.api.auth import require_camera_access from frigate.api.auth import (
allow_any_authenticated,
get_allowed_cameras_for_filter,
require_camera_access,
)
from frigate.api.defs.response.preview_response import ( from frigate.api.defs.response.preview_response import (
PreviewFramesResponse, PreviewFramesResponse,
PreviewsResponse, PreviewsResponse,
@@ -26,19 +30,32 @@ router = APIRouter(tags=[Tags.preview])
@router.get( @router.get(
"/preview/{camera_name}/start/{start_ts}/end/{end_ts}", "/preview/{camera_name}/start/{start_ts}/end/{end_ts}",
response_model=PreviewsResponse, response_model=PreviewsResponse,
dependencies=[Depends(require_camera_access)], dependencies=[Depends(allow_any_authenticated())],
summary="Get preview clips for time range", summary="Get preview clips for time range",
description="""Gets all preview clips for a specified camera and time range. description="""Gets all preview clips for a specified camera and time range.
Returns a list of preview video clips that overlap with the requested time period, Returns a list of preview video clips that overlap with the requested time period,
ordered by start time. Use camera_name='all' to get previews from all cameras. ordered by start time. Use camera_name='all' to get previews from all cameras.
Returns an error if no previews are found.""", Returns an error if no previews are found.""",
) )
def preview_ts(camera_name: str, start_ts: float, end_ts: float): def preview_ts(
camera_name: str,
start_ts: float,
end_ts: float,
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get all mp4 previews relevant for time period.""" """Get all mp4 previews relevant for time period."""
if camera_name != "all": if camera_name != "all":
camera_clause = Previews.camera == camera_name if camera_name not in allowed_cameras:
raise HTTPException(status_code=403, detail="Access denied for camera")
camera_list = [camera_name]
else: else:
camera_clause = True camera_list = allowed_cameras
if not camera_list:
return JSONResponse(
content={"success": False, "message": "No previews found."},
status_code=404,
)
previews = ( previews = (
Previews.select( Previews.select(
@@ -53,7 +70,7 @@ def preview_ts(camera_name: str, start_ts: float, end_ts: float):
| Previews.end_time.between(start_ts, end_ts) | Previews.end_time.between(start_ts, end_ts)
| ((start_ts > Previews.start_time) & (end_ts < Previews.end_time)) | ((start_ts > Previews.start_time) & (end_ts < Previews.end_time))
) )
.where(camera_clause) .where(Previews.camera << camera_list)
.order_by(Previews.start_time.asc()) .order_by(Previews.start_time.asc())
.dicts() .dicts()
.iterator() .iterator()
@@ -88,14 +105,21 @@ def preview_ts(camera_name: str, start_ts: float, end_ts: float):
@router.get( @router.get(
"/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}", "/preview/{year_month}/{day}/{hour}/{camera_name}/{tz_name}",
response_model=PreviewsResponse, response_model=PreviewsResponse,
dependencies=[Depends(require_camera_access)], dependencies=[Depends(allow_any_authenticated())],
summary="Get preview clips for specific hour", summary="Get preview clips for specific hour",
description="""Gets all preview clips for a specific hour in a given timezone. description="""Gets all preview clips for a specific hour in a given timezone.
Converts the provided date/time from the specified timezone to UTC and retrieves Converts the provided date/time from the specified timezone to UTC and retrieves
all preview clips for that hour. Use camera_name='all' to get previews from all cameras. all preview clips for that hour. Use camera_name='all' to get previews from all cameras.
The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).""", The tz_name should be a timezone like 'America/New_York' (use commas instead of slashes).""",
) )
def preview_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name: str): def preview_hour(
year_month: str,
day: int,
hour: int,
camera_name: str,
tz_name: str,
allowed_cameras: list[str] = Depends(get_allowed_cameras_for_filter),
):
"""Get all mp4 previews relevant for time period given the timezone""" """Get all mp4 previews relevant for time period given the timezone"""
parts = year_month.split("-") parts = year_month.split("-")
start_date = ( start_date = (
@@ -106,7 +130,7 @@ def preview_hour(year_month: str, day: int, hour: int, camera_name: str, tz_name
start_ts = start_date.timestamp() start_ts = start_date.timestamp()
end_ts = end_date.timestamp() end_ts = end_date.timestamp()
return preview_ts(camera_name, start_ts, end_ts) return preview_ts(camera_name, start_ts, end_ts, allowed_cameras)
@router.get( @router.get(
+6
View File
@@ -239,6 +239,12 @@ class ImprovedMotionDetector(MotionDetector):
) )
self.mask = np.where(resized_mask == [0]) self.mask = np.where(resized_mask == [0])
# Reset motion detection state when mask changes
# so motion detection can quickly recalibrate with the new mask
self.avg_frame = np.zeros(self.motion_frame_size, np.float32)
self.calibrating = True
self.motion_frame_count = 0
def stop(self) -> None: def stop(self) -> None:
"""stop the motion detector.""" """stop the motion detector."""
pass pass
+2
View File
@@ -78,6 +78,8 @@ class TrackedObjectProcessor(threading.Thread):
[ [
CameraConfigUpdateEnum.add, CameraConfigUpdateEnum.add,
CameraConfigUpdateEnum.enabled, CameraConfigUpdateEnum.enabled,
CameraConfigUpdateEnum.motion,
CameraConfigUpdateEnum.objects,
CameraConfigUpdateEnum.remove, CameraConfigUpdateEnum.remove,
CameraConfigUpdateEnum.zones, CameraConfigUpdateEnum.zones,
], ],
+1
View File
@@ -771,6 +771,7 @@ def process_frames(
camera_enabled = camera_config.enabled camera_enabled = camera_config.enabled
if "motion" in updated_configs: if "motion" in updated_configs:
motion_detector.config = camera_config.motion
motion_detector.update_mask() motion_detector.update_mask()
if ( if (
+4 -2
View File
@@ -330,7 +330,8 @@
"audioCodecRequired": "An audio stream is required to support audio detection.", "audioCodecRequired": "An audio stream is required to support audio detection.",
"restreamingWarning": "Reducing connections to the camera for the record stream may increase CPU usage slightly.", "restreamingWarning": "Reducing connections to the camera for the record stream may increase CPU usage slightly.",
"brands": { "brands": {
"reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard." "reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard.",
"reolink-http": "Reolink HTTP streams should use FFmpeg for better compatibility. Enable 'Use stream compatibility mode' for this stream."
}, },
"dahua": { "dahua": {
"substreamWarning": "Substream 1 is locked to a low resolution. Many Dahua / Amcrest / EmpireTech cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available." "substreamWarning": "Substream 1 is locked to a low resolution. Many Dahua / Amcrest / EmpireTech cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available."
@@ -769,7 +770,8 @@
"updatePassword": "Update Password for {{username}}", "updatePassword": "Update Password for {{username}}",
"setPassword": "Set Password", "setPassword": "Set Password",
"desc": "Create a strong password to secure this account.", "desc": "Create a strong password to secure this account.",
"multiDeviceWarning": "Any other devices where you are logged in will be required to re-login within {{refresh_time}}. You can also force all users to re-authenticate immediately by rotating your JWT secret." "multiDeviceWarning": "Any other devices where you are logged in will be required to re-login within {{refresh_time}}.",
"multiDeviceAdmin": "You can also force all users to re-authenticate immediately by rotating your JWT secret."
}, },
"changeRole": { "changeRole": {
"title": "Change User Role", "title": "Change User Role",
@@ -26,6 +26,7 @@ import ActivityIndicator from "../indicators/activity-indicator";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { useIsAdmin } from "@/hooks/use-is-admin";
type SetPasswordProps = { type SetPasswordProps = {
show: boolean; show: boolean;
@@ -46,6 +47,7 @@ export default function SetPasswordDialog({
}: SetPasswordProps) { }: SetPasswordProps) {
const { t } = useTranslation(["views/settings", "common"]); const { t } = useTranslation(["views/settings", "common"]);
const { getLocaleDocUrl } = useDocDomain(); const { getLocaleDocUrl } = useDocDomain();
const isAdmin = useIsAdmin();
const { data: config } = useSWR("config"); const { data: config } = useSWR("config");
const refreshSeconds: number | undefined = const refreshSeconds: number | undefined =
@@ -233,19 +235,28 @@ export default function SetPasswordDialog({
ns: "views/settings", ns: "views/settings",
})} })}
</p> </p>
<p className="text-sm text-primary-variant"> {isAdmin && (
<a <>
href={getLocaleDocUrl( <p className="text-sm text-muted-foreground">
"configuration/authentication#jwt-token-secret", {t("users.dialog.passwordSetting.multiDeviceAdmin", {
)} ns: "views/settings",
target="_blank" })}
rel="noopener noreferrer" </p>
className="inline-flex items-center text-primary" <p className="text-sm text-primary-variant">
> <a
{t("readTheDocumentation", { ns: "common" })} href={getLocaleDocUrl(
<LuExternalLink className="ml-2 size-3" /> "configuration/authentication#jwt-token-secret",
</a> )}
</p> target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center text-primary"
>
{t("readTheDocumentation", { ns: "common" })}
<LuExternalLink className="ml-2 size-3" />
</a>
</p>
</>
)}
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
+14 -10
View File
@@ -26,7 +26,7 @@ import {
toRGBColorString, toRGBColorString,
} from "@/utils/canvasUtil"; } from "@/utils/canvasUtil";
import { Polygon, PolygonType } from "@/types/canvas"; import { Polygon, PolygonType } from "@/types/canvas";
import { useCallback, useContext, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import axios from "axios"; import axios from "axios";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -34,7 +34,6 @@ import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { reviewQueries } from "@/utils/zoneEdutUtil"; import { reviewQueries } from "@/utils/zoneEdutUtil";
import IconWrapper from "../ui/icon-wrapper"; import IconWrapper from "../ui/icon-wrapper";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { buttonVariants } from "../ui/button"; import { buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
@@ -61,7 +60,6 @@ export default function PolygonItem({
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
useSWR<FrigateConfig>("config"); useSWR<FrigateConfig>("config");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const { addMessage } = useContext(StatusBarMessagesContext)!;
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
@@ -171,10 +169,22 @@ export default function PolygonItem({
} }
} }
const updateTopicType =
polygon.type === "zone"
? "zones"
: polygon.type === "motion_mask"
? "motion"
: polygon.type === "object_mask"
? "objects"
: polygon.type;
setIsLoading(true); setIsLoading(true);
await axios await axios
.put(`config/set?${url}`, { requires_restart: 0 }) .put(`config/set?${url}`, {
requires_restart: 0,
update_topic: `config/cameras/${polygon.camera}/${updateTopicType}`,
})
.then((res) => { .then((res) => {
if (res.status === 200) { if (res.status === 200) {
toast.success( toast.success(
@@ -220,12 +230,6 @@ export default function PolygonItem({
const handleDelete = () => { const handleDelete = () => {
setActivePolygonIndex(undefined); setActivePolygonIndex(undefined);
saveToConfig(polygon); saveToConfig(polygon);
addMessage(
"masks_zones",
t("masksAndZones.restart_required"),
undefined,
"masks_zones",
);
}; };
return ( return (
@@ -490,6 +490,13 @@ function StreamIssues({
message: t("cameraWizard.step4.issues.brands.reolink-rtsp"), message: t("cameraWizard.step4.issues.brands.reolink-rtsp"),
}); });
} }
if (streamUrl.startsWith("http://") && !stream.useFfmpeg) {
result.push({
type: "warning",
message: t("cameraWizard.step4.issues.brands.reolink-http"),
});
}
} }
// Video codec check // Video codec check