Compare commits

..

No commits in common. "c5fec3271f99493ee9afe5841e9fdd1c19dd498c" and "f6f555387e71e9806e23a57c52067add52143bfa" have entirely different histories.

11 changed files with 33 additions and 74 deletions

View File

@ -240,8 +240,6 @@ birdseye:
scaling_factor: 2.0
# Optional: Maximum number of cameras to show at one time, showing the most recent (default: show all cameras)
max_cameras: 1
# Optional: Frames-per-second to re-send the last composed Birdseye frame when idle (no motion or active updates). (default: shown below)
idle_heartbeat_fps: 0.0
# Optional: ffmpeg configuration
# More information about presets at https://docs.frigate.video/configuration/ffmpeg_presets

View File

@ -24,11 +24,6 @@ birdseye:
restream: True
```
:::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.
:::
### Securing Restream With Authentication
The go2rtc restream can be secured with RTSP based username / password authentication. Ex:
@ -169,4 +164,4 @@ NOTE: The output will need to be passed with two curly braces `{{output}}`
go2rtc:
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {{output}}
```
```

View File

@ -55,12 +55,6 @@ class BirdseyeConfig(FrigateBaseModel):
layout: BirdseyeLayoutConfig = Field(
default_factory=BirdseyeLayoutConfig, title="Birdseye Layout Config"
)
idle_heartbeat_fps: float = Field(
default=0.0,
ge=0.0,
le=10.0,
title="Idle heartbeat FPS (0 disables, max 10)",
)
# uses BaseModel because some global attributes are not available at the camera level

View File

@ -9,7 +9,6 @@ import os
import queue
import subprocess as sp
import threading
import time
import traceback
from typing import Any, Optional
@ -792,10 +791,6 @@ class Birdseye:
self.frame_manager = SharedMemoryFrameManager()
self.stop_event = stop_event
self.requestor = InterProcessRequestor()
self.idle_fps: float = self.config.birdseye.idle_heartbeat_fps
self._idle_interval: Optional[float] = (
(1.0 / self.idle_fps) if self.idle_fps > 0 else None
)
if config.birdseye.restream:
self.birdseye_buffer = self.frame_manager.create(
@ -853,15 +848,6 @@ class Birdseye:
if frame_layout_changed:
coordinates = self.birdseye_manager.get_camera_coordinates()
self.requestor.send_data(UPDATE_BIRDSEYE_LAYOUT, coordinates)
if self._idle_interval:
now = time.monotonic()
is_idle = len(self.birdseye_manager.camera_layout) == 0
if (
is_idle
and (now - self.birdseye_manager.last_output_time)
>= self._idle_interval
):
self.__send_new_frame()
def stop(self) -> None:
self.converter.join()

View File

@ -175,8 +175,8 @@
"exitEdit": "Exit Editing"
},
"noCameras": {
"title": "No Cameras Configured",
"description": "Get started by connecting a camera to Frigate.",
"title": "No Cameras Set Up",
"description": "Get started by connecting a camera.",
"buttonText": "Add Camera"
}
}

View File

@ -385,8 +385,7 @@
"mustNotBeSameWithCamera": "Zone name must not be the same as camera name.",
"alreadyExists": "A zone with this name already exists for this camera.",
"mustNotContainPeriod": "Zone name must not contain periods.",
"hasIllegalCharacter": "Zone name contains illegal characters.",
"mustHaveAtLeastOneLetter": "Zone name must have at least one letter."
"hasIllegalCharacter": "Zone name contains illegal characters."
}
},
"distance": {
@ -444,7 +443,7 @@
"name": {
"title": "Name",
"inputPlaceHolder": "Enter a name…",
"tips": "Name must be at least 2 characters, must have at least one letter, and must not be the name of a camera or another zone."
"tips": "Name must be at least 2 characters and must not be the name of a camera or another zone."
},
"inertia": {
"title": "Inertia",

View File

@ -1,30 +1,27 @@
import React from "react";
import { Button } from "../ui/button";
import Heading from "../ui/heading";
import { Link } from "react-router-dom";
type EmptyCardProps = {
icon: React.ReactNode;
title: string;
description: string;
buttonText?: string;
link?: string;
};
export function EmptyCard({
icon,
title,
description,
buttonText,
link,
}: EmptyCardProps) {
return (
<div className="flex flex-col items-center gap-2">
{icon}
<Heading as="h4">{title}</Heading>
<div className="mb-3 text-secondary-foreground">{description}</div>
<div className="text-secondary-foreground">{description}</div>
{buttonText?.length && (
<Button size="sm" variant="select">
<Link to={link ?? "#"}>{buttonText}</Link>
{buttonText}
</Button>
)}
</div>

View File

@ -150,9 +150,7 @@ export default function SearchThumbnail({
.filter(
(item) => item !== undefined && !item.includes("-verified"),
)
.map((text) =>
getTranslatedLabel(text, searchResult.data.type),
)
.map((text) => getTranslatedLabel(text, searchResult.data.type))
.sort()
.join(", ")
.replaceAll("-verified", "")}

View File

@ -152,38 +152,36 @@ export default function CameraEditForm({
}))
: defaultValues.ffmpeg.inputs;
// Load go2rtc streams for this camera
const go2rtcStreams = config.go2rtc?.streams || {};
const cameraStreams: Record<string, string[]> = {};
// get candidate stream names for this camera. could be the camera's own name,
// any restream names referenced by this camera, or any keys under live --> streams
const validNames = new Set<string>();
validNames.add(cameraName);
// deduce go2rtc stream names from rtsp restream inputs
camera.ffmpeg?.inputs?.forEach((input) => {
// exclude any query strings or trailing slashes from the stream name
const restreamMatch = input.path.match(
/^rtsp:\/\/127\.0\.0\.1:8554\/([^?#/]+)(?:[?#].*)?$/,
);
if (restreamMatch) {
const streamName = restreamMatch[1];
validNames.add(streamName);
// Find streams that match this camera's name pattern
Object.entries(go2rtcStreams).forEach(([streamName, urls]) => {
if (streamName.startsWith(cameraName) || streamName === cameraName) {
cameraStreams[streamName] = Array.isArray(urls) ? urls : [urls];
}
});
// Include live --> streams keys
const liveStreams = camera?.live?.streams;
if (liveStreams) {
Object.keys(liveStreams).forEach((key) => {
validNames.add(key);
});
}
// Map only go2rtc entries that match the collected names
Object.entries(go2rtcStreams).forEach(([name, urls]) => {
if (validNames.has(name)) {
cameraStreams[name] = Array.isArray(urls) ? urls : [urls];
// Also deduce go2rtc streams from restream URLs in camera inputs
camera.ffmpeg?.inputs?.forEach((input, index) => {
const restreamMatch = input.path.match(
/^rtsp:\/\/127\.0\.0\.1:8554\/(.+)$/,
);
if (restreamMatch) {
const streamName = restreamMatch[1];
// Find the corresponding go2rtc stream
const go2rtcStream = Object.entries(go2rtcStreams).find(
([name]) =>
name === streamName ||
name === `${cameraName}_${index + 1}` ||
name === cameraName,
);
if (go2rtcStream) {
cameraStreams[go2rtcStream[0]] = Array.isArray(go2rtcStream[1])
? go2rtcStream[1]
: [go2rtcStream[1]];
}
}
});

View File

@ -149,11 +149,6 @@ export default function ZoneEditPane({
)
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter"),
})
.refine((value: string) => /[a-zA-Z]/.test(value), {
message: t(
"masksAndZones.form.zoneName.error.mustHaveAtLeastOneLetter",
),
}),
inertia: z.coerce
.number()

View File

@ -354,7 +354,7 @@ export default function LiveDashboardView({
onSaveMuting(true);
};
if (cameras.length == 0 && !includeBirdseye) {
if (cameras.length == 0) {
return <NoCameraView />;
}
@ -625,7 +625,6 @@ function NoCameraView() {
title={t("noCameras.title")}
description={t("noCameras.description")}
buttonText={t("noCameras.buttonText")}
link="/settings?page=cameraManagement"
/>
</div>
);