Claude: HighResolution mit Limit
This commit is contained in:
193
public/viewer.js
193
public/viewer.js
@@ -4,13 +4,16 @@
|
|||||||
const MODE = 'webrtc,mse,mjpeg';
|
const MODE = 'webrtc,mse,mjpeg';
|
||||||
|
|
||||||
// ── Überwachungs-Parameter ───────────────────────────────────────────────────
|
// ── Überwachungs-Parameter ───────────────────────────────────────────────────
|
||||||
const MONITOR_INTERVAL = 2000; // ms zwischen Health-Checks
|
const MONITOR_INTERVAL = 2500; // ms zwischen Health-Checks (getStats)
|
||||||
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 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 WARMUP_MS = 15000; // Karenz nach 'playing', bis Überlast-Erkennung scharf wird
|
||||||
const STATS_INTERVAL = 3000; // ms zwischen WebRTC-Diagnose-Logs (getStats)
|
const OVERLOAD_TICKS = 3; // so viele kritische Checks in Folge → Auto-Abschaltung
|
||||||
|
|
||||||
|
// Schwellen (auf Basis der ZUVERLÄSSIGEN getStats-Werte, nicht Render-Drops):
|
||||||
|
const SERVER_LOW_FPS = 12; // recv < 12/s nach Aufwärmen → Server liefert wenig (nur Warnung)
|
||||||
|
const CLIENT_DECODE_RATIO = 0.6; // decoded < 60% von recv → Decoder kommt nicht nach (echte Client-Überlast)
|
||||||
|
const NET_LOST_PER_TICK = 5; // mehr verlorene Pakete/Intervall → Netz-Warnung
|
||||||
|
const NET_JITTER_MS = 60; // mehr Jitter → Netz-Warnung
|
||||||
|
|
||||||
// ── Logging (Browser DevTools → Console → F12) ───────────────────────────────
|
// ── Logging (Browser DevTools → Console → F12) ───────────────────────────────
|
||||||
const P = '[WebcamViewer]';
|
const P = '[WebcamViewer]';
|
||||||
@@ -19,33 +22,32 @@ 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, startedAt, playingSince, last, badTicks, autoOff }
|
const cameras = []; // { id, box, infoEl, toggleBtn, active, startedAt, playingSince, statsLast, badTicks, autoOff }
|
||||||
|
|
||||||
// ── Stream starten / stoppen (durch Erzeugen/Entfernen des Elements) ─────────
|
// ── Stream starten / stoppen ─────────────────────────────────────────────────
|
||||||
function startStream(cam) {
|
function startStream(cam) {
|
||||||
if (cam.box.querySelector('video-stream')) return; // läuft schon
|
if (cam.box.querySelector('video-stream')) return;
|
||||||
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.active = true;
|
||||||
cam.startedAt = performance.now();
|
cam.startedAt = performance.now();
|
||||||
cam.playingSince = null; // wird erst beim 'playing'-Event gesetzt
|
cam.playingSince = null; // wird erst beim 'playing'-Event gesetzt
|
||||||
cam.last = null;
|
cam.statsLast = null;
|
||||||
cam.badTicks = 0;
|
cam.badTicks = 0;
|
||||||
|
|
||||||
const stream = document.createElement('video-stream');
|
const stream = document.createElement('video-stream');
|
||||||
stream.mode = MODE;
|
stream.mode = MODE;
|
||||||
// 'playing' = ab jetzt fließen wirklich Frames → Aufwärmphase startet
|
|
||||||
stream.addEventListener('playing', () => {
|
stream.addEventListener('playing', () => {
|
||||||
cam.playingSince = performance.now();
|
cam.playingSince = performance.now();
|
||||||
cam.last = null;
|
cam.statsLast = null;
|
||||||
cam.badTicks = 0;
|
cam.badTicks = 0;
|
||||||
log(cam.id, '▶ Bild läuft (Aufwärmphase startet)');
|
log(cam.id, '▶ Bild läuft (Aufwärmphase startet)');
|
||||||
}, true);
|
}, 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;
|
||||||
|
|
||||||
cam.box.insertBefore(stream, cam.box.firstChild); // Overlays bleiben oben
|
cam.box.insertBefore(stream, cam.box.firstChild);
|
||||||
cam.toggleBtn.textContent = '⏸';
|
cam.toggleBtn.textContent = '⏸';
|
||||||
cam.toggleBtn.title = 'Stream ausschalten';
|
cam.toggleBtn.title = 'Stream ausschalten';
|
||||||
setInfo(cam, 'verbindet…', '');
|
setInfo(cam, 'verbindet…', '');
|
||||||
@@ -53,14 +55,14 @@ function startStream(cam) {
|
|||||||
|
|
||||||
function stopStream(cam, auto = false) {
|
function stopStream(cam, auto = false) {
|
||||||
const el = cam.box.querySelector('video-stream');
|
const el = cam.box.querySelector('video-stream');
|
||||||
if (el) el.remove(); // disconnectedCallback schließt WS/PeerConnection
|
if (el) el.remove();
|
||||||
cam.active = false;
|
cam.active = false;
|
||||||
cam.autoOff = auto;
|
cam.autoOff = auto;
|
||||||
cam.playingSince = null;
|
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', auto ? 'crit' : '');
|
setInfo(cam, auto ? 'auto-aus (Client überlastet)' : 'aus', auto ? 'crit' : '');
|
||||||
log(cam.id, auto ? 'AUTO-abgeschaltet (Browser überlastet)' : 'manuell aus');
|
log(cam.id, auto ? 'AUTO-abgeschaltet (Decoder kam nicht nach)' : 'manuell aus');
|
||||||
if (auto) showNotice();
|
if (auto) showNotice();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,124 +72,93 @@ 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) {
|
function showConnecting(cam) {
|
||||||
const secs = Math.round((performance.now() - cam.startedAt) / 1000);
|
const secs = Math.round((performance.now() - cam.startedAt) / 1000);
|
||||||
if (secs < CONNECT_GRACE_MS / 1000) {
|
setInfo(cam, secs < CONNECT_GRACE_MS / 1000 ? `verbindet… ${secs}s` : 'kein Signal',
|
||||||
setInfo(cam, `verbindet… ${secs}s`, ''); // grau, kein Alarm
|
secs < CONNECT_GRACE_MS / 1000 ? '' : 'crit');
|
||||||
} 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: liest getStats (inbound-rtp) = die verlässliche Wahrheit ─────────
|
||||||
function monitorTick() {
|
// recv = Frames über Netz → niedrig = Server liefert nicht (Encode-CPU)
|
||||||
|
// decoded = davon dekodiert → deutlich < recv = Client-Decoder überlastet
|
||||||
|
// lost/jitter → Netz/WiFi
|
||||||
|
async function monitor() {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
|
|
||||||
cameras.forEach(cam => {
|
|
||||||
if (!cam.active) return;
|
|
||||||
|
|
||||||
const video = cam.box.querySelector('video');
|
|
||||||
|
|
||||||
// (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 cur = { dropped: q.droppedVideoFrames, total: q.totalVideoFrames, t: now };
|
|
||||||
|
|
||||||
// Erster Messpunkt oder Zähler-Reset (Neustart) → nur Baseline merken
|
|
||||||
if (!cam.last || cur.total < cam.last.total) {
|
|
||||||
cam.last = cur;
|
|
||||||
setInfo(cam, warming ? 'startet…' : 'misst…', '');
|
|
||||||
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) {
|
|
||||||
// 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);
|
|
||||||
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 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) {
|
for (const cam of cameras) {
|
||||||
if (!cam.active) continue;
|
if (!cam.active) continue;
|
||||||
|
|
||||||
const el = cam.box.querySelector('video-stream');
|
const el = cam.box.querySelector('video-stream');
|
||||||
const pc = el && el.pc;
|
const pc = el && el.pc;
|
||||||
if (!pc || typeof pc.getStats !== 'function') continue;
|
// Noch nicht verbunden oder 'playing' noch nicht gefeuert → Aufbauphase
|
||||||
|
if (!pc || typeof pc.getStats !== 'function' || cam.playingSince === null) {
|
||||||
|
showConnecting(cam);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let stats;
|
let stats;
|
||||||
try { stats = await pc.getStats(); } catch { continue; }
|
try { stats = await pc.getStats(); } catch { continue; }
|
||||||
|
|
||||||
let v = null;
|
let v = null;
|
||||||
stats.forEach(r => { if (r.type === 'inbound-rtp' && r.kind === 'video') v = r; });
|
stats.forEach(r => { if (r.type === 'inbound-rtp' && r.kind === 'video') v = r; });
|
||||||
if (!v) continue;
|
if (!v) { showConnecting(cam); continue; }
|
||||||
|
|
||||||
const cur = {
|
const cur = {
|
||||||
t: v.timestamp,
|
t: v.timestamp,
|
||||||
frames: v.framesReceived ?? 0,
|
recv: v.framesReceived ?? 0,
|
||||||
decoded: v.framesDecoded ?? 0,
|
dec: v.framesDecoded ?? 0,
|
||||||
dropped: v.framesDropped ?? 0,
|
drop: v.framesDropped ?? 0,
|
||||||
lost: v.packetsLost ?? 0,
|
lost: v.packetsLost ?? 0,
|
||||||
bytes: v.bytesReceived ?? 0,
|
bytes: v.bytesReceived ?? 0,
|
||||||
};
|
};
|
||||||
const last = cam.statsLast;
|
const last = cam.statsLast;
|
||||||
cam.statsLast = cur;
|
cam.statsLast = cur;
|
||||||
if (!last || cur.t <= last.t) continue;
|
|
||||||
|
// Erster Messpunkt oder Zähler-Reset → nur Baseline
|
||||||
|
if (!last || cur.t <= last.t || cur.recv < last.recv) {
|
||||||
|
setInfo(cam, 'misst…', '');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const dt = (cur.t - last.t) / 1000;
|
const dt = (cur.t - last.t) / 1000;
|
||||||
const recvPs = Math.round((cur.frames - last.frames) / dt);
|
const recvPs = Math.round((cur.recv - last.recv) / dt);
|
||||||
const decPs = Math.round((cur.decoded - last.decoded) / dt);
|
const decPs = Math.round((cur.dec - last.dec) / dt);
|
||||||
const dropPs = Math.round((cur.dropped - last.dropped) / dt);
|
const dropPs = Math.round((cur.drop - last.drop) / dt);
|
||||||
const lostD = cur.lost - last.lost;
|
const lostD = Math.max(0, cur.lost - last.lost);
|
||||||
const mbps = (((cur.bytes - last.bytes) * 8) / dt / 1e6).toFixed(2);
|
const mbps = (((cur.bytes - last.bytes) * 8) / dt / 1e6).toFixed(2);
|
||||||
const jit = v.jitter != null ? Math.round(v.jitter * 1000) : '?';
|
const jitter = v.jitter != null ? Math.round(v.jitter * 1000) : 0;
|
||||||
const size = (v.frameWidth && v.frameHeight) ? `${v.frameWidth}x${v.frameHeight}` : '?';
|
const size = (v.frameWidth && v.frameHeight) ? `${v.frameWidth}x${v.frameHeight}` : '?';
|
||||||
|
|
||||||
log(cam.id, `[stats] recv=${recvPs}/s decoded=${decPs}/s dropped=${dropPs}/s ` +
|
log(cam.id, `[stats] recv=${recvPs}/s decoded=${decPs}/s dropped=${dropPs}/s ` +
|
||||||
`lost=+${lostD} jitter=${jit}ms ${size} ${mbps}Mbps`);
|
`lost=+${lostD} jitter=${jitter}ms ${size} ${mbps}Mbps`);
|
||||||
|
|
||||||
|
// Während Aufwärmphase: anzeigen, aber NICHT bewerten
|
||||||
|
if (now - cam.playingSince < WARMUP_MS) {
|
||||||
|
const secs = Math.ceil((WARMUP_MS - (now - cam.playingSince)) / 1000);
|
||||||
|
setInfo(cam, `startet… ${decPs} fps (${secs}s)`, '');
|
||||||
|
cam.badTicks = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Bewertung nach Aufwärmphase ──
|
||||||
|
const clientOverload = recvPs >= SERVER_LOW_FPS && decPs < recvPs * CLIENT_DECODE_RATIO;
|
||||||
|
const serverLow = recvPs < SERVER_LOW_FPS;
|
||||||
|
const netBad = lostD > NET_LOST_PER_TICK || jitter > NET_JITTER_MS;
|
||||||
|
|
||||||
|
if (clientOverload) {
|
||||||
|
setInfo(cam, `Decoder hängt ${decPs}/${recvPs} fps`, 'crit');
|
||||||
|
cam.badTicks++;
|
||||||
|
} else {
|
||||||
|
cam.badTicks = 0;
|
||||||
|
if (serverLow) setInfo(cam, `Server liefert ${recvPs}/s`, 'warn');
|
||||||
|
else if (netBad) setInfo(cam, `${decPs} fps · ⚠ ${jitter}ms / lost+${lostD}`, 'warn');
|
||||||
|
else setInfo(cam, `${decPs} fps · ${mbps} Mbps`, 'ok');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-Schutz NUR bei echter Client-Überlast (Decoder kommt nicht nach)
|
||||||
|
if (cam.badTicks >= OVERLOAD_TICKS) {
|
||||||
|
warn(cam.id, `Decoder überlastet (${cam.badTicks}× kritisch) → Auto-Abschaltung`);
|
||||||
|
stopStream(cam, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +167,7 @@ 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);
|
||||||
const bar = document.getElementById('notice');
|
const bar = document.getElementById('notice');
|
||||||
if (off.length === 0) { bar.style.display = 'none'; return; }
|
if (off.length === 0) { bar.style.display = 'none'; return; }
|
||||||
bar.innerHTML = `⚠ Browser überlastet – automatisch deaktiviert: <b>${off.join(', ')}</b> `;
|
bar.innerHTML = `⚠ Client überlastet – automatisch deaktiviert: <b>${off.join(', ')}</b> `;
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.textContent = 'Wieder aktivieren';
|
btn.textContent = 'Wieder aktivieren';
|
||||||
btn.onclick = () => {
|
btn.onclick = () => {
|
||||||
@@ -207,7 +178,7 @@ function showNotice() {
|
|||||||
bar.style.display = 'flex';
|
bar.style.display = 'flex';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Snapshot aller (auch abgeschalteter) Kameras ─────────────────────────────
|
// ── Snapshot aller Kameras ───────────────────────────────────────────────────
|
||||||
function snapshotAll() {
|
function snapshotAll() {
|
||||||
const ts = Date.now();
|
const ts = Date.now();
|
||||||
const ids = cameras.map(c => c.id);
|
const ids = cameras.map(c => c.id);
|
||||||
@@ -240,8 +211,7 @@ function buildCamera(camId, container) {
|
|||||||
|
|
||||||
const cam = {
|
const cam = {
|
||||||
id: camId, box, infoEl: info, toggleBtn: toggle,
|
id: camId, box, infoEl: info, toggleBtn: toggle,
|
||||||
active: false, startedAt: 0, playingSince: null, last: null, badTicks: 0, autoOff: false,
|
active: false, startedAt: 0, playingSince: null, statsLast: 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(); };
|
||||||
|
|
||||||
@@ -294,9 +264,8 @@ async function init() {
|
|||||||
camIds.forEach(id => buildCamera(id, container));
|
camIds.forEach(id => buildCamera(id, container));
|
||||||
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(monitor, MONITOR_INTERVAL); // getStats-basierte Überwachung + Diagnose
|
||||||
setInterval(pollStats, STATS_INTERVAL); // WebRTC-Diagnose in die Console
|
log('init', 'Fertig – Überwachung (getStats) aktiv');
|
||||||
log('init', 'Fertig – Überwachung + Diagnose aktiv');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
Reference in New Issue
Block a user