mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 21:44:13 +03:00
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:
parent
0d5cfa2e38
commit
81df534784
@ -199,19 +199,30 @@ def ffprobe(request: Request, paths: str = "", detailed: bool = False):
|
|||||||
request.app.frigate_config.ffmpeg, path.strip(), detailed=detailed
|
request.app.frigate_config.ffmpeg, path.strip(), detailed=detailed
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {
|
if ffprobe.returncode != 0:
|
||||||
"return_code": ffprobe.returncode,
|
try:
|
||||||
"stderr": (
|
stderr_decoded = ffprobe.stderr.decode("utf-8")
|
||||||
ffprobe.stderr.decode("unicode_escape").strip()
|
except UnicodeDecodeError:
|
||||||
if ffprobe.returncode != 0
|
try:
|
||||||
else ""
|
stderr_decoded = ffprobe.stderr.decode("unicode_escape")
|
||||||
),
|
except Exception:
|
||||||
"stdout": (
|
stderr_decoded = str(ffprobe.stderr)
|
||||||
json.loads(ffprobe.stdout.decode("unicode_escape").strip())
|
|
||||||
if ffprobe.returncode == 0
|
stderr_lines = [
|
||||||
else ""
|
line.strip() for line in stderr_decoded.split("\n") if line.strip()
|
||||||
),
|
]
|
||||||
}
|
|
||||||
|
result = {
|
||||||
|
"return_code": ffprobe.returncode,
|
||||||
|
"stderr": stderr_lines,
|
||||||
|
"stdout": "",
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
result = {
|
||||||
|
"return_code": ffprobe.returncode,
|
||||||
|
"stderr": [],
|
||||||
|
"stdout": json.loads(ffprobe.stdout.decode("unicode_escape").strip()),
|
||||||
|
}
|
||||||
|
|
||||||
# Add detailed metadata if requested and probe was successful
|
# Add detailed metadata if requested and probe was successful
|
||||||
if detailed and ffprobe.returncode == 0 and result["stdout"]:
|
if detailed and ffprobe.returncode == 0 and result["stdout"]:
|
||||||
|
|||||||
@ -577,7 +577,7 @@ def ffprobe_stream(ffmpeg, path: str, detailed: bool = False) -> sp.CompletedPro
|
|||||||
if detailed and format_entries:
|
if detailed and format_entries:
|
||||||
ffprobe_cmd.extend(["-show_entries", f"format={format_entries}"])
|
ffprobe_cmd.extend(["-show_entries", f"format={format_entries}"])
|
||||||
|
|
||||||
ffprobe_cmd.extend(["-loglevel", "quiet", clean_path])
|
ffprobe_cmd.extend(["-loglevel", "error", clean_path])
|
||||||
|
|
||||||
return sp.run(ffprobe_cmd, capture_output=True)
|
return sp.run(ffprobe_cmd, capture_output=True)
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,6 @@
|
|||||||
"ui": "UI",
|
"ui": "UI",
|
||||||
"enrichments": "Enrichments",
|
"enrichments": "Enrichments",
|
||||||
"cameraManagement": "Management",
|
"cameraManagement": "Management",
|
||||||
"cameraReview": "Review",
|
|
||||||
"masksAndZones": "Masks / Zones",
|
"masksAndZones": "Masks / Zones",
|
||||||
"motionTuner": "Motion Tuner",
|
"motionTuner": "Motion Tuner",
|
||||||
"triggers": "Triggers",
|
"triggers": "Triggers",
|
||||||
@ -188,6 +187,10 @@
|
|||||||
"testSuccess": "Connection test successful!",
|
"testSuccess": "Connection test successful!",
|
||||||
"testFailed": "Connection test failed. Please check your input and try again.",
|
"testFailed": "Connection test failed. Please check your input and try again.",
|
||||||
"streamDetails": "Stream Details",
|
"streamDetails": "Stream Details",
|
||||||
|
"testing": {
|
||||||
|
"probingMetadata": "Probing camera metadata...",
|
||||||
|
"fetchingSnapshot": "Fetching camera snapshot..."
|
||||||
|
},
|
||||||
"warnings": {
|
"warnings": {
|
||||||
"noSnapshot": "Unable to fetch a snapshot from the configured stream."
|
"noSnapshot": "Unable to fetch a snapshot from the configured stream."
|
||||||
},
|
},
|
||||||
@ -197,8 +200,9 @@
|
|||||||
"nameLength": "Camera name must be 64 characters or less",
|
"nameLength": "Camera name must be 64 characters or less",
|
||||||
"invalidCharacters": "Camera name contains invalid characters",
|
"invalidCharacters": "Camera name contains invalid characters",
|
||||||
"nameExists": "Camera name already exists",
|
"nameExists": "Camera name already exists",
|
||||||
|
"customUrlRtspRequired": "Custom URLs must begin with \"rtsp://\". Manual configuration is required for non-RTSP camera streams.",
|
||||||
"brands": {
|
"brands": {
|
||||||
"reolink-rtsp": "Reolink RTSP is not recommended. It is recommended to enable http in the camera settings and restart the camera wizard."
|
"reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export default function Step1NameCamera({
|
|||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [isTesting, setIsTesting] = useState(false);
|
const [isTesting, setIsTesting] = useState(false);
|
||||||
|
const [testStatus, setTestStatus] = useState<string>("");
|
||||||
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
const [testResult, setTestResult] = useState<TestResult | null>(null);
|
||||||
|
|
||||||
const existingCameraNames = useMemo(() => {
|
const existingCameraNames = useMemo(() => {
|
||||||
@ -88,7 +89,13 @@ export default function Step1NameCamera({
|
|||||||
username: z.string().optional(),
|
username: z.string().optional(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
brandTemplate: z.enum(CAMERA_BRAND_VALUES).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(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
@ -204,24 +211,17 @@ export default function Step1NameCamera({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setIsTesting(true);
|
setIsTesting(true);
|
||||||
|
setTestStatus("");
|
||||||
setTestResult(null);
|
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 {
|
try {
|
||||||
// First get probe data for metadata
|
// 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;
|
let probeData = null;
|
||||||
if (
|
if (
|
||||||
probeResponse.data &&
|
probeResponse.data &&
|
||||||
@ -234,8 +234,13 @@ export default function Step1NameCamera({
|
|||||||
// Then get snapshot for preview (only if probe succeeded)
|
// Then get snapshot for preview (only if probe succeeded)
|
||||||
let snapshotBlob = null;
|
let snapshotBlob = null;
|
||||||
if (probeData) {
|
if (probeData) {
|
||||||
|
setTestStatus(t("cameraWizard.step1.testing.fetchingSnapshot"));
|
||||||
try {
|
try {
|
||||||
const snapshotResponse = await snapshotPromise;
|
const snapshotResponse = await axios.get("ffprobe/snapshot", {
|
||||||
|
params: { url: streamUrl },
|
||||||
|
responseType: "blob",
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
snapshotBlob = snapshotResponse.data;
|
snapshotBlob = snapshotResponse.data;
|
||||||
} catch (snapshotError) {
|
} catch (snapshotError) {
|
||||||
// Snapshot is optional, don't fail if it doesn't work
|
// Snapshot is optional, don't fail if it doesn't work
|
||||||
@ -295,12 +300,18 @@ export default function Step1NameCamera({
|
|||||||
setTestResult(testResult);
|
setTestResult(testResult);
|
||||||
toast.success(t("cameraWizard.step1.testSuccess"));
|
toast.success(t("cameraWizard.step1.testSuccess"));
|
||||||
} else {
|
} 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({
|
setTestResult({
|
||||||
success: false,
|
success: false,
|
||||||
error: error,
|
error: error,
|
||||||
});
|
});
|
||||||
toast.error(t("cameraWizard.commonErrors.testFailed", { error }));
|
toast.error(t("cameraWizard.commonErrors.testFailed", { error }), {
|
||||||
|
duration: 6000,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const axiosError = error as {
|
const axiosError = error as {
|
||||||
@ -318,9 +329,13 @@ export default function Step1NameCamera({
|
|||||||
});
|
});
|
||||||
toast.error(
|
toast.error(
|
||||||
t("cameraWizard.commonErrors.testFailed", { error: errorMessage }),
|
t("cameraWizard.commonErrors.testFailed", { error: errorMessage }),
|
||||||
|
{
|
||||||
|
duration: 10000,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setIsTesting(false);
|
setIsTesting(false);
|
||||||
|
setTestStatus("");
|
||||||
}
|
}
|
||||||
}, [form, generateStreamUrl, t]);
|
}, [form, generateStreamUrl, t]);
|
||||||
|
|
||||||
@ -610,7 +625,9 @@ export default function Step1NameCamera({
|
|||||||
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" />}
|
{isTesting && <ActivityIndicator className="size-4" />}
|
||||||
{t("cameraWizard.step1.testConnection")}
|
{isTesting && testStatus
|
||||||
|
? testStatus
|
||||||
|
: t("cameraWizard.step1.testConnection")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -151,9 +151,9 @@ export default function Step2StreamConfig({
|
|||||||
? `${videoStream.width}x${videoStream.height}`
|
? `${videoStream.width}x${videoStream.height}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const fps = videoStream?.r_frame_rate
|
const fps = videoStream?.avg_frame_rate
|
||||||
? parseFloat(videoStream.r_frame_rate.split("/")[0]) /
|
? parseFloat(videoStream.avg_frame_rate.split("/")[0]) /
|
||||||
parseFloat(videoStream.r_frame_rate.split("/")[1])
|
parseFloat(videoStream.avg_frame_rate.split("/")[1])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const testResult: TestResult = {
|
const testResult: TestResult = {
|
||||||
|
|||||||
@ -85,9 +85,9 @@ export default function Step3Validation({
|
|||||||
? `${videoStream.width}x${videoStream.height}`
|
? `${videoStream.width}x${videoStream.height}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const fps = videoStream?.r_frame_rate
|
const fps = videoStream?.avg_frame_rate
|
||||||
? parseFloat(videoStream.r_frame_rate.split("/")[0]) /
|
? parseFloat(videoStream.avg_frame_rate.split("/")[0]) /
|
||||||
parseFloat(videoStream.r_frame_rate.split("/")[1])
|
parseFloat(videoStream.avg_frame_rate.split("/")[1])
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
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">
|
<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}
|
{stream.url}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -66,8 +66,13 @@ export default function CameraManagementView({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Toaster
|
||||||
|
richColors
|
||||||
|
className="z-[1000]"
|
||||||
|
position="top-center"
|
||||||
|
closeButton
|
||||||
|
/>
|
||||||
<div className="flex size-full flex-col md:flex-row">
|
<div className="flex size-full flex-col md:flex-row">
|
||||||
<Toaster position="top-center" closeButton={true} />
|
|
||||||
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
<div className="scrollbar-container order-last mb-10 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none">
|
||||||
{viewMode === "settings" ? (
|
{viewMode === "settings" ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user