Umbau mit cameraSwitch
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 ~1–3 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).
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user