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

6.5 KiB
Raw Blame History

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_hirescam): 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).