CoPilot Github: Stream fex 3
This commit is contained in:
@@ -66,11 +66,13 @@ Kamera ──live 1920×1080──► getFrame() → JPEG 1920×1080
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. Live-FFmpeg SIGTERM → warte auf close (= FD frei)
|
1. Live-FFmpeg SIGTERM → warte auf close (= FD frei)
|
||||||
2. sleep(300ms) ← v4l2-Buffer leeren, Kamera-Reset abwarten
|
2. sleep(800ms) ← Kamera-/Treiberreset, Puffer auslaufen lassen
|
||||||
3. hires-FFmpeg bei hiresSize/hiresFps starten
|
3. optional: 1280x720-Warmup-Format öffnen
|
||||||
4. Frames warmlaufen lassen (settleFrames=6) + minWidth-Check (90 % der Soll-Breite)
|
4. sleep(500ms) ← Warmup/Formatwechsel stabilisieren
|
||||||
5. Ersten validen Frame nehmen → hires-FFmpeg SIGTERM
|
5. hires-FFmpeg bei hiresSize/hiresFps starten
|
||||||
6. finally: Live-FFmpeg neu starten (immer, auch bei Fehler)
|
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 `<img>` friert ~2–3 s ein und läuft danach weiter. Kein Client-Handling nötig.
|
**Blackout:** Der `<img>` 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
|
Damit werden Frames abgelehnt, die noch auf der alten Live-Auflösung liegen
|
||||||
(v4l2-Buffer-Reste vom vorherigen Format).
|
(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
|
## 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 |
|
| 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 |
|
| `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 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -232,13 +232,15 @@ class CameraSwitch extends EventEmitter {
|
|||||||
// 1. Live-FFmpeg beenden, auf Prozess-Ende warten (= Device-FD frei)
|
// 1. Live-FFmpeg beenden, auf Prozess-Ende warten (= Device-FD frei)
|
||||||
await this._killCurrentAndWait();
|
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);
|
await sleep(800);
|
||||||
const warmupSize = this._chooseWarmupSize();
|
const warmupSize = this._chooseWarmupSize();
|
||||||
if (warmupSize) {
|
if (warmupSize) {
|
||||||
console.log(`[cam ${this.id}] HD: Zwischenformat ${warmupSize} zum Kamera-Reset`);
|
console.log(`[cam ${this.id}] HD: Zwischenformat ${warmupSize} zum Kamera-Reset`);
|
||||||
await this._warmupFormat(warmupSize);
|
await this._warmupFormat(warmupSize);
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
|
} else {
|
||||||
|
console.log(`[cam ${this.id}] HD: Kein Zwischenformat nötig (live ${this.liveSize} → hires ${this.hiresSize})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.state = 'grabbing';
|
this.state = 'grabbing';
|
||||||
@@ -283,6 +285,7 @@ class CameraSwitch extends EventEmitter {
|
|||||||
|
|
||||||
_warmupFormat(size) {
|
_warmupFormat(size) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
console.log(`[cam ${this.id}] HD: Warmup-Format ${size} starten`);
|
||||||
const args = [
|
const args = [
|
||||||
'-hide_banner', '-loglevel', 'warning',
|
'-hide_banner', '-loglevel', 'warning',
|
||||||
'-fflags', 'nobuffer',
|
'-fflags', 'nobuffer',
|
||||||
@@ -295,6 +298,7 @@ class CameraSwitch extends EventEmitter {
|
|||||||
try {
|
try {
|
||||||
p = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
p = spawn('ffmpeg', args, { stdio: ['ignore', 'pipe', 'ignore'] });
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
|
console.warn(`[cam ${this.id}] warmup ffmpeg start fehlgeschlagen: ${_e.message}`);
|
||||||
return resolve();
|
return resolve();
|
||||||
}
|
}
|
||||||
this.proc = p;
|
this.proc = p;
|
||||||
@@ -307,12 +311,18 @@ class CameraSwitch extends EventEmitter {
|
|||||||
if (p) {
|
if (p) {
|
||||||
try { p.kill('SIGTERM'); } catch (_e) {}
|
try { p.kill('SIGTERM'); } catch (_e) {}
|
||||||
}
|
}
|
||||||
|
console.log(`[cam ${this.id}] HD: Warmup-Format ${size} beendet`);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
p.stderr.on('data', (c) => {
|
p.stderr.on('data', (c) => {
|
||||||
const s = c.toString();
|
const s = c.toString().trim();
|
||||||
if (/error|busy|invalid|no such|cannot|denied/i.test(s)) console.error(`[cam ${this.id}] warmup ffmpeg: ${s.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('error', done);
|
||||||
p.on('close', done);
|
p.on('close', done);
|
||||||
@@ -377,15 +387,24 @@ class CameraSwitch extends EventEmitter {
|
|||||||
count++;
|
count++;
|
||||||
if (!best || frame.length > best.length) best = frame;
|
if (!best || frame.length > best.length) best = frame;
|
||||||
const w = readJpegWidth(frame);
|
const w = readJpegWidth(frame);
|
||||||
if (count >= settleFrames && frame.length >= minSize && (w === null || w >= minWidth)) {
|
if (count >= settleFrames) {
|
||||||
|
if (frame.length >= minSize && (w === null || w >= minWidth)) {
|
||||||
decide(Buffer.from(frame), null); // kopieren: subarray teilt den Parser-Puffer
|
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.stdout.on('data', (c) => parser.push(c));
|
||||||
p.stderr.on('data', (c) => {
|
p.stderr.on('data', (c) => {
|
||||||
const s = c.toString();
|
const s = c.toString().trim();
|
||||||
if (/error|busy|invalid|no such|cannot|denied/i.test(s)) console.error(`[cam ${this.id}] hires ffmpeg: ${s.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('error', (e) => { if (!decided) decide(null, e); });
|
||||||
p.on('close', () => {
|
p.on('close', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user