12 KiB
📦 Re-Render & Compress (H.264)
Status: Code implementiert (Phasen 1–3), unit-getestet. Noch NICHT auf dem Host verifiziert — die FFmpeg-VAAPI/QSV-Pipeline lief bisher nur in Tests, nicht gegen echte Kameras/GPU. Alle Bitraten/Latenz-Zahlen bleiben Hypothesen bis zur Messung auf der Intel- bzw. AMD-Box (Phase 0 + 4).
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)
Legende: ✅ Code fertig & unit-getestet · 🧪 nur auf dem Host verifizierbar · 🔲 offen
Phase 0 — Messen (Vorbedingung; auf User-Wunsch parallel zur Implementierung)
- 🔲 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: H.264/MSE vs. heutiges MJPEG, Methode wie in doc/03_Protocoll_roadmap.md (Stoppuhr-Foto).
Phase 1 — Encode-Pfad
4. ✅ /dev/dri-Passthrough (docker-compose.yaml) + GPU-Auswahl GPU=intel|amd|none / HWENC (src/hwencode.js).
5. ✅ FFmpeg-H.264-fMP4-Profil (src/hwencode.js). · 🧪 CPU/GPU/Latenz/Bitrate auf Intel- UND AMD-Box messen + Profil/Level ggf. anpassen.
Phase 2 — Server
6. ✅ encode='h264' in src/cameraSwitch.js (fMP4 + Init-Segment-Cache + MJPEG-Nebenausgang für Snapshots) + src/fmp4Parser.js.
7. ✅ H.264-Stream-Route (video/mp4, Init-first, Fan-out) + Modus/mseCodec in /api/snapshot & /api/cameras (src/snapshotService.js).
Phase 3 — Client
8. ✅ MSE-<video>-Player + isTypeSupported-Feature-Detection + Snapshot-Fallback (public/viewer.js).
9. ✅ config.html/config.js: Kompressions-Auswahl (MJPEG/H.264) + encode durch src/configService.js.
Phase 4 — Verifikation (auf dem Host, mit echten Kameras + GPU)
10. 🧪 Eine Kamera testweise auf encode='h264' (UI oder GPU=intel/amd setzen), Bild im Browser prüfen, Codec-Log (h264_vaapi/h264_qsv) kontrollieren.
11. 🧪 Bandbreite & CPU/GPU MJPEG vs. H.264 vergleichen; Profil (H264_PROFILE/H264_MSE_CODEC), GOP & Bitrate nachjustieren.
12. 🧪 AMD-Box gegenprüfen (GPU=amd), falls sie Zielhardware ist.