mirror of
https://github.com/onyx-and-iris/obsws-python.git
synced 2025-04-01 19:03:48 +01:00
Compare commits
26 Commits
ce6873f57a
...
4e45de17ea
Author | SHA1 | Date | |
---|---|---|---|
4e45de17ea | |||
491a26aaf7 | |||
d84d30b752 | |||
9e3c1d3f37 | |||
82b6cdcd04 | |||
|
64a7c2b753 | ||
|
15559fdb33 | ||
|
3adf094481 | ||
|
9c41f2bb59 | ||
d1c7462cc6 | |||
2de7151739 | |||
91ba90056c | |||
|
5e68262a80 | ||
|
ef0f770c0c | ||
48e90c82fb | |||
cc9b1e2c72 | |||
41b0dfbe4b | |||
cf888b0c4a | |||
92e2c29bd6 | |||
335fa42948 | |||
83afe31e04 | |||
5294e1afe2 | |||
c6cbe1c894 | |||
|
13ef8108df | ||
|
3786739eee | ||
|
71c1e65483 |
5
.gitignore
vendored
5
.gitignore
vendored
@ -47,4 +47,7 @@ venv.bak/
|
||||
|
||||
# Test/config
|
||||
quick.py
|
||||
config.toml
|
||||
config.toml
|
||||
obsws.log
|
||||
|
||||
.vscode/
|
12
README.md
12
README.md
@ -5,7 +5,6 @@
|
||||
|
||||
# A Python SDK for OBS Studio WebSocket v5.0
|
||||
|
||||
This is a wrapper around OBS Websocket.
|
||||
Not all endpoints in the official documentation are implemented.
|
||||
|
||||
## Requirements
|
||||
@ -13,7 +12,7 @@ Not all endpoints in the official documentation are implemented.
|
||||
- [OBS Studio](https://obsproject.com/)
|
||||
- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0)
|
||||
- With the release of OBS Studio version 28, Websocket plugin is included by default. But it should be manually installed for earlier versions of OBS.
|
||||
- Python 3.10 or greater
|
||||
- Python 3.9 or greater
|
||||
|
||||
### How to install using pip
|
||||
|
||||
@ -27,7 +26,8 @@ By default the clients connect with parameters:
|
||||
|
||||
- `host`: "localhost"
|
||||
- `port`: 4455
|
||||
- `password`: None
|
||||
- `password`: ""
|
||||
- `timeout`: None
|
||||
|
||||
You may override these parameters by storing them in a toml config file or passing them as keyword arguments.
|
||||
|
||||
@ -44,7 +44,7 @@ port = 4455
|
||||
password = "mystrongpass"
|
||||
```
|
||||
|
||||
It should be placed next to your `__main__.py` file.
|
||||
It should be placed in your user home directory.
|
||||
|
||||
#### Otherwise:
|
||||
|
||||
@ -54,7 +54,7 @@ Example `__main__.py`:
|
||||
import obsws_python as obs
|
||||
|
||||
# pass conn info if not in config.toml
|
||||
cl = obs.ReqClient(host='localhost', port=4455, password='mystrongpass')
|
||||
cl = obs.ReqClient(host='localhost', port=4455, password='mystrongpass', timeout=3)
|
||||
|
||||
# Toggle the mute state of your Mic input
|
||||
cl.toggle_input_mute('Mic/Aux')
|
||||
@ -132,6 +132,8 @@ If a request fails an `OBSSDKError` will be raised with a status code.
|
||||
|
||||
For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
|
||||
|
||||
If a timeout occurs during sending/receiving a request or receiving an event an `OBSSDKTimeoutError` will be raised.
|
||||
|
||||
### Logging
|
||||
|
||||
If you want to see the raw messages simply set log level to DEBUG
|
||||
|
@ -6,7 +6,7 @@ Registers a list of callback functions to hook into OBS events.
|
||||
|
||||
Simply run the code and trigger the events, press `<Enter>` to exit.
|
||||
|
||||
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
|
||||
This example assumes the existence of a `config.toml`, placed in your user home directory:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
|
@ -8,7 +8,7 @@ Requires [Python Keyboard library](https://github.com/boppreh/keyboard).
|
||||
|
||||
Simply run the code and press the assigned hotkeys. Press `ctrl+enter` to exit.
|
||||
|
||||
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
|
||||
This example assumes the existence of a `config.toml`, placed in your user home directory:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
|
@ -4,7 +4,7 @@ Prints POSTFADER level values for audio device `Desktop Audio`. If mute toggled
|
||||
|
||||
## Use
|
||||
|
||||
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
|
||||
This example assumes the existence of a `config.toml`, placed in your user home directory:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
|
@ -4,7 +4,7 @@ Collects the names of all available scenes, rotates through them and prints thei
|
||||
|
||||
## Use
|
||||
|
||||
This example assumes the existence of a `config.toml`, placed next to `__main__.py`:
|
||||
This example assumes the existence of a `config.toml`, placed in your user home directory:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
|
@ -1,6 +1,6 @@
|
||||
from .enum import Subs
|
||||
from .events import EventClient
|
||||
from .reqs import ReqClient
|
||||
from .subs import Subs
|
||||
from .version import version as __version__
|
||||
|
||||
__ALL__ = ["ReqClient", "EventClient", "Subs"]
|
||||
|
@ -4,22 +4,25 @@ import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from random import randint
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
from typing import Optional
|
||||
|
||||
import websocket
|
||||
from websocket import WebSocketTimeoutException
|
||||
|
||||
from .error import OBSSDKError
|
||||
from .error import OBSSDKError, OBSSDKTimeoutError
|
||||
|
||||
|
||||
class ObsClient:
|
||||
logger = logging.getLogger("baseclient.obsclient")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
defaultkwargs = {"host": "localhost", "port": 4455, "password": None, "subs": 0}
|
||||
defaultkwargs = {
|
||||
"host": "localhost",
|
||||
"port": 4455,
|
||||
"password": "",
|
||||
"subs": 0,
|
||||
"timeout": None,
|
||||
}
|
||||
if not any(key in kwargs for key in ("host", "port", "password")):
|
||||
kwargs |= self._conn_from_toml()
|
||||
kwargs = defaultkwargs | kwargs
|
||||
@ -27,21 +30,47 @@ class ObsClient:
|
||||
setattr(self, attr, val)
|
||||
|
||||
self.logger.info(
|
||||
"Connecting with parameters: {host} {port} {password} {subs}".format(
|
||||
"Connecting with parameters: host='{host}' port={port} password='{password}' subs={subs} timeout={timeout}".format(
|
||||
**self.__dict__
|
||||
)
|
||||
)
|
||||
|
||||
self.ws = websocket.WebSocket()
|
||||
self.ws.connect(f"ws://{self.host}:{self.port}")
|
||||
self.server_hello = json.loads(self.ws.recv())
|
||||
try:
|
||||
self.ws = websocket.WebSocket()
|
||||
self.ws.connect(f"ws://{self.host}:{self.port}", timeout=self.timeout)
|
||||
self.server_hello = json.loads(self.ws.recv())
|
||||
except ValueError as e:
|
||||
self.logger.error(f"{type(e).__name__}: {e}")
|
||||
raise
|
||||
except (ConnectionRefusedError, WebSocketTimeoutException) as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
def _conn_from_toml(self) -> dict:
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
def get_filepath() -> Optional[Path]:
|
||||
"""
|
||||
traverses a list of paths for a 'config.toml'
|
||||
returns the first config file found or None.
|
||||
"""
|
||||
filepaths = [
|
||||
Path.cwd() / "config.toml",
|
||||
Path.home() / "config.toml",
|
||||
Path.home() / ".config" / "obsws-python" / "config.toml",
|
||||
]
|
||||
for filepath in filepaths:
|
||||
if filepath.exists():
|
||||
return filepath
|
||||
|
||||
conn = {}
|
||||
filepath = Path.cwd() / "config.toml"
|
||||
if filepath.exists():
|
||||
if filepath := get_filepath():
|
||||
with open(filepath, "rb") as f:
|
||||
conn = tomllib.load(f)
|
||||
self.logger.info(f"loading config from {filepath}")
|
||||
return conn["connection"] if "connection" in conn else conn
|
||||
|
||||
def authenticate(self):
|
||||
@ -90,7 +119,11 @@ class ObsClient:
|
||||
if req_data:
|
||||
payload["d"]["requestData"] = req_data
|
||||
self.logger.debug(f"Sending request {payload}")
|
||||
self.ws.send(json.dumps(payload))
|
||||
response = json.loads(self.ws.recv())
|
||||
try:
|
||||
self.ws.send(json.dumps(payload))
|
||||
response = json.loads(self.ws.recv())
|
||||
except WebSocketTimeoutException as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise OBSSDKTimeoutError("Timeout while trying to send the request") from e
|
||||
self.logger.debug(f"Response received {response}")
|
||||
return response["d"]
|
||||
|
@ -1,4 +1,6 @@
|
||||
class OBSSDKError(Exception):
|
||||
"""general errors"""
|
||||
"""Exception raised when general errors occur"""
|
||||
|
||||
pass
|
||||
|
||||
class OBSSDKTimeoutError(Exception):
|
||||
"""Exception raised when a connection times out"""
|
||||
|
@ -3,9 +3,12 @@ import logging
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from websocket import WebSocketTimeoutException
|
||||
|
||||
from .baseclient import ObsClient
|
||||
from .callback import Callback
|
||||
from .enum import Subs
|
||||
from .error import OBSSDKTimeoutError
|
||||
from .subs import Subs
|
||||
|
||||
"""
|
||||
A class to interact with obs-websocket events
|
||||
@ -28,6 +31,13 @@ class EventClient:
|
||||
self.subscribe()
|
||||
|
||||
def __repr__(self):
|
||||
return type(
|
||||
self
|
||||
).__name__ + "(host='{host}', port={port}, password='{password}', subs={subs}, timeout={timeout})".format(
|
||||
**self.base_client.__dict__,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return type(self).__name__
|
||||
|
||||
def subscribe(self):
|
||||
@ -42,7 +52,11 @@ class EventClient:
|
||||
"""
|
||||
self.running = True
|
||||
while self.running:
|
||||
event = json.loads(self.base_client.ws.recv())
|
||||
try:
|
||||
event = json.loads(self.base_client.ws.recv())
|
||||
except WebSocketTimeoutException as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise OBSSDKTimeoutError("Timeout while waiting for event") from e
|
||||
self.logger.debug(f"Event received {event}")
|
||||
type_, data = (
|
||||
event["d"].get("eventType"),
|
||||
|
@ -26,9 +26,16 @@ class ReqClient:
|
||||
self.base_client.ws.close()
|
||||
|
||||
def __repr__(self):
|
||||
return type(
|
||||
self
|
||||
).__name__ + "(host='{host}', port={port}, password='{password}', timeout={timeout})".format(
|
||||
**self.base_client.__dict__,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return type(self).__name__
|
||||
|
||||
def send(self, param, data=None):
|
||||
def send(self, param, data=None, raw=False):
|
||||
response = self.base_client.req(param, data)
|
||||
if not response["requestStatus"]["result"]:
|
||||
error = (
|
||||
@ -38,6 +45,8 @@ class ReqClient:
|
||||
error += (f"With message: {response['requestStatus']['comment']}",)
|
||||
raise OBSSDKError("\n".join(error))
|
||||
if "responseData" in response:
|
||||
if raw:
|
||||
return response["responseData"]
|
||||
return as_dataclass(response["requestType"], response["responseData"])
|
||||
|
||||
def get_version(self):
|
||||
@ -1485,7 +1494,7 @@ class ReqClient:
|
||||
payload = {
|
||||
"sceneName": scene_name,
|
||||
"sceneItemId": item_id,
|
||||
"sceneItemLocked": item_index,
|
||||
"sceneItemIndex": item_index,
|
||||
}
|
||||
self.send("SetSceneItemIndex", payload)
|
||||
|
||||
@ -1729,7 +1738,10 @@ class ReqClient:
|
||||
|
||||
|
||||
"""
|
||||
self.send("SendStreamCaption")
|
||||
payload = {
|
||||
"captionText": caption,
|
||||
}
|
||||
self.send("SendStreamCaption", payload)
|
||||
|
||||
def get_record_status(self):
|
||||
"""
|
||||
|
@ -1 +1 @@
|
||||
version = "1.3.0"
|
||||
version = "1.5.0"
|
||||
|
@ -8,7 +8,7 @@ dynamic = ["version"]
|
||||
description = "A Python SDK for OBS Studio WebSocket v5.0"
|
||||
readme = "README.md"
|
||||
license = "GPL-3.0-only"
|
||||
requires-python = ">=3.10"
|
||||
requires-python = ">=3.9"
|
||||
authors = [
|
||||
{ name = "Adem Atikturk", email = "aatikturk@gmail.com" },
|
||||
]
|
||||
@ -37,7 +37,18 @@ include = [
|
||||
]
|
||||
|
||||
[tool.hatch.envs.e.scripts]
|
||||
events = "py {root}\\examples\\events\\."
|
||||
hotkeys = "py {root}\\examples\\hotkeys\\."
|
||||
levels = "py {root}\\examples\\levels\\."
|
||||
scene_rotate = "py {root}\\examples\\scene_rotate\\."
|
||||
events = "python {root}\\examples\\events\\."
|
||||
hotkeys = "python {root}\\examples\\hotkeys\\."
|
||||
levels = "python {root}\\examples\\levels\\."
|
||||
scene_rotate = "python {root}\\examples\\scene_rotate\\."
|
||||
|
||||
[tool.hatch.envs.test]
|
||||
dependencies = [
|
||||
"pytest",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.test.scripts]
|
||||
run = 'pytest -v'
|
||||
|
||||
[[tool.hatch.envs.test.matrix]]
|
||||
python = ["39", "310", "311"]
|
||||
|
2
setup.py
2
setup.py
@ -40,7 +40,7 @@ EXTRAS_REQUIRE = {
|
||||
}
|
||||
|
||||
# Python version requirement
|
||||
PYTHON_REQUIRES = ">=3.10"
|
||||
PYTHON_REQUIRES = ">=3.9"
|
||||
|
||||
setup(
|
||||
name=PACKAGE_NAME,
|
||||
|
@ -13,4 +13,7 @@ def teardown_module():
|
||||
req_cl.remove_scene("START_TEST")
|
||||
req_cl.remove_scene("BRB_TEST")
|
||||
req_cl.remove_scene("END_TEST")
|
||||
resp = req_cl.get_studio_mode_enabled()
|
||||
if resp.studio_mode_enabled:
|
||||
req_cl.set_studio_mode_enabled(False)
|
||||
req_cl.base_client.ws.close()
|
||||
|
Loading…
x
Reference in New Issue
Block a user