'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();