initial commit
This commit is contained in:
+389
@@ -0,0 +1,389 @@
|
||||
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" });
|
||||
}
|
||||
Reference in New Issue
Block a user