mirror of
https://github.com/onyx-and-iris/obsws-python.git
synced 2024-11-22 12:50:53 +00:00
add support for toml config.
subject module added, supports callbacks. events module added. Provides an event listener and callback trigger. import isorted, code run through black. toml section added to readme. added a couple of examples.
This commit is contained in:
parent
1793685be0
commit
b5b69de218
6
.gitignore
vendored
6
.gitignore
vendored
@ -2,4 +2,8 @@ __pycache__
|
|||||||
obsstudio_sdk.egg-info
|
obsstudio_sdk.egg-info
|
||||||
dist
|
dist
|
||||||
docs
|
docs
|
||||||
setup.py
|
setup.py
|
||||||
|
|
||||||
|
venv
|
||||||
|
quick.py
|
||||||
|
config.toml
|
33
README.md
33
README.md
@ -1,7 +1,8 @@
|
|||||||
# obs_sdk
|
# obs_sdk
|
||||||
|
|
||||||
### A Python SDK for OBS Studio WebSocket v5.0
|
### A Python SDK for OBS Studio WebSocket v5.0
|
||||||
|
|
||||||
This is a wrapper around OBS Websocket.
|
This is a wrapper around OBS Websocket.
|
||||||
Not all endpoints in the official documentation are implemented. But all endpoints in the Requests section is implemented. You can find the relevant document using below link.
|
Not all endpoints in the official documentation are implemented. But all endpoints in the Requests section is implemented. You can find the relevant document using below link.
|
||||||
[obs-websocket github page](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests)
|
[obs-websocket github page](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests)
|
||||||
|
|
||||||
@ -11,14 +12,26 @@ Not all endpoints in the official documentation are implemented. But all endpoin
|
|||||||
pip install obsstudio-sdk
|
pip install obsstudio-sdk
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### How to Use
|
### How to Use
|
||||||
|
|
||||||
* Import and start using
|
- Load connection info from toml config. A valid `config.toml` might look like this:
|
||||||
Required parameters are as follows:
|
|
||||||
host: obs websocket server
|
```toml
|
||||||
port: port to access server
|
[connection]
|
||||||
password: obs websocket server password
|
host = "localhost"
|
||||||
|
port = 4455
|
||||||
|
password = "mystrongpass"
|
||||||
|
```
|
||||||
|
|
||||||
|
It should be placed next to your `__main__.py` file.
|
||||||
|
|
||||||
|
Otherwise:
|
||||||
|
|
||||||
|
- Import and start using
|
||||||
|
Parameters are as follows:
|
||||||
|
host: obs websocket server
|
||||||
|
port: port to access server
|
||||||
|
password: obs websocket server password
|
||||||
|
|
||||||
```
|
```
|
||||||
>>>from obsstudio_sdk.reqs import ReqClient
|
>>>from obsstudio_sdk.reqs import ReqClient
|
||||||
@ -26,12 +39,12 @@ pip install obsstudio-sdk
|
|||||||
>>>client = ReqClient('192.168.1.1', 4444, 'somepassword')
|
>>>client = ReqClient('192.168.1.1', 4444, 'somepassword')
|
||||||
```
|
```
|
||||||
|
|
||||||
Now you can make calls to OBS
|
Now you can make calls to OBS
|
||||||
|
|
||||||
Example: Toggle the mute state of your Mic input
|
Example: Toggle the mute state of your Mic input
|
||||||
|
|
||||||
```
|
```
|
||||||
>>>cl.ToggleInputMute('Mic/Aux')
|
>>>cl.ToggleInputMute('Mic/Aux')
|
||||||
>>>
|
>>>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
4
build/lib/obsstudio_sdk/__init__.py
Normal file
4
build/lib/obsstudio_sdk/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
from .events import EventsClient
|
||||||
|
from .reqs import ReqClient
|
||||||
|
|
||||||
|
__ALL__ = ["ReqClient", "EventsClient"]
|
71
build/lib/obsstudio_sdk/baseclient.py
Normal file
71
build/lib/obsstudio_sdk/baseclient.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from random import randint
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
|
||||||
|
class ObsClient(object):
|
||||||
|
def __init__(self, host=None, port=None, password=None):
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.password = password
|
||||||
|
if not (self.host and self.port and self.password):
|
||||||
|
conn = self._conn_from_toml()
|
||||||
|
self.host = conn["host"]
|
||||||
|
self.port = conn["port"]
|
||||||
|
self.password = conn["password"]
|
||||||
|
self.ws = websocket.WebSocket()
|
||||||
|
self.ws.connect(f"ws://{self.host}:{self.port}")
|
||||||
|
self.server_hello = json.loads(self.ws.recv())
|
||||||
|
|
||||||
|
def _conn_from_toml(self):
|
||||||
|
filepath = Path.cwd() / "config.toml"
|
||||||
|
self._conn = dict()
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
self._conn = tomllib.load(f)
|
||||||
|
return self._conn["connection"]
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
secret = base64.b64encode(
|
||||||
|
hashlib.sha256(
|
||||||
|
(
|
||||||
|
self.password + self.server_hello["d"]["authentication"]["salt"]
|
||||||
|
).encode()
|
||||||
|
).digest()
|
||||||
|
)
|
||||||
|
|
||||||
|
auth = base64.b64encode(
|
||||||
|
hashlib.sha256(
|
||||||
|
(
|
||||||
|
secret.decode()
|
||||||
|
+ self.server_hello["d"]["authentication"]["challenge"]
|
||||||
|
).encode()
|
||||||
|
).digest()
|
||||||
|
).decode()
|
||||||
|
|
||||||
|
payload = {"op": 1, "d": {"rpcVersion": 1, "authentication": auth}}
|
||||||
|
|
||||||
|
self.ws.send(json.dumps(payload))
|
||||||
|
return self.ws.recv()
|
||||||
|
|
||||||
|
def req(self, req_type, req_data=None):
|
||||||
|
if req_data:
|
||||||
|
payload = {
|
||||||
|
"op": 6,
|
||||||
|
"d": {
|
||||||
|
"requestType": req_type,
|
||||||
|
"requestId": randint(1, 1000),
|
||||||
|
"requestData": req_data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payload = {
|
||||||
|
"op": 6,
|
||||||
|
"d": {"requestType": req_type, "requestId": randint(1, 1000)},
|
||||||
|
}
|
||||||
|
self.ws.send(json.dumps(payload))
|
||||||
|
return json.loads(self.ws.recv())
|
43
build/lib/obsstudio_sdk/events.py
Normal file
43
build/lib/obsstudio_sdk/events.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from .baseclient import ObsClient
|
||||||
|
from .subject import Callback
|
||||||
|
|
||||||
|
"""
|
||||||
|
A class to interact with obs-websocket events
|
||||||
|
defined in official github repo
|
||||||
|
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class EventsClient(object):
|
||||||
|
DELAY = 0.001
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.base_client = ObsClient(**kwargs)
|
||||||
|
self.base_client.authenticate()
|
||||||
|
self.callback = Callback()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
worker = Thread(target=self.trigger, daemon=True)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
"""
|
||||||
|
Continuously listen for events.
|
||||||
|
|
||||||
|
Triggers a callback on event received.
|
||||||
|
"""
|
||||||
|
while self.running:
|
||||||
|
self.data = json.loads(self.base_client.ws.recv())
|
||||||
|
event, data = (self.data["d"].get("eventType"), self.data["d"])
|
||||||
|
self.callback.trigger(event, data)
|
||||||
|
time.sleep(self.DELAY)
|
||||||
|
|
||||||
|
def unsubscribe(self):
|
||||||
|
"""
|
||||||
|
stop listening for events
|
||||||
|
"""
|
||||||
|
self.running = False
|
1928
build/lib/obsstudio_sdk/reqs.py
Normal file
1928
build/lib/obsstudio_sdk/reqs.py
Normal file
File diff suppressed because it is too large
Load Diff
58
build/lib/obsstudio_sdk/subject.py
Normal file
58
build/lib/obsstudio_sdk/subject.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Callback:
|
||||||
|
"""Adds support for callbacks"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""list of current callbacks"""
|
||||||
|
|
||||||
|
self._callbacks = list()
|
||||||
|
|
||||||
|
def to_camel_case(self, s):
|
||||||
|
s = "".join(word.title() for word in s.split("_"))
|
||||||
|
return s[2:]
|
||||||
|
|
||||||
|
def to_snake_case(self, s):
|
||||||
|
s = re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
|
||||||
|
return f"on_{s}"
|
||||||
|
|
||||||
|
def get(self) -> list:
|
||||||
|
"""returns a list of registered events"""
|
||||||
|
|
||||||
|
return [self.to_camel_case(fn.__name__) for fn in self._callbacks]
|
||||||
|
|
||||||
|
def trigger(self, event, data=None):
|
||||||
|
"""trigger callback on update"""
|
||||||
|
|
||||||
|
for fn in self._callbacks:
|
||||||
|
if fn.__name__ == self.to_snake_case(event):
|
||||||
|
if "eventData" in data:
|
||||||
|
fn(data["eventData"])
|
||||||
|
else:
|
||||||
|
fn()
|
||||||
|
|
||||||
|
def register(self, fns):
|
||||||
|
"""registers callback functions"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
iter(fns)
|
||||||
|
for fn in fns:
|
||||||
|
if fn not in self._callbacks:
|
||||||
|
self._callbacks.append(fn)
|
||||||
|
except TypeError as e:
|
||||||
|
if fns not in self._callbacks:
|
||||||
|
self._callbacks.append(fns)
|
||||||
|
|
||||||
|
def deregister(self, callback):
|
||||||
|
"""deregisters a callback from _callbacks"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._callbacks.remove(callback)
|
||||||
|
except ValueError:
|
||||||
|
print(f"Failed to remove: {callback}")
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""clears the _callbacks list"""
|
||||||
|
|
||||||
|
self._callbacks.clear()
|
26
examples/events/__main__.py
Normal file
26
examples/events/__main__.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import obsstudio_sdk as obs
|
||||||
|
|
||||||
|
|
||||||
|
class Observer:
|
||||||
|
def __init__(self, cl):
|
||||||
|
self._cl = cl
|
||||||
|
self._cl.callback.register(
|
||||||
|
[self.on_current_program_scene_changed, self.on_exit_started]
|
||||||
|
)
|
||||||
|
print(f"Registered events: {self._cl.callback.get()}")
|
||||||
|
|
||||||
|
def on_exit_started(self):
|
||||||
|
print(f"OBS closing!")
|
||||||
|
self._cl.unsubscribe()
|
||||||
|
|
||||||
|
def on_current_program_scene_changed(self, data):
|
||||||
|
print(f"Switched to scene {data['sceneName']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cl = obs.EventsClient()
|
||||||
|
observer = Observer(cl)
|
||||||
|
|
||||||
|
while cmd := input("<Enter> to exit\n"):
|
||||||
|
if not cmd:
|
||||||
|
break
|
19
examples/scene_rotate/__main__.py
Normal file
19
examples/scene_rotate/__main__.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import obsstudio_sdk as obs
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
res = cl.GetSceneList()
|
||||||
|
scenes = reversed(tuple(d["sceneName"] for d in res["d"]["responseData"]["scenes"]))
|
||||||
|
|
||||||
|
for sc in scenes:
|
||||||
|
print(f"Switching to scene {sc}")
|
||||||
|
cl.SetCurrentProgramScene(sc)
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
cl = obs.ReqClient()
|
||||||
|
|
||||||
|
main()
|
@ -1 +1,4 @@
|
|||||||
|
from .events import EventsClient
|
||||||
|
from .reqs import ReqClient
|
||||||
|
|
||||||
|
__ALL__ = ["ReqClient", "EventsClient"]
|
||||||
|
@ -1,53 +1,73 @@
|
|||||||
import websocket
|
|
||||||
import json
|
|
||||||
import hashlib
|
|
||||||
import base64
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
|
import tomllib
|
||||||
|
import websocket
|
||||||
|
|
||||||
|
|
||||||
class ObsClient(object):
|
class ObsClient(object):
|
||||||
def __init__(self, host, port, password):
|
def __init__(self, **kwargs):
|
||||||
self.host = host
|
defaultkwargs = {key: None for key in ["host", "port", "password"]}
|
||||||
self.port = port
|
kwargs = defaultkwargs | kwargs
|
||||||
self.password = password
|
for attr, val in kwargs.items():
|
||||||
|
setattr(self, attr, val)
|
||||||
|
if not (self.host and self.port and self.password):
|
||||||
|
conn = self._conn_from_toml()
|
||||||
|
self.host = conn["host"]
|
||||||
|
self.port = conn["port"]
|
||||||
|
self.password = conn["password"]
|
||||||
|
|
||||||
self.ws = websocket.WebSocket()
|
self.ws = websocket.WebSocket()
|
||||||
self.ws.connect(f"ws://{self.host}:{self.port}")
|
self.ws.connect(f"ws://{self.host}:{self.port}")
|
||||||
self.server_hello = json.loads(self.ws.recv())
|
self.server_hello = json.loads(self.ws.recv())
|
||||||
|
|
||||||
|
def _conn_from_toml(self):
|
||||||
|
filepath = Path.cwd() / "config.toml"
|
||||||
|
self._conn = dict()
|
||||||
|
with open(filepath, "rb") as f:
|
||||||
|
self._conn = tomllib.load(f)
|
||||||
|
return self._conn["connection"]
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
secret = base64.b64encode(
|
secret = base64.b64encode(
|
||||||
hashlib.sha256(
|
hashlib.sha256(
|
||||||
(self.password + self.server_hello['d']['authentication']['salt']).encode()).digest())
|
(
|
||||||
|
self.password + self.server_hello["d"]["authentication"]["salt"]
|
||||||
|
).encode()
|
||||||
|
).digest()
|
||||||
|
)
|
||||||
|
|
||||||
auth = base64.b64encode(
|
auth = base64.b64encode(
|
||||||
hashlib.sha256(
|
hashlib.sha256(
|
||||||
(secret.decode() + self.server_hello['d']['authentication']['challenge']).encode()).digest()).decode()
|
(
|
||||||
|
secret.decode()
|
||||||
payload = { "op":1, "d": {
|
+ self.server_hello["d"]["authentication"]["challenge"]
|
||||||
"rpcVersion": 1,
|
).encode()
|
||||||
"authentication": auth}
|
).digest()
|
||||||
}
|
).decode()
|
||||||
|
|
||||||
|
payload = {"op": 1, "d": {"rpcVersion": 1, "authentication": auth}}
|
||||||
|
|
||||||
self.ws.send(json.dumps(payload))
|
self.ws.send(json.dumps(payload))
|
||||||
return self.ws.recv()
|
return self.ws.recv()
|
||||||
|
|
||||||
def req(self, req_type, req_data=None):
|
def req(self, req_type, req_data=None):
|
||||||
if req_data == None:
|
if req_data:
|
||||||
payload = {
|
|
||||||
"op": 6,
|
|
||||||
"d": {
|
|
||||||
"requestType": req_type,
|
|
||||||
"requestId": randint(1, 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
payload = {
|
payload = {
|
||||||
"op": 6,
|
"op": 6,
|
||||||
"d": {
|
"d": {
|
||||||
"requestType": req_type,
|
"requestType": req_type,
|
||||||
"requestId": randint(1, 1000),
|
"requestId": randint(1, 1000),
|
||||||
"requestData": req_data
|
"requestData": req_data,
|
||||||
}
|
},
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
payload = {
|
||||||
|
"op": 6,
|
||||||
|
"d": {"requestType": req_type, "requestId": randint(1, 1000)},
|
||||||
}
|
}
|
||||||
self.ws.send(json.dumps(payload))
|
self.ws.send(json.dumps(payload))
|
||||||
return json.loads(self.ws.recv())
|
return json.loads(self.ws.recv())
|
||||||
|
|
58
obsstudio_sdk/callback.py
Normal file
58
obsstudio_sdk/callback.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class Callback:
|
||||||
|
"""Adds support for callbacks"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""list of current callbacks"""
|
||||||
|
|
||||||
|
self._callbacks = list()
|
||||||
|
|
||||||
|
def to_camel_case(self, s):
|
||||||
|
s = "".join(word.title() for word in s.split("_"))
|
||||||
|
return s[2:]
|
||||||
|
|
||||||
|
def to_snake_case(self, s):
|
||||||
|
s = re.sub(r"(?<!^)(?=[A-Z])", "_", s).lower()
|
||||||
|
return f"on_{s}"
|
||||||
|
|
||||||
|
def get(self) -> list:
|
||||||
|
"""returns a list of registered events"""
|
||||||
|
|
||||||
|
return [self.to_camel_case(fn.__name__) for fn in self._callbacks]
|
||||||
|
|
||||||
|
def trigger(self, event, data=None):
|
||||||
|
"""trigger callback on update"""
|
||||||
|
|
||||||
|
for fn in self._callbacks:
|
||||||
|
if fn.__name__ == self.to_snake_case(event):
|
||||||
|
if "eventData" in data:
|
||||||
|
fn(data["eventData"])
|
||||||
|
else:
|
||||||
|
fn()
|
||||||
|
|
||||||
|
def register(self, fns):
|
||||||
|
"""registers callback functions"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
iter(fns)
|
||||||
|
for fn in fns:
|
||||||
|
if fn not in self._callbacks:
|
||||||
|
self._callbacks.append(fn)
|
||||||
|
except TypeError as e:
|
||||||
|
if fns not in self._callbacks:
|
||||||
|
self._callbacks.append(fns)
|
||||||
|
|
||||||
|
def deregister(self, callback):
|
||||||
|
"""deregisters a callback from _callbacks"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._callbacks.remove(callback)
|
||||||
|
except ValueError:
|
||||||
|
print(f"Failed to remove: {callback}")
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""clears the _callbacks list"""
|
||||||
|
|
||||||
|
self._callbacks.clear()
|
43
obsstudio_sdk/events.py
Normal file
43
obsstudio_sdk/events.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import json
|
||||||
|
import time
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from .baseclient import ObsClient
|
||||||
|
from .callback import Callback
|
||||||
|
|
||||||
|
"""
|
||||||
|
A class to interact with obs-websocket events
|
||||||
|
defined in official github repo
|
||||||
|
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class EventsClient(object):
|
||||||
|
DELAY = 0.001
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.base_client = ObsClient(**kwargs)
|
||||||
|
self.base_client.authenticate()
|
||||||
|
self.callback = Callback()
|
||||||
|
|
||||||
|
self.running = True
|
||||||
|
worker = Thread(target=self.trigger, daemon=True)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
def trigger(self):
|
||||||
|
"""
|
||||||
|
Continuously listen for events.
|
||||||
|
|
||||||
|
Triggers a callback on event received.
|
||||||
|
"""
|
||||||
|
while self.running:
|
||||||
|
self.data = json.loads(self.base_client.ws.recv())
|
||||||
|
event, data = (self.data["d"].get("eventType"), self.data["d"])
|
||||||
|
self.callback.trigger(event, data)
|
||||||
|
time.sleep(self.DELAY)
|
||||||
|
|
||||||
|
def unsubscribe(self):
|
||||||
|
"""
|
||||||
|
stop listening for events
|
||||||
|
"""
|
||||||
|
self.running = False
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user