'use strict'; // go2rtc Player-Modi – Fallback-Reihenfolge: WebRTC → MSE → MJPEG const MODE = 'webrtc,mse,mjpeg'; // ── Überwachungs-Parameter ─────────────────────────────────────────────────── const MONITOR_INTERVAL = 2000; // ms zwischen Health-Checks const DROP_WARN = 0.10; // >10% verworfene Frames → gelb const DROP_CRIT = 0.30; // >30% verworfene Frames → rot const OVERLOAD_TICKS = 3; // so viele kritische Checks in Folge → Auto-Abschaltung // ── 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 ?? ''); let GO2RTC_PORT = 1984; const cameras = []; // { id, box, infoEl, toggleBtn, active, last, badTicks, autoOff } // ── Stream starten / stoppen (durch Erzeugen/Entfernen des Elements) ───────── function startStream(cam) { if (cam.box.querySelector('video-stream')) return; // läuft schon const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=${encodeURIComponent(cam.id)}`; log(cam.id, `Verbinde → ${wsUrl}`); const stream = document.createElement('video-stream'); stream.mode = MODE; stream.addEventListener('playing', () => log(cam.id, '▶ Bild läuft'), true); stream.addEventListener('error', (e) => logErr(cam.id, 'Video-Fehler', e), true); stream.src = wsUrl; // Vor das Label einfügen, damit Overlays oben liegen cam.box.insertBefore(stream, cam.box.firstChild); cam.active = true; cam.last = null; cam.badTicks = 0; cam.toggleBtn.textContent = '⏸'; cam.toggleBtn.title = 'Stream ausschalten'; } function stopStream(cam, auto = false) { const el = cam.box.querySelector('video-stream'); if (el) el.remove(); // disconnectedCallback schließt WS/PeerConnection cam.active = false; cam.autoOff = auto; cam.toggleBtn.textContent = '▶'; cam.toggleBtn.title = 'Stream einschalten'; setInfo(cam, auto ? 'auto-aus (überlastet)' : 'aus', 'crit'); log(cam.id, auto ? 'AUTO-abgeschaltet (Browser überlastet)' : 'manuell aus'); if (auto) showNotice(); } // ── Health-Anzeige ─────────────────────────────────────────────────────────── function setInfo(cam, text, cls) { cam.infoEl.textContent = text; cam.infoEl.className = 'cam-info ' + (cls ?? ''); } // ── Monitor-Schleife: misst fps + verworfene Frames pro Kamera ─────────────── function monitorTick() { cameras.forEach(cam => { if (!cam.active) return; const video = cam.box.querySelector('video'); if (!video || typeof video.getVideoPlaybackQuality !== 'function') { setInfo(cam, 'läuft', 'ok'); return; } const q = video.getVideoPlaybackQuality(); const now = performance.now(); const cur = { dropped: q.droppedVideoFrames, total: q.totalVideoFrames, t: now }; // Beim ersten Tick (oder nach Neustart = Zähler zurückgesetzt) nur Baseline merken if (!cam.last || cur.total < cam.last.total) { cam.last = cur; setInfo(cam, 'misst…', 'ok'); return; } const dTotal = cur.total - cam.last.total; const dDropped = cur.dropped - cam.last.dropped; const dSec = (cur.t - cam.last.t) / 1000; cam.last = cur; if (dTotal <= 0) { // kein neuer Frame → evtl. Freeze setInfo(cam, '⏳ keine Frames', 'warn'); cam.badTicks++; } else { const fps = Math.round(dTotal / dSec); const ratio = dDropped / dTotal; const pct = Math.round(ratio * 100); const cls = ratio >= DROP_CRIT ? 'crit' : ratio >= DROP_WARN ? 'warn' : 'ok'; setInfo(cam, `${fps} fps · ${pct}% drop`, cls); cam.badTicks = ratio >= DROP_CRIT ? cam.badTicks + 1 : 0; } // Auto-Schutz: anhaltende Überlast → diese Kamera abschalten if (cam.badTicks >= OVERLOAD_TICKS) { warn(cam.id, `überlastet (${cam.badTicks}× kritisch) → Auto-Abschaltung`); stopStream(cam, true); } }); } // ── Hinweis-Banner bei Auto-Abschaltung ────────────────────────────────────── function showNotice() { const off = cameras.filter(c => c.autoOff && !c.active).map(c => c.id); const bar = document.getElementById('notice'); if (off.length === 0) { bar.style.display = 'none'; return; } bar.innerHTML = `⚠ Browser überlastet – automatisch deaktiviert: ${off.join(', ')} `; const btn = document.createElement('button'); btn.textContent = 'Wieder aktivieren'; btn.onclick = () => { cameras.filter(c => c.autoOff && !c.active).forEach(c => { c.autoOff = false; startStream(c); }); showNotice(); }; bar.appendChild(btn); bar.style.display = 'flex'; } // ── Snapshot aller (auch abgeschalteter) Kameras ───────────────────────────── function snapshotAll() { const ts = Date.now(); const ids = cameras.map(c => c.id); log('snap', `Snapshot alle: ${ids.join(', ')}`); ids.forEach(id => { const a = document.createElement('a'); a.href = `/api/snapshot/${id}`; a.download = `${id}_${ts}.jpg`; document.body.appendChild(a); a.click(); a.remove(); }); } // ── Kamera-View aufbauen ───────────────────────────────────────────────────── function buildCamera(camId, container) { const box = document.createElement('div'); box.className = 'cam-box'; const label = document.createElement('div'); label.className = 'cam-label'; label.textContent = camId; const info = document.createElement('div'); info.className = 'cam-info'; info.textContent = '…'; const toggle = document.createElement('button'); toggle.className = 'cam-toggle'; const cam = { id: camId, box, infoEl: info, toggleBtn: toggle, active: false, last: null, badTicks: 0, autoOff: false }; toggle.onclick = () => { cam.autoOff = false; cam.active ? stopStream(cam) : startStream(cam); showNotice(); }; box.appendChild(label); box.appendChild(info); box.appendChild(toggle); container.appendChild(box); cameras.push(cam); startStream(cam); } // ── Init ───────────────────────────────────────────────────────────────────── async function init() { log('init', 'Starte...'); try { const d = await (await fetch('/config.json')).json(); GO2RTC_PORT = d.go2rtcPort ?? 1984; log('init', `go2rtc WS-Port: ${GO2RTC_PORT}`); } catch (e) { warn('init', `/config.json nicht ladbar, nehme Port ${GO2RTC_PORT}`); } try { await customElements.whenDefined('video-stream'); log('init', ' definiert'); } catch (e) { logErr('init', ' nicht geladen – /video-stream.js erreichbar?', e); return; } const container = document.getElementById('cameras'); const statusText = document.getElementById('statusText'); let camIds = []; try { const r = await fetch('/api/snapshot'); log('init', `/api/snapshot → HTTP ${r.status}`); if (r.ok) camIds = ((await r.json()).cameras ?? []).map(c => c.id); log('init', `Kameras: ${camIds.join(', ') || '(keine)'}`); } catch (e) { logErr('init', '/api/snapshot Fehler – Fallback', e); } if (camIds.length === 0) { warn('init', 'Fallback cam0, cam1'); camIds = ['cam0', 'cam1']; } const snapBtn = document.getElementById('snapAllBtn'); if (snapBtn) { snapBtn.onclick = snapshotAll; snapBtn.disabled = false; } camIds.forEach(id => buildCamera(id, container)); statusText.textContent = `${camIds.length} Kamera${camIds.length !== 1 ? 's' : ''} · WebRTC`; setInterval(monitorTick, MONITOR_INTERVAL); // Health-Überwachung starten log('init', 'Fertig – Überwachung aktiv'); } init();