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

8.2 KiB
Raw Blame History

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?

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:

ls -l /dev/dri        # renderD128 vorhanden?

Umsetzung (später):

# 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:

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:

# 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)