Files
appRobotWebcam/doc/05_screenShot_roadmap.md
2026-06-06 11:48:17 +02:00

248 lines
9.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# AppRobotWebcam Snapshot & HD-Grab
> Status: **Implementiert**. Architektur seit 2026-06-05 (Node-MJPEG-Schalter).
> Gemessene Werte: Latenz 139 ms, ~5 % idle, ~35 %/Kamera aktiv, HD-Grab ~23 s.
---
## Architektur
Eine `CameraSwitch`-Instanz pro physischem Gerät — der einzige Öffner von `/dev/videoN`.
Hält immer nur **einen** FFmpeg: entweder Live **oder** Grab. Nie beide.
```
cameras.json
└── server.js → CameraSwitch (besitzt /dev/videoN)
├── Live: ffmpeg → mpjpeg pipe → Fan-out an N Browser-Clients (<img>)
└── Grab: Live SIGTERM → close-Event = FD frei → hires-FFmpeg → zurück
```
Das `close`-Event des Kindprozesses ist der harte Beweis „Gerät frei" — kein Timing,
keine Polls. Das Race (zwei Encoder auf einem Gerät) ist konstruktiv ausgeschlossen.
---
## Live-Stream
```
ffmpeg -fflags nobuffer -f v4l2 -input_format mjpeg
-video_size <liveSize> -framerate <liveFps>
-i /dev/videoN
-c:v copy -bsf:v mjpeg2jpeg ← copybsf (Default)
-f mpjpeg -flush_packets 1 pipe:1
```
| Encode-Modus | CPU | Wann |
|---|---|---|
| `copybsf` (Default) | ~35 %/Kamera | Kamera liefert natives MJPEG |
| `mjpeg` (Fallback) | ~50 %/Kamera | Re-Encode, falls copybsf Probleme macht |
**`mjpeg2jpeg` ist Pflicht bei copybsf** — Kamera-MJPEG lässt JPEG-Huffman-Tabellen
weg; ohne den Filter verschluckt sich der mpjpeg-Muxer (107 % CPU + Hang).
**Latenz-Tuning:** `-fflags nobuffer` + `-flush_packets 1` + `socket.setNoDelay(true)` +
`cork/uncork` (Header+JPEG+Trailer = ein TCP-Segment). Gemessen: **139 ms** Kamera→Browser.
**On-Demand:** Live läuft nur wenn Clients verbunden sind. `acquire()`/`release()` zählen
Verbraucher. Nach dem letzten + `IDLE_GRACE_MS` (15 s): Stop → **0 % idle**. Abschaltbar
via `ON_DEMAND=false`.
---
## HD-Grab (`grabHires`)
### Fall A — liveSize == hiresSize
Wenn die Live-Auflösung bereits der gewünschten Hires-Auflösung entspricht (z.B. C920
mit `liveSize: "1920x1080"`): kein Format-Wechsel nötig. `grabHires` ruft direkt
`getFrame()` auf dem laufenden Live-Stream auf. Kein Neustart, kein Übergangs-Problem.
```
Kamera ──live 1920×1080──► getFrame() → JPEG 1920×1080
```
### Fall B — liveSize ≠ hiresSize (C270: 640×480 → 1280×960)
```
1. Live-FFmpeg SIGTERM → warte auf close (= FD frei)
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 `<img>` friert ~23 s ein und läuft danach weiter. Kein Client-Handling nötig.
**minWidth** wird automatisch aus `hiresSize` abgeleitet (`floor(hiresW × 0.9)`).
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
### C920 (HD Pro Webcam) — liveSize = hiresSize
Die C920 hat ein v4l2-Treiber-Eigenheit: nach einem Live-Stream bei 640×480 kann sie
beim Neustart auf 1920×1080 nicht sauber wechseln — der Treiber gibt 1280×720-Frames
zurück (Übergangs-Artefakt, trotz korrekter Format-Anforderung).
**Lösung:** `liveSize` und `hiresSize` identisch setzen. `grabHires` wechselt dann kein
Format — es liest direkt aus dem laufenden 1920×1080-Stream (Fall A).
```json
{
"id": "cam2",
"liveSize": "1920x1080",
"liveFps": 15,
"hiresSize": "1920x1080"
}
```
### C270 — Standard (Fall B)
```json
{
"id": "cam0",
"hiresSize": "1280x960"
}
```
Live bei 640×480, Grab bei 1280×960. Format-Wechsel + 300ms-Pause funktioniert.
### Auflösung prüfen
Nur MJPG-native Auflösungen in `liveSize`/`hiresSize` verwenden (kein Software-Encode):
```bash
v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG
```
---
## API-Endpunkte
| Endpunkt | Beschreibung |
|---|---|
| `GET /api/snapshot/:id` | 640er JPEG aus dem Live-Puffer (on-demand, sofort) |
| `GET /api/snapshot/:id/hires` | HD-JPEG via `grabHires` (23 s, Blackout) |
| `GET /api/stream/:id` | MJPEG multipart/x-mixed-replace (Live, Browser `<img>`) |
| `GET /api/cameras` | Metadaten aller Kameras aus cameras.json |
| `GET /health` | Zustand aller CameraSwitch-Instanzen |
---
## Bekannte Restprobleme
| Problem | Priorität | Zustand |
|---|---|---|
| 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 |
---
---
## Architektur-Entwicklung (Historie)
Dieser Abschnitt dokumentiert den Weg zur aktuellen Lösung.
Für die maßgebliche Implementierung gelten ausschliesslich die Abschnitte oben.
### go2rtc-Ansatz (2026-06-04) — abgelöst
#### Grundidee: Consumer-Umhängen
go2rtc verwaltete die Kameras. Für HD-Grabs wurde der Live-Consumer (`cam0`) losgelassen,
damit go2rtc das Gerät freigibt, dann ein separater `cam0_hires`-Stream bei 1280×960 geöffnet.
#### Phase 1 — Freigabe messen (2026-06-04, ✅ erfolgreich)
Endpunkt `GET /api/snapshot/:id/release-test` pollt go2rtcs `/api/streams` alle 200 ms
und misst, wann nach Consumer-Verlust der Producer stoppt (= Gerät frei).
**Ergebnis:**
```json
{ "freed": true, "msUntilFree": 0, "zeroConsumerAt": 4850 }
```
go2rtc gibt das Gerät sofort frei wenn der letzte Consumer weg ist. Kein „warm halten".
Die ~5 s (`zeroConsumerAt`) sind die WS-Abmeldelatenz von go2rtc selbst.
#### Phase 2 — HD-Grab implementiert (2026-06-04, ✅ funktionierte)
HD-Grab lieferte echte 1280×960-Frames (~76 KB). Einschränkungen:
- Nur bei einem einzigen Viewer-Tab zuverlässig (bei mehreren fiel Consumer-Count nie auf 0)
- Sequenz: Browser löst cam0 → 5 s warten → Grab → Browser hängt zurück
- Blackout ~810 s (5 s Pause + Grab-Zeit)
#### Race-Bug entdeckt (2026-06-05) → go2rtc entfernt
go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg `/dev/videoN` freigibt.
Folge: zwei FFmpeg gleichzeitig auf demselben Gerät → **106 % CPU + Hang** (reproduzierbar).
Details: `09_Bug_reports.md`.
**Erkenntnis:** Das Problem ist strukturell — go2rtc gibt keine harten Garantien über
den Gerätezustand. Der einzige zuverlässige Beweis „Gerät frei" ist das `close`-Event
des FFmpeg-Prozesses selbst.
**Entscheidung 2026-06-05:** go2rtc entfernen. Node startet FFmpeg direkt.
### Node-MJPEG-Schalter (2026-06-05) — aktuelle Architektur
Mit `src/cameraSwitch.js` besitzt Node die Kameras direkt. Das `close`-Event des eigenen
FFmpeg-Kindprozesses ist der harte Beweis „FD geschlossen = Gerät frei". Keine go2rtc-
Abhängigkeit, kein Race.
**Sofort gemessene Werte:**
- Latenz: 139 ms (vorher ~340 ms mit go2rtc)
- CPU idle: ~5 % (On-Demand)
- CPU aktiv: ~35 %/Kamera (copybsf)
- HD-Grab beide Kameras parallel: ~2,3 s, je 1280×960
Alle weiteren Details: aktuelle Architektur-Abschnitte oben.