## Multi-User ##
> ✅ **GELÖST (2026-06-05) durch den Node-MJPEG-Schalter.** Genau die „Schalter"-Idee aus
> Option 2 wurde umgesetzt: ein FFmpeg pro Gerät, der Server verteilt an alle Browser
> (Fan-out). Clients halten kein Gerät mehr → HD-Grab pausiert nur kurz die eine Quelle,
> unabhängig von der Client-Zahl. Details: `05_screenShot_roadmap.md`. (Beschreibung unten
> = ursprünglicher Bug-Report.)
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 (Default `ENCODE_MODE=copybsf`):** `-f v4l2 -input_format mjpeg -video_size
640x480 -framerate 30 -i … -c:v copy -bsf:v mjpeg2jpeg -f mpjpeg pipe:1` — Bitstream-Copy,
kein Transcode. Fallback `mjpeg` = Re-Encode mjpeg→mjpeg (~50%, wie go2rtc).
### ⚠ Regression am 2026-06-05 (eingebaut → korrigiert → optimiert)
1. **Fehler:** Erste Schalter-Fassung nutzte plain `-c:v copy` (ohne Bitstream-Filter)
→ **CPU 100%+ und hängendes Bild.** Das Kamera-MJPEG lässt JPEG-Tables weg, der
mpjpeg-Muxer verschluckt sich (`[mjpeg] unable to decode APP fields`) → keine validen
Frames. **Exakt der in 04/05 dokumentierte und von mir ignorierte Punkt.**
2. **Sofort-Fix:** `-c:v mjpeg` (Re-Encode, ~50%, wie go2rtc) — stabil, aber nicht besser
als der alte Stand.
3. **Host-Messung (User, 2026-06-05):** `-c:v copy -bsf:v mjpeg2jpeg` läuft 10 s sauber
durch (Copy bestätigt, nur einmalige Probe-Warnung). Der `mjpeg2jpeg`-Filter ergänzt
die fehlenden Tables ohne Decode → **kein Transcode, CPU < Re-Encode, valide JPEGs.**
→ als Default `copybsf` verdrahtet, `mjpeg` bleibt als Fallback (`ENCODE_MODE`).
Lehre erneut: **erst die Doku-Fakten anwenden, dann bauen** — und Optimierungen **messen**
(Punkt 3), nicht vorhersagen.
---
## Stream-Freeze (Einzelfall) — offen, niedrige Priorität
**Beobachtet 2026-06-05:** Ein Live-Stream ist **einmal** eingefroren (Bild stand still,
während die Last normal blieb). Vom User als akzeptabel eingestuft, später anschauen.
**Verdachtsmomente (zu prüfen, wenn es erneut auftritt):**
- Die multipart-Verbindung des `
` brach ab (z. B. ein fehlgeschlagener `res.write`
→ `cleanup()` meldet den Verbraucher ab), **ohne** dass der Browser von selbst neu
verbindet. Der `
`-`error`-Handler in `viewer.js` reconnectet nur bei einem echten
`error`-Event — ein „eingefrorenes" Bild ohne Fehler-Event fällt durch.
- Ein einzelnes korruptes JPEG aus dem Kamera-MJPEG (die `APP fields`-Warnung), das der
Browser nicht rendert und danach hängt.
**Möglicher Fix (wenn relevant):** clientseitiger Watchdog — Frames/Zeit zählen
(`img.onload`-Takt); kommt N Sekunden kein neues Bild, `img.src` neu setzen (Reconnect).
Server-seitig ist die Quelle stabil (CPU/Log unauffällig), daher zuerst clientseitig ansetzen.