From 512299208167a7c6a09d88e3a99e8784f679def4 Mon Sep 17 00:00:00 2001 From: chk <79915315+ChKendel@users.noreply.github.com> Date: Wed, 3 Jun 2026 22:59:06 +0200 Subject: [PATCH] Claude: HighResolution mit Limit --- public/viewer.js | 145 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 26 deletions(-) diff --git a/public/viewer.js b/public/viewer.js index 0207152..b55c965 100644 --- a/public/viewer.js +++ b/public/viewer.js @@ -5,9 +5,12 @@ 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 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 +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 STATS_INTERVAL = 3000; // ms zwischen WebRTC-Diagnose-Logs (getStats) // ── Logging (Browser DevTools → Console → F12) ─────────────────────────────── const P = '[WebcamViewer]'; @@ -16,7 +19,7 @@ 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 } +const cameras = []; // { id, box, infoEl, toggleBtn, active, startedAt, playingSince, last, badTicks, autoOff } // ── Stream starten / stoppen (durch Erzeugen/Entfernen des Elements) ───────── function startStream(cam) { @@ -24,19 +27,28 @@ function startStream(cam) { 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.last = null; + cam.badTicks = 0; + const stream = document.createElement('video-stream'); stream.mode = MODE; - stream.addEventListener('playing', () => log(cam.id, '▶ Bild läuft'), true); + // 'playing' = ab jetzt fließen wirklich Frames → Aufwärmphase startet + stream.addEventListener('playing', () => { + cam.playingSince = performance.now(); + cam.last = 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; - // 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.box.insertBefore(stream, cam.box.firstChild); // Overlays bleiben oben cam.toggleBtn.textContent = '⏸'; cam.toggleBtn.title = 'Stream ausschalten'; + setInfo(cam, 'verbindet…', ''); } function stopStream(cam, auto = false) { @@ -44,9 +56,10 @@ function stopStream(cam, auto = false) { if (el) el.remove(); // disconnectedCallback schließt WS/PeerConnection cam.active = false; cam.autoOff = auto; + cam.playingSince = null; cam.toggleBtn.textContent = '▶'; cam.toggleBtn.title = 'Stream einschalten'; - setInfo(cam, auto ? 'auto-aus (überlastet)' : 'aus', 'crit'); + setInfo(cam, auto ? 'auto-aus (überlastet)' : 'aus', auto ? 'crit' : ''); log(cam.id, auto ? 'AUTO-abgeschaltet (Browser überlastet)' : 'manuell aus'); if (auto) showNotice(); } @@ -57,24 +70,41 @@ function setInfo(cam, text, cls) { cam.infoEl.className = 'cam-info ' + (cls ?? ''); } +// Stream existiert, aber 'playing' noch nicht gefeuert → Verbindungsaufbau +function showConnecting(cam) { + const secs = Math.round((performance.now() - cam.startedAt) / 1000); + if (secs < CONNECT_GRACE_MS / 1000) { + setInfo(cam, `verbindet… ${secs}s`, ''); // grau, kein Alarm + } else { + setInfo(cam, 'kein Signal', 'crit'); // dauert zu lang – aber KEINE Auto-Abschaltung + } + // Wichtig: badTicks NICHT erhöhen, solange nicht stabil gespielt wird +} + // ── Monitor-Schleife: misst fps + verworfene Frames pro Kamera ─────────────── function monitorTick() { + const now = performance.now(); + cameras.forEach(cam => { if (!cam.active) return; + const video = cam.box.querySelector('video'); - if (!video || typeof video.getVideoPlaybackQuality !== 'function') { - setInfo(cam, 'läuft', 'ok'); + + // (1) Noch kein Video / noch nicht "playing" → Verbindungsaufbau, nicht bewerten + if (!video || typeof video.getVideoPlaybackQuality !== 'function' || cam.playingSince === null) { + showConnecting(cam); return; } + const warming = (now - cam.playingSince) < WARMUP_MS; + 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 + // Erster Messpunkt oder Zähler-Reset (Neustart) → nur Baseline merken if (!cam.last || cur.total < cam.last.total) { cam.last = cur; - setInfo(cam, 'misst…', 'ok'); + setInfo(cam, warming ? 'startet…' : 'misst…', ''); return; } @@ -83,26 +113,84 @@ function monitorTick() { 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++; + if (dTotal <= 0) { + // Keine neuen Frames im Intervall + if (warming) { + setInfo(cam, 'startet…', ''); // während Aufwärmen normal + } else { + 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; + if (warming) { + // Während Aufwärmphase: anzeigen, aber NICHT als Überlast werten + const secs = Math.ceil((WARMUP_MS - (now - cam.playingSince)) / 1000); + setInfo(cam, `startet… ${fps} fps (${secs}s)`, ''); + cam.badTicks = 0; + } else { + 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`); + // Auto-Schutz greift NUR nach der Aufwärmphase + if (!warming && cam.badTicks >= OVERLOAD_TICKS) { + warn(cam.id, `überlastet (${cam.badTicks}× kritisch nach Aufwärmen) → Auto-Abschaltung`); stopStream(cam, true); } }); } +// ── WebRTC-Diagnose: lokalisiert WO Frames verloren gehen ──────────────────── +// Liest die inbound-rtp-Statistik der PeerConnection und loggt sie in die Console. +// recv = Frames die ÜBER DAS NETZ ankommen → niedrig = Server liefert nicht (Encode-CPU) +// decoded = davon dekodiert +// dropped = beim Dekodieren verworfen → hoch = Client zu schwach (hier 10% CPU → unwahrscheinlich) +// lost = verlorene Pakete / jitter → hoch = Netz/WiFi +async function pollStats() { + for (const cam of cameras) { + if (!cam.active) continue; + const el = cam.box.querySelector('video-stream'); + const pc = el && el.pc; + if (!pc || typeof pc.getStats !== 'function') continue; + + let stats; + try { stats = await pc.getStats(); } catch { continue; } + + let v = null; + stats.forEach(r => { if (r.type === 'inbound-rtp' && r.kind === 'video') v = r; }); + if (!v) continue; + + const cur = { + t: v.timestamp, + frames: v.framesReceived ?? 0, + decoded: v.framesDecoded ?? 0, + dropped: v.framesDropped ?? 0, + lost: v.packetsLost ?? 0, + bytes: v.bytesReceived ?? 0, + }; + const last = cam.statsLast; + cam.statsLast = cur; + if (!last || cur.t <= last.t) continue; + + const dt = (cur.t - last.t) / 1000; + const recvPs = Math.round((cur.frames - last.frames) / dt); + const decPs = Math.round((cur.decoded - last.decoded) / dt); + const dropPs = Math.round((cur.dropped - last.dropped) / dt); + const lostD = cur.lost - last.lost; + const mbps = (((cur.bytes - last.bytes) * 8) / dt / 1e6).toFixed(2); + const jit = v.jitter != null ? Math.round(v.jitter * 1000) : '?'; + const size = (v.frameWidth && v.frameHeight) ? `${v.frameWidth}x${v.frameHeight}` : '?'; + + log(cam.id, `[stats] recv=${recvPs}/s decoded=${decPs}/s dropped=${dropPs}/s ` + + `lost=+${lostD} jitter=${jit}ms ${size} ${mbps}Mbps`); + } +} + // ── Hinweis-Banner bei Auto-Abschaltung ────────────────────────────────────── function showNotice() { const off = cameras.filter(c => c.autoOff && !c.active).map(c => c.id); @@ -150,7 +238,11 @@ function buildCamera(camId, container) { 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 }; + const cam = { + id: camId, box, infoEl: info, toggleBtn: toggle, + active: false, startedAt: 0, playingSince: null, last: null, badTicks: 0, autoOff: false, + statsLast: null, + }; toggle.onclick = () => { cam.autoOff = false; cam.active ? stopStream(cam) : startStream(cam); showNotice(); }; box.appendChild(label); @@ -203,7 +295,8 @@ async function init() { statusText.textContent = `${camIds.length} Kamera${camIds.length !== 1 ? 's' : ''} · WebRTC`; setInterval(monitorTick, MONITOR_INTERVAL); // Health-Überwachung starten - log('init', 'Fertig – Überwachung aktiv'); + setInterval(pollStats, STATS_INTERVAL); // WebRTC-Diagnose in die Console + log('init', 'Fertig – Überwachung + Diagnose aktiv'); } init();