Files
appRobotWebcam/doc/streamCompression.md
2026-06-16 21:39:19 +02:00

249 lines
17 KiB
Markdown
Raw Permalink 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.
# Stream-Komprimierung (MJPEG → H.264) — Abarbeitungsliste
> **Status (2026-06-16):** Der H.264-Pfad ist **im Code vollständig vorhanden und
> unit-getestet**, aber **noch nie auf dem Host scharf geschaltet oder gemessen**.
> Diese Datei ist die ausführbare ToDo-Liste, um ihn in Betrieb zu nehmen — destilliert
> aus [14_ReRender_roadmap.md](14_ReRender_roadmap.md) (Hintergrund/Entwurf),
> [02_HardwareEncoding.md](02_HardwareEncoding.md), [03_Protocoll_roadmap.md](03_Protocoll_roadmap.md)
> und [04_Delay_roadmap.md](04_Delay_roadmap.md).
>
> **Es ist also kein „von Null bauen".** Die offene Arbeit ist: *scharf schalten → messen →
> tunen → ausrollen* — und zwar **auf dem Host gemessen, nicht vorhergesagt**
> (Memory-Regel; alle Zahlen unten ohne Messung sind ausdrücklich **Hypothesen**).
---
## Ausgangslage in einem Satz
Der Live-Stream geht heute als **MJPEG** raus (jedes Frame ein vollständiges JPEG). Das ist
zwar pro Frame komprimiert, aber ohne Inter-Frame-Kompression → hohe Bitrate, und der Client
muss jedes Frame einzeln dekodieren und in ein `<img>` schieben. Bei mehreren Kameras bringt
das schwache Laptops an die Grenze. Mehr Kameras kommen → das Problem wächst.
## Warum H.264 dem Laptop hilft (Motivation + eine Korrektur)
„Unkomprimiert" trifft es nicht ganz — MJPEG **ist** komprimiert, nur eben **intra-frame**.
Der Gewinn von H.264 ist trotzdem real und doppelt:
1. **Inter-Frame-Kompression** (nur Bildänderungen übertragen) → deutlich weniger Bitrate
(Hypothese: ~25 MBit/s statt MJPEG-Bitrate; vor dem Umbau messen, siehe ToDo 0).
2. **Hardware-Decode im Browser** — H.264 dekodiert der Client in der GPU; MJPEG dekodiert
er pro Frame auf der CPU/im Main-Thread und tauscht das `<img>`. Genau **das** ist die
Last, die mehrere Streams auf dem Laptop erzeugen. → H.264 entlastet primär den Client.
> ⚠️ **Realität prüfen, nicht annehmen:** Alle Kameras laufen aktuell auf `liveSize`
> **320×240** ([../cameras.json](../cameras.json)). `1920x1080` dort ist die `hiresSize`
> (Einzelbild beim HD-Knopf), **kein** Dauerstream. Bei 320×240 ist die MJPEG-Bitrate schon
> klein → der Bandbreiten-Gewinn könnte gering sein, der **Client-Decode-Gewinn** aber
> trotzdem zählen. Das entscheidet ToDo 0.
---
## Was bereits im Code steckt (Datei-Pointer)
| Baustein | Datei | Zustand |
|---|---|---|
| Encoder-Wahl (VAAPI/QSV/libx264) + MSE-Codec-String + FFmpeg-Args | [../src/hwencode.js](../src/hwencode.js) | ✅ + Unit-Test [../test/hwencode.test.js](../test/hwencode.test.js) |
| fMP4-Box-Parser (Init-Segment + Fragmente) | [../src/fmp4Parser.js](../src/fmp4Parser.js) | ✅ + Unit-Test [../test/fmp4Parser.test.js](../test/fmp4Parser.test.js) |
| `encode='h264'`-Zweig: Init-Cache, Fan-out, MJPEG-Nebenausgang (fd 3) für Snapshots | [../src/cameraSwitch.js](../src/cameraSwitch.js) | ✅ |
| `video/mp4`-Route (Init-first Fan-out), `encode`/`mseCodec` in `/api/snapshot`+`/api/cameras` | [../src/snapshotService.js](../src/snapshotService.js) | ✅ |
| MSE-`<video>`-Player + Feature-Detection + Snapshot-Fallback + Live-Edge-Tuning | [../public/viewer.js](../public/viewer.js) | ✅ |
| Per-Kamera-Umschaltung „MJPEG / H.264 (GPU)" in der Config-UI | [../public/config.js](../public/config.js), [../src/configService.js](../src/configService.js) | ✅ |
| `/dev/dri`-Passthrough, VA-Treiber-Install beim Start, `LIBVA_DRIVER_NAME=i965`, alle H264-Env | [../docker-compose.yaml](../docker-compose.yaml) | ✅ |
| Verdrahtung (`resolveHwenc`, `H264`-Tuning, `mseCodec`) | [../server.js](../server.js) | ✅ |
**Transport-Entscheidung (steht):** MSE-fMP4, **nicht** WebRTC. WebRTC würde die bewusst
entfernte go2rtc-/Signaling-Maschinerie zurückholen. MSE erhält „Node besitzt die Kameras"
(Node → ffmpeg → Byte-Stream → Browser). Details: [14_ReRender_roadmap.md](14_ReRender_roadmap.md).
**Umschalten ist pro Kamera:** `encode` in [../cameras.json](../cameras.json) oder per UI
(`config.html`). Default bleibt MJPEG → für den LAN-Fall ändert sich nichts.
---
## Eiserne Regeln (gelten weiter — aus [04](04_Delay_roadmap.md)/[09](09_Bug_reports.md))
1. **Der Live-Stream hat absolute Priorität.** Im Zweifel kein Feature statt wackliger Stream.
2. **Auf dem Host messen, nicht vorhersagen.** Jede Bitrate/CPU/Latenz-Zahl ohne Messung ist Hypothese.
3. **Eine USB-Kamera = ein Öffner.** Der `CameraSwitch` bleibt einziger Geräte-Öffner; nie ein zweiter FFmpeg parallel.
4. **Config-Änderung + Rollback statt riskanter Laufzeit-Mutation.** Encode pro Kamera umschalten ist erlaubt, wenn die *andere* Kamera auf bekanntem gutem Stand bleibt und eine Rollback-Zeile existiert.
5. **Smoke-Test mutiert `cameras.json`** ([Memory](../memory/MEMORY.md)) — `POST /api/config` überschreibt die Datei. Nicht gegen die echte Config testen, ohne sie vorher zu sichern.
---
## ToDo-Liste
Legende: ⬜ offen · 🧪 nur auf dem Host verifizierbar (echte Kamera + GPU). Reihenfolge ist
bewusst: erst messen ob es sich lohnt, dann eine Kamera scharf schalten, dann tunen, dann ausrollen.
---
### Phase 0 — Lohnt es sich, und läuft die Basis?
#### ⬜ 0.1 🧪 Ist-Bandbreite & Client-Last des heutigen MJPEG-Streams messen
**Aktion:** Auf dem Host die Bitrate eines Live-Streams bei realer `liveSize` (320×240) messen,
bei 1 und bei n Clients; parallel die CPU-Last des empfangenden Laptops (Browser/OS-Taskmanager)
bei 1, 2, 3 … Streams notieren.
```bash
# Bitrate grob: 10 s Stream ziehen, Bytes messen
curl -s -o /dev/null -w '%{size_download} bytes in %{time_total}s\n' \
--max-time 10 http://<host>:8444/api/stream/cam0
# oder Netz-I/O des Containers:
docker stats --no-stream AppRobotWebcam
```
**Risiken:** Ohne Zahl baut man eventuell viel um für wenig Gewinn (bei 320×240 evtl. marginal).
Der Client-Last-Test ist der eigentliche Entscheider (das ist das Nutzer-Problem), nicht die Bitrate allein.
**Test/Entscheidung:** Tabelle MJPEG vs. (später) H.264 — Bitrate **und** Laptop-CPU pro Stream.
Lohnt sich nur, wenn die Client-CPU mit der Stream-Zahl klar hochläuft. **Gate:** nur weiter, wenn der Gewinn plausibel ist.
#### ⬜ 0.2 🧪 GPU/VAAPI im Container verifizieren (H.264-Encode überhaupt verfügbar?)
**Aktion:** Bestätigen, dass der Container den VA-Treiber laden und H.264 encoden kann.
```bash
docker exec AppRobotWebcam vainfo # erwartet: VAProfileH264* mit VAEntrypointEncSlice
docker exec AppRobotWebcam ls -l /dev/dri/ # renderD128 vorhanden?
docker logs AppRobotWebcam 2>&1 | grep -i -E "VA-Treiber|vainfo|H.264-GPU"
```
**Risiken:**
- VA-Treiber-Install beim Start schlägt **still** fehl (kein Netz/Paketquelle) → App läuft, aber H.264 ist tot (Compose loggt nur `WARN`).
- `i965` vs `iHD`: UHD 630 ist mit `LIBVA_DRIVER_NAME=i965` verdrahtet ([../docker-compose.yaml](../docker-compose.yaml)). Andere/AMD-Box braucht `radeonsi` + `mesa-va-drivers`.
- Auf Synology DSM existiert die `render`-Gruppe nicht (bewusst entfernt); Zugriff läuft über root. Auf der Ziel-Box (Lenovo i5/UHD 630) prüfen, ob das `video`-Group-Mapping + `/dev/dri` für den Node-User reicht.
**Test:** `vainfo` listet H264-Encode-Entrypoint → grünes Licht. Sonst Treiber/Env fixen, **bevor** eine Kamera auf h264 geht.
---
### Phase 1 — Eine Kamera scharf schalten
#### ⬜ 1.1 🧪 Genau eine Kamera auf `encode='h264'` und Bild im Browser prüfen
**Aktion:** **Eine** Kamera umschalten — am sichersten über die Config-UI (`http://<host>:8444/config.html`
→ Spalte Encode → „H.264 (GPU)" → speichern), die anderen auf MJPEG lassen. Alternativ `encode: "h264"`
am Eintrag in [../cameras.json](../cameras.json) + Redeploy. Vorher `cameras.json` sichern (Regel 5).
Erwartete Wirkung im Code: `reconfigure()` killt den Live-FFmpeg und startet ihn als H.264 neu (Hot-Reload);
der Viewer baut für diese Kamera ein `<video>`+MSE statt `<img>`.
**Risiken:**
- **Schwarzes/leeres `<video>`**: MSE-Codec-String passt nicht zu dem, was FFmpeg liefert (Profil/Level). Stellschraube `H264_MSE_CODEC` / `H264_PROFILE` (siehe 2.2). Der Viewer fällt bei nicht unterstütztem Codec automatisch auf den Snapshot-Modus zurück (kein schwarzes Bild, aber auch kein Video).
- **FFmpeg startet nicht** (`encode=h264 ohne hwenc-Konfig` oder VAAPI-Init-Fehler) → Auto-Restart-Schleife. Im Log sichtbar.
- **Andere Kamera nicht anfassen** — Live-Priorität (Regel 1/4). Rollback = Encode der Test-Kamera zurück auf MJPEG.
**Test:** Viewer zeigt flüssiges Live-Bild für die Test-Kamera; Statuszeile „H.264 · live".
Codec im Log prüfen:
```bash
docker logs AppRobotWebcam 2>&1 | grep -i -E "live gestartet|h264_vaapi|encode=h264"
```
#### ⬜ 1.2 🧪 Server-Last messen: encodet wirklich die GPU — und was kostet der MJPEG-Decode?
**Aktion:** `docker stats` für die Test-Kamera im H.264-Modus vs. MJPEG-Modus vergleichen.
**Wichtig (aus [14](14_ReRender_roadmap.md)):** Im h264-Modus muss FFmpeg das USB-**MJPEG erst dekodieren**
(CPU), bevor die GPU H.264 encodet (`format=nv12,hwupload`). Im copybsf-Modus gibt es **gar keinen Decode**.
H.264 kann also die Server-CPU **erhöhen**, obwohl die GPU encodet — das ist zu messen, nicht zu raten.
```bash
docker stats --no-stream AppRobotWebcam
# optional, falls verfügbar, GPU-Auslastung:
docker exec AppRobotWebcam sh -c 'command -v intel_gpu_top && intel_gpu_top -s 1000 || echo "kein intel_gpu_top"'
```
**Risiken:** „GPU-Encode = wenig CPU" ist eine **Hypothese**; der zusätzliche MJPEG-Decode kann sie kippen.
Bei höheren `liveSize` steigt die Decode-Last überproportional.
**Test:** CPU-Delta MJPEG↔H.264 dokumentieren; im FFmpeg-Log `h264_vaapi` bestätigen (nicht `libx264`).
Fällt der Encoder heimlich auf `libx264` zurück → CPU explodiert → Treiber/`HWENC` prüfen.
---
### Phase 2 — Latenz & Qualität tunen
#### ⬜ 2.1 🧪 Latenz H.264/MSE vs. MJPEG messen
**Aktion:** Stoppuhr-Foto-Methode aus [03_Protocoll_roadmap.md](03_Protocoll_roadmap.md): Handy-Stoppuhr (ms)
vor die Kamera, MJPEG- und H.264-Kamera nebeneinander, ein Foto von Monitor + Stoppuhr → Differenz ablesen.
Referenz heute: MJPEG ~139 ms (Kamera→Browser).
**Risiken:** MSE puffert (Init-Segment + Fragment-Dauer + Browser-Jitter-Buffer). Erwartung: H.264 hat **mehr**
Latenz als der MJPEG-Schalter. Der Viewer hält die Latenz klein, indem er an die „Live-Kante" springt
(`H264_MAX_LAG_S` in [../public/viewer.js](../public/viewer.js)) — aggressiver = niedrigere Latenz, mehr Ruckler-Risiko.
**Test:** Gemessene ms in eine Tabelle MJPEG vs. H.264. Entscheiden, ob die Mehrlatenz für die
Roboter-Überwachung vertretbar ist (bei reiner Überwachung meist ja).
#### ⬜ 2.2 🧪 Bitrate / GOP / Profil / Fragmentlänge nachjustieren
**Aktion:** Über Env in [../docker-compose.yaml](../docker-compose.yaml) tunen (Defaults in [../server.js](../server.js)):
| Env | Default | Wirkung |
|---|---|---|
| `H264_BITRATE` | `3M` | Zielbitrate ↓ = weniger Bandbreite, mehr Artefakte |
| `H264_GOP` | ~2×fps | Keyframe-Abstand; kleiner = schnellerer Einstieg/Reconnect, mehr Bitrate |
| `H264_PROFILE` | `main` | `constrained_baseline`/`main`/`high` — muss zum Treiber **und** zum MSE-Codec passen |
| `H264_FRAG_MS` | `200` | fMP4-Fragmentlänge; kleiner = niedrigere Latenz, mehr Overhead |
| `H264_MSE_CODEC` | aus Profil/Level abgeleitet | nur setzen, wenn der Browser den abgeleiteten String ablehnt (z. B. `avc1.640020`) |
**Risiken:** Profil/Level (Server) und MSE-Codec-String (Browser) müssen zusammenpassen, sonst schwarzes
Video / `addSourceBuffer`-Fehler → Snapshot-Fallback. Zu kleine GOP/Fragmente erhöhen Bitrate/CPU wieder.
**Test:** Nach jeder Änderung Bild + Latenz + Bitrate gegenprüfen (ToDo 0.1/2.1 wiederholen). Eine Stellschraube pro Durchgang.
---
### Phase 3 — Snapshot & HD-Grab im H.264-Modus verifizieren
#### ⬜ 3.1 🧪 `/api/snapshot/:id` liefert weiter JPEG, während die Kamera H.264 streamt
**Aktion:** Während die Test-Kamera live H.264 läuft, `GET /api/snapshot/<id>` abrufen.
Hintergrund: Der h264-FFmpeg hat einen **MJPEG-Nebenausgang** (fd 3, gedrosselt auf `H264_JPEG_FPS=2`),
der `latest` für Snapshots füllt ([../src/cameraSwitch.js](../src/cameraSwitch.js), [../src/hwencode.js](../src/hwencode.js)).
**Wichtig fürs Homing-Projekt:** dessen Snapshot-Abruf muss unverändert weiterlaufen.
**Risiken:** Snapshot ist nur ~2 fps frisch (Nebenausgang gedrosselt) — für ein Standbild ok, für „live"-Polling
nicht. Der `split=2`-Filter kostet etwas zusätzlichen CPU (zweiter, billiger MJPEG-Encode).
**Test:** `curl -o snap.jpg http://<host>:8444/api/snapshot/<id>` → valides JPEG in `liveSize`. Homing-Abruf gegenprüfen.
#### ⬜ 3.2 🧪 HD-Grab (`/hires`) im H.264-Modus: Blackout + sauberer Reconnect
**Aktion:** HD-Knopf bzw. `GET /api/snapshot/<id>/hires` auf der H.264-Kamera auslösen.
Ablauf im Code: `grabHires` killt den live-H.264-FFmpeg (`close` = FD frei) → greift HD-JPEG (`hiresEncode`
fällt für h264 automatisch auf `copybsf` → reines Kamera-JPEG, keine H.264-Artefakte) → startet H.264 neu.
Der Neustart erzeugt ein **neues Init-Segment** → die Route beendet bestehende MSE-Verbindungen (`onReinit`),
der Browser verbindet automatisch neu.
**Risiken:** Sichtbarer Blackout + MSE-Reconnect (~23 s) für Zuschauer dieser einen Kamera — wie der
bekannte HD-Blackout, nur mit zusätzlichem Player-Neuaufbau. Reconnect-Logik (`onReinit` + Viewer-Retry)
muss greifen, sonst bleibt das `<video>` stehen.
**Test:** HD-Bild wird heruntergeladen (volle `hiresSize`, scharf, keine H.264-Artefakte); das `<video>`
läuft nach dem Grab von selbst weiter. Andere Kameras unbeeinflusst.
---
### Phase 4 — Mehr Kameras / Rollout
#### ⬜ 4.1 🧪 Mehrere Kameras gleichzeitig auf H.264 — GPU- und USB-Kapazität
**Aktion:** Schrittweise eine zweite, dann dritte Kamera auf H.264 schalten, jeweils CPU/GPU/Bild messen.
**Risiken:** „Schwache OnBoard-Graphik" (UHD 630): Quick Sync schafft i. d. R. mehrere parallele H.264-Encodes,
aber die **CPU-MJPEG-Decodes summieren sich** (jede h264-Kamera dekodiert ihr USB-MJPEG auf der CPU).
USB-Bandbreite ist eine separate Grenze (siehe [07_multipleCam_roadmap.md](07_multipleCam_roadmap.md), `lsusb -t`).
**Test:** Mit jeder zugeschalteten H.264-Kamera CPU/GPU + Bild prüfen. Die Zahl finden, ab der es kippt → dokumentieren.
#### ⬜ 4.2 🧪 Der eigentliche Beweis: Client-Last sinkt
**Aktion:** Auf dem Ziel-Laptop denselben Mehr-Kamera-View einmal in MJPEG, einmal in H.264 öffnen und die
Browser-/OS-CPU vergleichen (das war der Auslöser des ganzen Umbaus).
**Risiken:** Wenn die Client-CPU **nicht** sinkt, lohnt der Server-Mehraufwand (Phase 1.2/4.1) nicht — dann
neu abwägen (z. B. nur Bitrate senken statt H.264). Ältere Browser ohne MSE → Snapshot-Fallback (deutlich schlechter).
**Test:** Laptop-CPU-Tabelle MJPEG vs. H.264 bei n Kameras. Erfolg = klar niedrigere Client-Last bei akzeptabler Latenz/Qualität.
#### ⬜ 4.3 Rollout-Strategie + Rollback festhalten
**Aktion:** Festlegen, welche Kameras dauerhaft H.264 fahren (z. B. die mit Dauer-Zuschauern), welche MJPEG
bleiben (LAN/niedrige Latenz). Default in [../cameras.json](../cameras.json) entsprechend setzen.
**Risiken:** Inkonsistenz zwischen UI-Umschaltung (persistiert in `cameras.json`) und Env-Defaults; Verwechslung
`encode` (Live) ↔ `hiresEncode` (Grab bleibt JPEG).
**Test:** Nach Redeploy `GET /api/cameras` prüfen (`encode`/`mseCodec` pro Kamera korrekt). Rollback = `encode`
zurück auf `copybsf` (UI oder Datei) — der MJPEG-Pfad ist unverändert und immer verfügbar.
---
### Phase 5 — Optional / später
-**AMD-Box gegenprüfen**, falls sie Zielhardware wird: `GPU=amd`, `LIBVA_DRIVER_NAME=radeonsi`, `mesa-va-drivers` in der Compose-`command`-Zeile ergänzen, `vainfo` gegenprüfen.
-**MSE-Watchdog**: eingefrorenes `<video>` ohne Fehler-Event erkennen und neu verbinden (Analogon zum offenen MJPEG-Freeze-Watchdog in [09_Bug_reports.md](09_Bug_reports.md)).
-**Bitrate-Re-Messung nach Tuning** → endgültige Bandbreiten-Zahl für die Doku (ersetzt die Hypothesen oben).
-**`H264_JPEG_FPS` anheben**, falls das Homing-Projekt frischere Snapshots braucht (kostet etwas CPU).
---
## Schnell-Rollback (jederzeit)
1. **Eine Kamera:** Encode in `config.html` zurück auf „MJPEG" (oder `encode` aus dem `cameras.json`-Eintrag entfernen) → Hot-Reload, MJPEG-`<img>`-Pfad sofort zurück.
2. **Komplett:** `cameras.json` aus der Sicherung zurückspielen + Redeploy. Der MJPEG-Schalter ist der unveränderte, bekannte gute Stand.
3. **GPU/Treiber kaputt:** H.264 startet nicht → App läuft trotzdem auf MJPEG weiter (Compose-`command` bricht bei Treiberfehler nicht ab). Kein Live-Ausfall.
## Offene Entscheidungen (vor Phase 1 klären, falls relevant)
1. **Lohnt es sich** bei der realen `liveSize`? → Gate in ToDo 0.1/4.2 (Client-Last ist der Maßstab).
2. **Zielhardware** wirklich die Lenovo-i5/UHD-630-Box (i965)? Falls AMD → Phase 5.
3. **Welche `liveSize`** für H.264-Kameras? Höher als 320×240 wird mit H.264 erst sinnvoll/erschwinglich — gemeinsam mit 2.2 entscheiden.