initial commit

This commit is contained in:
2026-07-22 01:12:45 +02:00
commit 124f2746ac
357 changed files with 159669 additions and 0 deletions
+389
View File
@@ -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" });
}