From a95dcec0a32c5e247d588155c6bab0a7a2d10129 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:15:21 +0200 Subject: [PATCH] CoPilot Github: Stream fex 4 --- tools/hires-probe.js | 108 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 tools/hires-probe.js diff --git a/tools/hires-probe.js b/tools/hires-probe.js new file mode 100644 index 0000000..5beaba3 --- /dev/null +++ b/tools/hires-probe.js @@ -0,0 +1,108 @@ +'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