Camera wizard improvements (#20636)

* use avg_frame_rate

* probe metadata and snapshot separately

* improve ffprobe error reporting

* show error messages in toaster
This commit is contained in:
Josh Hawkins
2025-10-23 08:34:52 -05:00
committed by GitHub
parent 0d5cfa2e38
commit 81df534784
7 changed files with 80 additions and 43 deletions

View File

@@ -65,6 +65,7 @@ export default function Step1NameCamera({
const { data: config } = useSWR<FrigateConfig>("config");
const [showPassword, setShowPassword] = useState(false);
const [isTesting, setIsTesting] = useState(false);
const [testStatus, setTestStatus] = useState<string>("");
const [testResult, setTestResult] = useState<TestResult | null>(null);
const existingCameraNames = useMemo(() => {
@@ -88,7 +89,13 @@ export default function Step1NameCamera({
username: z.string().optional(),
password: z.string().optional(),
brandTemplate: z.enum(CAMERA_BRAND_VALUES).optional(),
customUrl: z.string().optional(),
customUrl: z
.string()
.optional()
.refine(
(val) => !val || val.startsWith("rtsp://"),
t("cameraWizard.step1.errors.customUrlRtspRequired"),
),
})
.refine(
(data) => {
@@ -204,24 +211,17 @@ export default function Step1NameCamera({
}
setIsTesting(true);
setTestStatus("");
setTestResult(null);
// First get probe data for metadata
const probePromise = axios.get("ffprobe", {
params: { paths: streamUrl, detailed: true },
timeout: 10000,
});
// Then get snapshot for preview
const snapshotPromise = axios.get("ffprobe/snapshot", {
params: { url: streamUrl },
responseType: "blob",
timeout: 10000,
});
try {
// First get probe data for metadata
const probeResponse = await probePromise;
setTestStatus(t("cameraWizard.step1.testing.probingMetadata"));
const probeResponse = await axios.get("ffprobe", {
params: { paths: streamUrl, detailed: true },
timeout: 10000,
});
let probeData = null;
if (
probeResponse.data &&
@@ -234,8 +234,13 @@ export default function Step1NameCamera({
// Then get snapshot for preview (only if probe succeeded)
let snapshotBlob = null;
if (probeData) {
setTestStatus(t("cameraWizard.step1.testing.fetchingSnapshot"));
try {
const snapshotResponse = await snapshotPromise;
const snapshotResponse = await axios.get("ffprobe/snapshot", {
params: { url: streamUrl },
responseType: "blob",
timeout: 10000,
});
snapshotBlob = snapshotResponse.data;
} catch (snapshotError) {
// Snapshot is optional, don't fail if it doesn't work
@@ -295,12 +300,18 @@ export default function Step1NameCamera({
setTestResult(testResult);
toast.success(t("cameraWizard.step1.testSuccess"));
} else {
const error = probeData?.stderr || "Unknown error";
const error =
Array.isArray(probeResponse.data?.[0]?.stderr) &&
probeResponse.data[0].stderr.length > 0
? probeResponse.data[0].stderr.join("\n")
: "Unable to probe stream";
setTestResult({
success: false,
error: error,
});
toast.error(t("cameraWizard.commonErrors.testFailed", { error }));
toast.error(t("cameraWizard.commonErrors.testFailed", { error }), {
duration: 6000,
});
}
} catch (error) {
const axiosError = error as {
@@ -318,9 +329,13 @@ export default function Step1NameCamera({
});
toast.error(
t("cameraWizard.commonErrors.testFailed", { error: errorMessage }),
{
duration: 10000,
},
);
} finally {
setIsTesting(false);
setTestStatus("");
}
}, [form, generateStreamUrl, t]);
@@ -610,7 +625,9 @@ export default function Step1NameCamera({
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>
)}
</div>

View File

@@ -151,9 +151,9 @@ export default function Step2StreamConfig({
? `${videoStream.width}x${videoStream.height}`
: undefined;
const fps = videoStream?.r_frame_rate
? parseFloat(videoStream.r_frame_rate.split("/")[0]) /
parseFloat(videoStream.r_frame_rate.split("/")[1])
const fps = videoStream?.avg_frame_rate
? parseFloat(videoStream.avg_frame_rate.split("/")[0]) /
parseFloat(videoStream.avg_frame_rate.split("/")[1])
: undefined;
const testResult: TestResult = {

View File

@@ -85,9 +85,9 @@ export default function Step3Validation({
? `${videoStream.width}x${videoStream.height}`
: undefined;
const fps = videoStream?.r_frame_rate
? parseFloat(videoStream.r_frame_rate.split("/")[0]) /
parseFloat(videoStream.r_frame_rate.split("/")[1])
const fps = videoStream?.avg_frame_rate
? parseFloat(videoStream.avg_frame_rate.split("/")[0]) /
parseFloat(videoStream.avg_frame_rate.split("/")[1])
: undefined;
return {
@@ -323,7 +323,7 @@ export default function Step3Validation({
)}
<div className="mb-2 flex flex-col justify-between gap-1 md:flex-row md:items-center">
<span className="text-sm text-muted-foreground">
<span className="break-all text-sm text-muted-foreground">
{stream.url}
</span>
<Button