Files
appRobotWebcam/doc/09_Bug_reports.md
2026-06-05 06:03:05 +02:00

131 lines
6.5 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.
## 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).