Add dual-stream support for Frigate with optional sub-stream selection
Features: - Optional sub-stream selection from already discovered streams - No additional scanning required - reuse existing results - UI: "Add Sub Stream" button to select secondary stream - UI: "Remove Sub Stream" button to clear selection - Smart stream routing in Frigate configs - Go2RTC: generates _main and _sub stream names - Frigate: detect on sub (CPU efficient), record on main (quality) - Frigate: auto-detection of stream resolution - Object detection: person, car, cat, dog - Motion-based recording by default - Live view streams configuration - Support for any resolution: HD, 4K, 8K+ - Comprehensive documentation with examples
This commit is contained in:
@@ -1,50 +1,80 @@
|
||||
/**
|
||||
* Go2RTC Configuration Generator
|
||||
* Generates proper go2rtc YAML configs based on stream type
|
||||
* Following go2rtc documentation and best practices
|
||||
*/
|
||||
export class Go2RTCGenerator {
|
||||
static generate(stream) {
|
||||
const streamName = this.generateStreamName(stream);
|
||||
/**
|
||||
* Generate go2rtc config for streams (main + optional sub)
|
||||
* @param {Object} mainStream - Main stream object with type, protocol, and url
|
||||
* @param {Object} subStream - Optional sub stream object
|
||||
* @returns {string} YAML configuration string
|
||||
*/
|
||||
static generate(mainStream, subStream = null) {
|
||||
const configs = [];
|
||||
configs.push('streams:');
|
||||
|
||||
switch (stream.type) {
|
||||
case 'FFMPEG':
|
||||
if (stream.protocol === 'rtsp') {
|
||||
return this.generateRTSP(streamName, stream);
|
||||
}
|
||||
break;
|
||||
case 'JPEG':
|
||||
return this.generateJPEG(streamName, stream);
|
||||
case 'MJPEG':
|
||||
return this.generateMJPEG(streamName, stream);
|
||||
case 'HTTP_VIDEO':
|
||||
return this.generateHTTPVideo(streamName, stream);
|
||||
case 'HLS':
|
||||
return this.generateHLS(streamName, stream);
|
||||
case 'ONVIF':
|
||||
return `# ONVIF Device Service\n# This is a device management endpoint, not a stream\n# URL: ${stream.url}`;
|
||||
default:
|
||||
return this.generateRTSP(streamName, stream);
|
||||
// Generate main stream config
|
||||
const mainStreamName = this.generateStreamName(mainStream, 'main');
|
||||
const mainSource = this.generateSource(mainStream);
|
||||
configs.push(` '${mainStreamName}':`);
|
||||
configs.push(` - ${mainSource}`);
|
||||
|
||||
// Generate sub stream config if provided
|
||||
if (subStream) {
|
||||
configs.push('');
|
||||
const subStreamName = this.generateStreamName(subStream, 'sub');
|
||||
const subSource = this.generateSource(subStream);
|
||||
configs.push(` '${subStreamName}':`);
|
||||
configs.push(` - ${subSource}`);
|
||||
}
|
||||
|
||||
return configs.join('\n');
|
||||
}
|
||||
|
||||
static generateStreamName(stream) {
|
||||
/**
|
||||
* Generate stream name from IP address with suffix
|
||||
* Format: "192_168_1_100_main" or "192_168_1_100_sub"
|
||||
*/
|
||||
static generateStreamName(stream, suffix) {
|
||||
try {
|
||||
const urlObj = new URL(stream.url);
|
||||
const ip = urlObj.hostname.replace(/\./g, '_').replace(/:/g, '_');
|
||||
return `${ip}_0`;
|
||||
return `${ip}_${suffix}`;
|
||||
} catch (e) {
|
||||
return 'camera_stream_0';
|
||||
return `camera_stream_${suffix}`;
|
||||
}
|
||||
}
|
||||
|
||||
static generateRTSP(streamName, stream) {
|
||||
return `streams:\n '${streamName}':\n - ${stream.url}`;
|
||||
/**
|
||||
* Generate source configuration based on stream type
|
||||
*/
|
||||
static generateSource(stream) {
|
||||
// Handle JPEG snapshots with special exec:ffmpeg conversion
|
||||
if (stream.type === 'JPEG') {
|
||||
return this.generateJPEGSource(stream);
|
||||
}
|
||||
|
||||
// Handle ONVIF
|
||||
if (stream.type === 'ONVIF') {
|
||||
return this.generateONVIFSource(stream);
|
||||
}
|
||||
|
||||
// For all other types: use direct URL
|
||||
return stream.url;
|
||||
}
|
||||
|
||||
static generateJPEG(streamName, stream) {
|
||||
const framerate = 10;
|
||||
const ffmpegCmd = [
|
||||
/**
|
||||
* Generate JPEG snapshot conversion using exec:ffmpeg
|
||||
* Converts static JPEG to RTSP stream with H264 encoding
|
||||
*/
|
||||
static generateJPEGSource(stream) {
|
||||
return [
|
||||
'exec:ffmpeg',
|
||||
'-loglevel quiet',
|
||||
'-f image2',
|
||||
'-loop 1',
|
||||
`-framerate ${framerate}`,
|
||||
'-framerate 10',
|
||||
`-i ${stream.url}`,
|
||||
'-c:v libx264',
|
||||
'-preset ultrafast',
|
||||
@@ -52,36 +82,22 @@ export class Go2RTCGenerator {
|
||||
'-g 20',
|
||||
'-f rtsp {output}'
|
||||
].join(' ');
|
||||
|
||||
return `streams:\n '${streamName}':\n - ${ffmpegCmd}`;
|
||||
}
|
||||
|
||||
static generateMJPEG(streamName, stream) {
|
||||
const ffmpegCmd = [
|
||||
'exec:ffmpeg',
|
||||
'-loglevel quiet',
|
||||
`-i ${stream.url}`,
|
||||
'-c:v copy',
|
||||
'-f rtsp {output}'
|
||||
].join(' ');
|
||||
|
||||
return `streams:\n '${streamName}':\n - ${ffmpegCmd}`;
|
||||
}
|
||||
|
||||
static generateHTTPVideo(streamName, stream) {
|
||||
const ffmpegCmd = [
|
||||
'exec:ffmpeg',
|
||||
'-loglevel quiet',
|
||||
`-i ${stream.url}`,
|
||||
'-c:v copy',
|
||||
'-c:a copy',
|
||||
'-f rtsp {output}'
|
||||
].join(' ');
|
||||
|
||||
return `streams:\n '${streamName}':\n - ${ffmpegCmd}`;
|
||||
}
|
||||
|
||||
static generateHLS(streamName, stream) {
|
||||
return `streams:\n '${streamName}':\n - ${stream.url}`;
|
||||
/**
|
||||
* Generate ONVIF source
|
||||
* Converts HTTP device service endpoint to onvif:// format
|
||||
*/
|
||||
static generateONVIFSource(stream) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user