201 lines
7.7 KiB
Markdown
201 lines
7.7 KiB
Markdown
# 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).
|