## 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. ### ✅ GELÖST (2026-06-05) — Node-MJPEG-Schalter, go2rtc entfernt 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`. **Was sich änderte:** | 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` → `` | | 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** | **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. **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). **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.