HighRes von Cam im PC scalen
This commit is contained in:
@@ -8,12 +8,26 @@ function snapshot(outDir, cam0, cam1, ws){
|
|||||||
|
|
||||||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||||||
const picDate = Date.now();
|
const picDate = Date.now();
|
||||||
const name0 = `snapshot_video0_${picDate}.jpg`;
|
var name0 = `snapshot_video0_${picDate}.jpg`;
|
||||||
const name1 = `snapshot_video1_${picDate}.jpg`;
|
var name1 = `snapshot_video1_${picDate}.jpg`;
|
||||||
cam0.snapshot(path.join(outDir, name0));
|
cam0.snapshot(path.join(outDir, name0));
|
||||||
cam1.snapshot(path.join(outDir, name1));
|
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);
|
strFile0 = path.join(outDir, name0);
|
||||||
strFile1 = path.join(outDir, name1);
|
strFile1 = path.join(outDir, name1);
|
||||||
|
|
||||||
|
|||||||
@@ -103,14 +103,18 @@ class FFmpegStreamer {
|
|||||||
...(typeof inChannel === 'number' ? ['-channel', String(inChannel)] : []),
|
...(typeof inChannel === 'number' ? ['-channel', String(inChannel)] : []),
|
||||||
...(useWallclock ? ['-use_wallclock_as_timestamps', '1'] : []),
|
...(useWallclock ? ['-use_wallclock_as_timestamps', '1'] : []),
|
||||||
'-i', this.devicePath,
|
'-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) {
|
if (inFmt === 'mjpeg' && !scaling) {
|
||||||
args.push('-vsync', 'passthrough', '-c:v', 'copy', '-f', 'mjpeg', 'pipe:1');
|
args.push('-vsync', 'passthrough', '-c:v', 'copy', '-f', 'mjpeg', 'pipe:1');
|
||||||
return args;
|
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));
|
if (outFps) args.push('-r', String(outFps));
|
||||||
args.push('-f', 'mjpeg', '-q:v', String(quality), 'pipe:1');
|
args.push('-f', 'mjpeg', '-q:v', String(quality), 'pipe:1');
|
||||||
return args;
|
return args;
|
||||||
@@ -229,6 +233,53 @@ class FFmpegStreamer {
|
|||||||
try { ws.send(frame, { binary: true }); } catch {}
|
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 };
|
module.exports = { FFmpegStreamer };
|
||||||
@@ -113,6 +113,8 @@ console.log(`[DEV] Using devices: ${DEV0} (video0), ${DEV1} (video1)`);
|
|||||||
// Cam0: MJPEG pass-through if available (lowest latency)
|
// Cam0: MJPEG pass-through if available (lowest latency)
|
||||||
const cam0 = new FFmpegStreamer(DEV0, {
|
const cam0 = new FFmpegStreamer(DEV0, {
|
||||||
name: 'video0',
|
name: 'video0',
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
fps: 30,
|
fps: 30,
|
||||||
quality: 8, // 5 wäre besser
|
quality: 8, // 5 wäre besser
|
||||||
input: {
|
input: {
|
||||||
@@ -124,7 +126,7 @@ const cam0 = new FFmpegStreamer(DEV0, {
|
|||||||
threadQueueSize: 64,
|
threadQueueSize: 64,
|
||||||
channel: 0,
|
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
|
// Cam1: your working timing on /dev/video2; let driver pick format first
|
||||||
const cam1 = new FFmpegStreamer(DEV1, {
|
const cam1 = new FFmpegStreamer(DEV1, {
|
||||||
name: 'video1',
|
name: 'video1',
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
fps: 30,
|
fps: 30,
|
||||||
quality: 8, // 5 wäre besser
|
quality: 8, // 5 wäre besser
|
||||||
input: {
|
input: {
|
||||||
@@ -219,7 +223,7 @@ function handleControlMessage(ws, msg) {
|
|||||||
try {
|
try {
|
||||||
switch (msg.action) {
|
switch (msg.action) {
|
||||||
case 'snapshot': {
|
case 'snapshot': {
|
||||||
|
console.log('Snapshot requested');
|
||||||
const outDir = path.join(__dirname, 'public', 'snapshots');
|
const outDir = path.join(__dirname, 'public', 'snapshots');
|
||||||
screenShot.snapshot(outDir, cam0, cam1, ws);
|
screenShot.snapshot(outDir, cam0, cam1, ws);
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user