17 KiB
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:
- Inter-Frame-Kompression (nur Bildänderungen übertragen) → deutlich weniger Bitrate (Hypothese: ~2–5 MBit/s statt MJPEG-Bitrate; vor dem Umbau messen, siehe ToDo 0).
- 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
liveSize320×240 (../cameras.json).1920x1080dort ist diehiresSize(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)
- Der Live-Stream hat absolute Priorität. Im Zweifel kein Feature statt wackliger Stream.
- Auf dem Host messen, nicht vorhersagen. Jede Bitrate/CPU/Latenz-Zahl ohne Messung ist Hypothese.
- Eine USB-Kamera = ein Öffner. Der
CameraSwitchbleibt einziger Geräte-Öffner; nie ein zweiter FFmpeg parallel. - 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.
- 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). i965vsiHD: UHD 630 ist mitLIBVA_DRIVER_NAME=i965verdrahtet (../docker-compose.yaml). Andere/AMD-Box brauchtradeonsi+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 dasvideo-Group-Mapping +/dev/drifür den Node-User reicht. Test:vainfolistet 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). StellschraubeH264_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-Konfigoder 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 (~2–3 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-driversin der Compose-command-Zeile ergänzen,vainfogegenprü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_FPSanheben, falls das Homing-Projekt frischere Snapshots braucht (kostet etwas CPU).
Schnell-Rollback (jederzeit)
- Eine Kamera: Encode in
config.htmlzurück auf „MJPEG" (oderencodeaus demcameras.json-Eintrag entfernen) → Hot-Reload, MJPEG-<img>-Pfad sofort zurück. - Komplett:
cameras.jsonaus der Sicherung zurückspielen + Redeploy. Der MJPEG-Schalter ist der unveränderte, bekannte gute Stand. - GPU/Treiber kaputt: H.264 startet nicht → App läuft trotzdem auf MJPEG weiter (Compose-
commandbricht bei Treiberfehler nicht ab). Kein Live-Ausfall.
Offene Entscheidungen (vor Phase 1 klären, falls relevant)
- Lohnt es sich bei der realen
liveSize? → Gate in ToDo 0.1/4.2 (Client-Last ist der Maßstab). - Zielhardware wirklich die Lenovo-i5/UHD-630-Box (i965)? Falls AMD → Phase 5.
- Welche
liveSizefür H.264-Kameras? Höher als 320×240 wird mit H.264 erst sinnvoll/erschwinglich — gemeinsam mit 2.2 entscheiden.