mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-30 23:59:02 +03:00
Add dynamic Reolink stream configuration for stream URL (#20469)
* Migrate camera APIs to separate tag * Implement reolink detection to handle dynamic URL assignment * Cleanup codec handling * Use average framerate not relative framerate * Add reolink rtsp warning * Don't return exception * Use avg_frame_rate in final info * Clenaup * Validate host * Fix overlap
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { LuInfo } from "react-icons/lu";
|
||||
import { detectReolinkCamera } from "@/utils/cameraUtil";
|
||||
|
||||
type Step1NameCameraProps = {
|
||||
wizardData: Partial<WizardFormData>;
|
||||
@@ -134,8 +135,44 @@ export default function Step1NameCamera({
|
||||
? !!(watchedCustomUrl && watchedCustomUrl.trim())
|
||||
: !!(watchedHost && watchedHost.trim());
|
||||
|
||||
const generateDynamicStreamUrl = useCallback(
|
||||
async (data: z.infer<typeof step1FormData>): Promise<string | null> => {
|
||||
const brand = CAMERA_BRANDS.find((b) => b.value === data.brandTemplate);
|
||||
if (!brand || !data.host) return null;
|
||||
|
||||
let protocol = undefined;
|
||||
if (data.brandTemplate === "reolink" && data.username && data.password) {
|
||||
try {
|
||||
protocol = await detectReolinkCamera(
|
||||
data.host,
|
||||
data.username,
|
||||
data.password,
|
||||
);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Use detected protocol or fallback to rtsp
|
||||
const protocolKey = 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;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const generateStreamUrl = useCallback(
|
||||
(data: z.infer<typeof step1FormData>): string => {
|
||||
async (data: z.infer<typeof step1FormData>): Promise<string> => {
|
||||
if (data.brandTemplate === "other") {
|
||||
return data.customUrl || "";
|
||||
}
|
||||
@@ -143,17 +180,27 @@ export default function Step1NameCamera({
|
||||
const brand = CAMERA_BRANDS.find((b) => b.value === data.brandTemplate);
|
||||
if (!brand || !data.host) return "";
|
||||
|
||||
if (brand.template === "dynamic" && "dynamicTemplates" in brand) {
|
||||
const dynamicUrl = await generateDynamicStreamUrl(data);
|
||||
|
||||
if (dynamicUrl) {
|
||||
return dynamicUrl;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
return brand.template
|
||||
.replace("{username}", data.username || "")
|
||||
.replace("{password}", data.password || "")
|
||||
.replace("{host}", data.host);
|
||||
},
|
||||
[],
|
||||
[generateDynamicStreamUrl],
|
||||
);
|
||||
|
||||
const testConnection = useCallback(async () => {
|
||||
const data = form.getValues();
|
||||
const streamUrl = generateStreamUrl(data);
|
||||
const streamUrl = await generateStreamUrl(data);
|
||||
|
||||
if (!streamUrl) {
|
||||
toast.error(t("cameraWizard.commonErrors.noUrl"));
|
||||
@@ -208,14 +255,16 @@ export default function Step1NameCamera({
|
||||
(s: FfprobeStream) =>
|
||||
s.codec_type === "video" ||
|
||||
s.codec_name?.includes("h264") ||
|
||||
s.codec_name?.includes("h265"),
|
||||
s.codec_name?.includes("hevc"),
|
||||
);
|
||||
|
||||
const audioStream = streams.find(
|
||||
(s: FfprobeStream) =>
|
||||
s.codec_type === "audio" ||
|
||||
s.codec_name?.includes("aac") ||
|
||||
s.codec_name?.includes("mp3"),
|
||||
s.codec_name?.includes("mp3") ||
|
||||
s.codec_name?.includes("pcm_mulaw") ||
|
||||
s.codec_name?.includes("pcm_alaw"),
|
||||
);
|
||||
|
||||
const resolution = videoStream
|
||||
@@ -223,9 +272,9 @@ export default function Step1NameCamera({
|
||||
: undefined;
|
||||
|
||||
// Extract FPS from rational (e.g., "15/1" -> 15)
|
||||
const fps = videoStream?.r_frame_rate
|
||||
? parseFloat(videoStream.r_frame_rate.split("/")[0]) /
|
||||
parseFloat(videoStream.r_frame_rate.split("/")[1])
|
||||
const fps = videoStream?.avg_frame_rate
|
||||
? parseFloat(videoStream.avg_frame_rate.split("/")[0]) /
|
||||
parseFloat(videoStream.avg_frame_rate.split("/")[1])
|
||||
: undefined;
|
||||
|
||||
// Convert snapshot blob to base64 if available
|
||||
@@ -283,9 +332,9 @@ export default function Step1NameCamera({
|
||||
onUpdate(data);
|
||||
};
|
||||
|
||||
const handleContinue = useCallback(() => {
|
||||
const handleContinue = useCallback(async () => {
|
||||
const data = form.getValues();
|
||||
const streamUrl = generateStreamUrl(data);
|
||||
const streamUrl = await generateStreamUrl(data);
|
||||
const streamId = `stream_${Date.now()}`;
|
||||
|
||||
const streamConfig: StreamConfig = {
|
||||
@@ -381,7 +430,7 @@ export default function Step1NameCamera({
|
||||
<h4 className="font-medium">
|
||||
{selectedBrand.label}
|
||||
</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="break-all text-sm text-muted-foreground">
|
||||
{t("cameraWizard.step1.brandUrlFormat", {
|
||||
exampleUrl: selectedBrand.exampleUrl,
|
||||
})}
|
||||
|
||||
@@ -431,6 +431,16 @@ function StreamIssues({
|
||||
message: string;
|
||||
}> = [];
|
||||
|
||||
if (wizardData.brandTemplate === "reolink") {
|
||||
const streamUrl = stream.url.toLowerCase();
|
||||
if (streamUrl.startsWith("rtsp://")) {
|
||||
result.push({
|
||||
type: "warning",
|
||||
message: t("cameraWizard.step1.errors.brands.reolink-rtsp"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Video codec check
|
||||
if (stream.testResult?.videoCodec) {
|
||||
const videoCodec = stream.testResult.videoCodec.toLowerCase();
|
||||
|
||||
@@ -7,6 +7,7 @@ export const CAMERA_BRANDS = [
|
||||
"rtsp://{username}:{password}@{host}:554/cam/realmonitor?channel=1&subtype=0",
|
||||
exampleUrl:
|
||||
"rtsp://admin:password@192.168.1.100:554/cam/realmonitor?channel=1&subtype=0",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
{
|
||||
value: "hikvision" as const,
|
||||
@@ -14,42 +15,54 @@ export const CAMERA_BRANDS = [
|
||||
template: "rtsp://{username}:{password}@{host}:554/Streaming/Channels/101",
|
||||
exampleUrl:
|
||||
"rtsp://admin:password@192.168.1.100:554/Streaming/Channels/101",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
{
|
||||
value: "ubiquiti" as const,
|
||||
label: "Ubiquiti",
|
||||
template: "rtsp://{username}:{password}@{host}:554/live/ch0",
|
||||
exampleUrl: "rtsp://ubnt:password@192.168.1.100:554/live/ch0",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
{
|
||||
value: "reolink" as const,
|
||||
label: "Reolink",
|
||||
template: "rtsp://{username}:{password}@{host}:554/h264Preview_01_main",
|
||||
exampleUrl: "rtsp://admin:password@192.168.1.100:554/h264Preview_01_main",
|
||||
template: "dynamic",
|
||||
dynamicTemplates: {
|
||||
"http-flv":
|
||||
"http://{host}/flv?port=1935&app=bcs&stream=channel0_main.bcs&user={username}&password={password}",
|
||||
rtsp: "rtsp://{username}:{password}@{host}:554/Preview_01_main",
|
||||
},
|
||||
exampleUrl:
|
||||
"http://192.168.1.100/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password or rtsp://admin:password@192.168.1.100:554/Preview_01_main",
|
||||
},
|
||||
{
|
||||
value: "axis" as const,
|
||||
label: "Axis",
|
||||
template: "rtsp://{username}:{password}@{host}:554/axis-media/media.amp",
|
||||
exampleUrl: "rtsp://root:password@192.168.1.100:554/axis-media/media.amp",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
{
|
||||
value: "tplink" as const,
|
||||
label: "TP-Link",
|
||||
template: "rtsp://{username}:{password}@{host}:554/stream1",
|
||||
exampleUrl: "rtsp://admin:password@192.168.1.100:554/stream1",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
{
|
||||
value: "foscam" as const,
|
||||
label: "Foscam",
|
||||
template: "rtsp://{username}:{password}@{host}:88/videoMain",
|
||||
exampleUrl: "rtsp://admin:password@192.168.1.100:88/videoMain",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
{
|
||||
value: "other" as const,
|
||||
label: "Other",
|
||||
template: "",
|
||||
exampleUrl: "rtsp://username:password@host:port/path",
|
||||
dynamicTemplates: undefined,
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ==================== Camera Name Processing ====================
|
||||
|
||||
/**
|
||||
* Generates a fixed-length hash from a camera name for use as a valid camera identifier.
|
||||
* Works safely with Unicode input while outputting Latin-only identifiers.
|
||||
@@ -58,3 +60,49 @@ export function processCameraName(userInput: string): {
|
||||
friendlyName: userInput,
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== Reolink Camera Detection ====================
|
||||
|
||||
/**
|
||||
* Detect Reolink camera capabilities and recommend optimal protocol
|
||||
*
|
||||
* Calls the Frigate backend API which queries the Reolink camera to determine
|
||||
* its resolution and recommends either http-flv (for 5MP and below) or rtsp
|
||||
* (for higher resolutions).
|
||||
*
|
||||
* @param host - Camera IP address or hostname
|
||||
* @param username - Camera username
|
||||
* @param password - Camera password
|
||||
* @returns The recommended protocol key ("http-flv" or "rtsp"), or null if detection failed
|
||||
*/
|
||||
export async function detectReolinkCamera(
|
||||
host: string,
|
||||
username: string,
|
||||
password: string,
|
||||
): Promise<"http-flv" | "rtsp" | null> {
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
host,
|
||||
username,
|
||||
password,
|
||||
});
|
||||
|
||||
const response = await fetch(`/api/reolink/detect?${params.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.protocol) {
|
||||
return data.protocol;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user