28 Commits

Author SHA1 Message Date
0fe78197fc Merge branch 'aatikturk:main' into dev 2025-02-10 12:27:07 +00:00
Adem
9c4c5a1df9 Merge pull request #53 from Zynthasius39/dev-zynt
Fix Trigger Hotkey Methods
2025-02-10 15:02:36 +03:00
f52ac163b8 patch bump version 2025-02-10 09:12:09 +00:00
Zynthasius39
197a60a7cd Fix trigger_hot_key_by_key_sequence() method 2025-02-08 18:31:12 +04:00
Zynthasius39
633093ead4 Fix trigger_hot_key_by_name() method 2025-02-08 17:59:45 +04:00
935392a0b6 Merge branch 'aatikturk:main' into dev 2025-01-25 22:34:36 +00:00
d2f2926334 Merge pull request #51 from marzeq/patch-1
Fix project.license field in pyproject.toml so that setup.py doesn't fail
2025-01-25 21:48:55 +00:00
marzeq
58cd50dd6c Fix project.license field in pyproject.toml so that setup.py doesn't fail 2025-01-25 21:34:27 +01:00
7614cdfe4a add py12 to test matrix 2024-02-21 14:15:40 +00:00
Adem
9402f2e472 Merge pull request #43 from onyx-and-iris/fix-disconnect
Add disconnect() methods. Default ws timeout to None for event thread.
2024-01-21 15:45:06 +03:00
ef8df5cf4d bump to 1.7.0 2024-01-21 12:34:10 +00:00
1abca0c7e4 bump to 1.7.0b0 2024-01-09 15:37:33 +00:00
85180c1d94 upd variable name 2024-01-09 12:17:47 +00:00
f4db1ad95c fix prompt 2024-01-07 14:37:15 +00:00
efaee7594e should a socket operation be attempted after socket closed
then catch and log OSError and close thread.
2024-01-07 12:35:20 +00:00
2cebd5eedb upd examples, they now use context managers 2024-01-07 11:21:01 +00:00
cac236c004 removes timeout for socket before starting worker thread 2024-01-07 11:19:33 +00:00
6aa6db09eb adds an event object and listens until its set
sets the event object on WebSocketConnectionClosedException

adds __enter__(), __exit__() methods

adds disconnect() to event client. aliases it as unsubscribe

checks for non-empty response with:
`if r := self.base_client.ws.recv()`
before attempting to json.load() it.
2024-01-05 09:57:08 +00:00
f1c2efa4a1 adds disconnect() method to ReqClient
now calling disconnect() in __exit__()
2024-01-05 09:36:02 +00:00
Adem
4654d2529f Merge pull request #39 from onyx-and-iris/dev
patch bump for PR #37
2023-10-23 14:58:56 +03:00
1494208f63 patch bump for issue #37 2023-10-23 12:43:59 +01:00
Adem
d217630289 Merge pull request #37 from aatikturk/implement_v5.3_methods
Update reqs.py

implemented  set_record_directory method. (only availabe for obs websocket v5.3 or higher)
2023-10-23 14:00:08 +03:00
Adem
5bfe792fa6 Update reqs.py
added set_record_directory  method to ReqClient.
2023-10-23 09:29:16 +03:00
3c36619173 Merge pull request #36 from onyx-and-iris/add-projector-methods
Add projector methods
2023-10-10 17:38:53 +01:00
c4cf817042 split at full stop 2023-10-09 22:34:05 +01:00
ba5da8dfef upd obsbasic hotkey list in tests 2023-10-09 22:29:18 +01:00
83577e2d61 adds projector methods with a deprecation warning
patch bump

closes #35
2023-10-09 22:06:18 +01:00
Adem
8aa2e78ba6 Merge pull request #32 from onyx-and-iris/add-request-error-class
Error handling with base error class
2023-08-14 14:38:43 +03:00
8 changed files with 169 additions and 66 deletions

View File

@@ -17,6 +17,12 @@ 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}")
@@ -31,13 +37,11 @@ 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(f"OBS closing!") print("OBS closing!")
self._client.unsubscribe()
self.running = False self.running = False
if __name__ == "__main__": if __name__ == "__main__":
observer = Observer() with Observer() as observer:
while observer.running:
while observer.running: time.sleep(0.1)
time.sleep(0.1)

View File

@@ -1,6 +1,7 @@
import inspect import inspect
import keyboard import keyboard
import obsws_python as obs import obsws_python as obs
@@ -10,6 +11,12 @@ 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
@@ -31,13 +38,12 @@ def set_scene(scene, *args):
if __name__ == "__main__": if __name__ == "__main__":
req_client = obs.ReqClient() with obs.ReqClient() as req_client:
observer = Observer() with Observer() as 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",))
keyboard.add_hotkey("0", version) print("press ctrl+enter to quit")
keyboard.add_hotkey("1", set_scene, args=("START",)) keyboard.wait("ctrl+enter")
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,6 +9,8 @@ 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."""
@@ -32,15 +34,14 @@ def on_input_volume_meters(data):
def main(): def main():
client = obs.EventClient(subs=(obs.Subs.LOW_VOLUME | obs.Subs.INPUTVOLUMEMETERS)) with obs.EventClient(
client.callback.register([on_input_volume_meters, on_input_mute_state_changed]) subs=(obs.Subs.LOW_VOLUME | obs.Subs.INPUTVOLUMEMETERS)
) as client:
client.callback.register([on_input_volume_meters, on_input_mute_state_changed])
while cmd := input("<Enter> to exit>\n"): while _ := input("Press <Enter> to exit\n"):
if not cmd: pass
break
if __name__ == "__main__": if __name__ == "__main__":
DEVICE = "Desktop Audio"
main() main()

View File

@@ -1,9 +1,8 @@
import json import json
import logging import logging
import time import threading
from threading import Thread
from websocket import WebSocketTimeoutException from websocket import WebSocketConnectionClosedException, WebSocketTimeoutException
from .baseclient import ObsClient from .baseclient import ObsClient
from .callback import Callback from .callback import Callback
@@ -20,8 +19,6 @@ 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}
@@ -38,6 +35,12 @@ 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
@@ -49,33 +52,40 @@ class EventClient:
return type(self).__name__ return type(self).__name__
def subscribe(self): def subscribe(self):
worker = Thread(target=self.trigger, daemon=True) self.base_client.ws.settimeout(None)
worker.start() stop_event = threading.Event()
self.worker = threading.Thread(
target=self.trigger, daemon=True, args=(stop_event,)
)
self.worker.start()
def trigger(self): def trigger(self, stop_event):
""" """
Continuously listen for events. Continuously listen for events.
Triggers a callback on event received. Triggers a callback on event received.
""" """
self.running = True while not stop_event.is_set():
while self.running:
try: try:
event = json.loads(self.base_client.ws.recv()) if response := 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
self.logger.debug(f"Event received {event}") except (WebSocketConnectionClosedException, OSError) as e:
type_, data = ( self.logger.debug(f"{type(e).__name__} terminating the event thread")
event["d"].get("eventType"), stop_event.set()
event["d"].get("eventData"),
) def disconnect(self):
self.callback.trigger(type_, data if data else {}) """stop listening for events"""
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

View File

@@ -1,4 +1,5 @@
import logging import logging
from warnings import warn
from .baseclient import ObsClient from .baseclient import ObsClient
from .error import OBSSDKError, OBSSDKRequestError from .error import OBSSDKError, OBSSDKRequestError
@@ -30,7 +31,7 @@ class ReqClient:
return self return self
def __exit__(self, exc_type, exc_value, exc_traceback): def __exit__(self, exc_type, exc_value, exc_traceback):
self.base_client.ws.close() self.disconnect()
def __repr__(self): def __repr__(self):
return type( return type(
@@ -41,6 +42,9 @@ class ReqClient:
def __str__(self): def __str__(self):
return type(self).__name__ return type(self).__name__
def disconnect(self):
self.base_client.ws.close()
def send(self, param, data=None, raw=False): def send(self, param, data=None, raw=False):
try: try:
@@ -134,39 +138,39 @@ class ReqClient:
get_hotkey_list = get_hot_key_list get_hotkey_list = get_hot_key_list
def trigger_hot_key_by_name(self, hotkeyName): def trigger_hot_key_by_name(self, hotkeyName, contextName=None):
""" """
Triggers a hotkey using its name. For hotkey names Triggers a hotkey using its name. For hotkey names
See GetHotkeyList See GetHotkeyList
:param hotkeyName: Name of the hotkey to trigger :param hotkeyName: Name of the hotkey to trigger
:type hotkeyName: str :type hotkeyName: str
:param contextName: Name of context of the hotkey to trigger
:type contextName: str, optional
""" """
payload = {"hotkeyName": hotkeyName} payload = {"hotkeyName": hotkeyName, "contextName": contextName}
self.send("TriggerHotkeyByName", payload) self.send("TriggerHotkeyByName", payload)
trigger_hotkey_by_name = trigger_hot_key_by_name trigger_hotkey_by_name = trigger_hot_key_by_name
def trigger_hot_key_by_key_sequence( def trigger_hot_key_by_key_sequence(
self, keyId, pressShift, pressCtrl, pressAlt, pressCmd self, keyId, pressShift=None, pressCtrl=None, pressAlt=None, pressCmd=None
): ):
""" """
Triggers a hotkey using a sequence of keys. Triggers a hotkey using a sequence of keys.
:param keyId: The OBS key ID to use. See https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h :param keyId: The OBS key ID to use. See https://github.com/obsproject/obs-studio/blob/master/libobs/obs-hotkeys.h
:type keyId: str :type keyId: str
:param keyModifiers: Object containing key modifiers to apply. :param pressShift: Press Shift
:type keyModifiers: dict :type pressShift: bool, optional
:param keyModifiers.shift: Press Shift :param pressCtrl: Press CTRL
:type keyModifiers.shift: bool :type pressCtrl: bool, optional
:param keyModifiers.control: Press CTRL :param pressAlt: Press ALT
:type keyModifiers.control: bool :type pressAlt: bool, optional
:param keyModifiers.alt: Press ALT :param pressCmd: Press CMD (Mac)
:type keyModifiers.alt: bool :type pressCmd: bool, optional
:param keyModifiers.cmd: Press CMD (Mac)
:type keyModifiers.cmd: bool
""" """
@@ -432,6 +436,19 @@ class ReqClient:
""" """
return self.send("GetRecordDirectory") return self.send("GetRecordDirectory")
def set_record_directory(self, recordDirectory):
"""
Sets the current directory that the record output writes files to.
IMPORTANT NOTE: Requires obs websocket v5.3 or higher.
:param recordDirectory: Output directory
:type recordDirectory: str
"""
payload = {
"recordDirectory": recordDirectory,
}
return self.send("SetRecordDirectory", payload)
def get_source_active(self, name): def get_source_active(self, name):
""" """
Gets the active and show state of a source Gets the active and show state of a source
@@ -1938,3 +1955,66 @@ class ReqClient:
""" """
return self.send("GetMonitorList") return self.send("GetMonitorList")
def open_video_mix_projector(
self, video_mix_type, monitor_index=-1, projector_geometry=None
):
"""
Opens a projector for a specific output video mix.
The available mix types are:
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PROGRAM
OBS_WEBSOCKET_VIDEO_MIX_TYPE_MULTIVIEW
:param video_mix_type: Type of mix to open.
:type video_mix_type: str
:param monitor_index: Monitor index, use GetMonitorList to obtain index
:type monitor_index: int
:param projector_geometry:
Size/Position data for a windowed projector, in Qt Base64 encoded format.
Mutually exclusive with monitorIndex
:type projector_geometry: str
"""
warn(
"open_video_mix_projector request serves to provide feature parity with 4.x. "
"It is very likely to be changed/deprecated in a future release.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"videoMixType": video_mix_type,
"monitorIndex": monitor_index,
"projectorGeometry": projector_geometry,
}
self.send("OpenVideoMixProjector", payload)
def open_source_projector(
self, source_name, monitor_index=-1, projector_geometry=None
):
"""
Opens a projector for a source.
:param source_name: Name of the source to open a projector for
:type source_name: str
:param monitor_index: Monitor index, use GetMonitorList to obtain index
:type monitor_index: int
:param projector_geometry:
Size/Position data for a windowed projector, in Qt Base64 encoded format.
Mutually exclusive with monitorIndex
:type projector_geometry: str
"""
warn(
"open_source_projector request serves to provide feature parity with 4.x. "
"It is very likely to be changed/deprecated in a future release.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"sourceName": source_name,
"monitorIndex": monitor_index,
"projectorGeometry": projector_geometry,
}
self.send("OpenSourceProjector", payload)

View File

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

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 = "GPL-3.0-only" license = { text = "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" },
@@ -51,4 +51,4 @@ dependencies = [
run = 'pytest -v' run = 'pytest -v'
[[tool.hatch.envs.test.matrix]] [[tool.hatch.envs.test.matrix]]
python = ["39", "310", "311"] python = ["39", "310", "311", "312"]

View File

@@ -15,9 +15,9 @@ class TestRequests:
resp = req_cl.get_hot_key_list() resp = req_cl.get_hot_key_list()
obsbasic_hotkey_list = [ obsbasic_hotkey_list = [
"OBSBasic.SelectScene", "OBSBasic.SelectScene",
"OBSBasic.SelectScene", "OBSBasic.QuickTransition.1",
"OBSBasic.SelectScene", "OBSBasic.QuickTransition.2",
"OBSBasic.SelectScene", "OBSBasic.QuickTransition.3",
"OBSBasic.StartStreaming", "OBSBasic.StartStreaming",
"OBSBasic.StopStreaming", "OBSBasic.StopStreaming",
"OBSBasic.ForceStopStreaming", "OBSBasic.ForceStopStreaming",
@@ -25,15 +25,17 @@ class TestRequests:
"OBSBasic.StopRecording", "OBSBasic.StopRecording",
"OBSBasic.PauseRecording", "OBSBasic.PauseRecording",
"OBSBasic.UnpauseRecording", "OBSBasic.UnpauseRecording",
"OBSBasic.SplitFile",
"OBSBasic.StartReplayBuffer", "OBSBasic.StartReplayBuffer",
"OBSBasic.StopReplayBuffer", "OBSBasic.StopReplayBuffer",
"OBSBasic.StartVirtualCam", "OBSBasic.StartVirtualCam",
"OBSBasic.StopVirtualCam", "OBSBasic.StopVirtualCam",
"OBSBasic.EnablePreview", "OBSBasic.EnablePreview",
"OBSBasic.DisablePreview", "OBSBasic.DisablePreview",
"OBSBasic.EnablePreviewProgram",
"OBSBasic.DisablePreviewProgram",
"OBSBasic.ShowContextBar", "OBSBasic.ShowContextBar",
"OBSBasic.HideContextBar", "OBSBasic.HideContextBar",
"OBSBasic.TogglePreviewProgram",
"OBSBasic.Transition", "OBSBasic.Transition",
"OBSBasic.ResetStats", "OBSBasic.ResetStats",
"OBSBasic.Screenshot", "OBSBasic.Screenshot",