131 lines
6.5 KiB
Markdown
131 lines
6.5 KiB
Markdown
## Multi-User ##
|
||
|
||
Wenn zwei (oder mehr) User streamen, dann kann kein High-Res Bild mehr
|
||
gemacht werden. Das Problem: Der Stream bleibt irgendwo aufrecht erhalten.
|
||
|
||
|
||
Fix-Vorschlag Option 1: Alle Browser erhalten bei einem High-Res Request
|
||
die Anweisung, den Stream abzumelden.
|
||
|
||
Fix-Vorschlag Option 2: Der Stream wird umgeleitet zu einer Zwischenstelle "Schalter" und erst von dort aus an die Browser verteilt. Dann wird die
|
||
"unterbrechung" bzw. "umleitung" des Streams serverseitig erledigt.
|
||
|
||
|
||
|
||
|
||
## BugReport 106% ##
|
||
|
||
Anmelden abmelden funktioniert. Es bleibt bei 35% Prozessor-Last
|
||
|
||
Anmelden Screenshot funktioniert. Es bleibt bei 35% Prozessor-Last
|
||
|
||
Anmelden Screenshot neu anmelden > Es gibt 108% Prozessor-Last und ein Stream friert ein.
|
||
|
||
|
||
Wenn ich den Container restarte, funktioniert es.
|
||
|
||
siehe auch doc/04_Delay_roadmap.md sowie doc/05_screenshot_roadmap.md
|
||
|
||
### Root Cause (2026-06-05)
|
||
|
||
**Race condition:** `cam_hires` und `cam` belegen kurz gleichzeitig dasselbe `/dev/videoN`.
|
||
|
||
```
|
||
1. stopStream(cam0) → 0 Consumer → go2rtc stoppt cam0-FFmpeg → /dev/video0 frei
|
||
2. Server: frame.jpeg → go2rtc startet cam0_hires-FFmpeg auf /dev/video0 (1280×960)
|
||
3. Response fertig → go2rtc sendet SIGTERM an cam0_hires-FFmpeg
|
||
4. Server antwortet Client (OHNE auf FFmpeg-Exit zu warten!) ← Fehler
|
||
5. Client sleep(600ms) → startStream(cam0) → go2rtc startet cam0-FFmpeg auf /dev/video0
|
||
⚠ cam0_hires-FFmpeg hält /dev/video0 noch offen → 2 FFmpeg auf 1 Device → 108%
|
||
```
|
||
|
||
Der Konflikt in Schritt 5 versetzt `cam0_hires` in einen Fehlerzustand in go2rtc.
|
||
Beim nächsten Reconnect ("neu anmelden") startet go2rtc's Retry `cam0_hires` erneut —
|
||
gleichzeitig mit `cam0` → wieder 108% + Stream friert ein.
|
||
|
||
### ❌ Fix-Versuch 1 (2026-06-05) GESCHEITERT — „Warten bis cam_hires gestoppt"
|
||
|
||
Idee war: Server pollt nach dem frame.jpeg-Fetch, bis `cam_hires`-Producer nicht mehr
|
||
`state=='running'` ist (+400ms Puffer), erst dann Antwort an Client.
|
||
|
||
**Der Log beweist: der Poll ist ein No-op.**
|
||
```
|
||
[hires][cam1] Versuch 1: 90586 bytes, Breite=1280
|
||
[hires][cam1] cam_hires gestoppt nach 1ms – Gerät frei ← bricht SOFORT ab
|
||
[hires][cam0] cam_hires gestoppt nach 1ms – Gerät frei
|
||
```
|
||
go2rtc meldet den hires-Producer schon **1ms** nach dem Frame als „nicht running" —
|
||
obwohl der FFmpeg gerade eben noch ein 1280-Bild geliefert hat. Die tragende Annahme
|
||
(*API-State `running` ⇒ FFmpeg hält das Device*) ist **falsch**. Übrig bleibt nur das
|
||
blinde `sleep(400)`. Das ist exakt der Fehler-Typ aus 04 (#5/#7): „verifiziert"
|
||
behauptet, aber die sicherheitskritische Annahme war ungemessen.
|
||
|
||
**Folge: jetzt löst schon EIN Screenshot 106% aus** (vorher erst beim Reconnect). Erklärung:
|
||
Es war immer ein **Race auf dem geteilten `/dev/videoN`**. Die 400ms haben das Timing nur
|
||
verschoben — jetzt landen wir auf der schlechten Seite. **Timing-Pflaster verschieben das
|
||
Race, sie beseitigen es nicht.** → zurückgerollt (Schritt 3 wieder entfernt).
|
||
|
||
### Eigentliches Problem (eine Ebene tiefer)
|
||
|
||
`cam` (640) und `cam_hires` (1280) zeigen auf **dasselbe physische `/dev/videoN`**.
|
||
Eine USB-Kamera = ein Öffner (eiserne Regel 04). 106% = **zwei Encoder gleichzeitig** auf
|
||
einem Device (bestätigt: nicht „device busy → exit", sondern beide laufen).
|
||
|
||
**Kernhürde:** go2rtc's REST-API kann **nicht** zuverlässig sagen, wann FFmpeg den
|
||
Device-FD wirklich freigegeben hat. Die State-Felder flippen, bevor der Kernel-FD frei
|
||
ist (beim Start zeigte der Monitor sogar `cam1_hires producer state="unknown"`). Damit
|
||
ist **jede Timing-basierte Übergabe ein Ratespiel**.
|
||
|
||
Der **Hinweg** (Schritt 1: warten bis `cam` frei, bevor `cam_hires` startet) funktioniert
|
||
— 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)
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
**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.
|
||
|
||
**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).
|
||
|
||
### Messung Weg C (Probe) — Anleitung & Ergebnis
|
||
|
||
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). |