Recognizion with multiple

This commit is contained in:
ChK
2026-03-19 20:30:27 +01:00
parent a7ed2775dc
commit 929b738bcc
3 changed files with 83 additions and 79 deletions

View File

@@ -76,6 +76,9 @@ class FFmpegStreamer {
this._quickFailCount = 0;
this._quickFailLimit = 6;
this._suspendedUntil = 0;
this.latestHighResFrame = null;
this.hiSplitter = null;
}
get running() { return !!this.proc; }
@@ -111,6 +114,30 @@ class FFmpegStreamer {
args.push('-vsync', 'passthrough', '-c:v', 'copy', '-f', 'mjpeg', 'pipe:1');
return args;
}
if (scaling) {
const w = Number(this.opts.width), h = Number(this.opts.height);
const mjpegInput = (inFmt === 'mjpeg'); // nur wenn Input schon MJPEG ist
// Filtergraph: vor dem Scale splitten
// - [lo] wird skaliert für den Stream
// - [hi] bleibt unskaliert (High-Res), stark gedrosselt, mit FIFO damit nichts blockiert
args.push(
'-filter_complex', `[0:v]split=2[hi][lo];[lo]scale=${w}:${h}:flags=fast_bilinear,setsar=1[vlo];[hi]fps=0.5,fifo[vhi]`,
// Low-Res Stream -> stdout (pipe:1), hier mjpeg encoden
'-map', '[vlo]',
'-pix_fmt', 'yuvj422p',
...(outFps ? ['-r', String(outFps)] : []),
'-f', 'mjpeg', '-q:v', String(quality), 'pipe:1',
// High-Res -> pipe:3
'-map', '[vhi]',
// Wenn Input bereits MJPEG ist, können wir kopieren (kein Re-Encode!):
...(mjpegInput ? ['-c:v', 'copy'] : ['-pix_fmt', 'yuvj422p', '-q:v', '2']),
'-f', 'mjpeg', 'pipe:3'
);
return args;
}
if (scaling) {
args.push('-vf', `scale=${Number(this.opts.width)}:${Number(this.opts.height)}`);
args.push('-pix_fmt', 'yuvj422p'); // für mjpeg-Encoder robust
@@ -139,7 +166,7 @@ class FFmpegStreamer {
console.log(`[FFmpeg] Start ${this.devicePath} (${this.name}) :: ${args.join(' ')}`);
this._stderrBuf = [];
this.proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'], detached: true });
this.proc = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe', 'pipe'], detached: true });
this.startedAt = Date.now();
this.splitter = new JpegFrameSplitter((frame) => {
@@ -150,6 +177,17 @@ class FFmpegStreamer {
this.proc.stdout.on('data', (chunk) => this.splitter?.push(chunk));
this.proc.stderr.on('data', (d) => this._logStderr(d));
// High-Res (fd=3) mit 1 FPS drainen und letzten Frame merken
if (this.proc.stdio[3]) {
this.hiSplitter = new JpegFrameSplitter((frame) => {
this.latestHighResFrame = frame; // überschreibt nur den letzten → minimaler Speicher/CPU
});
this.proc.stdio[3].on('data', (chunk) => this.hiSplitter?.push(chunk));
}
this.proc.on('exit', (code, signal) => {
console.warn(`[FFmpeg] ${this.devicePath} exited code=${code} sig=${signal}`);
if (this._stderrBuf.length) console.warn(`[FFmpeg] ${this.name} last errors:\n - ${this._stderrBuf.join('\n - ')}`);
@@ -234,52 +272,14 @@ class FFmpegStreamer {
}
}
/**
* Nimmt einen Snapshot in hoher Auflösung auf, unabhängig vom Stream.
* Startet kurz einen separaten ffmpeg-Prozess und speichert 1 Frame als JPEG.
*
* @param {string} toFile - Pfad zur Zieldatei (z.B. '/tmp/snap.jpg')
* @param {object} [opts]
* @param {string} [opts.size] - 'WxH' z.B. '1280x960' (Default: opts.input.size)
* @param {string} [opts.format] - z.B. 'mjpeg' | 'yuyv422' (Default: opts.input.format)
* @param {number} [opts.quality] - FFmpeg JPEG-Qualität 2..31 (kleiner = besser). Default: 2
* @param {number} [opts.timeoutMs] - Abbruch nach ms. Default: 3000
*/
async snapshotHighRes(toFile, { size, format, quality = 2, timeoutMs = 3000 } = {}) {
return new Promise((resolve, reject) => {
const inFmt = format ?? this.opts.input.format ?? 'mjpeg';
const inSize = size ?? this.opts.input.size; // wenn undefined, nimmt ffmpeg die Kamera-Default
const fps = Math.min(5, this.opts.input.fps || 5); // niedrig reicht für Einzelbild
const args = [
'-hide_banner', '-loglevel', 'error',
'-f', 'video4linux2',
...(inFmt ? ['-input_format', String(inFmt)] : []),
...(fps ? ['-framerate', String(fps)] : []),
...(inSize ? ['-video_size', String(inSize)] : []),
'-i', this.devicePath,
'-frames:v', '1',
'-q:v', String(quality),
'-y', toFile,
];
const p = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] });
let stderr = '';
const t = setTimeout(() => {
try { p.kill('SIGKILL'); } catch {}
reject(new Error(`snapshotHighRes timeout after ${timeoutMs}ms`));
}, timeoutMs);
p.stderr.on('data', d => { stderr += d.toString(); });
p.on('close', code => {
clearTimeout(t);
if (code === 0) resolve(toFile);
else reject(new Error(`ffmpeg exited ${code}: ${stderr.trim()}`));
});
});
async snapshotHighRes(toFile) {
const buf = this.latestHighResFrame || this.latestFrame;
if (!buf) throw new Error('No frame available yet');
fs.writeFileSync(toFile, buf);
return toFile;
}
}
module.exports = { FFmpegStreamer };