From f873e2a938addde280be7c7ca552a93012897bc6 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Sat, 6 Jun 2026 11:48:17 +0200 Subject: [PATCH] CoPilot Github: Stream fex 3 --- doc/05_screenShot_roadmap.md | 53 ++++++++++++++++++++++++++++++++---- src/cameraSwitch.js | 33 +++++++++++++++++----- 2 files changed, 74 insertions(+), 12 deletions(-) diff --git a/doc/05_screenShot_roadmap.md b/doc/05_screenShot_roadmap.md index 06fbc3d..7a71b45 100644 --- a/doc/05_screenShot_roadmap.md +++ b/doc/05_screenShot_roadmap.md @@ -66,11 +66,13 @@ Kamera ──live 1920×1080──► getFrame() → JPEG 1920×1080 ``` 1. Live-FFmpeg SIGTERM → warte auf close (= FD frei) -2. sleep(300ms) ← v4l2-Buffer leeren, Kamera-Reset abwarten -3. hires-FFmpeg bei hiresSize/hiresFps starten -4. Frames warmlaufen lassen (settleFrames=6) + minWidth-Check (90 % der Soll-Breite) -5. Ersten validen Frame nehmen → hires-FFmpeg SIGTERM -6. finally: Live-FFmpeg neu starten (immer, auch bei Fehler) +2. sleep(800ms) ← Kamera-/Treiberreset, Puffer auslaufen lassen +3. optional: 1280x720-Warmup-Format öffnen +4. sleep(500ms) ← Warmup/Formatwechsel stabilisieren +5. hires-FFmpeg bei hiresSize/hiresFps starten +6. Frames warmlaufen lassen (settleFrames=6) + minWidth-Check (90 % der Soll-Breite) +7. Ersten validen Frame nehmen → hires-FFmpeg SIGTERM +8. finally: Live-FFmpeg neu starten (immer, auch bei Fehler) ``` **Blackout:** Der `` friert ~2–3 s ein und läuft danach weiter. Kein Client-Handling nötig. @@ -79,6 +81,45 @@ Kamera ──live 1920×1080──► getFrame() → JPEG 1920×1080 Damit werden Frames abgelehnt, die noch auf der alten Live-Auflösung liegen (v4l2-Buffer-Reste vom vorherigen Format). +**FFmpeg-Probe:** Für das finale hires-Open werden jetzt zusätzliche Probe-Parameter +gesetzt (`-probesize 5000000`, `-analyzeduration 1000000`), um das MJPEG-Format +und die Kameraparameter sicherer zu erkennen. + +--- + +## Aktueller Ablauf des HD-Grabs + +Der aktuelle Ablauf ist bewusst defensiv: +- Live-Stream beenden und auf FFmpeg-`close` warten. +- 800 ms Pause zum Zurücksetzen des Kameratreibers. +- Wenn liveSize deutlich kleiner als hiresSize ist, zunächst ein Zwischenformat + `1280x720` starten und kurz einlaufen lassen. +- 500 ms warten, damit das Gerät in den neuen Auflösungsmodus umschaltet. +- Hires-Stream starten, mehrere Frames puffern und den ersten validen Frame + mit genügend Bytes und Breite auswählen. +- Hires-Stream beenden, Live-Stream neu starten. + +### Log- und Fehlerbild + +Aktuelle Log-Meldungen zeigen typische MJPEG/Treiber-Phänomene: +- `unable to decode APP fields`: meist kosmetisch beim MJPEG-Parser, häufig bei + UVC-Webcams. Das bedeutet nicht zwingend einen fehlgeschlagenen Grab. +- `input is truncated` / `Error applying bitstream filters`: kann auftreten, wenn + FFmpeg beim Beenden gerade ein MJPEG-Paket verarbeitet. Das ist ein Hinweis auf + einen abrupten Stream-Abbruch, nicht zwingend auf ein ungültiges Bild. +- `Could not find codec parameters ... unspecified pixel format`: deutet darauf hin, + dass der neue HD-Stream noch nicht sauber genug erkannt wurde. Deshalb nutzen + wir jetzt größere Probe-Parameter. + +### Mögliche Ursachen + +- Treiberzustand der Kamera nach einem schnellen Formatwechsel: der C920 kann + noch Frames aus dem alten Modus liefern oder zwischen `640×480` und `1920×1080` + in einen inkonsistenten Zustand kommen. +- MJPEG-Frames ohne vollständige JPEG-Header oder Huffman-Tabellen, die erst durch + `mjpeg2jpeg` ergänzt werden. +- Abrupte Beendigung des `ffmpeg`-Prozesses während eines laufenden MJPEG-Pakets. + --- ## Kamera-spezifische Konfiguration @@ -140,6 +181,8 @@ v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG |---|---|---| | Stream friert selten dauerhaft ein | niedrig | Einzelfall; clientseitiger Watchdog (Frame-Timeout → `img.src` neu) noch nicht implementiert | | `unable to decode APP fields` im Log (C270) | kosmetisch | Einmalige Probe-Warnung von FFmpeg, kein Auswirkung | +| `input is truncated` / `Error applying bitstream filters` beim HD-Grab | mittel | Hinweis auf abrupten Stream-Abbruch beim Formatwechsel; Bild kann dennoch gültig sein | +| `Could not find codec parameters ... unspecified pixel format` | mittel | Zeichen für unvollständige FFmpeg-Probe nach Formatwechsel; größere probe-Parameter helfen | --- diff --git a/src/cameraSwitch.js b/src/cameraSwitch.js index c6c26ef..4383bea 100644 --- a/src/cameraSwitch.js +++ b/src/cameraSwitch.js @@ -232,13 +232,15 @@ class CameraSwitch extends EventEmitter { // 1. Live-FFmpeg beenden, auf Prozess-Ende warten (= Device-FD frei) await this._killCurrentAndWait(); - // 2. Kamera resetten: kurz warten, dann zwischenformat öffnen, wieder warten. + // 2. Kamera resetten: kurz warten, dann Zwischenformat öffnen, wieder warten. await sleep(800); const warmupSize = this._chooseWarmupSize(); if (warmupSize) { console.log(`[cam ${this.id}] HD: Zwischenformat ${warmupSize} zum Kamera-Reset`); await this._warmupFormat(warmupSize); await sleep(500); + } else { + console.log(`[cam ${this.id}] HD: Kein Zwischenformat nötig (live ${this.liveSize} → hires ${this.hiresSize})`); } this.state = 'grabbing'; @@ -283,6 +285,7 @@ class CameraSwitch extends EventEmitter { _warmupFormat(size) { return new Promise((resolve) => { + console.log(`[cam ${this.id}] HD: Warmup-Format ${size} starten`); const args = [ '-hide_banner', '-loglevel', 'warning', '-fflags', 'nobuffer', @@ -295,6 +298,7 @@ class CameraSwitch extends EventEmitter { try { p = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'ignore'] }); } catch (_e) { + console.warn(`[cam ${this.id}] warmup ffmpeg start fehlgeschlagen: ${_e.message}`); return resolve(); } this.proc = p; @@ -307,12 +311,18 @@ class CameraSwitch extends EventEmitter { if (p) { try { p.kill('SIGTERM'); } catch (_e) {} } + console.log(`[cam ${this.id}] HD: Warmup-Format ${size} beendet`); resolve(); }; p.stderr.on('data', (c) => { - const s = c.toString(); - if (/error|busy|invalid|no such|cannot|denied/i.test(s)) console.error(`[cam ${this.id}] warmup ffmpeg: ${s.trim()}`); + const s = c.toString().trim(); + if (!s) return; + if (/error|busy|invalid|no such|cannot|denied/i.test(s)) { + console.warn(`[cam ${this.id}] warmup ffmpeg: ${s}`); + } else { + console.log(`[cam ${this.id}] warmup ffmpeg: ${s}`); + } }); p.on('error', done); p.on('close', done); @@ -377,15 +387,24 @@ class CameraSwitch extends EventEmitter { count++; if (!best || frame.length > best.length) best = frame; const w = readJpegWidth(frame); - if (count >= settleFrames && frame.length >= minSize && (w === null || w >= minWidth)) { - decide(Buffer.from(frame), null); // kopieren: subarray teilt den Parser-Puffer + if (count >= settleFrames) { + if (frame.length >= minSize && (w === null || w >= minWidth)) { + decide(Buffer.from(frame), null); // kopieren: subarray teilt den Parser-Puffer + } else { + console.warn(`[cam ${this.id}] hires frame ${count} verworfen: ${frame.length} bytes width=${w ?? '?'} (minSize=${minSize}, minWidth=${minWidth})`); + } } }); p.stdout.on('data', (c) => parser.push(c)); p.stderr.on('data', (c) => { - const s = c.toString(); - if (/error|busy|invalid|no such|cannot|denied/i.test(s)) console.error(`[cam ${this.id}] hires ffmpeg: ${s.trim()}`); + const s = c.toString().trim(); + if (!s) return; + if (/error|busy|invalid|no such|cannot|denied|input is truncated|Could not find codec parameters|Immediate exit requested|Output file is empty/i.test(s)) { + console.warn(`[cam ${this.id}] hires ffmpeg: ${s}`); + } else { + console.log(`[cam ${this.id}] hires ffmpeg: ${s}`); + } }); p.on('error', (e) => { if (!decided) decide(null, e); }); p.on('close', () => {