Files
appRobotWebcam/tools/hires-probe.js
2026-06-06 12:15:21 +02:00

109 lines
3.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
// ── Hires-Grab-Diagnose — läuft DIREKT auf dem Host, ohne Docker/Express ──────
//
// Zweck: herausfinden, welche Auflösung /dev/videoN beim Grab WIRKLICH liefert,
// entkoppelt vom Container (kein Sync-Lag, kein Redeploy). Loggt JEDEN Frame mit
// Breite×Höhe und Bytes, speichert den besten als /tmp/hires-probe.jpg.
//
// Aufruf (auf dem ThinkCentre, im Projektverzeichnis):
// node tools/hires-probe.js /dev/video4 1920x1080 mjpeg
// node tools/hires-probe.js /dev/video4 1920x1080 copybsf
// node tools/hires-probe.js /dev/video0 1280x960 copybsf
//
// Args: <device> <size WxH> <encode: mjpeg|copybsf> [framerate=15] [frames=12]
const { spawn } = require('child_process');
const fs = require('fs');
const device = process.argv[2] || '/dev/video4';
const size = process.argv[3] || '1920x1080';
const encode = process.argv[4] || 'mjpeg';
const fps = process.argv[5] || '15';
const maxFrames = parseInt(process.argv[6] || '12', 10);
// SOF-Marker lesen: Höhe bei +5, Breite bei +7
function readJpegDims(buf) {
let i = 2;
while (i < buf.length - 8) {
if (buf[i] !== 0xFF) break;
const marker = buf[i + 1];
const segLen = buf.readUInt16BE(i + 2);
if (marker === 0xC0 || marker === 0xC2) {
return { h: buf.readUInt16BE(i + 5), w: buf.readUInt16BE(i + 7) };
}
i += 2 + segLen;
}
return { h: null, w: null };
}
// mpjpeg-Parser (identisch zur App: keyt auf Content-Length)
class MpjpegParser {
constructor(onFrame) { this.onFrame = onFrame; this.buf = Buffer.alloc(0); this.need = -1; }
push(chunk) {
this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk;
for (;;) {
if (this.need < 0) {
const he = this.buf.indexOf('\r\n\r\n');
if (he < 0) return;
const m = /content-length:\s*(\d+)/i.exec(this.buf.toString('latin1', 0, he));
if (!m) { this.buf = this.buf.subarray(he + 4); continue; }
this.need = parseInt(m[1], 10);
this.buf = this.buf.subarray(he + 4);
}
if (this.buf.length < this.need) return;
const f = this.buf.subarray(0, this.need);
this.buf = this.buf.subarray(this.need);
this.need = -1;
try { this.onFrame(f); } catch (_e) {}
}
}
}
// EXAKT die Args, die die App im hires-Pfad nutzt (videoOutArgs):
const outArgs = encode === 'mjpeg'
? ['-c:v', 'mjpeg', '-q:v', '5']
: ['-c:v', 'copy', '-bsf:v', 'mjpeg2jpeg'];
const args = [
'-hide_banner', '-loglevel', 'warning',
'-f', 'v4l2', '-input_format', 'mjpeg',
'-video_size', size, '-framerate', fps,
'-i', device,
...outArgs, '-f', 'mpjpeg', 'pipe:1',
];
console.log(`\n[probe] device=${device} size=${size} encode=${encode} fps=${fps}`);
console.log(`[probe] ffmpeg ${args.join(' ')}\n`);
const p = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'pipe'] });
let n = 0;
let best = null;
const parser = new MpjpegParser((frame) => {
n++;
const { w, h } = readJpegDims(frame);
console.log(`[probe] frame ${String(n).padStart(2)} ${String(w)}x${String(h)} ${frame.length} bytes`);
if (!best || frame.length > best.length) best = Buffer.from(frame);
if (n >= maxFrames) finish();
});
p.stdout.on('data', (c) => parser.push(c));
p.stderr.on('data', (c) => process.stderr.write(`[ffmpeg] ${c}`));
p.on('error', (e) => { console.error('[probe] spawn-Fehler:', e.message); process.exit(1); });
let finished = false;
function finish() {
if (finished) return; finished = true;
try { p.kill('SIGKILL'); } catch (_e) {}
if (best) {
const { w, h } = readJpegDims(best);
fs.writeFileSync('/tmp/hires-probe.jpg', best);
console.log(`\n[probe] ERGEBNIS: bester Frame ${w}x${h}, ${best.length} bytes → /tmp/hires-probe.jpg`);
} else {
console.log('\n[probe] ERGEBNIS: KEIN Frame empfangen');
}
process.exit(0);
}
setTimeout(finish, 8000); // Sicherheitsnetz