const API = "https://api.anjuma-crew.de"; const WS_URL = "wss://api.anjuma-crew.de"; const STREAM_URL = "https://stream.anjuma-crew.de/anjuma/"; const MAX_POINTS = 40; const SCENES = ["Starting", "Live1", "Live2", "Low", "Offline"]; let ws; let reconnectTimeout = null; let retryDelay = 1500; let lowThreshold = 1000; // ===== FIX STATE ===== let isFixing = false; // ===== OBS STATE ===== let obsStarting = false; // ===== DATA ===== const data = []; // ===== PREVIEW STATE ===== const previewState = { live1: false, live2: false }; // ===== DOM CACHE ===== const el = {}; // ===== INIT ===== window.addEventListener("load", () => { cacheDOM(); connectWS(); }); // ===== CACHE DOM ===== function cacheDOM() { el.scene = document.getElementById("scene"); el.live1 = document.getElementById("live1"); el.live2 = document.getElementById("live2"); el.chart = document.getElementById("chart"); el.obsStartBtn = document.getElementById("obsStartBtn"); el.obsStatus = document.getElementById("obsStatus"); el.fixBtn = document.getElementById("fixBtn"); } // ===== WS ===== function connectWS() { ws = new WebSocket(WS_URL); ws.onopen = () => { retryDelay = 1500; console.log("WS connected"); }; ws.onclose = () => scheduleReconnect(); ws.onerror = () => scheduleReconnect(); ws.onmessage = (msg) => { const packet = JSON.parse(msg.data); if (packet.type === "update") handle(packet.data); }; } function scheduleReconnect() { if (reconnectTimeout) return; reconnectTimeout = setTimeout(() => { reconnectTimeout = null; retryDelay = Math.min(retryDelay * 1.5, 10000); connectWS(); }, retryDelay); } // ===== OBS STATUS ===== function updateOBSStatus(d) { if (!el.obsStatus) return; if (d.obsConnected) { el.obsStatus.innerText = "🟢 Connected"; el.obsStatus.className = "text-success"; } else { el.obsStatus.innerText = "🔴 Disconnected"; el.obsStatus.className = "text-danger"; } } // ===== HANDLE ===== function handle(d) { evaluateStreams(d); updateOBSStatus(d); if (d.lowThreshold) { lowThreshold = parseInt(d.lowThreshold); } const v1 = d.streams?.live1?.bitrate || 0; const v2 = d.streams?.live2?.bitrate || 0; data.push({ live1: v1, live2: v2 }); if (data.length > MAX_POINTS) { data.shift(); } updateUI(d); scheduleDraw(); updatePreview("live1", v1); updatePreview("live2", v2); updateFixState(d); } // ===== OBS START (WOL) ===== function startOBS() { if (obsStarting) return; obsStarting = true; if (el.obsStartBtn) { el.obsStartBtn.innerText = "Starting OBS..."; el.obsStartBtn.disabled = true; } const timeout = setTimeout(() => { reset(); }, 20000); fetch(`${API}/obs/start`, { method: "POST" }) .then(res => res.json()) .then(data => { console.log("OBS WOL:", data); }) .catch(err => { console.error(err); reset(); }); function reset() { clearTimeout(timeout); obsStarting = false; if (el.obsStartBtn) { el.obsStartBtn.innerText = "OBS Starten"; el.obsStartBtn.disabled = false; } } } // ===== FIX UI ===== function updateFixState(d) { if (!el.fixBtn) return; if (isFixing) return; if (d.isFixRunning) { isFixing = true; el.fixBtn.innerText = "FIXING..."; el.fixBtn.disabled = true; } else { isFixing = false; el.fixBtn.innerText = "FIX STREAM"; el.fixBtn.disabled = false; } } // ===== FIX ACTION ===== async function fixStream() { if (isFixing) return; isFixing = true; if (el.fixBtn) { el.fixBtn.innerText = "FIXING..."; el.fixBtn.disabled = true; } try { const res = await fetch(`${API}/fix`, { method: "POST" }); const result = await res.json(); console.log("FIX RESULT:", result); } catch (e) { console.error(e); } setTimeout(() => { isFixing = false; if (el.fixBtn) { el.fixBtn.innerText = "FIX STREAM"; el.fixBtn.disabled = false; } }, 2000); } // ===== UI ===== function updateUI(d) { if (el.scene) { el.scene.innerText = d.currentScene || "-"; } if (el.live1) { el.live1.innerText = (d.streams?.live1?.bitrate || 0) + " kbps"; } if (el.live2) { el.live2.innerText = (d.streams?.live2?.bitrate || 0) + " kbps"; } SCENES.forEach(s => { const btn = document.getElementById("scene-" + s); if (!btn) return; btn.classList.remove("active"); if (d.currentScene === s) { btn.classList.add("active"); } }); } // ===== PREVIEW ===== function updatePreview(stream, bitrate) { const frame = document.getElementById("preview-" + stream); const offline = document.getElementById("offline-" + stream); if (!frame || !offline) return; if (bitrate > 0) { if (!previewState[stream]) { frame.src = STREAM_URL + stream; frame.style.display = "block"; offline.style.display = "none"; previewState[stream] = true; } } else { if (previewState[stream]) { frame.src = ""; frame.style.display = "none"; previewState[stream] = false; } offline.style.display = "flex"; } } // ===== CHART ===== let drawPending = false; function scheduleDraw() { if (drawPending) return; drawPending = true; requestAnimationFrame(() => { draw(); drawPending = false; }); } function draw() { const canvas = el.chart; if (!canvas) return; const ctx = canvas.getContext("2d"); const w = canvas.width = canvas.offsetWidth; const h = canvas.height = canvas.offsetHeight; ctx.clearRect(0, 0, w, h); const values = data.flatMap(d => [d.live1, d.live2, lowThreshold]); const maxVal = Math.ceil(Math.max(...values, 100) * 1.15); ctx.strokeStyle = "#e5e7eb"; ctx.lineWidth = 1; for (let i = 0; i <= 5; i++) { const y = (h / 5) * i; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); const label = Math.round(maxVal * (1 - i / 5)); ctx.fillStyle = "#666"; ctx.font = "11px Arial"; ctx.fillText(label + " kbps", 5, y - 3); } const lowY = h - (lowThreshold / maxVal) * h; ctx.fillStyle = "rgba(239,68,68,0.08)"; ctx.fillRect(0, lowY, w, h - lowY); ctx.beginPath(); ctx.strokeStyle = "#f59e0b"; ctx.setLineDash([8, 4]); ctx.moveTo(0, lowY); ctx.lineTo(w, lowY); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = "#f59e0b"; ctx.font = "bold 11px Arial"; ctx.fillText(`LOW (${lowThreshold})`, w - 140, lowY - 6); drawLine(ctx, data.map(d => d.live1), "#22c55e", w, h, maxVal); drawLine(ctx, data.map(d => d.live2), "#3b82f6", w, h, maxVal); } function drawLine(ctx, arr, color, w, h, maxVal) { if (arr.length < 2) return; ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = 3; arr.forEach((v, i) => { const x = (i / (arr.length - 1)) * w; const y = h - (v / maxVal) * h; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); } // ===== ACTIONS ===== function setScene(scene) { fetch(`${API}/scene`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ scene }) }).catch(console.error); } function startStream() { fetch(`${API}/stream/start`, { method: "POST" }); } function stopStream() { fetch(`${API}/stream/stop`, { method: "POST" }); }