10 KiB
📦 Re-Render & Compress (H.264)
Status: Entwurf / noch nichts implementiert. Alle Zahlen unten sind Hypothesen, bis sie auf dem Host gemessen sind (siehe Phase 0).
Problem
MJPEG überträgt jedes Frame als vollständiges JPEG → die Bandbreite skaliert linear mit Auflösung × Framerate × Clients. Bei höheren Auflösungen oder mehreren gleichzeitigen Clients kann das im WAN / mobil teuer werden.
⚠️ Wichtig – aktuelle Realität prüfen, nicht annehmen:
Die Live-Streams laufen derzeit auf 320×240 (liveSize pro Kamera in
cameras.json), nicht 1080p. Das 1920x1080 dort ist die
hiresSize — also das Einzelbild beim HD-Knopf bzw. im Snapshot-Modus, kein
Dauerstream. Die oft zitierten „~30 MBit/s" gelten also bestenfalls für einen
hypothetischen 1080p-Dauerstream, nicht für den heutigen Betrieb.
👉 Bevor hier irgendwas gebaut wird, gilt Phase 0: messen. Ohne belastbare Bandbreiten-Zahl ist unklar, ob sich der ganze Umbau überhaupt lohnt.
Ziel
Reduktion der Bandbreite durch optionale, pro Kamera schaltbare Neukodierung
MJPEG → H.264 (GPU), ohne die schlanke Default-Architektur (Node besitzt die
Kameras, MJPEG-Passthrough, <img>-Viewer) für den LAN-Fall aufzugeben.
Zielbitrate (Hypothese): ~2–5 MBit/s bei vergleichbarer wahrgenommener Qualität.
⚠️ Der eigentliche Knackpunkt zuerst: Wiedergabe im Browser
Das ist der Teil, der den Umbau groß macht — und der im ersten Entwurf als „UI-Checkbox" verharmlost war.
Der aktuelle Viewer rendert den Stream in einem <img>
(public/viewer.js), gespeist aus
multipart/x-mixed-replace (src/snapshotService.js).
Ein <img> kann ausschließlich MJPEG darstellen.
H.264 läuft niemals in einem
<img>. „Der Browser soll mit beidem umgehen" ist daher kein Schalter, sondern ein zweiter, vollständiger Wiedergabe-Pfad.
H.264 + MJPEG schließen sich auch im Transport gegenseitig aus — H.264 lässt sich nicht in MJPEG-multipart verpacken. Es braucht einen eigenen Container und einen eigenen Player. Optionen:
| Transport | Client | Aufwand | Latenz (Erwartung, zu messen) |
|---|---|---|---|
| MSE (fMP4) | <video> + MediaSource + JS-Feeder |
mittel | gut, mit Low-Latency-Tuning; sonst 200 ms–1 s Puffer |
| WebRTC | RTCPeerConnection + Signaling |
hoch | am niedrigsten |
| HLS/DASH | <video> / hls.js |
gering | Sekunden — für Live untauglich |
Achtung – Déjà-vu: WebRTC + H.264 ist genau das, was go2rtc gemacht hat und was bewusst entfernt wurde (siehe Architektur-Doku). WebRTC würde Signaling- Infrastruktur wieder einführen. Empfehlung: MSE-fMP4, weil es die „Node besitzt die Kameras"-Architektur erhält (Node → ffmpeg → Byte-Stream → Browser) und keine ICE/STUN/TURN-Maschinerie braucht. Endgültige Wahl erst nach der Latenz-Messung (Phase 0).
MSE-Besonderheit: Init-Segment für späte Clients
Anders als bei MJPEG (jedes Frame eigenständig) muss bei fragmentiertem MP4 ein
Client, der mitten im Stream dazukommt, zuerst das Init-Segment
(ftyp+moov) bekommen, dann die Media-Fragmente. Der Server muss das
Init-Segment also zwischenspeichern und jedem neuen Client zuerst schicken,
bevor er ihn in den Fan-out hängt. Das ist neue Logik gegenüber dem heutigen
„jedes Frame an jeden"-Modell.
Lösungsansatz: zwei Modi pro Kamera
1. 🟢 MJPEG-Passthrough (Default, unverändert)
- bestehender Pfad:
copybsf→mpjpeg→multipart→<img> - minimale Latenz, keine GPU-Abhängigkeit, ~5 % idle-CPU
- ideal im LAN / bei wenigen Clients
2. 🔵 H.264 (optional, GPU)
- MJPEG → H.264 (VAAPI/QSV) → fMP4 → MSE-
<video> - drastisch reduzierte Bandbreite, für mobil / WAN / viele Clients
- höhere Komplexität + GPU-Abhängigkeit + (zu messende) Zusatzlatenz
Der Browser wählt pro Kamera anhand der Server-Metadaten den richtigen Player
(<img> oder <video>).
Architektur-Entscheidung
- Encoding erfolgt direkt in Node via FFmpeg + GPU (kein go2rtc mehr).
- Kamera liefert weiterhin MJPEG; der
CameraSwitchbleibt einziger Geräte-Öffner. - Der Modus hängt am vorhandenen
encode-Feld (siehe Konfigurationsmodell).
Kamera (MJPEG, v4l2)
│
┌─┴─────────────────────────── encode = 'copybsf' | 'mjpeg'
│ ffmpeg -c:v copy -bsf mjpeg2jpeg -f mpjpeg
│ → multipart/x-mixed-replace → <img> (heutiger Pfad, unverändert)
│
└───────────────────────────── encode = 'h264'
ffmpeg -c:v h264_vaapi -f mp4 (fragmentiert)
→ Byte-Stream (+ gecachtes Init-Segment) → MSE → <video> (neu)
Konfigurationsmodell
Kein neues compress-Flag — das würde sich mit dem bestehenden Encode-Schalter
überschneiden. Stattdessen das vorhandene encode-Feld erweitern, das in
server.js und src/cameraSwitch.js bereits
pro Kamera verdrahtet ist:
encode |
Bedeutung |
|---|---|
copybsf |
Default, Bitstream-Copy, niedrigste CPU (heute) |
mjpeg |
Re-Encode MJPEG→MJPEG, Fallback (heute) |
h264 |
neu: GPU-H.264 → fMP4 (VAAPI/QSV, Auto-Erkennung) |
Beispiel cameras.json:
{
"id": "cam2",
"device": "/dev/video4",
"stream": true,
"encode": "h264",
"liveSize": "640x480"
}
hiresEncode bleibt davon unberührt (HD-Snapshot bleibt JPEG — sinnvoll, da ein
Einzelbild bandbreiten-unkritisch ist).
Technische Integration (was wirklich zu tun ist)
1. GPU in den Container durchreichen
/dev/driins Docker-devices(wie in doc/02_HardwareEncoding.md für die Intel-Box bestätigt:/dev/dri/renderD128vorhanden).- Encoder dynamisch wählen (
h264_vaapivs.h264_qsv) statt Hardcoding.
2. FFmpeg-H.264-Profil (Node spawnt direkt — #hardware von go2rtc gibt es nicht mehr)
Skizze (VAAPI, Werte in Phase 1 zu tunen/messen):
ffmpeg -fflags nobuffer \
-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i /dev/video4 \
-vaapi_device /dev/dri/renderD128 -vf 'format=nv12,hwupload' \
-c:v h264_vaapi -b:v 3M -g 60 \
-f mp4 -movflags +frag_keyframe+empty_moov+default_base_moof -frag_duration 100000 \
pipe:1
- MJPEG-Decode bleibt vorerst CPU (USB-MJPEG via VAAPI dekodieren ist wackelig) → die „nur GPU"-Erwartung in Phase 0/1 messen, nicht annehmen.
- Kurze GOP (
-g) +frag_durationklein = niedrige Latenz, mehr Overhead → Trade-off messen.
3. CameraSwitch erweitern (src/cameraSwitch.js)
Nicht „nur neue Args" — betroffen sind mehrere Stellen:
videoOutArgs()um denh264-Zweig erweitern (anderer Muxer als-f mpjpeg).- Der
MpjpegParserist MJPEG-spezifisch und greift hier nicht; für fMP4 wird der Byte-Stream durchgereicht (Init-Segment cachen, Media-Fragmente fan-out). - On-Demand / idle-Stop / Auto-Restart gelten weiter — die Pipeline startet wie heute erst bei Verbrauchern.
4. Neue Stream-Route (src/snapshotService.js)
createStreamRoutersendet heutemultipart/x-mixed-replace. Für H.264 braucht es eine Variante (oder zweite Route), dievideo/mp4als fortlaufenden Byte-Stream liefert und neuen Clients zuerst das Init-Segment schickt./api/camerasmuss den Modus (mjpeg|h264) mitliefern, damit der Viewer den Player wählen kann.
5. Viewer erweitern (public/viewer.js)
- Bei
encode==='h264':<video>+MediaSource+ SourceBuffer-Feeder statt<img>. - Auto-Fallback statt schwarzem Bild: client-seitig
MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E"')prüfen; bei fehlender Unterstützung sichtbare Meldung + automatischer Rückfall auf MJPEG, nicht stilles Schwarz.
6. UI (public/config.html)
- Statt Checkbox „Compress": Dropdown/Select für
encode(copybsf / mjpeg / h264).
Hardware-Bewertung (Erwartung — in Phase 0/1 zu bestätigen)
🖥️ Intel UHD 630 (Coffee Lake) — die heutige Box
- VAAPI / Quick Sync (H.264/H.265),
/dev/dri/renderD128bestätigt. - Erwartung: 1–2 H.264-Streams stabil, niedrige CPU bei GPU-Encode.
🖥️ AMD Radeon 680M (Rembrandt) — falls Zielhardware
- VAAPI / VCN 3.x; erwartet deutlich mehr Encode-Reserve.
- ⚠️ Erst prüfen, ob diese Box überhaupt Ziel ist und ob
/dev/dri+ VAAPI dort laufen — die bisherige Doku basiert auf der Intel-Box.
Design-Prinzipien
- ✅ Backward-compatible — Default bleibt MJPEG, nichts ändert sich für LAN.
- ✅ Pro Kamera schaltbar über das vorhandene
encode-Feld. - ✅ Node behält die Kameras — kein go2rtc/WebRTC-Rückbau.
- ⚠️ Low latency nur, wenn die Messung es bestätigt (MSE puffert).
- ⚠️ GPU optional — H.264-Kameras hängen an funktionierendem VAAPI/QSV.
Offene Entscheidungen
- Lohnt es sich überhaupt? → Phase 0 (Bandbreite der realen Live-Streams messen).
- Transport: MSE-fMP4 (empfohlen) oder WebRTC? → entscheidet Latenz + Aufwand, abhängig von Phase-0-Messung.
- Decode auf CPU oder GPU? → messen, ob USB-MJPEG-VAAPI-Decode stabil ist.
- Zielhardware Intel-Box oder AMD 680M?
Nächste Schritte (ToDo — ehrlicher Stand: nichts erledigt)
Phase 0 — Messen (Vorbedingung, blockiert alles andere)
- 🔲 Bandbreite der heutigen Live-Streams (320×240 bzw. konfigurierte
liveSize) auf dem Host messen, bei 1 und bei n Clients. - 🔲 Hypothese 1080p-Stream gegen die echte Zielauflösung abgleichen — lohnt der Umbau?
- 🔲 Transport-Latenz-Test: ein Test-fMP4-Stream (MSE) vs. heutiges MJPEG, Methode wie in doc/03_Protocoll_roadmap.md (Stoppuhr-Foto).
Phase 1 — Encode-Pfad (nur wenn Phase 0 positiv)
4. 🔲 /dev/dri-Passthrough + VAAPI/QSV-Auto-Erkennung.
5. 🔲 FFmpeg-H.264-fMP4-Profil definieren; CPU/GPU/Latenz auf dem Host messen.
Phase 2 — Server
6. 🔲 encode='h264' in videoOutArgs / CameraSwitch (inkl. Init-Segment-Cache + Byte-Stream-Fan-out).
7. 🔲 H.264-Stream-Route + Modus in /api/cameras.
Phase 3 — Client
8. 🔲 MSE-<video>-Player + Feature-Detection + Auto-Fallback auf MJPEG.
9. 🔲 config.html: encode-Auswahl.
Phase 4 — Verifikation 10. 🔲 Bandbreite & CPU/GPU vorher/nachher vergleichen (gemessen, auf dem Host).