diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-09 18:16:37 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-09 18:16:37 +0100 |
| commit | 4f6df8303745d0781a7dfbaff8ce57f0a0851e30 (patch) | |
| tree | 8bf797f1dff9e550bfc791d2ab77bf1b0104e110 /deckit | |
| parent | 9275f5fe05629574bffb39c7b5ea8eaaa4867e3a (diff) | |
| download | deck-it-4f6df8303745d0781a7dfbaff8ce57f0a0851e30.tar.gz deck-it-4f6df8303745d0781a7dfbaff8ce57f0a0851e30.zip | |
feat: OBS stream-deck module
Diffstat (limited to 'deckit')
| -rw-r--r-- | deckit/module.py | 16 | ||||
| -rw-r--r-- | deckit/modules/obs.py | 113 | ||||
| -rw-r--r-- | deckit/server.py | 1 |
3 files changed, 130 insertions, 0 deletions
diff --git a/deckit/module.py b/deckit/module.py index fd5997c..f1a7fa1 100644 --- a/deckit/module.py +++ b/deckit/module.py @@ -37,6 +37,22 @@ class Registry: _get = get + def start_all(self) -> None: + for m in self._modules.values(): + if m.start: + try: + m.start() + except Exception: + pass + + def stop_all(self) -> None: + for m in self._modules.values(): + if m.stop: + try: + m.stop() + except Exception: + pass + def state(self, name: str) -> dict: return self._get(name).state() diff --git a/deckit/modules/obs.py b/deckit/modules/obs.py new file mode 100644 index 0000000..f4bf71a --- /dev/null +++ b/deckit/modules/obs.py @@ -0,0 +1,113 @@ +"""OBS stream-deck: recording, scenes, audio channels, transitions and sources.""" + +import threading + +from ..module import Module, ModuleError +from ..obsclient import ObsClient + +AUDIO_KINDS = { + "pulse_input_capture", "pulse_output_capture", "alsa_input_capture", "alsa_output_capture", + "wasapi_input_capture", "wasapi_output_capture", "coreaudio_input_capture", "coreaudio_output_capture", + "sndio_input_capture", "sndio_output_capture", "jack_input_capture", "jack_output_capture", +} + + +def make(cfg: dict) -> Module: + o = cfg.get("obs", {}) + client = cfg.get("client") or ObsClient( + host=o.get("host", "127.0.0.1"), port=o.get("port", 4455), password=o.get("password", "")) + lock = threading.Lock() + stop = threading.Event() + + def req(kind, data=None): + with lock: + return client.request(kind, data) + + def connect_loop(): + while not stop.is_set(): + if not getattr(client, "connected", False): + try: + client.connect() + except Exception: + pass + stop.wait(5) + + def state(): + if not getattr(client, "connected", False): + return {"connected": False} + try: + scenes = req("GetSceneList") + rec = req("GetRecordStatus") + stream = req("GetStreamStatus") + trans = req("GetCurrentSceneTransition") + kinds = req("GetTransitionKindList").get("transitionKinds", []) + current = scenes.get("currentProgramSceneName") + sources = [] + if current: + for item in req("GetSceneItemList", {"sceneName": current}).get("sceneItems", []): + sources.append({"id": item["sceneItemId"], "name": item["sourceName"], + "on": item["sceneItemEnabled"]}) + inputs = [] + for inp in req("GetInputList").get("inputs", []): + if inp.get("inputKind") not in AUDIO_KINDS: + continue + try: + vol = req("GetInputVolume", {"inputName": inp["inputName"]}) + muted = req("GetInputMute", {"inputName": inp["inputName"]}) + except ModuleError: + continue + inputs.append({"name": inp["inputName"], + "volume": vol.get("inputVolumeMul", 0), + "muted": muted.get("inputMuted", False)}) + except ModuleError: + return {"connected": False} + return { + "connected": True, + "scenes": [s["sceneName"] for s in scenes.get("scenes", [])], + "currentScene": current, + "recording": rec.get("outputActive", False), + "recordingTime": rec.get("outputDuration", 0) / 1000, + "streaming": stream.get("outputActive", False), + "transitions": kinds, + "transition": trans.get("transitionName"), + "sources": sources, + "inputs": inputs, + } + + def scene(body): + name = body.get("scene") + if not isinstance(name, str) or not name: + raise ModuleError("scene required") + req("SetCurrentProgramScene", {"sceneName": name}) + + def record(body): + req("StartRecord" if body.get("on") else "StopRecord") + + def stream(body): + req("StartStream" if body.get("on") else "StopStream") + + def mute(body): + req("SetInputMute", {"inputName": str(body.get("input")), "inputMuted": bool(body.get("muted"))}) + + def volume(body): + req("SetInputVolume", {"inputName": str(body.get("input")), + "inputVolumeMul": float(body.get("level", 0))}) + + def transition(body): + req("SetCurrentSceneTransition", {"transitionName": str(body.get("name"))}) + + def trigger(body): + req("TriggerStudioModeTransition") + + def source(body): + req("SetSceneItemEnabled", {"sceneName": str(body.get("scene")), + "sceneItemId": int(body.get("id")), + "sceneItemEnabled": bool(body.get("on"))}) + + return Module( + name="obs", title="OBS", state=state, + actions={"scene": scene, "record": record, "stream": stream, "mute": mute, + "volume": volume, "transition": transition, "trigger": trigger, "source": source}, + start=lambda: threading.Thread(target=connect_loop, daemon=True).start(), + stop=lambda: (stop.set(), client.close()), + ) diff --git a/deckit/server.py b/deckit/server.py index f51621e..bbd92cf 100644 --- a/deckit/server.py +++ b/deckit/server.py @@ -157,6 +157,7 @@ def serve(): TOKEN = load_token() REGISTRY = build(CFG) + REGISTRY.start_all() RUNDIR.mkdir(mode=0o700, parents=True, exist_ok=True) RUNDIR.chmod(0o700) SOCKET.unlink(missing_ok=True) |
