Files
appRobotWebcam/doc/04_Delay_roadmap.md
2026-06-04 05:46:59 +02:00

7.7 KiB
Raw Blame History

AppRobotWebcam Delay / Ruckler-Analyse

Symptom

Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils 12 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) 65114 %
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=1335ms

→ 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 timeout auf 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 -re dabei 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 13 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).