From f2bc97620545b6581f5bd4cef6c30c142881178d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 18:17:10 +0100 Subject: feat: OBS stream-deck panel --- static/modules/obs.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 static/modules/obs.js (limited to 'static/modules') diff --git a/static/modules/obs.js b/static/modules/obs.js new file mode 100644 index 0000000..c6a3ebc --- /dev/null +++ b/static/modules/obs.js @@ -0,0 +1,104 @@ +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 = ` +
+
+
+
`; + 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"); + + if (!state.connected) { + controls.textContent = "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); + } + }, +}; -- cgit v1.2.3