diff --git a/doc/04_Delay_roadmap.md b/doc/04_Delay_roadmap.md index 9e098c3..f9aac22 100644 --- a/doc/04_Delay_roadmap.md +++ b/doc/04_Delay_roadmap.md @@ -2,190 +2,199 @@ ## Symptom -Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils >1 s ein, manchmal +Nach Umstieg auf WebRTC/H.264: Bild ruckelt, friert teils 1–2 s ein, manchmal bleibt ein Einzelbild ganz stehen. Im reinen MJPEG-Modus trat das **nicht** auf. -## Messung (2026-06-03) +--- + +## Diagnose-Verlauf + +### Schritt 1 — CPU-Messung (erste Verdachtsphase) | Quelle | CPU | |--------|-----| | System gesamt | ~40 % | -| **Container AppRobotGo2RTC** | **~95 %** | +| AppRobotGo2RTC, 1 Client | **~35 %** | +| AppRobotGo2RTC, 2 Clients (Laptop + Handy) | **65–114 %** | +| AppRobotWebcam (Node.js) | **0 %** | -`docker stats` rechnet pro Kern: **95 % ≈ ein CPU-Kern voll ausgelastet.** -→ Flaschenhals ist **go2rtc (Encoding)**, nicht Netzwerk und nicht der Node-Server. +`docker stats` rechnet pro Kern: 114 % = mehr als ein Kern voll ausgelastet. + +**Erkenntnis:** go2rtc re-encodiert nicht einmal pro Stream, sondern aufwändiger +pro Client-Verbindung (WebRTC-Session). Zwei Clients = fast doppelte CPU-Last. + +### Schritt 2 — Browser-Client als Ursache ausgeschlossen + +WebRTC `getStats()` lieferte über mehrere Minuten: +``` +recv=30/s decoded=30/s dropped=0/s lost=+0 jitter=13–35ms +``` +→ Server liefert alle Frames, Netz verliert nichts, Decoder schafft alles. +**Der Browser ist nicht das Problem.** + +### Schritt 3 — Netz als Ursache ausgeschlossen + +Zwei Browser-Fenster (Laptop + Handy) zeigen exakt dieselbe Verzögerung +für cam0 bzw. cam1 — synchron auf die Millisekunde. Ändert sich die Latenz +von cam0, ändert sie sich auf beiden Clients gleichzeitig. + +→ **Das Problem sitzt in go2rtc/FFmpeg, nicht im Netz oder Browser.** + +### Schritt 4 — Root Cause: FFmpeg-Flags und `exec timeout` + +go2rtc generiert intern folgenden FFmpeg-Befehl: +``` +-readrate_initial_burst 0.001 -re -i /dev/video2 +-c:v libx264 -g 50 -profile:v high -preset:v superfast -tune:v zerolatency +``` + +Zwei Probleme identifiziert: + +**Problem A — `-re` (Rate-Emulation für Live-Input):** +`-re` = „lies Input im Echtzeit-Takt". Für Datei-Wiedergabe gedacht. +Für eine Live-Kamera (die ohnehin Echtzeit-Frames liefert) puffert `-re` +Frames künstlich, statt sie sofort durchzureichen. Wenn der Encoder unter +Last minimal in Rückstand gerät, baut sich ein Puffer auf → variable Latenz. +`-readrate_initial_burst 0.001` macht den Start besonders langsam → erklärt +den langsamen Stream-Aufbau. + +**Problem B — `-g 50` (Keyframe-Abstand 1,67 Sekunden bei 30 fps):** +H.264 überträgt zwischen Keyframes nur Differenzbilder. Der Browser kann erst +ab einem Keyframe decodieren. Nach jedem Paket-Verlust oder Neuverbindung +wartet der Browser bis zu 1,67 s auf den nächsten Keyframe → Standbild. +Da cam0 und cam1 ihre Keyframe-Takte unabhängig haben, friert mal der eine, +mal der andere ein — aber auf allen Clients gleichzeitig (wegen Schritt 3). + +**Problem C — `ERR [exec] timeout` für /dev/video2 (cam1):** +go2rtc's FFmpeg für cam1 läuft gelegentlich in einen Timeout (Kamera-Init +zu langsam, USB-Bandbreitenproblem, Treiberproblem). go2rtc startet den +Encoder neu → cam1 friert für mehrere Sekunden ein, während cam0 läuft. + +### Was bisher versucht wurde + +| Massnahme | Ergebnis | +|-----------|----------| +| `#video=h264#video=mjpeg` entfernt → nur `#video=h264` | CPU-Last von ~95% auf ~35% reduziert | +| `getVideoPlaybackQuality()` als Überlast-Detektor | Fehlalarm (misst Render-Drops, nicht echte Überlast) | +| Umstieg auf `getStats()` (inbound-rtp) | Verlässlich, bestätigt: Client ist nicht das Problem | +| Aufwärmphase (15s nach `playing`) in Browser-Überwachung | Fehlalarme beim Stream-Aufbau beseitigt | --- -## Ursachenanalyse +## Ursachen-Zusammenfassung -### Ursache 1 — Software-H.264-Encoding sättigt die CPU -Die Kamera liefert **MJPEG** nativ. WebRTC im Browser braucht aber **H.264**. -go2rtc transcodiert also jeden Frame MJPEG→H.264 in Software (libx264). -Zwei Kameras parallel → ein Kern voll. Wenn der Encoder nicht nachkommt, -stauen sich Frames → Ruckeln und Aussetzer. +| Ursache | Symptom | Behebbar ohne go2rtc-Patch? | +|---------|---------|----------------------------| +| `-re` + `-readrate_initial_burst 0.001` | Variable Latenz, langsamer Aufbau | Ja (anderer Source-Typ) | +| `-g 50` (1,67s GOP) | Bis zu 1,67s Standbild | Ja (exec: mit eigenem FFmpeg) | +| Software-H.264 × 2 Kameras × n Clients | CPU-Sättigung ab 2 Clients | Ja (Hardware-Encode) | +| cam1 FFmpeg timeout | Multi-Sekunden-Freeze cam1 | Teilweise (v4l2: Source) | -> Verstärkt wurde es vorher durch `#video=h264#video=mjpeg`: das ließ go2rtc -> **doppelt** encodieren (H.264 *und* MJPEG). Das `#video=mjpeg` ist inzwischen -> in der Config entfernt — der Hauptkostenfaktor (H.264-Software-Encode) bleibt. - -### Ursache 2 — Großes GOP (Keyframe-Abstand `-g 50`) -go2rtc setzt standardmäßig ein Keyframe alle 50 Frames = **1,67 s bei 30 fps**. -H.264 überträgt zwischen Keyframes nur Differenzbilder. Geht ein Paket verloren -oder verbindet sich der Client neu, **wartet der Browser bis zum nächsten Keyframe** -— bis zu 1,67 s Standbild. Das erklärt exakt das „ein Bild bleibt ganz stehen". - -### Der Grundkonflikt -- **MJPEG**: kein Encode (Kamera-nativ), kein GOP → flüssig, ~200 ms, höhere Bandbreite -- **H.264/WebRTC**: ~130 ms, geringe Bandbreite → aber Encode-Last + GOP-Freezes - -Wir zahlen also CPU-Last und Komplexität für ~70 ms Latenzgewinn. Ob sich das -lohnt, hängt davon ab, ob wir die CPU-Last loswerden (Hardware-Encode / native H.264). +**go2rtc kann diese FFmpeg-Flags nicht per einfacher URL-Syntax konfiguriert werden.** +Sie sind hard-coded im `ffmpeg:` Source-Handler von go2rtc 1.9.x. --- ## Lösungsweg — geordnet nach Aufwand/Wirkung -### Schritt 1 (5 min) — Prüfen: kann die Kamera H.264 nativ? -```bash -docker exec AppRobotGo2RTC v4l2-ctl --list-formats-ext -d /dev/video0 -# v4l2-ctl fehlt im go2rtc-Image? → auf dem Host ausführen: -v4l2-ctl --list-formats-ext -d /dev/video0 +### Option A — `v4l2:` Source statt `ffmpeg:` (sofort probieren) +go2rtc hat einen nativen v4l2-Treiber, der FFmpeg für den Capture umgeht: +```yaml +streams: + cam0: "v4l2:/dev/video0#video=h264" + cam1: "v4l2:/dev/video2#video=h264" ``` -- **Steht „H264" in der Liste** → go2rtc kann den Stream **durchreichen** (passthrough), - praktisch NULL Encode-Last und niedrigste Latenz. Bestfall. -- Steht nur MJPEG/YUYV → weiter mit Schritt 2. - -### Schritt 2 (Hauptfix) — Hardware-Encoding (Intel QuickSync / VAAPI) -Ein ThinkCentre hat fast sicher eine Intel-iGPU mit QuickSync. Damit wandert das -H.264-Encoding von der CPU auf die GPU → **CPU von ~95 % auf ~10 %**. +- Kein `-re`, kein `-readrate_initial_burst` → direkter Frame-Durchsatz +- Encoding (libx264) bleibt, aber ohne künstliches Puffern +- Könnte den `exec timeout` auf cam1 beheben (anderer Kamera-Öffnungspfad) +- **Risiko:** v4l2-Source in go2rtc ist weniger getestet als ffmpeg-Source +### Option B — Hardware-Encoding Intel QuickSync / VAAPI Prüfen ob GPU verfügbar: ```bash ls -l /dev/dri # renderD128 vorhanden? ``` -Umsetzung (später): +Config: ```yaml -# beim go2rtc-Service: -devices: - - /dev/video0:/dev/video0 - - /dev/video2:/dev/video2 - - /dev/dri:/dev/dri # ← GPU durchreichen -# in der go2rtc-Config: +# go2rtc-Service: devices: + /dev/dri:/dev/dri streams: cam0: "ffmpeg:/dev/video0#video=h264#hardware" cam1: "ffmpeg:/dev/video2#video=h264#hardware" ``` +- Encoding auf GPU → CPU von ~35 % auf ~5 % +- go2rtc erzeugt anderen FFmpeg-Befehl (h264_vaapi statt libx264) +- Ob `-re` dabei ebenfalls wegfällt: **muss am Gerät verifiziert werden** -### Schritt 3 — GOP verkürzen (gegen Freeze nach Loss/Reconnect) -Standard-Format erlaubt kein `-g`. Dafür `exec:`-Source mit eigenem FFmpeg-Befehl: +### Option C — Eigener FFmpeg-Befehl via exec: Source +Vollständige Kontrolle über alle FFmpeg-Flags: ```yaml streams: cam0: - - "exec:ffmpeg -hide_banner -f v4l2 -input_format mjpeg -video_size 640x480 - -framerate 30 -i /dev/video0 - -c:v h264_vaapi -g 15 -bf 0 -tune zerolatency - -f rtsp {output}" + - "ffmpeg:-f v4l2 -input_format mjpeg -video_size 640x480 -framerate 30 + -fflags nobuffer -flags low_delay + -i /dev/video0 + -c:v libx264 -preset ultrafast -tune zerolatency -g 15 -bf 0 + #video=h264" ``` -`-g 15` = Keyframe alle 0,5 s → Freeze nach Störung max 0,5 s statt 1,67 s. -`-bf 0` = keine B-Frames (kein Lookahead-Delay). +- Kein `-re` (nicht angegeben) +- `-g 15` = Keyframe alle 0,5 s → max 0,5 s Freeze +- `-fflags nobuffer -flags low_delay` = minimaler Input-Buffer +- **Problem:** go2rtc's `ffmpeg:` Source-Handler mit Custom-Args ist Version-abhängig; + korrekte Syntax muss verifiziert werden -### Schritt 4 — Stellschrauben (zusätzliche Reserve) -- Auflösung 640×480 → **320×240** (viertelt die Encode-Pixel) -- Framerate 30 → **15 fps** (halbiert die Encode-Frequenz) +### Option D — Separater MediaMTX-Container +MediaMTX (rtsp-simple-server) als Zwischenstufe: +``` +v4l2 → FFmpeg (eigene Flags, g=15, kein -re) → RTSP (MediaMTX) → go2rtc → WebRTC +``` +- Volle FFmpeg-Kontrolle +- go2rtc liest einfach `rtsp://mediamtx:8554/cam0` +- Zusätzlicher Container, aber sauber und wartbar -### Schritt 5 (Fallback) — zurück zu MJPEG -Falls Hardware-Encode nicht verfügbar ist oder zickt: -- **kein Encode, kein GOP → keine Freezes**, stabil flüssig -- ~200 ms Latenz (statt 130 ms), höhere Bandbreite — bei 1–3 LAN-Usern egal -- go2rtc liefert MJPEG direkt; Viewer: `MODE = 'mjpeg'` oder simples `` +### Option E — Fallback: MJPEG +```yaml +streams: + cam0: "ffmpeg:/dev/video0#video=mjpeg" +``` +- Kein H.264-Encode, kein GOP, keine `-re`-Problematik +- ~200 ms Latenz (statt 130 ms) — bei 1–3 Usern und Roboter-Überwachung ausreichend +- War nachweislich stabil und flüssig --- -## Entscheidungsbaum +## Empfohlene Reihenfolge ``` -Kamera kann H.264 nativ? ──ja──► Passthrough (Schritt 1) ✓ fertig - │ nein - ▼ -/dev/dri vorhanden? ──ja──► Hardware-Encode (Schritt 2) ✓ Hauptfix - │ nein + GOP kürzen (Schritt 3) - ▼ -Latenz 200ms akzeptabel? ──ja──► MJPEG-Fallback (Schritt 5) ✓ robust - │ nein - ▼ - Auflösung/fps senken (Schritt 4), notfalls 1 Kamera +1. Option A (v4l2: Source) → 5 min, kein Aufwand, könnte alles lösen +2. Option B (Hardware-Encode) → 15 min, braucht /dev/dri-Check +3. Option C (custom FFmpeg) → 30 min, volle Kontrolle +4. Option D (MediaMTX) → 60 min, sauberste Architektur +5. Option E (MJPEG) → 5 min, sicherer Hafen ``` -## Empfehlung - -Reihenfolge **1 → 2 → 3**: -1. Erst native H.264 prüfen (kostet 5 min, evtl. löst es alles). -2. Sonst Hardware-Encoding aktivieren — das ist der eigentliche Hebel gegen die 95 %. -3. Dann GOP kürzen, damit auch die Restfreezes verschwinden. - -**MJPEG (Schritt 5) ist der sichere Hafen**, falls die GPU nicht mitspielt: -es war nachweislich flüssig, nur 70 ms langsamer. Für diesen Anwendungsfall -(Roboter-Überwachung, 1–3 User) völlig ausreichend. +--- ## Hi-Res-Snapshots — Analyse (Live-Video + Foto alle ~10 s) -Ziel: schnelles Live-Video **und** gelegentlich (≈ alle 10 s) ein hochauflösendes Foto. +**Grundprinzip:** Snapshot-Auflösung = Stream-Auflösung (USB-Kamera kann nur +in einer Auflösung gleichzeitig geöffnet sein). Für Hi-Res-Fotos muss der +Stream selbst hochauflösend laufen, Browser skaliert fürs Display herunter. -### Die entscheidende Einschränkung -Eine USB-Kamera kann **gleichzeitig nur in einer Auflösung** geöffnet werden. -Solange go2rtc das Device für den Live-Stream hält, kann kein zweiter Prozess -parallel ein höher aufgelöstes Foto ziehen (Device belegt). +### Weg A — MJPEG hochauflösend (Passthrough, Option E oben) +- Kamera liefert MJPEG nativ → go2rtc reicht 1:1 durch, kein Encode +- Snapshot: `/api/frame.jpeg` = voller Frame, native JPEG-Qualität, gratis +- CPU ~5 %, keine Freezes +- Empfohlen wenn Hardware-Encoding nicht verfügbar -→ **Snapshot-Auflösung = Stream-Auflösung.** Es gibt keinen billigen Nebenweg zu -einem höher aufgelösten Foto, solange der Stream klein läuft. `/api/frame.jpeg` -decodiert immer einen Frame **aus dem laufenden Stream**. - -Konsequenz: Für Hi-Res-Fotos muss der **Stream selbst hochauflösend** laufen und -fürs Live-Bild im Browser heruntergerechnet werden. Der Trick ist, das billig zu halten. - -### Weg A — MJPEG hochauflösend (Passthrough) -- Quelle: Kamera hochauflösend MJPEG → go2rtc reicht 1:1 durch, **kein Encode** -- Snapshot: `/api/frame.jpeg` = voller Frame, **native JPEG-Qualität**, gratis -- Live: MJPEG, im Browser auf 480 skaliert (~200 ms, war flüssig) -- CPU ~5 %, keine Freezes. Preis: höhere LAN-Bandbreite (unkritisch bei 1–3 Usern) - -### Weg B — WebRTC + Hardware-Encoding ◄ favorisiert, mit Bedingung -- Quelle: Kamera hochauflösend; Live-Track H.264 **per Intel-GPU (QuickSync)** -- Live: WebRTC ~130 ms, CPU ~10 % - -**Bedingung des Users: der Frame aus dem Stream MUSS hochauflösend sein.** -Antwort: **ja, per Definition** — `/api/frame.jpeg` hat dieselbe Auflösung wie der -Stream. Läuft H.264 in 1280×960, ist das Foto 1280×960. Garantiert durch die Config -(Stream-Auflösung explizit hochauflösend setzen → WebRTC überträgt hochauflösend, -Browser skaliert fürs Display herunter). - -**Qualitäts-Nuance:** Ein aus H.264 decodierter Frame ist leicht verlustbehaftet -(H.264 → JPEG). Für ArUco meist ausreichend, aber nicht optimal. - -**Beste Variante (Hi-Res UND native Qualität)** — erst durch HW-Encode praktikabel: +### Weg B — H.264 Hardware-Encode + MJPEG-Passthrough (Option B oben) ```yaml -# Quelle hochauflösend; H.264 (GPU) für Live + MJPEG-Passthrough für Snapshot cam0: "ffmpeg:/dev/video0#video=h264#hardware#video=mjpeg" ``` -- Live-Track: H.264 per GPU (billig) -- Snapshot-Track: MJPEG-Passthrough (gratis, kamera-nativ) -- `/api/frame.jpeg` sollte den **MJPEG-Track** nehmen → volle Auflösung, native Qualität -- Das ist `#video=h264#video=mjpeg` wie früher — aber OHNE Flaschenhals, weil nur - H.264 die GPU nutzt und MJPEG reines Durchreichen ist. +- Live: H.264 per GPU (~130 ms, niedrige Bandbreite) +- Snapshot: MJPEG-Passthrough (native Qualität, gratis) +- Zu verifizieren: welchen Track nimmt `/api/frame.jpeg` — H.264 oder MJPEG? -### Vor Weg B zu verifizieren („sichergestellt" erst danach) -1. `ls -l /dev/dri` → ist `renderD128` vorhanden? (Intel-GPU verfügbar) -2. Hardware-Encode testweise aktivieren (`#hardware`) → fällt CPU wirklich von 95 %? -3. `/api/frame.jpeg?src=cam0` abrufen → **Auflösung prüfen** (hoch?) **und Qualität** -4. Klären, welchen Track `/api/frame.jpeg` bei `#video=h264#hardware#video=mjpeg` - tatsächlich verwendet (MJPEG-Passthrough = native Qualität gewünscht) - -> Diese 4 Checks können nicht aus der Ferne garantiert werden — sie müssen am -> ThinkCentre laufen. Erst danach ist Weg B „sichergestellt". - -### Snapshot-Takt (alle ~10 s) -Der 10-s-Takt erzeugt **keine** Dauerlast: pro Foto wird nur ein Frame aus dem -ohnehin laufenden Stream abgegriffen. Trigger wahlweise: -- Pull: Homing-Projekt ruft `/api/snapshot/cam0` alle 10 s ab (aktuell so vorgesehen) -- Push: kleiner Timer im Node-Server, der das Foto ablegt / per Webhook sendet (Phase 5) +### Snapshot-Takt +Der 10-s-Takt erzeugt keine Dauerlast: pro Foto wird ein Frame aus dem +laufenden Stream abgegriffen. Trigger: Homing-Projekt ruft +`GET /api/snapshot/cam0` alle 10 s ab (aktuell so implementiert). diff --git a/server.js b/server.js index 62d2980..627b917 100644 --- a/server.js +++ b/server.js @@ -6,15 +6,14 @@ const path = require('path'); const { createProxyMiddleware } = require('http-proxy-middleware'); const { createSnapshotRouter } = require('./src/snapshotService'); -const PORT = parseInt(process.env.PORT ?? '8444', 10); -const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984'; -const GO2RTC_PORT = parseInt(process.env.GO2RTC_PORT ?? '1984', 10); +const PORT = parseInt(process.env.PORT ?? '8444', 10); +const GO2RTC_URL = process.env.GO2RTC_URL ?? 'http://localhost:1984'; +const GO2RTC_PORT = parseInt(process.env.GO2RTC_PORT ?? '1984', 10); const app = express(); // ── 1. Eigene Endpunkte (vor dem Proxy registrieren) ───────────────────────── -// Stabile Snapshot-API für das Homing-Projekt app.use('/api/snapshot', createSnapshotRouter(GO2RTC_URL)); app.get('/health', async (_req, res) => { @@ -27,21 +26,18 @@ app.get('/health', async (_req, res) => { } }); -// Gibt dem Viewer die go2rtc-Port-Nummer mit – Browser baut WS direkt zu go2rtc. -// Trennung HTTP (Node) / WebSocket (go2rtc) ist sauberer als ein fragiler WS-Proxy. app.get('/config.json', (_req, res) => { res.json({ go2rtcPort: GO2RTC_PORT }); }); -// ── 2. HTTP-Proxy zu go2rtc (nur für Script-Dateien und API ohne WS) ───────── -// /api/ws NICHT hier proxy-en – das macht der Browser direkt (s. viewer.js). +// ── 2. HTTP-Proxy zu go2rtc ─────────────────────────────────────────────────── const go2rtcProxy = createProxyMiddleware({ - target: GO2RTC_URL, + target: GO2RTC_URL, changeOrigin: true, - pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'], - logger: console, + pathFilter: ['/api', '/video-rtc.js', '/video-stream.js'], + logger: console, on: { - error: (err, req, res) => { + error: (err, _req, res) => { console.error('[HPM] proxy error:', err.message); if (!res.headersSent) res.status(502).json({ error: 'go2rtc nicht erreichbar' }); }, @@ -49,9 +45,70 @@ const go2rtcProxy = createProxyMiddleware({ }); app.use(go2rtcProxy); -// ── 3. Statische Dateien (eigener Viewer) ──────────────────────────────────── +// ── 3. Statische Dateien ────────────────────────────────────────────────────── app.use(express.static(path.join(__dirname, 'public'))); +// ── 4. go2rtc Stream-Monitor (server-seitiges Logging) ─────────────────────── +// Pollt alle 5 s go2rtc /api/streams und loggt Änderungen. +// Sichtbar im Portainer-Log von AppRobotWebcam. +// Logt: Producer-Starts/-Stops, Consumer-Anzahl, Timeouts/Restarts. +// +// go2rtc /api/streams liefert z.B.: +// { "cam0": { "producers": [{"url":"...","state":"running"}], "consumers": [...] } } +// +const STREAM_POLL_MS = 5000; +let prevStreamState = {}; + +async function pollGo2rtcStreams() { + try { + const r = await fetch(`${GO2RTC_URL}/api/streams`); + if (!r.ok) { console.warn(`[monitor] /api/streams → HTTP ${r.status}`); return; } + const streams = await r.json(); + + for (const [name, data] of Object.entries(streams)) { + const producers = data.producers ?? []; + const consumers = data.consumers ?? []; + const nConsumers = consumers.length; + const prev = prevStreamState[name] ?? {}; + + // Producer-Status + for (let i = 0; i < producers.length; i++) { + const p = producers[i]; + const state = p.state ?? 'unknown'; + const key = `${name}.p${i}`; + const pPrev = prevStreamState[key]; + + if (pPrev !== state) { + if (state === 'running') console.log(`[monitor][${name}] producer #${i} LÄUFT (${p.url ?? ''})`); + if (state === 'error') console.error(`[monitor][${name}] producer #${i} FEHLER (${p.url ?? ''})`); + if (state === 'stop') console.warn(`[monitor][${name}] producer #${i} GESTOPPT`); + if (!['running','error','stop'].includes(state)) + console.log(`[monitor][${name}] producer #${i} state="${state}"`); + prevStreamState[key] = state; + } + } + + // Consumer-Anzahl — nur loggen wenn sie sich ändert + if (prev.nConsumers !== nConsumers) { + console.log(`[monitor][${name}] consumers: ${prev.nConsumers ?? '?'} → ${nConsumers}`); + prevStreamState[name] = { ...prev, nConsumers }; + } + } + + // Streams die verschwunden sind (Timeout/Restart) + for (const name of Object.keys(prevStreamState)) { + if (name.includes('.')) continue; // skip producer-state keys + if (!streams[name]) { + console.warn(`[monitor][${name}] Stream verschwunden aus go2rtc`); + delete prevStreamState[name]; + } + } + + } catch (err) { + console.error('[monitor] go2rtc nicht erreichbar:', err.message); + } +} + // ── Start ───────────────────────────────────────────────────────────────────── const server = http.createServer(app); @@ -61,6 +118,13 @@ server.listen(PORT, '0.0.0.0', () => { console.log(` go2rtc WS: ws://[host]:${GO2RTC_PORT} (Browser verbindet direkt)`); console.log(` Viewer: http://0.0.0.0:${PORT}/`); console.log(` Snapshot API: http://0.0.0.0:${PORT}/api/snapshot/cam0`); + console.log(` Stream-Monitor: alle ${STREAM_POLL_MS / 1000}s → Portainer-Log`); + + // Ersten Poll nach 3 s (go2rtc braucht einen Moment zum Starten) + setTimeout(() => { + pollGo2rtcStreams(); + setInterval(pollGo2rtcStreams, STREAM_POLL_MS); + }, 3000); }); const shutdown = (sig) => {