mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-06 23:02:49 +03:00
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.
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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);
|
||||
@@ -434,7 +444,7 @@ export default function Step2ProbeOrSnapshot({
|
||||
setTestStatus("");
|
||||
}
|
||||
},
|
||||
[wizardData, generateStreamUrl, t, onUpdate, probeUri],
|
||||
[wizardData, generateStreamUrls, t, onUpdate, probeUri],
|
||||
);
|
||||
|
||||
const handleContinue = useCallback(() => {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<string, string | string[]> | 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user