# 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 ~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. 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 ``` | 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 `` friert ~2–3 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). ```json { "id": "cam2", "liveSize": "1920x1080", "liveFps": 15, "hiresSize": "1920x1080" } ``` ### C270 — Standard (Fall B) ```json { "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): ```bash 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` (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 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:** ```json { "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 ~8–10 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.