'use strict'; // go2rtc Player-Modi. // 'mjpeg' → Passthrough: go2rtc reicht Kamera-MJPEG 1:1 durch. // KEIN Transcode → ~0% CPU, keine Freezes, ~200ms Latenz. // 'webrtc,mse,mjpeg' → WebRTC bevorzugt. ABER: WebRTC/MSE können kein MJPEG → // go2rtc transcodiert MJPEG→H.264 in Software (libx264) → // ~55% CPU pro Kamera + Keyframe-Freezes. ~130ms Latenz. const MODE = 'mjpeg'; const IS_MJPEG = !MODE.includes('webrtc'); // MJPEG-Modus: kein WebRTC/getStats/Health // ── Überwachungs-Parameter ─────────────────────────────────────────────────── const MONITOR_INTERVAL = 2500; // ms zwischen Health-Checks (getStats) const CONNECT_GRACE_MS = 25000; // so lange darf der Verbindungsaufbau dauern (kein Alarm) const WARMUP_MS = 15000; // Karenz nach 'playing', bis Überlast-Erkennung scharf wird const OVERLOAD_TICKS = 3; // so viele kritische Checks in Folge → Auto-Abschaltung // Schwellen (auf Basis der ZUVERLÄSSIGEN getStats-Werte, nicht Render-Drops): const SERVER_LOW_FPS = 12; // recv < 12/s nach Aufwärmen → Server liefert wenig (nur Warnung) const CLIENT_DECODE_RATIO = 0.6; // decoded < 60% von recv → Decoder kommt nicht nach (echte Client-Überlast) const NET_LOST_PER_TICK = 5; // mehr verlorene Pakete/Intervall → Netz-Warnung const NET_JITTER_MS = 60; // mehr Jitter → Netz-Warnung // ── Logging (Browser DevTools → Console → F12) ─────────────────────────────── const P = '[WebcamViewer]'; const log = (c, m) => console.log(`${P}[${c}] ${m}`); const warn = (c, m) => console.warn(`${P}[${c}] ⚠ ${m}`); const logErr = (c, m, e) => console.error(`${P}[${c}] ✗ ${m}`, e ?? ''); const sleep = ms => new Promise(r => setTimeout(r, ms)); let GO2RTC_PORT = 1984; const cameras = []; // { id, box, infoEl, toggleBtn, active, startedAt, playingSince, statsLast, badTicks, autoOff } // ── Stream starten / stoppen ───────────────────────────────────────────────── function startStream(cam) { if (cam.box.querySelector('video-stream')) return; const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=${encodeURIComponent(cam.id)}`; log(cam.id, `Verbinde → ${wsUrl}`); cam.active = true; cam.startedAt = performance.now(); cam.playingSince = null; // wird erst beim 'playing'-Event gesetzt cam.statsLast = null; cam.badTicks = 0; const stream = document.createElement('video-stream'); stream.mode = MODE; stream.addEventListener('playing', () => { cam.playingSince = performance.now(); cam.statsLast = null; cam.badTicks = 0; log(cam.id, '▶ Bild läuft (Aufwärmphase startet)'); }, true); stream.addEventListener('error', (e) => logErr(cam.id, 'Video-Fehler', e), true); stream.src = wsUrl; cam.box.insertBefore(stream, cam.box.firstChild); cam.toggleBtn.textContent = '⏸'; cam.toggleBtn.title = 'Stream ausschalten'; setInfo(cam, 'verbindet…', ''); } function stopStream(cam, auto = false) { const el = cam.box.querySelector('video-stream'); if (el) el.remove(); cam.active = false; cam.autoOff = auto; cam.playingSince = null; cam.toggleBtn.textContent = '▶'; cam.toggleBtn.title = 'Stream einschalten'; setInfo(cam, auto ? 'auto-aus (Client überlastet)' : 'aus', auto ? 'crit' : ''); log(cam.id, auto ? 'AUTO-abgeschaltet (Decoder kam nicht nach)' : 'manuell aus'); if (auto) showNotice(); } // ── Hi-Res Canvas-Freeze + Grab (Phase 2) ─────────────────────────────────── // Holt letzten 640er-Frame von /api/snapshot und zeichnet ihn auf canvas. // Robuster als drawImage(video-stream): go2rtc MJPEG-Modus hat kein