From 6dfe42dd18c4c3029e63b3fb7a04be982ccd6430 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Fri, 5 Jun 2026 05:46:59 +0200 Subject: [PATCH] Bug 106% raceCondition --- doc/09_Bug_reports.md | 58 ++++++++++++++++++++++++++++++++++++++++++ src/snapshotService.js | 24 +++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 doc/09_Bug_reports.md diff --git a/doc/09_Bug_reports.md b/doc/09_Bug_reports.md new file mode 100644 index 0000000..0eca924 --- /dev/null +++ b/doc/09_Bug_reports.md @@ -0,0 +1,58 @@ +## 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 (umgesetzt in src/snapshotService.js) + +Server wartet nach dem frame.jpeg-Fetch, bis `cam_hires`-Producer in go2rtc wirklich +gestoppt ist (`pRunning=false`, `nConsumers=0`), plus 400ms Puffer für den FFmpeg-Exit. +Erst dann geht die Antwort zum Client → `/dev/videoN` ist garantiert frei wenn +`startStream(cam)` startet. + +### Noch offen: Multi-User (siehe Abschnitt oben) + +Das Multi-User-Problem bleibt. Bei ≥2 aktiven Clients kann `/hires` nicht starten, +weil der `/hires`-Endpoint wartet bis `cam` 0 Consumer hat (max 8s), aber ein +zweiter Browser die Consumer-Zahl nie auf 0 sinken lässt → Timeout → 503. +Fix-Optionen: siehe Multi-User-Abschnitt oben. \ No newline at end of file diff --git a/src/snapshotService.js b/src/snapshotService.js index 7e1c18d..4debbe9 100644 --- a/src/snapshotService.js +++ b/src/snapshotService.js @@ -114,6 +114,30 @@ function createSnapshotRouter(go2rtcUrl) { return res.status(503).json({ error: 'kein verwertbarer Hi-Res-Frame (Warmup-Timeout)' }); } + // Schritt 3: Warten bis cam_hires Producer gestoppt ist bevor Client cam0 reconnectet. + // Ohne dieses Warten: cam_hires-FFmpeg hält /dev/videoN noch offen, wenn startStream(cam) + // go2rtc's cam-Producer startet → Race, zwei FFmpeg auf demselben Device → 108% CPU. + { + const t2 = Date.now(); + while (Date.now() - t2 < 5000) { + try { + const rp = await fetch(`${go2rtcUrl}/api/streams`, { signal: AbortSignal.timeout(1000) }); + if (rp.ok) { + const ss = await rp.json(); + const sh = ss[hiresId]; + const nCh = sh ? (sh.consumers ?? []).length : 0; + const pHRunning = sh ? (sh.producers ?? []).some(p => (p.state ?? '') === 'running') : false; + if (nCh === 0 && !pHRunning) { + console.log(`[hires][${id}] cam_hires gestoppt nach ${Date.now() - t2}ms – Gerät frei`); + break; + } + } + } catch (_e) { /* ignore */ } + await sleep(300); + } + await sleep(400); // Puffer: FFmpeg-Prozess-Exit bis Kernel Device-FD freigibt + } + console.log(`[hires][${id}] OK – ${jpeg.length} bytes, Breite=${lastWidth}, Dauer=${Date.now() - t0}ms`); res.set({ 'Content-Type': 'image/jpeg',