From a7ed2775dcdaf6d828118af8894e9df7dcc4784a Mon Sep 17 00:00:00 2001 From: ChK Date: Sun, 15 Mar 2026 10:19:28 +0100 Subject: [PATCH] HighRes von Cam im PC scalen --- programs/screenShot.js | 18 ++++++++++++-- programs/videoServer.js | 55 +++++++++++++++++++++++++++++++++++++++-- server.js | 8 ++++-- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/programs/screenShot.js b/programs/screenShot.js index 86a0205..39aecc4 100755 --- a/programs/screenShot.js +++ b/programs/screenShot.js @@ -8,10 +8,24 @@ function snapshot(outDir, cam0, cam1, ws){ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); const picDate = Date.now(); - const name0 = `snapshot_video0_${picDate}.jpg`; - const name1 = `snapshot_video1_${picDate}.jpg`; + var name0 = `snapshot_video0_${picDate}.jpg`; + var name1 = `snapshot_video1_${picDate}.jpg`; cam0.snapshot(path.join(outDir, name0)); cam1.snapshot(path.join(outDir, name1)); + + + console.log('Taking snapshot from cam0 async'); + (async () => { + try { + console.log('Taking snapshot from cam1 a…'); + var name1 = `snapshot_video1a_${picDate}.jpg`; + await cam1.snapshotHighRes(path.join(outDir, name1)); + console.log('Snapshot gespeichert:', name1); + } catch (err) { + console.error('Snapshot fehlgeschlagen:', err); + } + })(); + strFile0 = path.join(outDir, name0); diff --git a/programs/videoServer.js b/programs/videoServer.js index ed3c55d..a991656 100755 --- a/programs/videoServer.js +++ b/programs/videoServer.js @@ -103,14 +103,18 @@ class FFmpegStreamer { ...(typeof inChannel === 'number' ? ['-channel', String(inChannel)] : []), ...(useWallclock ? ['-use_wallclock_as_timestamps', '1'] : []), '-i', this.devicePath, - '-fflags', 'nobuffer', '-flags', 'low_delay', '-an', '-sn', + //'-fflags', 'nobuffer', '-flags', 'low_delay', '-an', '-sn', + '-fflags', 'nobuffer', '-an', '-sn', ]; if (inFmt === 'mjpeg' && !scaling) { args.push('-vsync', 'passthrough', '-c:v', 'copy', '-f', 'mjpeg', 'pipe:1'); return args; } - if (scaling) args.push('-vf', `scale=${Number(this.opts.width)}:${Number(this.opts.height)}`); + if (scaling) { + args.push('-vf', `scale=${Number(this.opts.width)}:${Number(this.opts.height)}`); + args.push('-pix_fmt', 'yuvj422p'); // für mjpeg-Encoder robust + } if (outFps) args.push('-r', String(outFps)); args.push('-f', 'mjpeg', '-q:v', String(quality), 'pipe:1'); return args; @@ -229,6 +233,53 @@ class FFmpegStreamer { try { ws.send(frame, { binary: true }); } catch {} } } + + /** + * 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()}`)); + }); + }); + } } module.exports = { FFmpegStreamer }; \ No newline at end of file diff --git a/server.js b/server.js index fc9dab2..05c659f 100755 --- a/server.js +++ b/server.js @@ -113,6 +113,8 @@ console.log(`[DEV] Using devices: ${DEV0} (video0), ${DEV1} (video1)`); // Cam0: MJPEG pass-through if available (lowest latency) const cam0 = new FFmpegStreamer(DEV0, { name: 'video0', + width: 640, + height: 480, fps: 30, quality: 8, // 5 wäre besser input: { @@ -124,7 +126,7 @@ const cam0 = new FFmpegStreamer(DEV0, { threadQueueSize: 64, channel: 0, }, - tryFormats: ['mjpeg', 'yuyv422', 'rgb24'], + tryFormats: ['yuyv422', 'mjpeg', 'rgb24'], }); @@ -132,6 +134,8 @@ const cam0 = new FFmpegStreamer(DEV0, { // Cam1: your working timing on /dev/video2; let driver pick format first const cam1 = new FFmpegStreamer(DEV1, { name: 'video1', + width: 640, + height: 480, fps: 30, quality: 8, // 5 wäre besser input: { @@ -219,7 +223,7 @@ function handleControlMessage(ws, msg) { try { switch (msg.action) { case 'snapshot': { - + console.log('Snapshot requested'); const outDir = path.join(__dirname, 'public', 'snapshots'); screenShot.snapshot(outDir, cam0, cam1, ws); break;