7.7 KiB
AppRobotWebcam – Delay / Ruckler-Analyse
Symptom
Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils 1–2 s ein, manchmal bleibt ein Einzelbild ganz stehen. Im reinen MJPEG-Modus trat das nicht auf.
Diagnose-Verlauf
Schritt 1 — CPU-Messung (erste Verdachtsphase)
| Quelle | CPU |
|---|---|
| System gesamt | ~40 % |
| AppRobotGo2RTC, 1 Client | ~35 % |
| AppRobotGo2RTC, 2 Clients (Laptop + Handy) | 65–114 % |
| AppRobotWebcam (Node.js) | 0 % |
docker stats rechnet pro Kern: 114 % = mehr als ein Kern voll ausgelastet.
Erkenntnis: go2rtc re-encodiert nicht einmal pro Stream, sondern aufwändiger pro Client-Verbindung (WebRTC-Session). Zwei Clients = fast doppelte CPU-Last.
Schritt 2 — Browser-Client als Ursache ausgeschlossen
WebRTC getStats() lieferte über mehrere Minuten:
recv=30/s decoded=30/s dropped=0/s lost=+0 jitter=13–35ms
→ Server liefert alle Frames, Netz verliert nichts, Decoder schafft alles. Der Browser ist nicht das Problem.
Schritt 3 — Netz als Ursache ausgeschlossen
Zwei Browser-Fenster (Laptop + Handy) zeigen exakt dieselbe Verzögerung für cam0 bzw. cam1 — synchron auf die Millisekunde. Ändert sich die Latenz von cam0, ändert sie sich auf beiden Clients gleichzeitig.
→ Das Problem sitzt in go2rtc/FFmpeg, nicht im Netz oder Browser.
Schritt 4 — Root Cause: FFmpeg-Flags und exec timeout
go2rtc generiert intern folgenden FFmpeg-Befehl:
-readrate_initial_burst 0.001 -re -i /dev/video2
-c:v libx264 -g 50 -profile:v high -preset:v superfast -tune:v zerolatency
Zwei Probleme identifiziert:
Problem A — -re (Rate-Emulation für Live-Input):
-re = „lies Input im Echtzeit-Takt". Für Datei-Wiedergabe gedacht.
Für eine Live-Kamera (die ohnehin Echtzeit-Frames liefert) puffert -re
Frames künstlich, statt sie sofort durchzureichen. Wenn der Encoder unter
Last minimal in Rückstand gerät, baut sich ein Puffer auf → variable Latenz.
-readrate_initial_burst 0.001 macht den Start besonders langsam → erklärt
den langsamen Stream-Aufbau.
Problem B — -g 50 (Keyframe-Abstand 1,67 Sekunden bei 30 fps):
H.264 überträgt zwischen Keyframes nur Differenzbilder. Der Browser kann erst
ab einem Keyframe decodieren. Nach jedem Paket-Verlust oder Neuverbindung
wartet der Browser bis zu 1,67 s auf den nächsten Keyframe → Standbild.
Da cam0 und cam1 ihre Keyframe-Takte unabhängig haben, friert mal der eine,
mal der andere ein — aber auf allen Clients gleichzeitig (wegen Schritt 3).
Problem C — ERR [exec] timeout für /dev/video2 (cam1):
go2rtc's FFmpeg für cam1 läuft gelegentlich in einen Timeout (Kamera-Init
zu langsam, USB-Bandbreitenproblem, Treiberproblem). go2rtc startet den
Encoder neu → cam1 friert für mehrere Sekunden ein, während cam0 läuft.
Was bisher versucht wurde
| Massnahme | Ergebnis |
|---|---|
#video=h264#video=mjpeg entfernt → nur #video=h264 |
CPU-Last von ~95% auf ~35% reduziert |
getVideoPlaybackQuality() als Überlast-Detektor |
Fehlalarm (misst Render-Drops, nicht echte Überlast) |
Umstieg auf getStats() (inbound-rtp) |
Verlässlich, bestätigt: Client ist nicht das Problem |
Aufwärmphase (15s nach playing) in Browser-Überwachung |
Fehlalarme beim Stream-Aufbau beseitigt |
Ursachen-Zusammenfassung
| Ursache | Symptom | Behebbar ohne go2rtc-Patch? |
|---|---|---|
-re + -readrate_initial_burst 0.001 |
Variable Latenz, langsamer Aufbau | Ja (anderer Source-Typ) |
-g 50 (1,67s GOP) |
Bis zu 1,67s Standbild | Ja (exec: mit eigenem FFmpeg) |
| Software-H.264 × 2 Kameras × n Clients | CPU-Sättigung ab 2 Clients | Ja (Hardware-Encode) |
| cam1 FFmpeg timeout | Multi-Sekunden-Freeze cam1 | Teilweise (v4l2: Source) |
go2rtc kann diese FFmpeg-Flags nicht per einfacher URL-Syntax konfiguriert werden.
Sie sind hard-coded im ffmpeg: Source-Handler von go2rtc 1.9.x.
Lösungsweg — geordnet nach Aufwand/Wirkung
Option A — v4l2: Source statt ffmpeg: (sofort probieren)
go2rtc hat einen nativen v4l2-Treiber, der FFmpeg für den Capture umgeht:
streams:
cam0: "v4l2:/dev/video0#video=h264"
cam1: "v4l2:/dev/video2#video=h264"
- Kein
-re, kein-readrate_initial_burst→ direkter Frame-Durchsatz - Encoding (libx264) bleibt, aber ohne künstliches Puffern
- Könnte den
exec timeoutauf cam1 beheben (anderer Kamera-Öffnungspfad) - Risiko: v4l2-Source in go2rtc ist weniger getestet als ffmpeg-Source
Option B — Hardware-Encoding Intel QuickSync / VAAPI
Prüfen ob GPU verfügbar:
ls -l /dev/dri # renderD128 vorhanden?
Config:
# go2rtc-Service: devices: + /dev/dri:/dev/dri
streams:
cam0: "ffmpeg:/dev/video0#video=h264#hardware"
cam1: "ffmpeg:/dev/video2#video=h264#hardware"
- Encoding auf GPU → CPU von ~35 % auf ~5 %
- go2rtc erzeugt anderen FFmpeg-Befehl (h264_vaapi statt libx264)
- Ob
-redabei ebenfalls wegfällt: muss am Gerät verifiziert werden
Option C — Eigener FFmpeg-Befehl via exec: Source
Vollständige Kontrolle über alle FFmpeg-Flags:
streams:
cam0:
- "ffmpeg:-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30
-fflags nobuffer -flags low_delay
-i /dev/video0
-c:v libx264 -preset ultrafast -tune zerolatency -g 15 -bf 0
#video=h264"
- Kein
-re(nicht angegeben) -g 15= Keyframe alle 0,5 s → max 0,5 s Freeze-fflags nobuffer -flags low_delay= minimaler Input-Buffer- Problem: go2rtc's
ffmpeg:Source-Handler mit Custom-Args ist Version-abhängig; korrekte Syntax muss verifiziert werden
Option D — Separater MediaMTX-Container
MediaMTX (rtsp-simple-server) als Zwischenstufe:
v4l2 → FFmpeg (eigene Flags, g=15, kein -re) → RTSP (MediaMTX) → go2rtc → WebRTC
- Volle FFmpeg-Kontrolle
- go2rtc liest einfach
rtsp://mediamtx:8554/cam0 - Zusätzlicher Container, aber sauber und wartbar
Option E — Fallback: MJPEG
streams:
cam0: "ffmpeg:/dev/video0#video=mjpeg"
- Kein H.264-Encode, kein GOP, keine
-re-Problematik - ~200 ms Latenz (statt 130 ms) — bei 1–3 Usern und Roboter-Überwachung ausreichend
- War nachweislich stabil und flüssig
Empfohlene Reihenfolge
1. Option A (v4l2: Source) → 5 min, kein Aufwand, könnte alles lösen
2. Option B (Hardware-Encode) → 15 min, braucht /dev/dri-Check
3. Option C (custom FFmpeg) → 30 min, volle Kontrolle
4. Option D (MediaMTX) → 60 min, sauberste Architektur
5. Option E (MJPEG) → 5 min, sicherer Hafen
Hi-Res-Snapshots — Analyse (Live-Video + Foto alle ~10 s)
Grundprinzip: Snapshot-Auflösung = Stream-Auflösung (USB-Kamera kann nur in einer Auflösung gleichzeitig geöffnet sein). Für Hi-Res-Fotos muss der Stream selbst hochauflösend laufen, Browser skaliert fürs Display herunter.
Weg A — MJPEG hochauflösend (Passthrough, Option E oben)
- Kamera liefert MJPEG nativ → go2rtc reicht 1:1 durch, kein Encode
- Snapshot:
/api/frame.jpeg= voller Frame, native JPEG-Qualität, gratis - CPU ~5 %, keine Freezes
- Empfohlen wenn Hardware-Encoding nicht verfügbar
Weg B — H.264 Hardware-Encode + MJPEG-Passthrough (Option B oben)
cam0: "ffmpeg:/dev/video0#video=h264#hardware#video=mjpeg"
- Live: H.264 per GPU (~130 ms, niedrige Bandbreite)
- Snapshot: MJPEG-Passthrough (native Qualität, gratis)
- Zu verifizieren: welchen Track nimmt
/api/frame.jpeg— H.264 oder MJPEG?
Snapshot-Takt
Der 10-s-Takt erzeugt keine Dauerlast: pro Foto wird ein Frame aus dem
laufenden Stream abgegriffen. Trigger: Homing-Projekt ruft
GET /api/snapshot/cam0 alle 10 s ab (aktuell so implementiert).