import { baseUrl } from "@/api/baseUrl"; import { generateFixedHash, isValidId } from "./stringUtil"; import type { LiveStreamMetadata } from "@/types/live"; /** * Processes a user-entered camera name and returns both the final camera name * and friendly name for Frigate configuration. * * @param userInput - The name entered by the user (could be display name) * @returns Object with finalCameraName and friendlyName */ export function processCameraName(userInput: string): { finalCameraName: string; friendlyName?: string; } { const normalizedInput = userInput.replace(/\s+/g, "_").toLowerCase(); if (isValidId(normalizedInput)) { return { finalCameraName: normalizedInput, friendlyName: userInput.includes(" ") ? userInput : undefined, }; } return { finalCameraName: generateFixedHash(userInput, "cam"), friendlyName: userInput, }; } /** * 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( `${baseUrl}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; } } /** * Mask credentials in RTSP URIs for display */ export function maskUri(uri: string): string { try { // Handle RTSP URLs with user:pass@host format const rtspMatch = uri.match(/rtsp:\/\/([^:]+):([^@]+)@(.+)/); if (rtspMatch) { return `rtsp://${rtspMatch[1]}:${"*".repeat(4)}@${rtspMatch[3]}`; } // Handle HTTP/HTTPS URLs with password query parameter const urlObj = new URL(uri); if (urlObj.searchParams.has("password")) { urlObj.searchParams.set("password", "*".repeat(4)); return urlObj.toString(); } } catch (e) { // ignore } return uri; } /** * Represents the audio features supported by a camera stream */ export type CameraAudioFeatures = { twoWayAudio: boolean; audioOutput: boolean; }; /** * Detects camera audio features from go2rtc stream metadata. * Checks for two-way audio (backchannel) and audio output capabilities. * * @param metadata - The LiveStreamMetadata from go2rtc stream * @param requireSecureContext - If true, two-way audio requires secure context (default: true) * @returns CameraAudioFeatures object with detected capabilities */ export function detectCameraAudioFeatures( metadata: LiveStreamMetadata | null | undefined, requireSecureContext: boolean = true, ): CameraAudioFeatures { if (!metadata) { return { twoWayAudio: false, audioOutput: false, }; } const twoWayAudio = (!requireSecureContext || window.isSecureContext) && metadata.producers.find( (prod) => prod.medias && prod.medias.find((media) => media.includes("audio, sendonly")) != undefined, ) != undefined; const audioOutput = metadata.producers.find( (prod) => prod.medias && prod.medias.find((media) => media.includes("audio, recvonly")) != undefined, ) != undefined; return { twoWayAudio: !!twoWayAudio, audioOutput: !!audioOutput, }; }