# 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: ```yaml 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: ```bash ls -l /dev/dri # renderD128 vorhanden? ``` Config: ```yaml # 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: ```yaml 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 ```yaml 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) ```yaml 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).