mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
Camera Wizard tweaks (#20773)
* add switch to use go2rtc ffmpeg mode * i18n * move testing state outside of button
This commit is contained in:
parent
31fa87ce73
commit
59963fc47e
@ -271,6 +271,8 @@
|
|||||||
"disconnectStream": "Disconnect",
|
"disconnectStream": "Disconnect",
|
||||||
"estimatedBandwidth": "Estimated Bandwidth",
|
"estimatedBandwidth": "Estimated Bandwidth",
|
||||||
"roles": "Roles",
|
"roles": "Roles",
|
||||||
|
"ffmpegModule": "Use stream compatibility mode",
|
||||||
|
"ffmpegModuleDescription": "If the stream does not load after several attempts, try enabling this. When enabled, Frigate will use the ffmpeg module with go2rtc. This may provide better compatibility with some camera streams.",
|
||||||
"none": "None",
|
"none": "None",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"streamValidated": "Stream {{number}} validated successfully",
|
"streamValidated": "Stream {{number}} validated successfully",
|
||||||
|
|||||||
@ -174,9 +174,7 @@ export default function CameraWizardDialog({
|
|||||||
...(friendlyName && { friendly_name: friendlyName }),
|
...(friendlyName && { friendly_name: friendlyName }),
|
||||||
ffmpeg: {
|
ffmpeg: {
|
||||||
inputs: wizardData.streams.map((stream, index) => {
|
inputs: wizardData.streams.map((stream, index) => {
|
||||||
const isRestreamed =
|
if (stream.restream) {
|
||||||
wizardData.restreamIds?.includes(stream.id) ?? false;
|
|
||||||
if (isRestreamed) {
|
|
||||||
const go2rtcStreamName =
|
const go2rtcStreamName =
|
||||||
wizardData.streams!.length === 1
|
wizardData.streams!.length === 1
|
||||||
? finalCameraName
|
? finalCameraName
|
||||||
@ -234,7 +232,11 @@ export default function CameraWizardDialog({
|
|||||||
wizardData.streams!.length === 1
|
wizardData.streams!.length === 1
|
||||||
? finalCameraName
|
? finalCameraName
|
||||||
: `${finalCameraName}_${index + 1}`;
|
: `${finalCameraName}_${index + 1}`;
|
||||||
go2rtcStreams[streamName] = [stream.url];
|
|
||||||
|
const streamUrl = stream.useFfmpeg
|
||||||
|
? `ffmpeg:${stream.url}`
|
||||||
|
: stream.url;
|
||||||
|
go2rtcStreams[streamName] = [streamUrl];
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Object.keys(go2rtcStreams).length > 0) {
|
if (Object.keys(go2rtcStreams).length > 0) {
|
||||||
|
|||||||
@ -608,6 +608,12 @@ export default function Step1NameCamera({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{isTesting && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<ActivityIndicator className="size-4" />
|
||||||
|
{testStatus}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
<div className="flex flex-col gap-3 pt-3 sm:flex-row sm:justify-end sm:gap-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@ -635,10 +641,7 @@ export default function Step1NameCamera({
|
|||||||
variant="select"
|
variant="select"
|
||||||
className="flex items-center justify-center gap-2 sm:flex-1"
|
className="flex items-center justify-center gap-2 sm:flex-1"
|
||||||
>
|
>
|
||||||
{isTesting && <ActivityIndicator className="size-4" />}
|
{t("cameraWizard.step1.testConnection")}
|
||||||
{isTesting && testStatus
|
|
||||||
? testStatus
|
|
||||||
: t("cameraWizard.step1.testConnection")}
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -201,16 +201,12 @@ export default function Step2StreamConfig({
|
|||||||
|
|
||||||
const setRestream = useCallback(
|
const setRestream = useCallback(
|
||||||
(streamId: string) => {
|
(streamId: string) => {
|
||||||
const currentIds = wizardData.restreamIds || [];
|
const stream = streams.find((s) => s.id === streamId);
|
||||||
const isSelected = currentIds.includes(streamId);
|
if (!stream) return;
|
||||||
const newIds = isSelected
|
|
||||||
? currentIds.filter((id) => id !== streamId)
|
updateStream(streamId, { restream: !stream.restream });
|
||||||
: [...currentIds, streamId];
|
|
||||||
onUpdate({
|
|
||||||
restreamIds: newIds,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[wizardData.restreamIds, onUpdate],
|
[streams, updateStream],
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasDetectRole = streams.some((s) => s.roles.includes("detect"));
|
const hasDetectRole = streams.some((s) => s.roles.includes("detect"));
|
||||||
@ -435,9 +431,7 @@ export default function Step2StreamConfig({
|
|||||||
{t("cameraWizard.step2.go2rtc")}
|
{t("cameraWizard.step2.go2rtc")}
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
checked={(wizardData.restreamIds || []).includes(
|
checked={stream.restream || false}
|
||||||
stream.id,
|
|
||||||
)}
|
|
||||||
onCheckedChange={() => setRestream(stream.id)}
|
onCheckedChange={() => setRestream(stream.id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { LuRotateCcw } from "react-icons/lu";
|
import { LuRotateCcw, LuInfo } from "react-icons/lu";
|
||||||
import { useState, useCallback, useMemo, useEffect } from "react";
|
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
@ -216,7 +222,6 @@ export default function Step3Validation({
|
|||||||
brandTemplate: wizardData.brandTemplate,
|
brandTemplate: wizardData.brandTemplate,
|
||||||
customUrl: wizardData.customUrl,
|
customUrl: wizardData.customUrl,
|
||||||
streams: wizardData.streams,
|
streams: wizardData.streams,
|
||||||
restreamIds: wizardData.restreamIds,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onSave(configData);
|
onSave(configData);
|
||||||
@ -322,6 +327,51 @@ export default function Step3Validation({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{result?.success && (
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">
|
||||||
|
{t("cameraWizard.step3.ffmpegModule")}
|
||||||
|
</span>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-4 w-4 p-0"
|
||||||
|
>
|
||||||
|
<LuInfo className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="pointer-events-auto w-80 text-xs">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium">
|
||||||
|
{t("cameraWizard.step3.ffmpegModule")}
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"cameraWizard.step3.ffmpegModuleDescription",
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={stream.useFfmpeg || false}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
onUpdate({
|
||||||
|
streams: streams.map((s) =>
|
||||||
|
s.id === stream.id
|
||||||
|
? { ...s, useFfmpeg: checked }
|
||||||
|
: s,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mb-2 flex flex-col justify-between gap-1 md:flex-row md:items-center">
|
<div className="mb-2 flex flex-col justify-between gap-1 md:flex-row md:items-center">
|
||||||
<span className="break-all text-sm text-muted-foreground">
|
<span className="break-all text-sm text-muted-foreground">
|
||||||
{stream.url}
|
{stream.url}
|
||||||
@ -491,8 +541,7 @@ function StreamIssues({
|
|||||||
|
|
||||||
// Restreaming check
|
// Restreaming check
|
||||||
if (stream.roles.includes("record")) {
|
if (stream.roles.includes("record")) {
|
||||||
const restreamIds = wizardData.restreamIds || [];
|
if (stream.restream) {
|
||||||
if (restreamIds.includes(stream.id)) {
|
|
||||||
result.push({
|
result.push({
|
||||||
type: "warning",
|
type: "warning",
|
||||||
message: t("cameraWizard.step3.issues.restreamingWarning"),
|
message: t("cameraWizard.step3.issues.restreamingWarning"),
|
||||||
@ -660,9 +709,10 @@ function StreamPreview({ stream, onBandwidthUpdate }: StreamPreviewProps) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Register stream with go2rtc
|
// Register stream with go2rtc
|
||||||
|
const streamUrl = stream.useFfmpeg ? `ffmpeg:${stream.url}` : stream.url;
|
||||||
axios
|
axios
|
||||||
.put(`go2rtc/streams/${streamId}`, null, {
|
.put(`go2rtc/streams/${streamId}`, null, {
|
||||||
params: { src: stream.url },
|
params: { src: streamUrl },
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Add small delay to allow go2rtc api to run and initialize the stream
|
// Add small delay to allow go2rtc api to run and initialize the stream
|
||||||
@ -680,7 +730,7 @@ function StreamPreview({ stream, onBandwidthUpdate }: StreamPreviewProps) {
|
|||||||
// do nothing on cleanup errors - go2rtc won't consume the streams
|
// do nothing on cleanup errors - go2rtc won't consume the streams
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}, [stream.url, streamId]);
|
}, [stream.url, stream.useFfmpeg, streamId]);
|
||||||
|
|
||||||
const resolution = stream.testResult?.resolution;
|
const resolution = stream.testResult?.resolution;
|
||||||
let aspectRatio = "16/9";
|
let aspectRatio = "16/9";
|
||||||
|
|||||||
@ -85,6 +85,8 @@ export type StreamConfig = {
|
|||||||
quality?: string;
|
quality?: string;
|
||||||
testResult?: TestResult;
|
testResult?: TestResult;
|
||||||
userTested?: boolean;
|
userTested?: boolean;
|
||||||
|
useFfmpeg?: boolean;
|
||||||
|
restream?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TestResult = {
|
export type TestResult = {
|
||||||
@ -105,7 +107,6 @@ export type WizardFormData = {
|
|||||||
brandTemplate?: CameraBrand;
|
brandTemplate?: CameraBrand;
|
||||||
customUrl?: string;
|
customUrl?: string;
|
||||||
streams?: StreamConfig[];
|
streams?: StreamConfig[];
|
||||||
restreamIds?: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// API Response Types
|
// API Response Types
|
||||||
@ -146,6 +147,7 @@ export type CameraConfigData = {
|
|||||||
inputs: {
|
inputs: {
|
||||||
path: string;
|
path: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
|
input_args?: string;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
live?: {
|
live?: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user