Files
appRobotWebcam/doc/04_Delay_roadmap.md
2026-06-03 22:22:30 +02:00

192 lines
8.2 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 >1 s ein, manchmal
bleibt ein Einzelbild ganz stehen. Im reinen MJPEG-Modus trat das **nicht** auf.
## Messung (2026-06-03)
| Quelle | CPU |
|--------|-----|
| System gesamt | ~40 % |
| **Container AppRobotGo2RTC** | **~95 %** |
`docker stats` rechnet pro Kern: **95 % ≈ ein CPU-Kern voll ausgelastet.**
→ Flaschenhals ist **go2rtc (Encoding)**, nicht Netzwerk und nicht der Node-Server.
---
## Ursachenanalyse
### Ursache 1 — Software-H.264-Encoding sättigt die CPU
Die Kamera liefert **MJPEG** nativ. WebRTC im Browser braucht aber **H.264**.
go2rtc transcodiert also jeden Frame MJPEG→H.264 in Software (libx264).
Zwei Kameras parallel → ein Kern voll. Wenn der Encoder nicht nachkommt,
stauen sich Frames → Ruckeln und Aussetzer.
> Verstärkt wurde es vorher durch `#video=h264#video=mjpeg`: das ließ go2rtc
> **doppelt** encodieren (H.264 *und* MJPEG). Das `#video=mjpeg` ist inzwischen
> in der Config entfernt — der Hauptkostenfaktor (H.264-Software-Encode) bleibt.
### Ursache 2 — Großes GOP (Keyframe-Abstand `-g 50`)
go2rtc setzt standardmäßig ein Keyframe alle 50 Frames = **1,67 s bei 30 fps**.
H.264 überträgt zwischen Keyframes nur Differenzbilder. Geht ein Paket verloren
oder verbindet sich der Client neu, **wartet der Browser bis zum nächsten Keyframe**
— bis zu 1,67 s Standbild. Das erklärt exakt das „ein Bild bleibt ganz stehen".
### Der Grundkonflikt
- **MJPEG**: kein Encode (Kamera-nativ), kein GOP → flüssig, ~200 ms, höhere Bandbreite
- **H.264/WebRTC**: ~130 ms, geringe Bandbreite → aber Encode-Last + GOP-Freezes
Wir zahlen also CPU-Last und Komplexität für ~70 ms Latenzgewinn. Ob sich das
lohnt, hängt davon ab, ob wir die CPU-Last loswerden (Hardware-Encode / native H.264).
---
## Lösungsweg — geordnet nach Aufwand/Wirkung
### Schritt 1 (5 min) — Prüfen: kann die Kamera H.264 nativ?
```bash
docker exec AppRobotGo2RTC v4l2-ctl --list-formats-ext -d /dev/video0
# v4l2-ctl fehlt im go2rtc-Image? → auf dem Host ausführen:
v4l2-ctl --list-formats-ext -d /dev/video0
```
- **Steht „H264" in der Liste** → go2rtc kann den Stream **durchreichen** (passthrough),
praktisch NULL Encode-Last und niedrigste Latenz. Bestfall.
- Steht nur MJPEG/YUYV → weiter mit Schritt 2.
### Schritt 2 (Hauptfix) — Hardware-Encoding (Intel QuickSync / VAAPI)
Ein ThinkCentre hat fast sicher eine Intel-iGPU mit QuickSync. Damit wandert das
H.264-Encoding von der CPU auf die GPU → **CPU von ~95 % auf ~10 %**.
Prüfen ob GPU verfügbar:
```bash
ls -l /dev/dri # renderD128 vorhanden?
```
Umsetzung (später):
```yaml
# beim go2rtc-Service:
devices:
- /dev/video0:/dev/video0
- /dev/video2:/dev/video2
- /dev/dri:/dev/dri # ← GPU durchreichen
# in der go2rtc-Config:
streams:
cam0: "ffmpeg:/dev/video0#video=h264#hardware"
cam1: "ffmpeg:/dev/video2#video=h264#hardware"
```
### Schritt 3 — GOP verkürzen (gegen Freeze nach Loss/Reconnect)
Standard-Format erlaubt kein `-g`. Dafür `exec:`-Source mit eigenem FFmpeg-Befehl:
```yaml
streams:
cam0:
- "exec:ffmpeg -hide_banner -f v4l2 -input_format mjpeg -video_size 640x480
-framerate 30 -i /dev/video0
-c:v h264_vaapi -g 15 -bf 0 -tune zerolatency
-f rtsp {output}"
```
`-g 15` = Keyframe alle 0,5 s → Freeze nach Störung max 0,5 s statt 1,67 s.
`-bf 0` = keine B-Frames (kein Lookahead-Delay).
### Schritt 4 — Stellschrauben (zusätzliche Reserve)
- Auflösung 640×480 → **320×240** (viertelt die Encode-Pixel)
- Framerate 30 → **15 fps** (halbiert die Encode-Frequenz)
### Schritt 5 (Fallback) — zurück zu MJPEG
Falls Hardware-Encode nicht verfügbar ist oder zickt:
- **kein Encode, kein GOP → keine Freezes**, stabil flüssig
- ~200 ms Latenz (statt 130 ms), höhere Bandbreite — bei 13 LAN-Usern egal
- go2rtc liefert MJPEG direkt; Viewer: `MODE = 'mjpeg'` oder simples `<img>`
---
## Entscheidungsbaum
```
Kamera kann H.264 nativ? ──ja──► Passthrough (Schritt 1) ✓ fertig
│ nein
/dev/dri vorhanden? ──ja──► Hardware-Encode (Schritt 2) ✓ Hauptfix
│ nein + GOP kürzen (Schritt 3)
Latenz 200ms akzeptabel? ──ja──► MJPEG-Fallback (Schritt 5) ✓ robust
│ nein
Auflösung/fps senken (Schritt 4), notfalls 1 Kamera
```
## Empfehlung
Reihenfolge **1 → 2 → 3**:
1. Erst native H.264 prüfen (kostet 5 min, evtl. löst es alles).
2. Sonst Hardware-Encoding aktivieren — das ist der eigentliche Hebel gegen die 95 %.
3. Dann GOP kürzen, damit auch die Restfreezes verschwinden.
**MJPEG (Schritt 5) ist der sichere Hafen**, falls die GPU nicht mitspielt:
es war nachweislich flüssig, nur 70 ms langsamer. Für diesen Anwendungsfall
(Roboter-Überwachung, 13 User) völlig ausreichend.
## Hi-Res-Snapshots — Analyse (Live-Video + Foto alle ~10 s)
Ziel: schnelles Live-Video **und** gelegentlich (≈ alle 10 s) ein hochauflösendes Foto.
### Die entscheidende Einschränkung
Eine USB-Kamera kann **gleichzeitig nur in einer Auflösung** geöffnet werden.
Solange go2rtc das Device für den Live-Stream hält, kann kein zweiter Prozess
parallel ein höher aufgelöstes Foto ziehen (Device belegt).
**Snapshot-Auflösung = Stream-Auflösung.** Es gibt keinen billigen Nebenweg zu
einem höher aufgelösten Foto, solange der Stream klein läuft. `/api/frame.jpeg`
decodiert immer einen Frame **aus dem laufenden Stream**.
Konsequenz: Für Hi-Res-Fotos muss der **Stream selbst hochauflösend** laufen und
fürs Live-Bild im Browser heruntergerechnet werden. Der Trick ist, das billig zu halten.
### Weg A — MJPEG hochauflösend (Passthrough)
- Quelle: Kamera hochauflösend MJPEG → go2rtc reicht 1:1 durch, **kein Encode**
- Snapshot: `/api/frame.jpeg` = voller Frame, **native JPEG-Qualität**, gratis
- Live: MJPEG, im Browser auf 480 skaliert (~200 ms, war flüssig)
- CPU ~5 %, keine Freezes. Preis: höhere LAN-Bandbreite (unkritisch bei 13 Usern)
### Weg B — WebRTC + Hardware-Encoding ◄ favorisiert, mit Bedingung
- Quelle: Kamera hochauflösend; Live-Track H.264 **per Intel-GPU (QuickSync)**
- Live: WebRTC ~130 ms, CPU ~10 %
**Bedingung des Users: der Frame aus dem Stream MUSS hochauflösend sein.**
Antwort: **ja, per Definition**`/api/frame.jpeg` hat dieselbe Auflösung wie der
Stream. Läuft H.264 in 1280×960, ist das Foto 1280×960. Garantiert durch die Config
(Stream-Auflösung explizit hochauflösend setzen → WebRTC überträgt hochauflösend,
Browser skaliert fürs Display herunter).
**Qualitäts-Nuance:** Ein aus H.264 decodierter Frame ist leicht verlustbehaftet
(H.264 → JPEG). Für ArUco meist ausreichend, aber nicht optimal.
**Beste Variante (Hi-Res UND native Qualität)** — erst durch HW-Encode praktikabel:
```yaml
# Quelle hochauflösend; H.264 (GPU) für Live + MJPEG-Passthrough für Snapshot
cam0: "ffmpeg:/dev/video0#video=h264#hardware#video=mjpeg"
```
- Live-Track: H.264 per GPU (billig)
- Snapshot-Track: MJPEG-Passthrough (gratis, kamera-nativ)
- `/api/frame.jpeg` sollte den **MJPEG-Track** nehmen → volle Auflösung, native Qualität
- Das ist `#video=h264#video=mjpeg` wie früher — aber OHNE Flaschenhals, weil nur
H.264 die GPU nutzt und MJPEG reines Durchreichen ist.
### Vor Weg B zu verifizieren („sichergestellt" erst danach)
1. `ls -l /dev/dri` → ist `renderD128` vorhanden? (Intel-GPU verfügbar)
2. Hardware-Encode testweise aktivieren (`#hardware`) → fällt CPU wirklich von 95 %?
3. `/api/frame.jpeg?src=cam0` abrufen → **Auflösung prüfen** (hoch?) **und Qualität**
4. Klären, welchen Track `/api/frame.jpeg` bei `#video=h264#hardware#video=mjpeg`
tatsächlich verwendet (MJPEG-Passthrough = native Qualität gewünscht)
> Diese 4 Checks können nicht aus der Ferne garantiert werden — sie müssen am
> ThinkCentre laufen. Erst danach ist Weg B „sichergestellt".
### Snapshot-Takt (alle ~10 s)
Der 10-s-Takt erzeugt **keine** Dauerlast: pro Foto wird nur ein Frame aus dem
ohnehin laufenden Stream abgegriffen. Trigger wahlweise:
- Pull: Homing-Projekt ruft `/api/snapshot/cam0` alle 10 s ab (aktuell so vorgesehen)
- Push: kleiner Timer im Node-Server, der das Foto ablegt / per Webhook sendet (Phase 5)