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,3 +1,10 @@
> # Architektur abgelöst (2026-06-05): go2rtc → Node-MJPEG-Schalter
> Dieses Dokument beschreibt den **go2rtc**-Aufbau (historisch wertvoll: Messungen,
> Fehler-Log, eiserne Regeln gelten weiter sinngemäß). Der Live-Stream läuft seit
> 2026-06-05 **nicht mehr über go2rtc**, sondern über einen Node-eigenen MJPEG-Schalter
> (`src/cameraSwitch.js`). Grund: das 106%-Race beim HD-Snapshot. Maßgeblich:
> `05_screenshot_roadmap.md` (Abschnitt „Node-MJPEG-Schalter") und `09_Bug_reports.md`.
# AppRobotWebcam Delay / Ruckler-Analyse
## Symptom

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).

View File

@@ -80,52 +80,32 @@ Der **Hinweg** (Schritt 1: warten bis `cam` frei, bevor `cam_hires` startet) fun
— er wartet echt (4870ms / 5069ms im Log). Kaputt ist der **Rückweg** (`cam_hires``cam`):
dort wird nicht zuverlässig gewartet.
### Lösungsvorschläge (geordnet nach Robustheit)
### ✅ GELÖST (2026-06-05) — Node-MJPEG-Schalter, go2rtc entfernt
**A — Separate Hi-Res-Kamera (Weg A aus 04). GARANTIERT.**
Zusätzliche USB-Kamera, die go2rtc nicht öffnet; Node grabbt sie on-demand per one-shot
FFmpeg. Anderes Device → kein Race möglich. Kostet Hardware. Einzige 100%-Lösung.
Statt go2rtc zu orchestrieren (blind, racet weiter) **besitzt Node die Kameras jetzt
selbst**. Damit liefert das `close`-Event des selbst gestarteten FFmpeg den harten Beweis
„Gerät frei" — genau das Signal, das go2rtcs API verweigerte. Das Race ist **eliminiert,
nicht getimt**. Details der finalen Architektur: `doc/05_screenshot_roadmap.md`.
**B — Feature streichen, zurück auf KONSOLIDIERT (04). GARANTIERT.**
`cam0_hires`/`cam1_hires` aus docker-compose, `/hires` aus snapshotService, `HD`-Button
aus dem Viewer. Nur noch 640er-Snapshot (read-only `frame.jpeg`). Stabil, kein Hi-Res.
**Was sich änderte:**
**C — No-Hardware-Versuch: Rückweg robust machen. MITTLERE Sicherheit, MUSS gemessen werden.**
Statt auf `state` zu pollen: warten bis go2rtc das **Producer-Objekt entfernt hat**
(`producers`-Array leer für `cam_hires`) + großzügiger Settle. Vorher zwingend
**empirisch messen** (rein lesend, erlaubt): bei abgeschaltetem Live-Stream einmal
`frame.jpeg?src=cam0_hires` holen, dann `GET /api/streams` alle 100ms für ~10s loggen —
zeigt, ob/wann der Producer wirklich verschwindet. Erst wenn die Daten das belegen,
ausliefern. Bleibt prinzipiell ein Race auf geteiltem Device → kein Versprechen.
| Komponente | vorher (go2rtc) | jetzt (Node-Schalter) |
|-----------|-----------------|----------------------|
| Geräte-Öffner | go2rtc | **Node** (`src/cameraSwitch.js`, eine Instanz pro Gerät) |
| Live-Auslieferung | go2rtc WS + `video-stream.js` | MJPEG `multipart/x-mixed-replace``<img>` |
| HD-Snapshot | 2. go2rtc-Stream `cam_hires` (Race!) | Schalter stoppt Live (Prozess-`close` = FD frei), greift 1280, zurück |
| Multi-User | brach (Consumer ≠ 0) | **gelöst**: ein FFmpeg → Fan-out an alle, Clients halten kein Gerät |
| go2rtc | nötig | **entfernt** |
**Empfehlung:** Wenn Hi-Res verzichtbar → **B**. Wenn Hi-Res zwingend → **A**.
**C** nur, wenn kein Hardware-Budget UND Hi-Res nötig — und nur nach Messung (nie wieder
„verifiziert" ohne Messung auf `cam`, 04 eiserne Regel 3).
**Warum 106% jetzt nicht mehr auftritt:** Pro Gerät hält der Schalter immer nur **einen**
FFmpeg. Übergang Live→HD und HD→Live wird über das `close`-Event synchronisiert — zwei
Encoder auf einem `/dev/videoN` sind konstruktiv ausgeschlossen.
### Messung Weg C (Probe) — Anleitung & Ergebnis
**Verifiziert (lokal, ohne Kamera):** MJPEG-Parser (Content-Length-basiert, Chunk-robust,
`\r\n\r\n` im Body) per Unittest; HTTP-Routing (snapshot/stream/health, 404/503-Pfade);
Crash-Auto-Restart rate-limitiert. **Auf der Hardware noch zu verifizieren:** CPU-Last,
Latenz, HD-Blackout-Dauer, kein 106% nach Screenshot+Reconnect (Testplan in 05).
Temporäre, rein lesende Diagnose-Route in `snapshotService.js`: `GET /:id/hires-probe`.
Sie wartet bis `cam` frei ist, holt **einen** `cam_hires`-Frame und schreibt dann 12s lang
alle 100ms den `cam_hires`-Zustand mit (`cons`, `prods`, `states`).
Ablauf:
1. Code auf Server syncen, **`AppRobotWebcam` neu starten** (lädt `server.js`; go2rtc unberührt).
2. Im Viewer die zu messende Kamera **ausschalten** (⏸) → `cam` hat 0 Consumer.
3. `curl http://<host>:8444/api/snapshot/cam0/hires-probe` (oder im Browser öffnen).
4. JSON-Antwort + Container-Log (`[probe]…`) hierher.
Entscheidend: **`producerGoneAtMs`** (wann `prods` auf 0 fällt) und wie sich `states`
entwickelt. Daraus wird der robuste Rückweg gebaut (warten bis `prods===0` + Settle).
Wenn `prods` **nie** 0 wird → go2rtc baut den Producer gar nicht ab → Weg C ist tot,
dann bleibt nur A oder B.
**Ergebnis:** _(hier eintragen nach der Messung)_
Danach die `hires-probe`-Route wieder entfernen.
### Noch offen: Multi-User (siehe Abschnitt oben)
Unabhängig vom 106%-Race: bei ≥2 aktiven Clients kann `/hires` nicht starten, weil
Schritt 1 wartet bis `cam` 0 Consumer hat (max 8s), ein zweiter Browser die Consumer-Zahl
aber nie auf 0 fallen lässt → Timeout → 503. Variante A löst das mit (separates Device,
kein Warten auf 0 Consumer). Sonst: „Schalter"-Idee oben (ein Producer, Server verteilt).
**FFmpeg-Argumente** sind identisch zu denen, die die *bisher funktionierende* go2rtc-
Quelle erzeugte (`-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i …`),
nur `-c:v copy -f mpjpeg pipe:1` als Ausgabe → kein Re-Encode.