diff --git a/docs/docs/configuration/camera_specific.md b/docs/docs/configuration/camera_specific.md
index c83942e495..cd5d4ff7f8 100644
--- a/docs/docs/configuration/camera_specific.md
+++ b/docs/docs/configuration/camera_specific.md
@@ -169,10 +169,9 @@ The [Add Camera Wizard](cameras.md#adding-a-camera-with-the-add-camera-wizard) i
1. Click **Add Camera** in .
2. Choose **Manual selection** as the stream detection method and select **Reolink** as the camera brand.
-3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower, or an RTSP stream for higher resolution cameras.
-4. In the validation step, enable **Use stream compatibility mode** for http-flv streams when the wizard recommends it.
+3. The wizard queries the camera and automatically uses an http-flv stream for cameras 5MP and lower. For higher resolution cameras, it tries the http-flv stream first and falls back to RTSP when the camera does not support it.
-If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras where http-flv is recommended, the wizard will show a warning in the validation step.
+If you use the **Probe camera** method instead, the discovered stream URLs will be RTSP. For Reolink cameras 5MP and lower, where http-flv is recommended, the wizard will show a warning in the validation step.
The wizard covers standard single-camera setups. For two way talk, cameras connected through a Reolink NVR, or audio transcoding for WebRTC live view, configure the camera manually as shown below.
@@ -203,20 +202,20 @@ go2rtc:
your_reolink_camera:
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus"
your_reolink_camera_sub:
- - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password"
+ - "http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password"
# example for connecting to a Reolink camera that supports two way talk
your_reolink_camera_twt:
- "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password#video=copy#audio=copy#audio=opus"
- "rtsp://username:password@reolink_ip/Preview_01_sub"
your_reolink_camera_twt_sub:
- - "ffmpeg:http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password"
+ - "http://reolink_ip/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=username&password=password"
- "rtsp://username:password@reolink_ip/Preview_01_sub"
# example for connecting to a Reolink NVR
your_reolink_camera_via_nvr:
- - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_main.bcs&user=username&password=password" # channel numbers are 0-15
+ - "http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_main.bcs&user=username&password=password" # channel numbers are 0-15
- "ffmpeg:your_reolink_camera_via_nvr#audio=aac"
your_reolink_camera_via_nvr_sub:
- - "ffmpeg:http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_ext.bcs&user=username&password=password"
+ - "http://reolink_nvr_ip/flv?port=1935&app=bcs&stream=channel3_ext.bcs&user=username&password=password"
cameras:
your_reolink_camera:
diff --git a/docs/docs/troubleshooting/go2rtc.md b/docs/docs/troubleshooting/go2rtc.md
index 2ee8bd756e..12ddf72d65 100644
--- a/docs/docs/troubleshooting/go2rtc.md
+++ b/docs/docs/troubleshooting/go2rtc.md
@@ -190,14 +190,14 @@ If you rename or remove a go2rtc stream while experimenting and the live stream
Several camera brands have well-known quirks with go2rtc. Rather than repeat them here, see the [camera-specific configuration](/configuration/camera_specific) page, which covers them in detail. The highlights:
-- **Reolink**: RTSP is unreliable on many models; the **http-flv** stream through the FFmpeg module is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras).
+- **Reolink**: RTSP is unreliable on many models; the **http-flv** stream is recommended, and you must enable HTTP/RTMP in the camera and **reboot** it. 6MP+ models stream H.265 over http-flv-enhanced, which requires FFmpeg 8.0. See [Reolink Cameras](/configuration/camera_specific#reolink-cameras).
- **TP-Link Tapo**: use go2rtc's native `tapo://` source for stability and two-way audio; a stale RTSP credential can often be revived by clicking play once in the go2rtc web UI.
- **Ubiquiti/UniFi Protect**: use the `rtspx://` scheme (not `rtsps://…?enableSrtp`).
- **Amcrest/Dahua**: use the `/cam/realmonitor?channel=1&subtype=N` scheme, where `subtype=0` is the main stream. See [Amcrest & Dahua](/configuration/camera_specific#amcrest--dahua).
## Non-RTSP sources and the FFmpeg module
-go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else (MJPEG, HTTP/HTTP-FLV, RTMP, or unusual codecs) must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example.
+go2rtc's native zero-copy handling only supports well-formed RTSP H.264/H.265. Anything else (MJPEG, RTMP, or unusual codecs) must be handed to the FFmpeg module by prefixing the source with `ffmpeg:`. This is also necessary for some camera streams to be parsed at all, at the cost of slightly slower startup. MJPEG and other non-H.264 sources additionally need `#video=h264` (with `#hardware`) before they can be used for the `record`, `detect`, or restream roles. See [MJPEG Cameras](/configuration/camera_specific#mjpeg-cameras) for a complete example.
## Hardware-accelerated transcoding with FFmpeg 8
diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json
index 5bbe7b80d1..6246fe0629 100644
--- a/web/public/locales/en/views/settings.json
+++ b/web/public/locales/en/views/settings.json
@@ -505,8 +505,7 @@
"audioCodecRequired": "An audio stream is required to support audio detection.",
"restreamingWarning": "Reducing connections to the camera for the record stream may increase CPU usage slightly.",
"brands": {
- "reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard.",
- "reolink-http": "Reolink HTTP streams should use FFmpeg for better compatibility. Enable 'Use stream compatibility mode' for this stream."
+ "reolink-rtsp": "Reolink RTSP is not recommended. Enable HTTP in the camera's firmware settings and restart the wizard."
},
"dahua": {
"substreamWarning": "Substream 1 is locked to a low resolution. Many Dahua / Amcrest / EmpireTech cameras support additional substreams that need to be enabled in the camera's settings. It is recommended to check and utilize those streams if available."
diff --git a/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx b/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx
index 8ddbac1860..84818d9d40 100644
--- a/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx
+++ b/web/src/components/settings/wizard/Step2ProbeOrSnapshot.tsx
@@ -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): Promise => {
+ const generateDynamicStreamUrls = useCallback(
+ async (data: Partial): Promise => {
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 = 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): Promise => {
+ const generateStreamUrls = useCallback(
+ async (data: Partial): Promise => {
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);
@@ -434,7 +444,7 @@ export default function Step2ProbeOrSnapshot({
setTestStatus("");
}
},
- [wizardData, generateStreamUrl, t, onUpdate, probeUri],
+ [wizardData, generateStreamUrls, t, onUpdate, probeUri],
);
const handleContinue = useCallback(() => {
diff --git a/web/src/components/settings/wizard/Step4Validation.tsx b/web/src/components/settings/wizard/Step4Validation.tsx
index 0a35e22f6c..4516379d68 100644
--- a/web/src/components/settings/wizard/Step4Validation.tsx
+++ b/web/src/components/settings/wizard/Step4Validation.tsx
@@ -499,7 +499,7 @@ function StreamIssues({
url: stream.url,
roles: stream.roles,
brand: wizardData.brandTemplate,
- useFfmpeg: stream.useFfmpeg,
+ reolinkProtocol: wizardData.reolinkProtocol,
restream: stream.restream,
testResult: stream.testResult,
},
diff --git a/web/src/types/cameraWizard.ts b/web/src/types/cameraWizard.ts
index 06cf6d4a6a..7b481ddc1e 100644
--- a/web/src/types/cameraWizard.ts
+++ b/web/src/types/cameraWizard.ts
@@ -110,6 +110,7 @@ export type WizardFormData = {
username?: string;
password?: string;
brandTemplate?: CameraBrand;
+ reolinkProtocol?: "http-flv" | "rtsp" | null; // null when detection failed
customUrl?: string;
streams?: StreamConfig[];
probeMode?: boolean; // true for probe, false for manual
diff --git a/web/src/utils/streamHealth.ts b/web/src/utils/streamHealth.ts
index 6fe72d0a2b..4dd1fc1d7b 100644
--- a/web/src/utils/streamHealth.ts
+++ b/web/src/utils/streamHealth.ts
@@ -12,7 +12,7 @@ import {
} from "@/utils/streamIssues";
// rules the add camera wizard shows that do not belong on the Health tab
-const WIZARD_ONLY_RULES = new Set(["restream", "reolink-rtsp", "reolink-http"]);
+const WIZARD_ONLY_RULES = new Set(["restream", "reolink-rtsp"]);
/**
* Whether the record output keeps the camera's audio codec. The default
@@ -115,7 +115,6 @@ export function streamHealth(
url,
roles: input.roles as StreamRole[],
brand: inferCameraBrand(url),
- useFfmpeg: restream?.useFfmpeg,
restream: !!restream,
testResult: result,
},
diff --git a/web/src/utils/streamIssues.ts b/web/src/utils/streamIssues.ts
index a5db8716c0..d5b848b008 100644
--- a/web/src/utils/streamIssues.ts
+++ b/web/src/utils/streamIssues.ts
@@ -13,7 +13,7 @@ export type StreamIssueInput = {
url: string;
roles: StreamRole[];
brand?: CameraBrand;
- useFfmpeg?: boolean;
+ reolinkProtocol?: "http-flv" | "rtsp" | null;
restream?: boolean;
testResult?: TestResult;
};
@@ -94,27 +94,18 @@ export function getStreamIssues(
const result: StreamIssue[] = [];
const { roles, testResult } = input;
- if (input.brand === "reolink") {
- const streamUrl = input.url.toLowerCase();
- if (streamUrl.startsWith("rtsp://")) {
- result.push({
- type: "warning",
- rule: "reolink-rtsp",
- message: t("cameraWizard.step4.issues.brands.reolink-rtsp", {
- ns: "views/settings",
- }),
- });
- }
-
- if (streamUrl.startsWith("http://") && !input.useFfmpeg) {
- result.push({
- type: "warning",
- rule: "reolink-http",
- message: t("cameraWizard.step4.issues.brands.reolink-http", {
- ns: "views/settings",
- }),
- });
- }
+ if (
+ input.brand === "reolink" &&
+ input.reolinkProtocol !== "rtsp" &&
+ input.url.toLowerCase().startsWith("rtsp://")
+ ) {
+ result.push({
+ type: "warning",
+ rule: "reolink-rtsp",
+ message: t("cameraWizard.step4.issues.brands.reolink-rtsp", {
+ ns: "views/settings",
+ }),
+ });
}
if (testResult?.videoCodec) {
@@ -268,7 +259,7 @@ export function getStreamIssues(
export function resolveRestreamSource(
path: string,
streams: Record | undefined,
-): { url: string; useFfmpeg: boolean } | undefined {
+): { url: string } | undefined {
const name = parseRestreamStreamName(path);
if (!name || !streams) {
@@ -288,11 +279,8 @@ export function resolveRestreamSource(
}
if (source.startsWith("ffmpeg:")) {
- return {
- url: source.slice("ffmpeg:".length).split("#")[0],
- useFfmpeg: true,
- };
+ return { url: source.slice("ffmpeg:".length).split("#")[0] };
}
- return { url: source, useFfmpeg: false };
+ return { url: source };
}