Files
appRobotWebcam/doc/05_screenShot_roadmap.md
2026-06-06 11:48:17 +02:00

9.3 KiB
Raw Blame History

AppRobotWebcam Snapshot & HD-Grab

Status: Implementiert. Architektur seit 2026-06-05 (Node-MJPEG-Schalter). Gemessene Werte: Latenz 139 ms, ~5 % idle, ~35 %/Kamera aktiv, HD-Grab ~23 s.


Architektur

Eine CameraSwitch-Instanz pro physischem Gerät — der einzige Öffner von /dev/videoN. Hält immer nur einen FFmpeg: entweder Live oder Grab. Nie beide.

cameras.json
    └── server.js  →  CameraSwitch (besitzt /dev/videoN)
            │
            ├── Live:  ffmpeg → mpjpeg pipe → Fan-out an N Browser-Clients (<img>)
            └── Grab:  Live SIGTERM → close-Event = FD frei → hires-FFmpeg → zurück

Das close-Event des Kindprozesses ist der harte Beweis „Gerät frei" — kein Timing, keine Polls. Das Race (zwei Encoder auf einem Gerät) ist konstruktiv ausgeschlossen.


Live-Stream

ffmpeg -fflags nobuffer -f v4l2 -input_format mjpeg
       -video_size <liveSize> -framerate <liveFps>
       -i /dev/videoN
       -c:v copy -bsf:v mjpeg2jpeg   ← copybsf (Default)
       -f mpjpeg -flush_packets 1 pipe:1
Encode-Modus CPU Wann
copybsf (Default) ~35 %/Kamera Kamera liefert natives MJPEG
mjpeg (Fallback) ~50 %/Kamera Re-Encode, falls copybsf Probleme macht

mjpeg2jpeg ist Pflicht bei copybsf — Kamera-MJPEG lässt JPEG-Huffman-Tabellen weg; ohne den Filter verschluckt sich der mpjpeg-Muxer (107 % CPU + Hang).

Latenz-Tuning: -fflags nobuffer + -flush_packets 1 + socket.setNoDelay(true) + cork/uncork (Header+JPEG+Trailer = ein TCP-Segment). Gemessen: 139 ms Kamera→Browser.

On-Demand: Live läuft nur wenn Clients verbunden sind. acquire()/release() zählen Verbraucher. Nach dem letzten + IDLE_GRACE_MS (15 s): Stop → 0 % idle. Abschaltbar via ON_DEMAND=false.


HD-Grab (grabHires)

Fall A — liveSize == hiresSize

Wenn die Live-Auflösung bereits der gewünschten Hires-Auflösung entspricht (z.B. C920 mit liveSize: "1920x1080"): kein Format-Wechsel nötig. grabHires ruft direkt getFrame() auf dem laufenden Live-Stream auf. Kein Neustart, kein Übergangs-Problem.

Kamera  ──live 1920×1080──►  getFrame()  →  JPEG 1920×1080

Fall B — liveSize ≠ hiresSize (C270: 640×480 → 1280×960)

1. Live-FFmpeg SIGTERM → warte auf close (= FD frei)
2. sleep(800ms)  ← Kamera-/Treiberreset, Puffer auslaufen lassen
3. optional: 1280x720-Warmup-Format öffnen
4. sleep(500ms)  ← Warmup/Formatwechsel stabilisieren
5. hires-FFmpeg bei hiresSize/hiresFps starten
6. Frames warmlaufen lassen (settleFrames=6) + minWidth-Check (90 % der Soll-Breite)
7. Ersten validen Frame nehmen → hires-FFmpeg SIGTERM
8. finally: Live-FFmpeg neu starten (immer, auch bei Fehler)

Blackout: Der <img> friert ~23 s ein und läuft danach weiter. Kein Client-Handling nötig.

minWidth wird automatisch aus hiresSize abgeleitet (floor(hiresW × 0.9)). Damit werden Frames abgelehnt, die noch auf der alten Live-Auflösung liegen (v4l2-Buffer-Reste vom vorherigen Format).

FFmpeg-Probe: Für das finale hires-Open werden jetzt zusätzliche Probe-Parameter gesetzt (-probesize 5000000, -analyzeduration 1000000), um das MJPEG-Format und die Kameraparameter sicherer zu erkennen.


Aktueller Ablauf des HD-Grabs

Der aktuelle Ablauf ist bewusst defensiv:

  • Live-Stream beenden und auf FFmpeg-close warten.
  • 800 ms Pause zum Zurücksetzen des Kameratreibers.
  • Wenn liveSize deutlich kleiner als hiresSize ist, zunächst ein Zwischenformat 1280x720 starten und kurz einlaufen lassen.
  • 500 ms warten, damit das Gerät in den neuen Auflösungsmodus umschaltet.
  • Hires-Stream starten, mehrere Frames puffern und den ersten validen Frame mit genügend Bytes und Breite auswählen.
  • Hires-Stream beenden, Live-Stream neu starten.

Log- und Fehlerbild

Aktuelle Log-Meldungen zeigen typische MJPEG/Treiber-Phänomene:

  • unable to decode APP fields: meist kosmetisch beim MJPEG-Parser, häufig bei UVC-Webcams. Das bedeutet nicht zwingend einen fehlgeschlagenen Grab.
  • input is truncated / Error applying bitstream filters: kann auftreten, wenn FFmpeg beim Beenden gerade ein MJPEG-Paket verarbeitet. Das ist ein Hinweis auf einen abrupten Stream-Abbruch, nicht zwingend auf ein ungültiges Bild.
  • Could not find codec parameters ... unspecified pixel format: deutet darauf hin, dass der neue HD-Stream noch nicht sauber genug erkannt wurde. Deshalb nutzen wir jetzt größere Probe-Parameter.

Mögliche Ursachen

  • Treiberzustand der Kamera nach einem schnellen Formatwechsel: der C920 kann noch Frames aus dem alten Modus liefern oder zwischen 640×480 und 1920×1080 in einen inkonsistenten Zustand kommen.
  • MJPEG-Frames ohne vollständige JPEG-Header oder Huffman-Tabellen, die erst durch mjpeg2jpeg ergänzt werden.
  • Abrupte Beendigung des ffmpeg-Prozesses während eines laufenden MJPEG-Pakets.

Kamera-spezifische Konfiguration

C920 (HD Pro Webcam) — liveSize = hiresSize

Die C920 hat ein v4l2-Treiber-Eigenheit: nach einem Live-Stream bei 640×480 kann sie beim Neustart auf 1920×1080 nicht sauber wechseln — der Treiber gibt 1280×720-Frames zurück (Übergangs-Artefakt, trotz korrekter Format-Anforderung).

Lösung: liveSize und hiresSize identisch setzen. grabHires wechselt dann kein Format — es liest direkt aus dem laufenden 1920×1080-Stream (Fall A).

{
  "id":       "cam2",
  "liveSize":  "1920x1080",
  "liveFps":   15,
  "hiresSize": "1920x1080"
}

C270 — Standard (Fall B)

{
  "id":       "cam0",
  "hiresSize": "1280x960"
}

Live bei 640×480, Grab bei 1280×960. Format-Wechsel + 300ms-Pause funktioniert.

Auflösung prüfen

Nur MJPG-native Auflösungen in liveSize/hiresSize verwenden (kein Software-Encode):

v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG

API-Endpunkte

Endpunkt Beschreibung
GET /api/snapshot/:id 640er JPEG aus dem Live-Puffer (on-demand, sofort)
GET /api/snapshot/:id/hires HD-JPEG via grabHires (23 s, Blackout)
GET /api/stream/:id MJPEG multipart/x-mixed-replace (Live, Browser <img>)
GET /api/cameras Metadaten aller Kameras aus cameras.json
GET /health Zustand aller CameraSwitch-Instanzen

Bekannte Restprobleme

Problem Priorität Zustand
Stream friert selten dauerhaft ein niedrig Einzelfall; clientseitiger Watchdog (Frame-Timeout → img.src neu) noch nicht implementiert
unable to decode APP fields im Log (C270) kosmetisch Einmalige Probe-Warnung von FFmpeg, kein Auswirkung
input is truncated / Error applying bitstream filters beim HD-Grab mittel Hinweis auf abrupten Stream-Abbruch beim Formatwechsel; Bild kann dennoch gültig sein
Could not find codec parameters ... unspecified pixel format mittel Zeichen für unvollständige FFmpeg-Probe nach Formatwechsel; größere probe-Parameter helfen


Architektur-Entwicklung (Historie)

Dieser Abschnitt dokumentiert den Weg zur aktuellen Lösung. Für die maßgebliche Implementierung gelten ausschliesslich die Abschnitte oben.

go2rtc-Ansatz (2026-06-04) — abgelöst

Grundidee: Consumer-Umhängen

go2rtc verwaltete die Kameras. Für HD-Grabs wurde der Live-Consumer (cam0) losgelassen, damit go2rtc das Gerät freigibt, dann ein separater cam0_hires-Stream bei 1280×960 geöffnet.

Phase 1 — Freigabe messen (2026-06-04, erfolgreich)

Endpunkt GET /api/snapshot/:id/release-test pollt go2rtcs /api/streams alle 200 ms und misst, wann nach Consumer-Verlust der Producer stoppt (= Gerät frei).

Ergebnis:

{ "freed": true, "msUntilFree": 0, "zeroConsumerAt": 4850 }

go2rtc gibt das Gerät sofort frei wenn der letzte Consumer weg ist. Kein „warm halten". Die ~5 s (zeroConsumerAt) sind die WS-Abmeldelatenz von go2rtc selbst.

Phase 2 — HD-Grab implementiert (2026-06-04, funktionierte)

HD-Grab lieferte echte 1280×960-Frames (~76 KB). Einschränkungen:

  • Nur bei einem einzigen Viewer-Tab zuverlässig (bei mehreren fiel Consumer-Count nie auf 0)
  • Sequenz: Browser löst cam0 → 5 s warten → Grab → Browser hängt zurück
  • Blackout ~810 s (5 s Pause + Grab-Zeit)

Race-Bug entdeckt (2026-06-05) → go2rtc entfernt

go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg /dev/videoN freigibt. Folge: zwei FFmpeg gleichzeitig auf demselben Gerät → 106 % CPU + Hang (reproduzierbar). Details: 09_Bug_reports.md.

Erkenntnis: Das Problem ist strukturell — go2rtc gibt keine harten Garantien über den Gerätezustand. Der einzige zuverlässige Beweis „Gerät frei" ist das close-Event des FFmpeg-Prozesses selbst.

Entscheidung 2026-06-05: go2rtc entfernen. Node startet FFmpeg direkt.

Node-MJPEG-Schalter (2026-06-05) — aktuelle Architektur

Mit src/cameraSwitch.js besitzt Node die Kameras direkt. Das close-Event des eigenen FFmpeg-Kindprozesses ist der harte Beweis „FD geschlossen = Gerät frei". Keine go2rtc- Abhängigkeit, kein Race.

Sofort gemessene Werte:

  • Latenz: 139 ms (vorher ~340 ms mit go2rtc)
  • CPU idle: ~5 % (On-Demand)
  • CPU aktiv: ~35 %/Kamera (copybsf)
  • HD-Grab beide Kameras parallel: ~2,3 s, je 1280×960

Alle weiteren Details: aktuelle Architektur-Abschnitte oben.