Files
appRobotWebcam/doc/05_screenShot_roadmap.md
2026-06-06 13:12:18 +02:00

213 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ~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, **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 <liveSize> -framerate <liveFps>
-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 `<img>` friert ~23 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 (~300450 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 (~36 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` (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 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.