From 4f6df8303745d0781a7dfbaff8ce57f0a0851e30 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 9 Nov 2026 18:16:37 +0100 Subject: feat: OBS stream-deck module --- deckit/modules/obs.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 deckit/modules/obs.py (limited to 'deckit/modules/obs.py') 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()), + ) -- cgit v1.2.3