Files
appRobotWebcam/public/viewer.js
2026-06-03 22:28:29 +02:00

210 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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: <b>${off.join(', ')}</b> `;
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', '<video-stream> definiert');
} catch (e) {
logErr('init', '<video-stream> 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();