# AppRobotWebcam – Snapshot & HD-Grab > Status: **Implementiert & verifiziert** (3 Kameras: 2× C270, 1× C920). > Architektur seit 2026-06-05 (Node-MJPEG-Schalter), C920-Hires + Qualität 2026-06-06. > Werte: Live-Latenz 139 ms, ~5 % idle, ~35 %/Kamera aktiv, HD-Grab ~2–3 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 () └── 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, **kein zweiter Öffner**. Das Race (zwei Encoder auf einem Gerät) ist konstruktiv ausgeschlossen. --- ## Live-Stream ``` ffmpeg -fflags nobuffer -f v4l2 -input_format mjpeg -video_size -framerate -i /dev/videoN -c:v copy -bsf:v mjpeg2jpeg ← copybsf (Default) -f mpjpeg -flush_packets 1 pipe:1 ``` **`mjpeg2jpeg` ist Pflicht bei copybsf** — Kamera-MJPEG lässt die 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` Kein Format-Wechsel nötig: `grabHires` gibt direkt den laufenden Live-Frame zurück (`getFrame()`). Schnell, kein Geräte-Neustart. ### Fall B — `liveSize ≠ hiresSize` (der Normalfall) ``` 1. Live-FFmpeg SIGTERM → warte auf close (= FD frei) 2. hires-FFmpeg bei hiresSize/hiresFps starten 3. Frames warmlaufen lassen (settleFrames = 6) + minWidth-Check 4. ersten validen Frame nehmen → hires-FFmpeg beenden 5. finally: Live-FFmpeg neu starten (immer, auch bei Fehler) ``` **Kein Warmup, kein `sleep` zwischen Stop und Grab.** Auf dem Host verifiziert (A/B-Test 2026-06-06): ein direkter Open auf die Zielauflösung liefert sofort korrekte Frames — sowohl 1280×960 (C270) als auch 1920×1080 (C920), in beiden Encode-Modi. Ein zweites FFmpeg dazwischen (früher als „Warmup-Zwischenformat" eingebaut) erzeugt nur `Device or resource busy` und ist entfernt. **`minWidth`** wird automatisch aus `hiresSize` abgeleitet (`floor(hiresW × 0.9)`). Beispiel `1920x1080` → `minWidth = 1728`. Damit werden etwaige Übergangs-Frames in falscher Auflösung (v4l2-Buffer-Reste) abgelehnt. **`_captureHires`** setzt zusätzlich `-probesize 5000000 -analyzeduration 1000000`, damit FFmpeg das MJPEG-Format sicher erkennt. **Blackout:** Der `` friert ~2–3 s ein und läuft danach weiter. Kein Client-Handling nötig. --- ## Standbild-Qualität: Encode-Wahl Die Kamera liefert bei `input_format mjpeg` bereits **JPEG-komprimiert** (Hardware, unvermeidbar). Entscheidend ist, ob wir eine **zweite** Kompression daraufsetzen: | `hiresEncode` | FFmpeg | Effekt | Empfehlung | |---|---|---|---| | `copybsf` (Default) | `-c:v copy -bsf:v mjpeg2jpeg` | Kamera-JPEG **pur durchgereicht**, keine zweite Kompression | **Standardwahl, auch für Standbilder** | | `mjpeg` | `-c:v mjpeg -q:v 5` | Re-Encode = **zweite** Kompression → sichtbare Artefakte | nur Fallback, falls copybsf zickt | - `hiresEncode` überschreibt `encode` nur für den Grab; fehlt es, gilt `encode`. - cam2 lief kurzzeitig auf `mjpeg` (Workaround) → sichtbare Artefakte, **131 kB**. Mit `copybsf` kommt das ungekürzte Kamera-JPEG (~300–450 kB) → keine Re-Encode-Artefakte. **Wirklich verlustfrei** (kein JPEG-Verlust überhaupt) ginge nur über das rohe **YUYV**-Format → PNG. Eigener Grab-Pfad, große Dateien (~3–6 MB), langsamer, USB-intensiv. **Noch nicht implementiert** — nur bauen, wenn die JPEG-Qualität nicht reicht. --- ## Kamera-spezifische Konfiguration ### C270 (`cam0`, `cam1`) ```json { "hiresSize": "1280x960" } ``` Live 640×480 (Default), Grab 1280×960, `copybsf`. Bewährter Standardfall. ### C920 (`cam2`) ```json { "hiresSize": "1920x1080" } ``` Live 640×480 (Default, USB-schonend), Grab 1920×1080, `copybsf`. Der Wechsel 640→1920 funktioniert direkt — **kein** `liveSize`-Override und **kein** Warmup nötig. ### Auflösung prüfen ```bash v4l2-ctl --list-formats-ext -d /dev/videoN | grep -A 20 MJPG ``` Nur unter `'MJPG'` gelistete Auflösungen verwenden (sonst Software-Encode = teuer). --- ## Harmlose Log-Meldungen Diese erscheinen im Normalbetrieb und bedeuten **keinen** Fehler: | Meldung | Bedeutung | |---|---| | `unable to decode APP fields: Invalid data found` | einmalige FFmpeg-Probe-Warnung beim Stream-Start (C270) | | `Dequeued v4l2 buffer contains corrupted data` | erster Frame nach Geräte-Open ist oft unvollständig — wird verworfen | | `input is truncated` / `Error applying bitstream filters` | copybsf droppt den ersten korrupten Frame; `settleFrames` nimmt einen späteren | Entscheidend ist die `HD OK`-Zeile mit `Breite=…px` passend zur `Soll`-Breite. --- ## 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` (2–3 s, Blackout) | | `GET /api/stream/:id` | MJPEG multipart/x-mixed-replace (Live, Browser ``) | | `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 gebaut | | Verlustfreie Standbilder (YUYV→PNG) | niedrig | offen; siehe „Standbild-Qualität" | --- --- ## Architektur-Entwicklung (Historie) Für die maßgebliche Implementierung gelten ausschliesslich die Abschnitte oben. Dieser Teil dokumentiert den Weg dorthin. ### go2rtc-Ansatz (2026-06-04) — abgelöst go2rtc verwaltete die Kameras. Für HD-Grabs wurde der Live-Consumer losgelassen, damit go2rtc das Gerät freigibt, dann ein separater `*_hires`-Stream geöffnet. Phase 1 (Freigabe messen) und Phase 2 (Grab) funktionierten grundsätzlich, aber: **Race-Bug (2026-06-05):** go2rtcs API konnte nicht zuverlässig melden, wann FFmpeg `/dev/videoN` freigibt → zwei FFmpeg gleichzeitig auf demselben Gerät → **106 % CPU + Hang** (Details: `09_Bug_reports.md`). Strukturell nicht lösbar, weil go2rtc keine harte Garantie über den Gerätezustand gibt. → **Entscheidung:** go2rtc entfernen. Node startet FFmpeg selbst; dessen `close`-Event ist der harte Beweis „Gerät frei". Die alte `go2rtc.yaml` wurde 2026-06-06 gelöscht (liegt in der Git-Historie). ### Node-MJPEG-Schalter (2026-06-05) — aktuelle Architektur Sofort gemessen: Latenz 139 ms (vorher ~340 ms), Idle-CPU ~5 % (On-Demand), ~35 %/Kamera aktiv, HD-Grab beider C270 parallel ~2,3 s. ### C920-Hires & der Warmup-Irrweg (2026-06-06) Beim Einbau der dritten Kamera (C920, 1920×1080) schien der Grab „nur 720" zu liefern. Mehrere falsche Theorien (Kamera kann kein 1080-MJPEG; Treiber stuft 1080→720 zurück) wurden **durch Host-Tests widerlegt**: `v4l2-ctl` + direkte `ffmpeg`-Grabs zeigten 1920×1080-MJPEG auf jedem Frame, in beiden Encode-Modi. Die zwei realen Ursachen: 1. **Veralteter Code im Container** — eine fehlgeschlagene Git-Übertragung; der Container lief alten Code (verifiziert via `docker exec … grep`). Jede „Fix→Redeploy"-Runde testete Code, der gar nicht lief. 2. **Warmup-Zwischenformat** — ein nachträglich eingebauter Schritt öffnete ein zweites FFmpeg (1280×720) vor dem Grab → `Device or resource busy` → alle Grabs brachen ab. **Lehre (festgehalten):** Erst auf dem **Host** mit `ffmpeg`/`ffprobe`/`v4l2-ctl` verifizieren, was die Kamera real liefert, und sicherstellen, dass der Container den aktuellen Code fährt — **bevor** Code-Theorien gebaut werden. Diagnose-Tool dafür: `tools/hires-probe.js`. Nach Entfernen des Warmups und mit aktuellem Code: alle drei Kameras liefern korrekte HD-Grabs (cam2 = 1920×1080). Encode auf `copybsf` für artefaktfreie Standbilder.