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

17 KiB
Raw Blame History

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 (Hintergrund/Entwurf), 02_HardwareEncoding.md, 03_Protocoll_roadmap.md und 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). 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 + Unit-Test ../test/hwencode.test.js
fMP4-Box-Parser (Init-Segment + Fragmente) ../src/fmp4Parser.js + Unit-Test ../test/fmp4Parser.test.js
encode='h264'-Zweig: Init-Cache, Fan-out, MJPEG-Nebenausgang (fd 3) für Snapshots ../src/cameraSwitch.js
video/mp4-Route (Init-first Fan-out), encode/mseCodec in /api/snapshot+/api/cameras ../src/snapshotService.js
MSE-<video>-Player + Feature-Detection + Snapshot-Fallback + Live-Edge-Tuning ../public/viewer.js
Per-Kamera-Umschaltung „MJPEG / H.264 (GPU)" in der Config-UI ../public/config.js, ../src/configService.js
/dev/dri-Passthrough, VA-Treiber-Install beim Start, LIBVA_DRIVER_NAME=i965, alle H264-Env ../docker-compose.yaml
Verdrahtung (resolveHwenc, H264-Tuning, mseCodec) ../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.

Umschalten ist pro Kamera: encode in ../cameras.json oder per UI (config.html). Default bleibt MJPEG → für den LAN-Fall ändert sich nichts.


Eiserne Regeln (gelten weiter — aus 04/09)

  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) — 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.

# 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.

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). 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 + 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:
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): 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.

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: 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) — 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 tunen (Defaults in ../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/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, 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 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).
  • 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.