Draft compression

This commit is contained in:
chk
2026-06-16 21:39:19 +02:00
parent c6711de922
commit 6d558fd5a6

248
doc/streamCompression.md Normal file
View File

@@ -0,0 +1,248 @@
# 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.