const clamp = (v) => Math.max(0, Math.min(1, v)); async function post(action, body = {}) { try { await fetch(`/api/obs/${action}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); } catch { // the next poll shows the real state } } function button(label) { const b = document.createElement("button"); b.textContent = label; return b; } export default { title: "OBS", interval: 1, mount(el) { const wrap = document.createElement("div"); wrap.className = "obs"; wrap.innerHTML = `
Scenes
Audio
Sources
`; el.append(wrap); }, update(state, el) { const controls = el.querySelector(".obs-controls"); const scenes = el.querySelector(".obs-scenes"); const audio = el.querySelector(".obs-audio"); const sources = el.querySelector(".obs-sources"); el.querySelectorAll(".section-label").forEach((label) => { label.style.display = state.connected ? "" : "none"; }); if (!state.connected) { controls.textContent = state.error ? `OBS offline: ${state.error}` : "OBS offline"; scenes.textContent = audio.textContent = sources.textContent = ""; return; } controls.textContent = ""; const rec = button(state.recording ? "Stop recording" : "Record"); rec.classList.toggle("on", state.recording); rec.onclick = () => post("record", { on: !state.recording }); const stream = button(state.streaming ? "Stop stream" : "Stream"); stream.classList.toggle("on", state.streaming); stream.onclick = () => post("stream", { on: !state.streaming }); controls.append(rec, stream); if (state.transitions?.length) { const sel = document.createElement("select"); for (const t of state.transitions) { const o = document.createElement("option"); o.value = o.textContent = t; if (t === state.transition) o.selected = true; sel.append(o); } sel.onchange = () => post("transition", { name: sel.value }); const trigger = button("Transition"); trigger.onclick = () => post("trigger"); controls.append(sel, trigger); } scenes.textContent = ""; for (const name of state.scenes ?? []) { const b = button(name); b.classList.toggle("on", name === state.currentScene); b.onclick = () => post("scene", { scene: name }); scenes.append(b); } audio.textContent = ""; for (const inp of state.inputs ?? []) { const row = document.createElement("div"); row.className = "row"; const label = document.createElement("span"); label.textContent = inp.name; const mute = button(inp.muted ? "Unmute" : "Mute"); mute.classList.toggle("on", inp.muted); mute.onclick = () => post("mute", { input: inp.name, muted: !inp.muted }); const down = button("−"); down.onclick = () => post("volume", { input: inp.name, level: clamp(inp.volume - 0.1) }); const up = button("+"); up.onclick = () => post("volume", { input: inp.name, level: clamp(inp.volume + 0.1) }); row.append(label, mute, down, up); audio.append(row); } sources.textContent = ""; for (const s of state.sources ?? []) { const b = button(s.name); b.classList.toggle("on", s.on); b.onclick = () => post("source", { scene: state.currentScene, id: s.id, on: !s.on }); sources.append(b); } }, };