'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: [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