Camera Wizard tweaks (#20773)

* add switch to use go2rtc ffmpeg mode

* i18n

* move testing state outside of button
This commit is contained in:
Josh Hawkins 2025-11-03 09:42:38 -06:00 committed by GitHub
parent 31fa87ce73
commit 59963fc47e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 80 additions and 27 deletions

View File

@ -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",

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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";

View File

@ -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?: {