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 }; }