Improve Reolink camera wizard detection (#24570)
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / AMD64 Build (push) Waiting to run
CI / AMD64 Smoke Test (push) Blocked by required conditions
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / Assemble and push default build (push) Blocked by required conditions

* try http-flv first for reolink cameras above 5MP in the camera wizard

The wizard picked RTSP for every Reolink above 5MP and then warned that RTSP isn't recommended, telling users to enable HTTP and restart, which just produced the same result. It now tries the http-flv stream first for those cameras and falls back to RTSP when the stream doesn't probe, since only the newer models serve H.265 over enhanced FLV. The RTSP warning is skipped when the camera's resolution made RTSP a valid choice.

http-flv no longer needs go2rtc's ffmpeg module, so the compatibility mode warning for Reolink HTTP streams is removed and the docs drop the `ffmpeg:` prefix from http-flv sources that have no transcoding modifiers.

* add reolink tests

* keep using ffmpeg module
This commit is contained in:
Josh Hawkins
2026-10-06 15:42:21 -06:00
committed by GitHub
parent 28d8f5bbfa
commit f1c4a9f769
6 changed files with 213 additions and 42 deletions
@@ -222,6 +222,7 @@ export default function Step2ProbeOrSnapshot({
wizardData.username,
wizardData.password,
);
update.reolinkProtocol = protocol;
if (protocol === "http-flv") {
update.brandTemplate = "reolink";
}
@@ -294,73 +295,72 @@ export default function Step2ProbeOrSnapshot({
[probeUri],
);
const generateDynamicStreamUrl = useCallback(
async (data: Partial<WizardFormData>): Promise<string | null> => {
const generateDynamicStreamUrls = useCallback(
async (data: Partial<WizardFormData>): Promise<string[]> => {
const brand = CAMERA_BRANDS.find((b) => b.value === data.brandTemplate);
if (!brand || !data.host) return null;
const host = data.host;
if (!brand || !host) return [];
let protocol = undefined;
if (data.brandTemplate === "reolink" && data.username && data.password) {
try {
protocol = await detectReolinkCamera(
data.host,
host,
data.username,
data.password,
);
} catch {
return null;
return [];
}
onUpdate({ reolinkProtocol: protocol });
}
const protocolKey = protocol || "rtsp";
// Only some Reolink cameras above 5MP serve http-flv, so RTSP is the
// fallback when the http-flv stream does not probe.
const protocolKeys =
protocol === "rtsp" ? ["http-flv", "rtsp"] : [protocol || "rtsp"];
const templates: Record<string, string> = brand.dynamicTemplates || {};
if (Object.keys(templates).includes(protocolKey)) {
const template =
templates[protocolKey as keyof typeof brand.dynamicTemplates];
return template
.replace("{username}", data.username || "")
.replace("{password}", data.password || "")
.replace("{host}", data.host);
}
return null;
return protocolKeys
.filter((key) => key in templates)
.map((key) =>
templates[key]
.replace("{username}", data.username || "")
.replace("{password}", data.password || "")
.replace("{host}", host),
);
},
[],
[onUpdate],
);
const generateStreamUrl = useCallback(
async (data: Partial<WizardFormData>): Promise<string> => {
const generateStreamUrls = useCallback(
async (data: Partial<WizardFormData>): Promise<string[]> => {
if (data.brandTemplate === "other") {
return data.customUrl || "";
return data.customUrl ? [data.customUrl] : [];
}
const brand = CAMERA_BRANDS.find((b) => b.value === data.brandTemplate);
if (!brand || !data.host) return "";
if (!brand || !data.host) return [];
if (brand.template === "dynamic" && "dynamicTemplates" in brand) {
const dynamicUrl = await generateDynamicStreamUrl(data);
if (dynamicUrl) {
return dynamicUrl;
}
return "";
return generateDynamicStreamUrls(data);
}
return brand.template
.replace("{username}", data.username || "")
.replace("{password}", data.password || "")
.replace("{host}", data.host);
return [
brand.template
.replace("{username}", data.username || "")
.replace("{password}", data.password || "")
.replace("{host}", data.host),
];
},
[generateDynamicStreamUrl],
[generateDynamicStreamUrls],
);
const testConnection = useCallback(
async (showToast = true) => {
const streamUrl = await generateStreamUrl(wizardData);
const streamUrls = await generateStreamUrls(wizardData);
if (!streamUrl) {
if (streamUrls.length === 0) {
toast.error(t("cameraWizard.commonErrors.noUrl"));
return;
}
@@ -370,8 +370,18 @@ export default function Step2ProbeOrSnapshot({
setTestResult(null);
try {
setTestStatus(t("cameraWizard.step2.testing.probingMetadata"));
const result = await probeUri(streamUrl, true, setTestStatus);
let streamUrl = streamUrls[0];
let result: TestResult | undefined;
for (const url of streamUrls) {
streamUrl = url;
setTestStatus(t("cameraWizard.step2.testing.probingMetadata"));
result = await probeUri(url, true, setTestStatus);
if (result.success && result.resolution) {
break;
}
}
if (result && result.success) {
setTestResult(result);
@@ -383,6 +393,9 @@ export default function Step2ProbeOrSnapshot({
url: streamUrl,
roles: ["detect"] as StreamRole[],
testResult: result,
useFfmpeg:
wizardData.brandTemplate === "reolink" &&
streamUrl.startsWith("http://"),
},
],
});
@@ -434,7 +447,7 @@ export default function Step2ProbeOrSnapshot({
setTestStatus("");
}
},
[wizardData, generateStreamUrl, t, onUpdate, probeUri],
[wizardData, generateStreamUrls, t, onUpdate, probeUri],
);
const handleContinue = useCallback(() => {
@@ -499,6 +499,7 @@ function StreamIssues({
url: stream.url,
roles: stream.roles,
brand: wizardData.brandTemplate,
reolinkProtocol: wizardData.reolinkProtocol,
useFfmpeg: stream.useFfmpeg,
restream: stream.restream,
testResult: stream.testResult,