Files
Strix/webui/web/js/config-generators/frigate/index.js
T
eduard256 627409cf56 Add Frigate config merging and camera database updates
- Refactor Frigate generator to support adding cameras to existing configs
- Add text-based YAML parsing to preserve formatting and comments
- Implement duplicate camera/stream name detection and auto-numbering
- Add support for inserting cameras into existing go2rtc and cameras sections
- Update UI: add textarea for existing config input and generate button
- Preserve user's existing configuration when adding new cameras
- Add example config template for new users
- Update ConfigPanel to initialize Frigate tab instead of auto-generating
- Add FrigateGenerator import to main.js
- Add custom styles for Frigate config input and output sections
- Support both empty config (create from scratch) and existing config (merge) modes

Camera database updates:
- Add OpenIPC firmware camera support (257 models)
- Add Yi-Hack firmware variants (v4, v5, Allwinner, MStar)
- Add Fang-Hacks firmware support
- Add OpenMiko firmware support
- Update Sonoff camera models
- Update Thingino firmware camera models
2025-11-11 22:32:59 +03:00

412 lines
14 KiB
JavaScript

/**
* Frigate NVR Configuration Generator
* Adds cameras to existing Frigate configuration
* Based on frigate-conf-generate logic
*/
export class FrigateGenerator {
/**
* Main entry point - generates config (new or adds to existing)
* @param {string} existingConfig - Existing YAML config text (or empty string)
* @param {Object} mainStream - Main stream object
* @param {Object} subStream - Optional sub stream object
* @returns {string} YAML configuration string
*/
static generate(existingConfig, mainStream, subStream = null) {
if (!existingConfig || existingConfig.trim() === '') {
// Create new config from scratch
return this.createNewConfig(mainStream, subStream);
}
// Add to existing config
return this.addToExistingConfig(existingConfig, mainStream, subStream);
}
/**
* Add camera to existing config (text-based, preserves everything)
*/
static addToExistingConfig(existingConfig, mainStream, subStream) {
const lines = existingConfig.split('\n');
// Find existing camera names and stream names to avoid duplicates
const existingCameras = this.findExistingCameras(lines);
const existingStreams = this.findExistingStreams(lines);
// Generate unique camera info
const cameraInfo = this.generateUniqueCameraInfo(mainStream, subStream, existingCameras, existingStreams);
// Find insertion points
const go2rtcStreamIndex = this.findGo2rtcStreamsInsertionPoint(lines);
const camerasInsertIndex = this.findCamerasInsertionPoint(lines);
if (go2rtcStreamIndex === -1 || camerasInsertIndex === -1) {
throw new Error('Could not find go2rtc streams or cameras section in config');
}
// Generate new stream lines
const streamLines = this.generateStreamLines(cameraInfo);
// Generate new camera lines
const cameraLines = this.generateCameraLines(cameraInfo);
// Insert streams into go2rtc section
lines.splice(go2rtcStreamIndex, 0, ...streamLines);
// Insert camera into cameras section (adjust index after first insertion)
const adjustedCameraIndex = camerasInsertIndex + streamLines.length;
lines.splice(adjustedCameraIndex, 0, ...cameraLines);
return lines.join('\n');
}
/**
* Find existing camera names
*/
static findExistingCameras(lines) {
const cameras = new Set();
let inCamerasSection = false;
for (const line of lines) {
if (line.match(/^cameras:/)) {
inCamerasSection = true;
continue;
}
if (inCamerasSection && line.match(/^[a-z]/)) {
break; // Next top-level section
}
if (inCamerasSection && line.match(/^\s{2}(\w+):/)) {
const match = line.match(/^\s{2}(\w+):/);
cameras.add(match[1]);
}
}
return cameras;
}
/**
* Find existing stream names
*/
static findExistingStreams(lines) {
const streams = new Set();
let inStreamsSection = false;
for (const line of lines) {
if (line.match(/^\s{2}streams:/)) {
inStreamsSection = true;
continue;
}
if (inStreamsSection && line.match(/^[a-z]/)) {
break; // Next top-level section
}
if (inStreamsSection && line.match(/^\s{4}'?(\w+)'?:/)) {
const match = line.match(/^\s{4}'?(\w+)'?:/);
streams.add(match[1]);
}
}
return streams;
}
/**
* Find where to insert new streams in go2rtc section
*/
static findGo2rtcStreamsInsertionPoint(lines) {
let inStreamsSection = false;
let lastStreamIndex = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.match(/^\s{2}streams:/)) {
inStreamsSection = true;
continue;
}
if (inStreamsSection) {
// Check if this is a stream definition or its content
if (line.match(/^\s{4,}/)) {
lastStreamIndex = i;
} else if (line.match(/^[a-z#]/)) {
// Found next section - insert before empty line if it exists
if (lastStreamIndex >= 0 && lines[lastStreamIndex + 1]?.trim() === '') {
return lastStreamIndex + 2; // After existing empty line
}
return lastStreamIndex + 1;
}
}
}
return lastStreamIndex + 1;
}
/**
* Find where to insert new camera in cameras section
*/
static findCamerasInsertionPoint(lines) {
let inCamerasSection = false;
let lastCameraLineIndex = -1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.match(/^cameras:/)) {
inCamerasSection = true;
continue;
}
if (inCamerasSection) {
// Check if we're still in a camera definition
if (line.match(/^\s{2}\w+:/)) {
// New camera starting
lastCameraLineIndex = i;
} else if (line.match(/^\s{2,}\S/)) {
// Still inside camera definition
lastCameraLineIndex = i;
} else if (line.match(/^[a-z]/) && !line.match(/^cameras:/)) {
// Found next top-level section
// Skip any empty lines before it
let insertIndex = lastCameraLineIndex + 1;
while (insertIndex < lines.length && lines[insertIndex].trim() === '') {
insertIndex++;
}
return insertIndex;
} else if (line.match(/^version:/)) {
// Insert before version, skip empty lines
let insertIndex = i;
while (insertIndex > 0 && lines[insertIndex - 1].trim() === '') {
insertIndex--;
}
return insertIndex;
}
}
}
// If we reach end of file, insert at end
return lines.length;
}
/**
* Generate unique camera info avoiding duplicates
*/
static generateUniqueCameraInfo(mainStream, subStream, existingCameras, existingStreams) {
const ip = this.extractIP(mainStream.url);
const baseName = ip ? `camera_${ip.replace(/\./g, '_').replace(/:/g, '_')}` : 'camera';
const streamBaseName = ip ? ip.replace(/\./g, '_').replace(/:/g, '_') : 'stream';
// Find unique camera name
let cameraName = baseName;
let suffix = 0;
while (existingCameras.has(cameraName)) {
suffix++;
cameraName = `${baseName}_${suffix}`;
}
// Find unique stream names
let mainStreamName = `${streamBaseName}_main${suffix ? `_${suffix}` : ''}`;
while (existingStreams.has(mainStreamName)) {
suffix++;
mainStreamName = `${streamBaseName}_main_${suffix}`;
}
let subStreamName = null;
if (subStream) {
subStreamName = `${streamBaseName}_sub${suffix ? `_${suffix}` : ''}`;
while (existingStreams.has(subStreamName)) {
suffix++;
subStreamName = `${streamBaseName}_sub_${suffix}`;
}
}
return {
cameraName,
mainStreamName,
subStreamName,
mainStream,
subStream
};
}
/**
* Generate stream lines for go2rtc section
*/
static generateStreamLines(cameraInfo) {
const lines = [];
// Add main stream
const mainSource = this.generateGo2RTCSource(cameraInfo.mainStream);
lines.push(` '${cameraInfo.mainStreamName}':`);
lines.push(` - ${mainSource}`);
// Add sub stream if provided
if (cameraInfo.subStream) {
lines.push('');
const subSource = this.generateGo2RTCSource(cameraInfo.subStream);
lines.push(` '${cameraInfo.subStreamName}':`);
lines.push(` - ${subSource}`);
}
lines.push('');
return lines;
}
/**
* Generate camera lines for cameras section
*/
static generateCameraLines(cameraInfo) {
const lines = [];
lines.push(` ${cameraInfo.cameraName}:`);
lines.push(' ffmpeg:');
lines.push(' inputs:');
if (cameraInfo.subStream) {
// Use sub for detect, main for record
lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.subStreamName}`);
lines.push(' input_args: preset-rtsp-restream');
lines.push(' roles:');
lines.push(' - detect');
lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.mainStreamName}`);
lines.push(' input_args: preset-rtsp-restream');
lines.push(' roles:');
lines.push(' - record');
// Add live view configuration
lines.push(' live:');
lines.push(' streams:');
lines.push(` Main Stream: ${cameraInfo.mainStreamName} # HD для просмотра`);
lines.push(` Sub Stream: ${cameraInfo.subStreamName} # Низкое разрешение (опционально)`);
} else {
// Use main for both detect and record
lines.push(` - path: rtsp://127.0.0.1:8554/${cameraInfo.mainStreamName}`);
lines.push(' input_args: preset-rtsp-restream');
lines.push(' roles:');
lines.push(' - detect');
lines.push(' - record');
}
// Add objects configuration
lines.push(' objects:');
lines.push(' track:');
lines.push(' - person');
lines.push(' - car');
lines.push(' - cat');
lines.push(' - dog');
// Add record configuration
lines.push(' record:');
lines.push(' enabled: true');
lines.push('');
return lines;
}
/**
* Create new configuration from scratch
*/
static createNewConfig(mainStream, subStream) {
const cameraInfo = this.generateUniqueCameraInfo(mainStream, subStream, new Set(), new Set());
const lines = [];
// MQTT
lines.push('mqtt:');
lines.push(' enabled: false');
lines.push('');
// Record
lines.push('# Global Recording Settings');
lines.push('record:');
lines.push(' enabled: true');
lines.push(' retain:');
lines.push(' days: 7');
lines.push(' mode: motion # Record only on motion detection');
lines.push('');
// Go2RTC
lines.push('# Go2RTC Configuration (Frigate built-in)');
lines.push('go2rtc:');
lines.push(' streams:');
lines.push(...this.generateStreamLines(cameraInfo));
// Cameras
lines.push('# Frigate Camera Configuration');
lines.push('cameras:');
lines.push(...this.generateCameraLines(cameraInfo));
// Version
lines.push('version: 0.16-0');
return lines.join('\n');
}
/**
* Extract IP address from URL
*/
static extractIP(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (e) {
// Try to extract IP with regex
const match = url.match(/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/);
return match ? match[1] : null;
}
}
/**
* Generate Go2RTC source configuration based on stream type
*/
static generateGo2RTCSource(stream) {
// Handle JPEG snapshots with exec:ffmpeg conversion
if (stream.type === 'JPEG') {
return [
'exec:/usr/lib/ffmpeg/7.0/bin/ffmpeg',
'-loglevel quiet',
'-f image2',
'-loop 1',
'-framerate 10',
`-i ${stream.url}`,
'-c:v libx264',
'-preset ultrafast',
'-tune zerolatency',
'-g 20',
'-f rtsp {{output}}'
].join(' ');
}
// Handle ONVIF - convert to onvif:// format if needed
if (stream.type === 'ONVIF') {
try {
const urlObj = new URL(stream.url);
const username = urlObj.username || 'admin';
const password = urlObj.password || '';
const host = urlObj.hostname;
const port = urlObj.port || '80';
return `onvif://${username}:${password}@${host}:${port}`;
} catch (e) {
return stream.url;
}
}
// Handle BUBBLE protocol - convert to bubble:// format for go2rtc
if (stream.type === 'BUBBLE') {
try {
const urlObj = new URL(stream.url);
const username = urlObj.username || 'admin';
const password = urlObj.password || '';
const host = urlObj.hostname;
const port = urlObj.port || '80';
const path = urlObj.pathname + urlObj.search;
return `bubble://${username}:${password}@${host}:${port}${path}#video=copy`;
} catch (e) {
return stream.url;
}
}
// For all other types: use direct URL
return stream.url;
}
}