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);
}
},
};