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

201 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```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 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)
```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).