const HEALTH_CONFIG = { warningThreshold: 60, dangerThreshold: 30, staleWarningMs: 2500, staleDangerMs: 5000, bitrateLow: 1000, bitrateGood: 1001, autoSceneSwitch: false, sceneLow: "Low", sceneOffline: "Offline", enableAlerts: false }; // ================= STATE ================= let lastHealthState = { live1: "UNKNOWN", live2: "UNKNOWN" }; // ================= MAIN ENTRY ================= function evaluateStreams(status) { if (!status?.streams) return; const results = {}; for (const key of Object.keys(status.streams)) { const stream = status.streams[key]; const health = calculateHealth(stream); results[key] = health; handleStateChange(key, health, status); updateHealthUI(key, health); } return results; } // ================= HEALTH CALC ================= function calculateHealth(stream) { const now = Date.now(); const bitrate = stream.bitrate || 0; const staleness = now - (stream.time || now); let score = 100; // 🔴 STALENESS (most important) if (staleness > HEALTH_CONFIG.staleDangerMs) score -= 60; else if (staleness > HEALTH_CONFIG.staleWarningMs) score -= 30; // 📉 BITRATE if (bitrate === 0) score -= 50; else if (bitrate < HEALTH_CONFIG.bitrateLow) score -= 30; else if (bitrate < HEALTH_CONFIG.bitrateGood) score -= 10; // clamp score = Math.max(0, Math.min(100, score)); return { score, bitrate, staleness, status: getStatus(score, bitrate, staleness) }; } // ================= STATUS ================= function getStatus(score, bitrate, staleness) { if (bitrate === 0 && staleness > HEALTH_CONFIG.staleDangerMs) { return "DOWN"; } if (score < HEALTH_CONFIG.dangerThreshold) { return "DANGER"; } if (score < HEALTH_CONFIG.warningThreshold) { return "WEAK"; } return "GOOD"; } // ================= STATE HANDLER ================= function handleStateChange(streamName, health, status) { const prev = lastHealthState[streamName]; const current = health.status; if (prev === current) return; lastHealthState[streamName] = current; console.log(`[HEALTH] ${streamName}: ${prev} → ${current}`); // 🚨 ALERTS if (HEALTH_CONFIG.enableAlerts) { sendAlert(streamName, health, status); } // 🎛 AUTO SCENE SWITCH if (HEALTH_CONFIG.autoSceneSwitch) { handleAutoScene(streamName, health, status); } } // ================= AUTO SCENE ================= function handleAutoScene(streamName, health, status) { if (streamName !== status.activeStream) return; if (health.status === "DOWN") { setSceneSafe(HEALTH_CONFIG.sceneOffline); } if (health.status === "DANGER") { setSceneSafe(HEALTH_CONFIG.sceneLow); } } // ================= SAFE SCENE SWITCH ================= function setSceneSafe(scene) { fetch("https://api.anjuma-crew.de/scene", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scene }) }).catch(console.error); } // ================= ALERT SYSTEM ================= function sendAlert(streamName, health, status) { const msg = { stream: streamName, status: health.status, score: health.score, bitrate: health.bitrate, staleness: health.staleness }; console.warn("[ALERT]", msg); // 👉 optional webhook /* fetch("YOUR_WEBHOOK_URL", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(msg) }); */ } // ================= UI ================= function updateHealthUI(streamName, health) { const el = document.getElementById(`health-${streamName}`); if (el) { el.innerText = health.score; el.style.color = health.status === "GOOD" ? "green" : health.status === "WEAK" ? "orange" : "red"; } const label = document.getElementById(`status-${streamName}`); if (label) { label.innerText = health.status; } const staleEl = document.getElementById(`stale-${streamName}`); if (staleEl) { staleEl.innerText = Math.round(health.staleness / 1000) + "s"; } }