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

205 lines
7.0 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**. 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(300ms) ← v4l2-Buffer leeren, Kamera-Reset abwarten
3. hires-FFmpeg bei hiresSize/hiresFps starten
4. Frames warmlaufen lassen (settleFrames=6) + minWidth-Check (90 % der Soll-Breite)
5. Ersten validen Frame nehmen → hires-FFmpeg SIGTERM
6. 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).
---
## 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` (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 |
---
---
## 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 ~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.