aboutsummaryrefslogtreecommitdiffstats
path: root/deckit/modules/obs.py
blob: 96217c4fa3290d367d7b592fb339c506c38200cb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""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()),
    )