"""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() last_error = {"msg": None} 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() last_error["msg"] = None except Exception as e: last_error["msg"] = str(e) or type(e).__name__ stop.wait(5) def state(): if not getattr(client, "connected", False): return {"connected": False, "error": last_error["msg"]} 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 flag(body): on = body.get("on") if not isinstance(on, bool): raise ModuleError('"on" must be true or false') return on def record(body): req("StartRecord" if flag(body) else "StopRecord") def stream(body): req("StartStream" if flag(body) 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()), )