Claude: HighResolution mit Limit

This commit is contained in:
chk
2026-06-03 22:59:06 +02:00
parent ad0a44245d
commit 5122992081

View File

@@ -5,9 +5,12 @@ const MODE = 'webrtc,mse,mjpeg';
// ── Überwachungs-Parameter ─────────────────────────────────────────────────── // ── Überwachungs-Parameter ───────────────────────────────────────────────────
const MONITOR_INTERVAL = 2000; // ms zwischen Health-Checks const MONITOR_INTERVAL = 2000; // ms zwischen Health-Checks
const DROP_WARN = 0.10; // >10% verworfene Frames → gelb const DROP_WARN = 0.10; // >10% verworfene Frames → gelb
const DROP_CRIT = 0.30; // >30% verworfene Frames → rot const DROP_CRIT = 0.30; // >30% verworfene Frames → rot
const OVERLOAD_TICKS = 3; // so viele kritische Checks in Folge → Auto-Abschaltung 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) ─────────────────────────────── // ── Logging (Browser DevTools → Console → F12) ───────────────────────────────
const P = '[WebcamViewer]'; 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 ?? ''); const logErr = (c, m, e) => console.error(`${P}[${c}] ✗ ${m}`, e ?? '');
let GO2RTC_PORT = 1984; 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) ───────── // ── Stream starten / stoppen (durch Erzeugen/Entfernen des Elements) ─────────
function startStream(cam) { function startStream(cam) {
@@ -24,19 +27,28 @@ function startStream(cam) {
const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=${encodeURIComponent(cam.id)}`; const wsUrl = `ws://${location.hostname}:${GO2RTC_PORT}/api/ws?src=${encodeURIComponent(cam.id)}`;
log(cam.id, `Verbinde → ${wsUrl}`); 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'); const stream = document.createElement('video-stream');
stream.mode = MODE; 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.addEventListener('error', (e) => logErr(cam.id, 'Video-Fehler', e), true);
stream.src = wsUrl; stream.src = wsUrl;
// Vor das Label einfügen, damit Overlays oben liegen cam.box.insertBefore(stream, cam.box.firstChild); // Overlays bleiben oben
cam.box.insertBefore(stream, cam.box.firstChild);
cam.active = true;
cam.last = null;
cam.badTicks = 0;
cam.toggleBtn.textContent = '⏸'; cam.toggleBtn.textContent = '⏸';
cam.toggleBtn.title = 'Stream ausschalten'; cam.toggleBtn.title = 'Stream ausschalten';
setInfo(cam, 'verbindet…', '');
} }
function stopStream(cam, auto = false) { function stopStream(cam, auto = false) {
@@ -44,9 +56,10 @@ function stopStream(cam, auto = false) {
if (el) el.remove(); // disconnectedCallback schließt WS/PeerConnection if (el) el.remove(); // disconnectedCallback schließt WS/PeerConnection
cam.active = false; cam.active = false;
cam.autoOff = auto; cam.autoOff = auto;
cam.playingSince = null;
cam.toggleBtn.textContent = '▶'; cam.toggleBtn.textContent = '▶';
cam.toggleBtn.title = 'Stream einschalten'; 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'); log(cam.id, auto ? 'AUTO-abgeschaltet (Browser überlastet)' : 'manuell aus');
if (auto) showNotice(); if (auto) showNotice();
} }
@@ -57,24 +70,41 @@ function setInfo(cam, text, cls) {
cam.infoEl.className = 'cam-info ' + (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 ─────────────── // ── Monitor-Schleife: misst fps + verworfene Frames pro Kamera ───────────────
function monitorTick() { function monitorTick() {
const now = performance.now();
cameras.forEach(cam => { cameras.forEach(cam => {
if (!cam.active) return; if (!cam.active) return;
const video = cam.box.querySelector('video'); 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; return;
} }
const warming = (now - cam.playingSince) < WARMUP_MS;
const q = video.getVideoPlaybackQuality(); const q = video.getVideoPlaybackQuality();
const now = performance.now();
const cur = { dropped: q.droppedVideoFrames, total: q.totalVideoFrames, t: 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) { if (!cam.last || cur.total < cam.last.total) {
cam.last = cur; cam.last = cur;
setInfo(cam, 'misst…', 'ok'); setInfo(cam, warming ? 'startet…' : 'misst…', '');
return; return;
} }
@@ -83,26 +113,84 @@ function monitorTick() {
const dSec = (cur.t - cam.last.t) / 1000; const dSec = (cur.t - cam.last.t) / 1000;
cam.last = cur; cam.last = cur;
if (dTotal <= 0) { // kein neuer Frame → evtl. Freeze if (dTotal <= 0) {
setInfo(cam, '⏳ keine Frames', 'warn'); // Keine neuen Frames im Intervall
cam.badTicks++; if (warming) {
setInfo(cam, 'startet…', ''); // während Aufwärmen normal
} else {
setInfo(cam, '⏳ keine Frames', 'warn');
cam.badTicks++;
}
} else { } else {
const fps = Math.round(dTotal / dSec); const fps = Math.round(dTotal / dSec);
const ratio = dDropped / dTotal; const ratio = dDropped / dTotal;
const pct = Math.round(ratio * 100); const pct = Math.round(ratio * 100);
const cls = ratio >= DROP_CRIT ? 'crit' : ratio >= DROP_WARN ? 'warn' : 'ok'; if (warming) {
setInfo(cam, `${fps} fps · ${pct}% drop`, cls); // Während Aufwärmphase: anzeigen, aber NICHT als Überlast werten
cam.badTicks = ratio >= DROP_CRIT ? cam.badTicks + 1 : 0; 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 // Auto-Schutz greift NUR nach der Aufwärmphase
if (cam.badTicks >= OVERLOAD_TICKS) { if (!warming && cam.badTicks >= OVERLOAD_TICKS) {
warn(cam.id, `überlastet (${cam.badTicks}× kritisch) → Auto-Abschaltung`); warn(cam.id, `überlastet (${cam.badTicks}× kritisch nach Aufwärmen) → Auto-Abschaltung`);
stopStream(cam, true); 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 ────────────────────────────────────── // ── Hinweis-Banner bei Auto-Abschaltung ──────────────────────────────────────
function showNotice() { function showNotice() {
const off = cameras.filter(c => c.autoOff && !c.active).map(c => c.id); 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'); const toggle = document.createElement('button');
toggle.className = 'cam-toggle'; 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(); }; toggle.onclick = () => { cam.autoOff = false; cam.active ? stopStream(cam) : startStream(cam); showNotice(); };
box.appendChild(label); box.appendChild(label);
@@ -203,7 +295,8 @@ async function init() {
statusText.textContent = `${camIds.length} Kamera${camIds.length !== 1 ? 's' : ''} · WebRTC`; statusText.textContent = `${camIds.length} Kamera${camIds.length !== 1 ? 's' : ''} · WebRTC`;
setInterval(monitorTick, MONITOR_INTERVAL); // Health-Überwachung starten 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(); init();