From f36dd603e718724136a2bc4248562c16e77248e4 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:22:30 +0200 Subject: [PATCH] Claude: WebRTC arbeiten und Roadmap --- doc/04_Delay_roadmap.md | 191 ++++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 19 ++-- 2 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 doc/04_Delay_roadmap.md diff --git a/doc/04_Delay_roadmap.md b/doc/04_Delay_roadmap.md new file mode 100644 index 0000000..9e098c3 --- /dev/null +++ b/doc/04_Delay_roadmap.md @@ -0,0 +1,191 @@ +# 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 1–3 LAN-Usern egal +- go2rtc liefert MJPEG direkt; Viewer: `MODE = 'mjpeg'` oder simples `` + +--- + +## 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, 1–3 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 1–3 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) diff --git a/docker-compose.yaml b/docker-compose.yaml index cc1e09d..32a7130 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -25,12 +25,19 @@ configs: # Komplette go2rtc-Config eingebettet – keine separate Datei nötig. content: | streams: - # Einfache Form: von go2rtc bestätigt funktionsfähig für beide Kameras. - # device? wurde verworfen (generiert FFmpeg ohne -input_format mjpeg → Timeout). - # #video=h264 → für WebRTC (transcodiert) - # #video=mjpeg → für MJPEG-Fallback + /api/frame.jpeg (Snapshot) - cam0: "ffmpeg:/dev/video0#video=h264#video=mjpeg" - cam1: "ffmpeg:/dev/video2#video=h264#video=mjpeg" + # NUR #video=h264 (kein #video=mjpeg) → halbiert CPU-Last pro Kamera. + # go2rtc erzeugt intern: -preset superfast -tune zerolatency (gut). + # Standard-GOP: -g 50 = 1.67s @30fps. Für kürzeres GOP → exec: Source nötig (TODO). + # + # Variante A – Software-Encoding (immer kompatibel): + cam0: "ffmpeg:/dev/video0#video=h264" + cam1: "ffmpeg:/dev/video2#video=h264" + # + # Variante B – Hardware-Encoding (Intel QuickSync / VAAPI, falls vorhanden): + # Drastisch weniger CPU-Last. Aktivieren wenn "Variante A" CPU-Bottleneck zeigt. + # Braucht: /dev/dri/renderD128 im Container (devices: unten ergänzen) + # cam0: "ffmpeg:/dev/video0#video=h264#hardware" + # cam1: "ffmpeg:/dev/video2#video=h264#hardware" webrtc: listen: ":8555" candidates: