6.1 KiB
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 → <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 |
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 = der bewährte go2rtc-#video=mjpeg-Pfad: -f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 -i … -c:v mjpeg -q:v 5 -f mpjpeg pipe:1 (Re-Encode
mjpeg→mjpeg, ~50% für 2 Kameras).
⚠ Regression am 2026-06-05 (eingebaut und korrigiert): -c:v copy
Erste Fassung des Schalters nutzte -c:v copy (Passthrough) → CPU 100%+ und wieder
hängendes Bild. Ursache: copy ist auf dieser Kamera empirisch tot — 04/09 dokumentieren
es (107%), FFmpeg verschluckt sich an den APP-Feldern des Kamera-MJPEG
([mjpeg] unable to decode APP fields: Invalid data) → keine validen Frames → Freeze.
Das war exakt der in 04/05 dokumentierte und ignorierte Punkt. Fix: -c:v copy →
-c:v mjpeg (Re-Encode, wie go2rtc). Lehre erneut: erst die Doku-Fakten anwenden,
dann bauen.