3 Commits

Author SHA1 Message Date
dc7bec3ed0 fix docstring. 2023-10-08 14:47:40 +01:00
e4a87609ee fix desc 2023-10-08 03:55:29 +01:00
79a056a5c8 adds projector methods with deprecation warning 2023-10-08 03:53:47 +01:00
15 changed files with 759 additions and 1442 deletions

2
.gitignore vendored
View File

@@ -51,7 +51,7 @@ venv.bak/
.python-version .python-version
# Test/config # Test/config
test-*.py quick.py
config.toml config.toml
obsws.log obsws.log

View File

@@ -1,10 +0,0 @@
repos:
- repo: local
hooks:
- id: format
name: format
entry: hatch run style:fmt
language: system
pass_filenames: false
verbose: true
files: \.(py)$

View File

@@ -1,6 +1,5 @@
[![PyPI version](https://badge.fury.io/py/obsws-python.svg)](https://badge.fury.io/py/obsws-python) [![PyPI version](https://badge.fury.io/py/obsws-python.svg)](https://badge.fury.io/py/obsws-python)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/aatikturk/obsstudio_sdk/blob/main/LICENSE) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://github.com/aatikturk/obsstudio_sdk/blob/main/LICENSE)
[![Hatch project](https://img.shields.io/badge/%F0%9F%A5%9A-Hatch-4051b5.svg)](https://github.com/pypa/hatch)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
@@ -93,7 +92,7 @@ resp = cl_req.send("GetVersion", raw=True)
print(f"response data: {resp}") print(f"response data: {resp}")
``` ```
For a full list of requests refer to [Requests][obsws-reqs] For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
### Events ### Events
@@ -126,7 +125,7 @@ cl.callback.deregister(on_input_mute_state_changed)
`register(fns)` and `deregister(fns)` accept both single functions and lists of functions. `register(fns)` and `deregister(fns)` accept both single functions and lists of functions.
For a full list of events refer to [Events][obsws-events] For a full list of events refer to [Events](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events)
### Attributes ### Attributes
@@ -150,7 +149,7 @@ def on_scene_created(data):
- The following attributes are available: - The following attributes are available:
- `req_name`: name of the request. - `req_name`: name of the request.
- `code`: request status code. - `code`: request status code.
- For a full list of status codes refer to [Codes][obsws-codes] - For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
### Logging ### Logging
@@ -169,21 +168,18 @@ logging.basicConfig(level=logging.DEBUG)
### Tests ### Tests
Install [hatch][hatch-install] and then: First install development dependencies:
`pip install -e .['dev']`
To run all tests:
``` ```
hatch test pytest -v
``` ```
### Official Documentation ### Official Documentation
For the full documentation: For the full documentation:
- [OBS Websocket SDK][obsws-pro] - [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol)
[obsws-reqs]: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests
[obsws-events]: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
[obsws-codes]: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus
[obsws-pro]: https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol
[hatch-install]: https://hatch.pypa.io/latest/install/

View File

@@ -17,12 +17,6 @@ class Observer:
print(f"Registered events: {self._client.callback.get()}") print(f"Registered events: {self._client.callback.get()}")
self.running = True self.running = True
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self._client.disconnect()
def on_current_program_scene_changed(self, data): def on_current_program_scene_changed(self, data):
"""The current program scene has changed.""" """The current program scene has changed."""
print(f"Switched to scene {data.scene_name}") print(f"Switched to scene {data.scene_name}")
@@ -37,11 +31,13 @@ class Observer:
def on_exit_started(self, _): def on_exit_started(self, _):
"""OBS has begun the shutdown process.""" """OBS has begun the shutdown process."""
print("OBS closing!") print(f"OBS closing!")
self._client.unsubscribe()
self.running = False self.running = False
if __name__ == "__main__": if __name__ == "__main__":
with Observer() as observer: observer = Observer()
while observer.running:
time.sleep(0.1) while observer.running:
time.sleep(0.1)

View File

@@ -1,7 +1,6 @@
import inspect import inspect
import keyboard import keyboard
import obsws_python as obs import obsws_python as obs
@@ -11,12 +10,6 @@ class Observer:
self._client.callback.register(self.on_current_program_scene_changed) self._client.callback.register(self.on_current_program_scene_changed)
print(f"Registered events: {self._client.callback.get()}") print(f"Registered events: {self._client.callback.get()}")
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self._client.disconnect()
@property @property
def event_identifier(self): def event_identifier(self):
return inspect.stack()[1].function return inspect.stack()[1].function
@@ -38,12 +31,13 @@ def set_scene(scene, *args):
if __name__ == "__main__": if __name__ == "__main__":
with obs.ReqClient() as req_client: req_client = obs.ReqClient()
with Observer() as observer: observer = Observer()
keyboard.add_hotkey("0", version)
keyboard.add_hotkey("1", set_scene, args=("START",))
keyboard.add_hotkey("2", set_scene, args=("BRB",))
keyboard.add_hotkey("3", set_scene, args=("END",))
print("press ctrl+enter to quit") keyboard.add_hotkey("0", version)
keyboard.wait("ctrl+enter") keyboard.add_hotkey("1", set_scene, args=("START",))
keyboard.add_hotkey("2", set_scene, args=("BRB",))
keyboard.add_hotkey("3", set_scene, args=("END",))
print("press ctrl+enter to quit")
keyboard.wait("ctrl+enter")

View File

@@ -9,8 +9,6 @@ LEVELTYPE = IntEnum(
start=0, start=0,
) )
DEVICE = "Desktop Audio"
def on_input_mute_state_changed(data): def on_input_mute_state_changed(data):
"""An input's mute state has changed.""" """An input's mute state has changed."""
@@ -34,14 +32,15 @@ def on_input_volume_meters(data):
def main(): def main():
with obs.EventClient( client = obs.EventClient(subs=(obs.Subs.LOW_VOLUME | obs.Subs.INPUTVOLUMEMETERS))
subs=(obs.Subs.LOW_VOLUME | obs.Subs.INPUTVOLUMEMETERS) client.callback.register([on_input_volume_meters, on_input_mute_state_changed])
) as client:
client.callback.register([on_input_volume_meters, on_input_mute_state_changed])
while _ := input("Press <Enter> to exit\n"): while cmd := input("<Enter> to exit>\n"):
pass if not cmd:
break
if __name__ == "__main__": if __name__ == "__main__":
DEVICE = "Desktop Audio"
main() main()

View File

@@ -96,8 +96,10 @@ class ObsClient:
auth = base64.b64encode( auth = base64.b64encode(
hashlib.sha256( hashlib.sha256(
secret (
+ self.server_hello["d"]["authentication"]["challenge"].encode() secret
+ self.server_hello["d"]["authentication"]["challenge"].encode()
)
).digest() ).digest()
).decode() ).decode()

View File

@@ -1,5 +1,4 @@
from collections.abc import Callable, Iterable from typing import Callable, Iterable, Union
from typing import Union
from .util import as_dataclass, to_camel_case, to_snake_case from .util import as_dataclass, to_camel_case, to_snake_case

View File

@@ -1,8 +1,9 @@
import json import json
import logging import logging
import threading import time
from threading import Thread
from websocket import WebSocketConnectionClosedException, WebSocketTimeoutException from websocket import WebSocketTimeoutException
from .baseclient import ObsClient from .baseclient import ObsClient
from .callback import Callback from .callback import Callback
@@ -19,6 +20,8 @@ logger = logging.getLogger(__name__)
class EventClient: class EventClient:
DELAY = 0.001
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__) self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = {"subs": Subs.LOW_VOLUME} defaultkwargs = {"subs": Subs.LOW_VOLUME}
@@ -35,12 +38,6 @@ class EventClient:
self.callback = Callback() self.callback = Callback()
self.subscribe() self.subscribe()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self.disconnect()
def __repr__(self): def __repr__(self):
return type( return type(
self self
@@ -52,40 +49,33 @@ class EventClient:
return type(self).__name__ return type(self).__name__
def subscribe(self): def subscribe(self):
self.base_client.ws.settimeout(None) worker = Thread(target=self.trigger, daemon=True)
stop_event = threading.Event() worker.start()
self.worker = threading.Thread(
target=self.trigger, daemon=True, args=(stop_event,)
)
self.worker.start()
def trigger(self, stop_event): def trigger(self):
""" """
Continuously listen for events. Continuously listen for events.
Triggers a callback on event received. Triggers a callback on event received.
""" """
while not stop_event.is_set(): self.running = True
while self.running:
try: try:
if response := self.base_client.ws.recv(): event = json.loads(self.base_client.ws.recv())
event = json.loads(response)
self.logger.debug(f"Event received {event}")
type_, data = (
event["d"].get("eventType"),
event["d"].get("eventData"),
)
self.callback.trigger(type_, data if data else {})
except WebSocketTimeoutException as e: except WebSocketTimeoutException as e:
self.logger.exception(f"{type(e).__name__}: {e}") self.logger.exception(f"{type(e).__name__}: {e}")
raise OBSSDKTimeoutError("Timeout while waiting for event") from e raise OBSSDKTimeoutError("Timeout while waiting for event") from e
except (WebSocketConnectionClosedException, OSError) as e: self.logger.debug(f"Event received {event}")
self.logger.debug(f"{type(e).__name__} terminating the event thread") type_, data = (
stop_event.set() event["d"].get("eventType"),
event["d"].get("eventData"),
def disconnect(self): )
"""stop listening for events""" self.callback.trigger(type_, data if data else {})
time.sleep(self.DELAY)
def unsubscribe(self):
"""
stop listening for events
"""
self.running = False
self.base_client.ws.close() self.base_client.ws.close()
self.worker.join()
unsubscribe = disconnect

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
version = "1.8.0" version = "1.6.0"

View File

@@ -7,7 +7,7 @@ name = "obsws-python"
dynamic = ["version"] dynamic = ["version"]
description = "A Python SDK for OBS Studio WebSocket v5.0" description = "A Python SDK for OBS Studio WebSocket v5.0"
readme = "README.md" readme = "README.md"
license = { text = "GPL-3.0-only" } license = "GPL-3.0-only"
requires-python = ">=3.9" requires-python = ">=3.9"
authors = [ authors = [
{ name = "Adem Atikturk", email = "aatikturk@gmail.com" }, { name = "Adem Atikturk", email = "aatikturk@gmail.com" },
@@ -17,6 +17,14 @@ dependencies = [
"websocket-client", "websocket-client",
] ]
[project.optional-dependencies]
dev = [
"black",
"isort",
"pytest",
"pytest-randomly",
]
[project.urls] [project.urls]
Homepage = "https://github.com/aatikturk/obsws-python" Homepage = "https://github.com/aatikturk/obsws-python"
@@ -28,58 +36,19 @@ include = [
"/obsws_python", "/obsws_python",
] ]
[tool.hatch.envs.default]
dependencies = ["pre-commit"]
[tool.hatch.envs.e]
dependencies = ["keyboard"]
[tool.hatch.envs.e.scripts] [tool.hatch.envs.e.scripts]
events = "python {root}\\examples\\events\\." events = "python {root}\\examples\\events\\."
hotkeys = "python {root}\\examples\\hotkeys\\." hotkeys = "python {root}\\examples\\hotkeys\\."
levels = "python {root}\\examples\\levels\\." levels = "python {root}\\examples\\levels\\."
scene_rotate = "python {root}\\examples\\scene_rotate\\." scene_rotate = "python {root}\\examples\\scene_rotate\\."
[tool.hatch.envs.hatch-test] [tool.hatch.envs.test]
randomize = true
[tool.hatch.envs.hatch-test.scripts]
run = "pytest{env:HATCH_TEST_ARGS:} {args}"
[[tool.hatch.envs.hatch-test.matrix]]
python = ["313", "312", "311", "310", "39"]
[tool.hatch.envs.style]
detached = true
dependencies = [ dependencies = [
"black", "pytest",
"isort",
] ]
[tool.hatch.envs.style.scripts] [tool.hatch.envs.test.scripts]
check = [ run = 'pytest -v'
"black --check --diff .",
"isort --check-only --diff .",
]
fmt = [
"isort .",
"black .",
]
[tool.black] [[tool.hatch.envs.test.matrix]]
line-length = 88 python = ["39", "310", "311"]
include = '\.pyi?$'
# 'extend-exclude' excludes files or directories in addition to the defaults
extend-exclude = '''
(
^/\.git/ # exclude all files in the .git directory
^/\.hatch/ # exclude all files in the .hatch directory
^/\.pytest_cache/ # exclude all files in the .pytest_cache directory
| .*_pb2.py # exclude autogenerated Protocol Buffer files anywhere in the project
)
'''
[tool.isort]
profile = "black"
skip = [".gitignore", ".dockerignore"]
skip_glob = [".git/*", ".hatch/*", ".pytest_cache/*"]

View File

@@ -18,12 +18,7 @@ class TestAttrs:
def test_get_current_program_scene_attrs(self): def test_get_current_program_scene_attrs(self):
resp = req_cl.get_current_program_scene() resp = req_cl.get_current_program_scene()
assert resp.attrs() == [ assert resp.attrs() == ["current_program_scene_name"]
"current_program_scene_name",
"current_program_scene_uuid",
"scene_name",
"scene_uuid",
]
def test_get_transition_kind_list_attrs(self): def test_get_transition_kind_list_attrs(self):
resp = req_cl.get_transition_kind_list() resp = req_cl.get_transition_kind_list()

View File

@@ -1,5 +1,4 @@
import pytest import pytest
from obsws_python.callback import Callback from obsws_python.callback import Callback

View File

@@ -13,8 +13,33 @@ class TestRequests:
def test_get_hot_key_list(self): def test_get_hot_key_list(self):
resp = req_cl.get_hot_key_list() resp = req_cl.get_hot_key_list()
assert resp.hotkeys obsbasic_hotkey_list = [
assert any(x.startswith("OBSBasic.") for x in resp.hotkeys) "OBSBasic.SelectScene",
"OBSBasic.SelectScene",
"OBSBasic.SelectScene",
"OBSBasic.SelectScene",
"OBSBasic.StartStreaming",
"OBSBasic.StopStreaming",
"OBSBasic.ForceStopStreaming",
"OBSBasic.StartRecording",
"OBSBasic.StopRecording",
"OBSBasic.PauseRecording",
"OBSBasic.UnpauseRecording",
"OBSBasic.StartReplayBuffer",
"OBSBasic.StopReplayBuffer",
"OBSBasic.StartVirtualCam",
"OBSBasic.StopVirtualCam",
"OBSBasic.EnablePreview",
"OBSBasic.DisablePreview",
"OBSBasic.ShowContextBar",
"OBSBasic.HideContextBar",
"OBSBasic.TogglePreviewProgram",
"OBSBasic.Transition",
"OBSBasic.ResetStats",
"OBSBasic.Screenshot",
"OBSBasic.SelectedSourceScreenshot",
]
assert all(x in resp.hotkeys for x in obsbasic_hotkey_list)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"name,data", "name,data",
@@ -28,7 +53,6 @@ class TestRequests:
resp = req_cl.get_persistent_data("OBS_WEBSOCKET_DATA_REALM_PROFILE", name) resp = req_cl.get_persistent_data("OBS_WEBSOCKET_DATA_REALM_PROFILE", name)
assert resp.slot_value == data assert resp.slot_value == data
@pytest.mark.skip(reason="possible bug in obs-websocket, needs checking")
def test_profile_list(self): def test_profile_list(self):
req_cl.create_profile("test") req_cl.create_profile("test")
resp = req_cl.get_profile_list() resp = req_cl.get_profile_list()
@@ -71,15 +95,11 @@ class TestRequests:
"START_TEST", "test", "color_source_v3", {"color": 4294945535}, True "START_TEST", "test", "color_source_v3", {"color": 4294945535}, True
) )
resp = req_cl.get_input_list() resp = req_cl.get_input_list()
for input_item in resp.inputs: assert {
if input_item["inputName"] == "test": "inputKind": "color_source_v3",
assert input_item["inputKind"] == "color_source_v3" "inputName": "test",
assert input_item["unversionedInputKind"] == "color_source" "unversionedInputKind": "color_source",
break } in resp.inputs
else:
# This else block is executed if the for loop completes without finding the input_item with inputName "test"
raise AssertionError("Input with inputName 'test' not found")
resp = req_cl.get_input_settings("test") resp = req_cl.get_input_settings("test")
assert resp.input_kind == "color_source_v3" assert resp.input_kind == "color_source_v3"
assert resp.input_settings == {"color": 4294945535} assert resp.input_settings == {"color": 4294945535}