CoPilot Github: Stream fex 4

This commit is contained in:
chk
2026-06-06 12:15:21 +02:00
parent f873e2a938
commit a95dcec0a3

108
tools/hires-probe.js Normal file
View File

@@ -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: <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