diff --git a/doc/04_Delay_roadmap.md b/doc/04_Delay_roadmap.md index 7daabf4..f763830 100644 --- a/doc/04_Delay_roadmap.md +++ b/doc/04_Delay_roadmap.md @@ -373,3 +373,38 @@ GET /api/snapshot/cam0/hires ``` Umgesetzt in `src/snapshotService.js` und `docker-compose.yaml`. + +### Erster Live-Test (2026-06-04): erfolgreich + 2 Bugs behoben + +Live-Stream nahezu Echtzeit, stabil. Hi-Res-Bild 1280×960 über `/hires` da. +Zwei Bugs gefunden und sofort behoben: + +1. **Schwarzer Player nach Reload** ✓ behoben + Ursache: Stream-Restore rief die go2rtc-API falsch auf. Verifiziert gegen die + go2rtc-OpenAPI-Spec: `PUT /api/streams` erwartet `src` = **Quelle (URI)** und + `name` = Stream-Name, beide als Query-Param. Der Code schickte aber `src=cam0` + (den Namen) und die Quelle im **Body** (den go2rtc ignoriert). Folge: `cam0` wurde + mit Quelle „cam0" = Selbstreferenz neu angelegt → kaputt → beim nächsten + Verbindungsaufbau (Reload) schwarz. Fix: `buildPutUrl()` → + `PUT /api/streams?name=cam0&src=`, kein Body. + (DELETE `?src=cam0` war korrekt — DELETE nutzt `src` als Namen, API-Asymmetrie.) + +2. **Hi-Res-Bild manchmal leer (~1KB schwarz)** ✓ behoben + Ursache: USB-Kamera liefert direkt nach Geräte-Öffnen unbelichtete Frames + (Auto-Belichtung/Weissabgleich brauchen einen Moment). `-frames:v 1` griff den + ersten, schwarzen Frame. Fix: erste 15 Frames verwerfen + (`-vf select=gte(n,15)`), dann einen greifen. Kostet ~1 s mehr Blackout. + +### Offene Punkte (ToDo) + +- **go2rtc-CPU ~53% bei 2 aktiven Live-Streams.** Besser als H.264-Transcode (~127%), + aber kein echtes Null. go2rtc re-encodiert MJPEG→MJPEG (kein `-c:v copy`) statt + reinem Durchreichen. Das sind ~0,5 CPU-Kerne für 2 Kameras → stabil und unkritisch + auf dieser Maschine. **Optionaler Hebel falls je nötig:** prüfen ob go2rtc-Quelle + auf echtes Copy/Passthrough umstellbar ist. Risiko: Stabilität des laufenden, + funktionierenden Streams — daher nur anfassen wenn CPU real zum Problem wird. +- **Geräte-Race bei Hi-Res mit gleichzeitig offenem Live-Tab.** Ist ein Live-Consumer + aktiv, kann go2rtc das Gerät nach dem DELETE per on-demand-Reconnect sofort wieder + greifen und mit dem Hi-Res-Grab kollidieren. Warmup + Frame-Verwerfen fängt das + meist ab. Falls doch leere Bilder auftreten: kurzer Retry im Grab, oder Live-Tab + vor dem Hi-Res-Klick kurz pausieren. diff --git a/src/snapshotService.js b/src/snapshotService.js index 01a03fe..db7436d 100644 --- a/src/snapshotService.js +++ b/src/snapshotService.js @@ -105,15 +105,10 @@ function createSnapshotRouter(go2rtcUrl) { const jpeg = await captureOneFrame(cfg.device, cfg.hiresSize); console.log(`[snapshot][${id}] Frame captured (${jpeg.length} bytes)`); - // 3. go2rtc-Stream wiederherstellen - const putRes = await fetch( - `${go2rtcUrl}/api/streams?src=${encodeURIComponent(id)}`, - { - method: 'PUT', - headers: { 'Content-Type': 'text/plain' }, - body: cfg.streamUrl, - } - ); + // 3. go2rtc-Stream wiederherstellen. + // go2rtc-API: PUT /api/streams?name=&src= + // Quelle steht im `src`-Query-Param (URL-encoded), NICHT im Body. + const putRes = await fetch(buildPutUrl(go2rtcUrl, id, cfg.streamUrl), { method: 'PUT' }); console.log(`[snapshot][${id}] go2rtc PUT stream → HTTP ${putRes.status}`); res.set({ @@ -131,11 +126,7 @@ function createSnapshotRouter(go2rtcUrl) { // Stream auf jeden Fall wiederherstellen, auch im Fehlerfall try { - await fetch(`${go2rtcUrl}/api/streams?src=${encodeURIComponent(id)}`, { - method: 'PUT', - headers: { 'Content-Type': 'text/plain' }, - body: cfg.streamUrl, - }); + await fetch(buildPutUrl(go2rtcUrl, id, cfg.streamUrl), { method: 'PUT' }); console.log(`[snapshot][${id}] Stream nach Fehler wiederhergestellt`); } catch (restoreErr) { console.error(`[snapshot][${id}] Stream-Wiederherstellung fehlgeschlagen:`, restoreErr.message); @@ -158,6 +149,16 @@ function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } +// go2rtc-API zum (Wieder-)Anlegen eines Streams: +// PUT /api/streams?name=&src= +// Beide Werte als Query-Param. `src` ist die QUELLE (nicht der Name) — go2rtc +// liest sie NICHT aus dem Body. +function buildPutUrl(go2rtcUrl, name, streamUrl) { + return `${go2rtcUrl}/api/streams` + + `?name=${encodeURIComponent(name)}` + + `&src=${encodeURIComponent(streamUrl)}`; +} + // Startet FFmpeg, liest genau einen MJPEG-Frame aus stdout, gibt ihn als Buffer zurück. function captureOneFrame(device, size, timeoutMs = 8000) { return new Promise((resolve, reject) => { @@ -166,8 +167,12 @@ function captureOneFrame(device, size, timeoutMs = 8000) { '-f', 'v4l2', '-input_format', 'mjpeg', '-video_size', size, - '-framerate', '10', // niedrige FPS → schnellerer erster Frame + '-framerate', '15', '-i', device, + // Erste ~15 Frames verwerfen: die USB-Kamera liefert direkt nach dem Öffnen + // noch unbelichtete (schwarze) Frames – Auto-Belichtung/Weissabgleich brauchen + // einen Moment. Ohne das kommt das "1KB leer/schwarz"-Bild. + '-vf', 'select=gte(n\\,15)', '-frames:v', '1', '-q:v', '1', // beste JPEG-Qualität '-f', 'mjpeg',