Umbau mit cameraSwitch

This commit is contained in:
chk
2026-06-05 06:36:48 +02:00
parent 0ea475d6b6
commit 8c8c769e22
10 changed files with 641 additions and 839 deletions

View File

@@ -1,4 +1,27 @@
# AppRobotWebcam Hi-Res-Snapshot via Consumer-Umhängen
> # ⛔ ABGELÖST (2026-06-05) — dieser Ansatz war die Ursache des 106%-Bugs
>
> Der unten beschriebene **Consumer-Umhängen-Ansatz mit go2rtc** (`cam0` loslassen →
> go2rtc gibt Gerät frei → `cam0_hires` greifen) hat sich als **prinzipiell racy**
> erwiesen: go2rtcs API kann nicht zuverlässig melden, wann FFmpeg `/dev/videoN`
> freigibt → zwei Encoder auf einem Gerät → **106% CPU + Freeze** (siehe `09_Bug_reports.md`).
>
> **Aktuelle, maßgebliche Architektur:** **Node-MJPEG-Schalter, go2rtc entfernt.**
> Node besitzt die Kameras selbst; das `close`-Event des eigenen FFmpeg ist der harte
> Beweis „Gerät frei". Das Race ist damit konstruktiv ausgeschlossen.
>
> | | alt (unten, abgelöst) | **neu (maßgeblich)** |
> |-|----------------------|----------------------|
> | Geräte-Öffner | go2rtc | **Node** `src/cameraSwitch.js` |
> | Live | go2rtc-WS + `video-stream.js` | MJPEG multipart → `<img>` |
> | HD-Grab | 2. go2rtc-Stream `cam_hires` (Race) | Schalter: Live stoppen (`close`=FD frei) → 1280 → zurück |
> | Multi-User | brach | gelöst (ein FFmpeg → Fan-out) |
>
> **→ Neue Architektur + Hardware-Testplan stehen weiter unten in diesem Dokument
> (Abschnitt „## Node-MJPEG-Schalter").** Alles ab hier bis dorthin ist **Historie**.
---
# AppRobotWebcam Hi-Res-Snapshot via Consumer-Umhängen ⛔ (historisch)
> Status: **Phase 2 implementiert und funktional** (2026-06-04):
> HD-Grab liefert echten 1280×960-Frame (76071 bytes bestätigt). Bekanntes Problem
@@ -334,3 +357,87 @@ und zur Laufzeit wird go2rtc nur **gelesen**, nie verändert.
**Reihenfolge:** Phase 1 (messen, ~null Risiko) → Pausen aus der Messung setzen →
Phase 2 (Grab). Fällt Phase 1, bleibt Weg A (separate Kamera) aus `04_*` der sichere
Fallback.
---
---
# ✅ Node-MJPEG-Schalter (2026-06-05) — maßgebliche Architektur
> Ersetzt den gesamten go2rtc-Ansatz oben. Bei Widerspruch gilt dieser Abschnitt.
## Kernidee: Node besitzt die Kamera selbst
Das 106%-Race entstand, weil **zwei** FFmpeg (Live 640 + HD 1280) gleichzeitig auf
**demselben** `/dev/videoN` liefen, und go2rtcs API nicht zuverlässig melden konnte, wann
ein FFmpeg das Gerät freigibt. **Lösung:** Node startet die FFmpeg-Prozesse selbst → das
`close`-Event des Kindprozesses ist der harte Beweis „Prozess weg ⇒ Kernel-FD geschlossen
⇒ Gerät frei". Race konstruktiv ausgeschlossen, nicht über Timing entschärft.
```
go2rtc ── ENTFERNT
Node (server.js)
├─ CameraSwitch cam0 ── besitzt /dev/video0 ── EIN FFmpeg (Live ODER HD)
├─ CameraSwitch cam1 ── besitzt /dev/video2 ── EIN FFmpeg (Live ODER HD)
├─ /api/stream/<id> ── MJPEG multipart/x-mixed-replace → Browser <img>
└─ /api/snapshot/<id> ── 640 aus RAM · /<id>/hires → HD-Grab über den Schalter
```
## Der Schalter (`src/cameraSwitch.js`)
Eine `CameraSwitch`-Instanz pro Gerät — der **einzige** Öffner von `/dev/videoN`. Hält
immer nur **einen** FFmpeg. Zustände `stopped | live | grabbing`, Mutex pro Kamera.
- **Live (Dauerbetrieb):** `ffmpeg -f v4l2 -input_format mjpeg -video_size 640x480
-framerate 30 -i /dev/videoN -c:v copy -f mpjpeg pipe:1` → kein Re-Encode. Node parst
die mpjpeg-Frames (Content-Length-basiert), hält den letzten (für `/api/snapshot`),
sendet sie an alle Stream-Clients. Crash → Auto-Restart nach 1,5 s.
- **HD-Grab (`grabHires`):** Live-FFmpeg `SIGTERM` → **auf `close` warten** (FD frei) →
1280-FFmpeg, warmlaufen (ab Frame ≥6, ≥15 KB, Breite ≥1000), besten Frame greifen →
beenden, auf `close` warten → `finally`: **immer** Live zurück (Live hat Priorität).
- **Blackout:** `<img>` friert ~13 s ein, läuft dann weiter. **Kein Client-Handling
nötig** (das war früher die Fehlerquelle).
## Auslieferung / Multi-User
`/api/stream/<id>` = `multipart/x-mixed-replace`; ein FFmpeg → Fan-out an N Clients.
Backpressure: voller Socket-Puffer (>1 MB) eines langsamen Clients → Frames für ihn
droppen, andere bleiben flüssig. Clients halten **kein** Gerät → **Multi-User gelöst.**
## Konfiguration (`docker-compose.yaml`)
Ein Node-Container mit FFmpeg, Geräte durchgereicht, `group_add: video`. Env-Overrides:
`DEV0/DEV1`, `LIVE_SIZE/LIVE_FPS`, `HIRES_SIZE/HIRES_FPS`. Firewall: nur noch **TCP 8444**.
## Verifiziert vs. offen
- **Lokal verifiziert (ohne Kamera):** MJPEG-Parser (Unittest, Chunk-robust, `\r\n\r\n`
im Body), HTTP-Routing (snapshot/stream/health, 404/503), Crash-Auto-Restart rate-limitiert.
- **FFmpeg-Args = die der bisher funktionierenden go2rtc-Quelle** (`-f v4l2 -input_format
mjpeg -video_size … -framerate …`), nur Ausgabe `-c:v copy -f mpjpeg`.
- **Auf der Hardware noch zu verifizieren:** CPU-Last, Latenz, HD-Blackout-Dauer, und der
Bug-Reproweg unten.
## Hardware-Testplan
1. Code syncen, Stack neu deployen (Image baut FFmpeg ein — erster Build dauert länger).
2. Viewer öffnen → beide Kameras Live (`MJPEG · live`). **CPU messen** (Erwartung < 50 %).
3. **Bug-Reproweg:** Anmelden → „HD" → Download → Stream nach kurzem Freeze weiter →
**neu anmelden / Tab neu laden.** Erwartung: **keine 106%, kein Dauer-Freeze.**
4. Zwei Browser gleichzeitig → „HD" während beide verbunden → **kein 503** (Multi-User).
5. HD-Bild: 1280×960, nicht schwarz. Blackout-Dauer notieren.
6. Eine Kamera abziehen → Log rate-limitierter Restart, andere Kamera + Node unberührt.
`docker logs AppRobotWebcam` zeigt jeden Zustandswechsel des Schalters.
## Rollback
`git checkout <commit-vor-umbau> -- docker-compose.yaml server.js package.json public/ src/`
(der go2rtc-Stand liegt vollständig in der Git-Historie).
## Mögliche Folgeschritte
- **Hi-Res nativ?** `v4l2-ctl --list-formats-ext -d /dev/video0` — liefert die Kamera
1280×960 als **MJPEG**? Falls nur YUYV → `-c:v copy` scheitert → andere native Auflösung
oder bewusst Re-Encode (teurer).
- **On-Demand Live** (FFmpeg erst bei erstem Client) wäre stromsparender, ist aber bewusst
weggelassen — Dauerbetrieb hält die Übergabe-Logik simpel (weniger Race-Fläche).