From b5b69de21838844bee3cbd2239ed9a73a8bd358f Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Mon, 25 Jul 2022 23:51:30 +0100 Subject: [PATCH 01/27] 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. --- .gitignore | 6 +- README.md | 33 +- build/lib/obsstudio_sdk/__init__.py | 4 + build/lib/obsstudio_sdk/baseclient.py | 71 + build/lib/obsstudio_sdk/events.py | 43 + build/lib/obsstudio_sdk/reqs.py | 1928 +++++++++++++++++++++++++ build/lib/obsstudio_sdk/subject.py | 58 + examples/events/__main__.py | 26 + examples/scene_rotate/__main__.py | 19 + obsstudio_sdk/__init__.py | 3 + obsstudio_sdk/baseclient.py | 78 +- obsstudio_sdk/callback.py | 58 + obsstudio_sdk/events.py | 43 + obsstudio_sdk/reqs.py | 830 +++++------ 14 files changed, 2719 insertions(+), 481 deletions(-) create mode 100644 build/lib/obsstudio_sdk/__init__.py create mode 100644 build/lib/obsstudio_sdk/baseclient.py create mode 100644 build/lib/obsstudio_sdk/events.py create mode 100644 build/lib/obsstudio_sdk/reqs.py create mode 100644 build/lib/obsstudio_sdk/subject.py create mode 100644 examples/events/__main__.py create mode 100644 examples/scene_rotate/__main__.py create mode 100644 obsstudio_sdk/callback.py create mode 100644 obsstudio_sdk/events.py diff --git a/.gitignore b/.gitignore index bf1ec86..7e6985d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ __pycache__ obsstudio_sdk.egg-info dist docs -setup.py \ No newline at end of file +setup.py + +venv +quick.py +config.toml \ No newline at end of file diff --git a/README.md b/README.md index 92e1fc3..ff0d837 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # obs_sdk + ### 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. [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 ``` - ### How to Use -* Import and start using - Required parameters are as follows: - host: obs websocket server - port: port to access server - password: obs websocket server password +- Load connection info from toml config. A valid `config.toml` might look like this: + +```toml +[connection] +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 @@ -26,12 +39,12 @@ pip install obsstudio-sdk >>>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') >>> -``` \ No newline at end of file +``` diff --git a/build/lib/obsstudio_sdk/__init__.py b/build/lib/obsstudio_sdk/__init__.py new file mode 100644 index 0000000..7c892fc --- /dev/null +++ b/build/lib/obsstudio_sdk/__init__.py @@ -0,0 +1,4 @@ +from .events import EventsClient +from .reqs import ReqClient + +__ALL__ = ["ReqClient", "EventsClient"] diff --git a/build/lib/obsstudio_sdk/baseclient.py b/build/lib/obsstudio_sdk/baseclient.py new file mode 100644 index 0000000..cc403f8 --- /dev/null +++ b/build/lib/obsstudio_sdk/baseclient.py @@ -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()) diff --git a/build/lib/obsstudio_sdk/events.py b/build/lib/obsstudio_sdk/events.py new file mode 100644 index 0000000..3e007b0 --- /dev/null +++ b/build/lib/obsstudio_sdk/events.py @@ -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 diff --git a/build/lib/obsstudio_sdk/reqs.py b/build/lib/obsstudio_sdk/reqs.py new file mode 100644 index 0000000..924682e --- /dev/null +++ b/build/lib/obsstudio_sdk/reqs.py @@ -0,0 +1,1928 @@ +from .baseclient import ObsClient + +""" +A class to interact with obs-websocket requests +defined in official github repo +https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests +""" + + +class ReqClient(object): + def __init__(self, **kwargs): + self.base_client = ObsClient(**kwargs) + self.base_client.authenticate() + + def GetVersion(self): + """ + Gets data about the current plugin and RPC version. + + :return: The version info as a dictionary + :rtype: dict + + + """ + response = self.base_client.req("GetVersion") + return response + + def GetStats(self): + """ + Gets statistics about OBS, obs-websocket, and the current session. + + :return: The stats info as a dictionary + :rtype: dict + + + """ + response = self.base_client.req("GetStats") + return response + + def BroadcastCustomEvent(self, eventData): + """ + Broadcasts a CustomEvent to all WebSocket clients. Receivers are clients which are identified and subscribed. + + :param eventData: Data payload to emit to all receivers + :type eventData: object + :return: empty response + :rtype: str + + + """ + req_data = eventData + response = self.base_client.req("BroadcastCustomEvent", req_data) + return response + + def CallVendorRequest(self, vendorName, requestType, requestData=None): + """ + Call a request registered to a vendor. + + A vendor is a unique name registered by a + third-party plugin or script, which allows + for custom requests and events to be added + to obs-websocket. If a plugin or script + implements vendor requests or events, + documentation is expected to be provided with them. + + :param vendorName: Name of the vendor to use + :type vendorName: str + :param requestType: The request type to call + :type requestType: str + :param requestData: Object containing appropriate request data + :type requestData: dict, optional + :return: responseData + :rtype: dict + + + """ + response = self.base_client.req(req_type=requestType, req_data=requestData) + return response + + def GetHotkeyList(self): + """ + Gets an array of all hotkey names in OBS + + :return: hotkeys + :rtype: list[str] + + + """ + response = self.base_client.req("GetHotkeyList") + return response + + def TriggerHotkeyByName(self, hotkeyName): + """ + Triggers a hotkey using its name. For hotkey names + See GetHotkeyList + + :param hotkeyName: Name of the hotkey to trigger + :type hotkeyName: str + + + """ + payload = {"hotkeyName": hotkeyName} + response = self.base_client.req("TriggerHotkeyByName", payload) + return response + + def TriggerHotkeyByKeySequence( + self, keyId, pressShift, pressCtrl, pressAlt, pressCmd + ): + """ + 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 + :type keyId: str + :param keyModifiers: Object containing key modifiers to apply. + :type keyModifiers: dict + :param keyModifiers.shift: Press Shift + :type keyModifiers.shift: bool + :param keyModifiers.control: Press CTRL + :type keyModifiers.control: bool + :param keyModifiers.alt: Press ALT + :type keyModifiers.alt: bool + :param keyModifiers.cmd: Press CMD (Mac) + :type keyModifiers.cmd: bool + + + """ + payload = { + "keyId": keyId, + "keyModifiers": { + "shift": pressShift, + "control": pressCtrl, + "alt": pressAlt, + "cmd": pressCmd, + }, + } + + response = self.base_client.req("TriggerHotkeyByKeySequence", payload) + return response + + def Sleep(self, sleepMillis=None, sleepFrames=None): + """ + Sleeps for a time duration or number of frames. + Only available in request batches with types SERIAL_REALTIME or SERIAL_FRAME + + :param sleepMillis: Number of milliseconds to sleep for (if SERIAL_REALTIME mode) 0 <= sleepMillis <= 50000 + :type sleepMillis: int + :param sleepFrames: Number of frames to sleep for (if SERIAL_FRAME mode) 0 <= sleepFrames <= 10000 + :type sleepFrames: int + + + """ + payload = {"sleepMillis": sleepMillis, "sleepFrames": sleepFrames} + response = self.base_client.req("Sleep", payload) + return response + + def GetPersistentData(self, realm, slotName): + """ + Gets the value of a "slot" from the selected persistent data realm. + + :param realm: The data realm to select + OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE + :type realm: str + :param slotName: The name of the slot to retrieve data from + :type slotName: str + :return: slotValue Value associated with the slot + :rtype: any + + + """ + payload = {"realm": realm, "slotName": slotName} + response = self.base_client.req("GetPersistentData", payload) + return response + + def SetPersistentData(self, realm, slotName, slotValue): + """ + Sets the value of a "slot" from the selected persistent data realm. + + :param realm: The data realm to select. + OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE + :type realm: str + :param slotName: The name of the slot to retrieve data from + :type slotName: str + :param slotValue: The value to apply to the slot + :type slotValue: any + + + """ + payload = {"realm": realm, "slotName": slotName, "slotValue": slotValue} + response = self.base_client.req("SetPersistentData", payload) + return response + + def GetSceneCollectionList(self): + """ + Gets an array of all scene collections + + :return: sceneCollections + :rtype: list[str] + + + """ + response = self.base_client.req("GetSceneCollectionList") + return response + + def SetCurrentSceneCollection(self, name): + """ + Creates a new scene collection, switching to it in the process + Note: This will block until the collection has finished changing + + :param name: Name of the scene collection to switch to + :type name: str + + + """ + payload = {"sceneCollectionName": name} + response = self.base_client.req("SetCurrentSceneCollection", payload) + return response + + def CreateSceneCollection(self, name): + """ + Creates a new scene collection, switching to it in the process. + Note: This will block until the collection has finished changing. + + :param name: Name for the new scene collection + :type name: str + + + """ + payload = {"sceneCollectionName": name} + response = self.base_client.req("CreateSceneCollection", payload) + return response + + def GetProfileList(self): + """ + Gets a list of all profiles + + :return: profiles (List of all profiles) + :rtype: list[str] + + + """ + response = self.base_client.req("GetProfileList") + return response + + def SetCurrentProfile(self, name): + """ + Switches to a profile + + :param name: Name of the profile to switch to + :type name: str + + + """ + payload = {"profileName": name} + response = self.base_client.req("SetCurrentProfile", payload) + return response + + def CreateProfile(self, name): + """ + Creates a new profile, switching to it in the process + + :param name: Name for the new profile + :type name: str + + + """ + payload = {"profileName": name} + response = self.base_client.req("CreateProfile", payload) + return response + + def RemoveProfile(self, name): + """ + Removes a profile. If the current profile is chosen, + it will change to a different profile first. + + :param name: Name of the profile to remove + :type name: str + + + """ + payload = {"profileName": name} + response = self.base_client.req("RemoveProfile", payload) + return response + + def GetProfileParameter(self, category, name): + """ + Gets a parameter from the current profile's configuration.. + + :param category: Category of the parameter to get + :type category: str + :param name: Name of the parameter to get + :type name: str + + :return: Value and default value for the parameter + :rtype: str + + + """ + payload = {"parameterCategory": category, "parameterName": name} + response = self.base_client.req("GetProfileParameter", payload) + return response + + def SetProfileParameter(self, category, name, value): + """ + Gets a parameter from the current profile's configuration.. + + :param category: Category of the parameter to set + :type category: str + :param name: Name of the parameter to set + :type name: str + :param value: Value of the parameter to set. Use null to delete + :type value: str + + :return: Value and default value for the parameter + :rtype: str + + + """ + payload = { + "parameterCategory": category, + "parameterName": name, + "parameterValue": value, + } + response = self.base_client.req("SetProfileParameter", payload) + return response + + def GetVideoSettings(self): + """ + Gets the current video settings. + Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. + Example: 60000/1001 + + + """ + response = self.base_client.req("GetVideoSettings") + return response + + def SetVideoSettings( + self, numerator, denominator, base_width, base_height, out_width, out_height + ): + """ + Sets the current video settings. + Note: Fields must be specified in pairs. + For example, you cannot set only baseWidth without needing to specify baseHeight. + + :param numerator: Numerator of the fractional FPS value >=1 + :type numerator: int + :param denominator: Denominator of the fractional FPS value >=1 + :type denominator: int + :param base_width: Width of the base (canvas) resolution in pixels (>= 1, <= 4096) + :type base_width: int + :param base_height: Height of the base (canvas) resolution in pixels (>= 1, <= 4096) + :type base_height: int + :param out_width: Width of the output resolution in pixels (>= 1, <= 4096) + :type out_width: int + :param out_height: Height of the output resolution in pixels (>= 1, <= 4096) + :type out_height: int + + + """ + payload = { + "fpsNumerator": numerator, + "fpsDenominator": denominator, + "baseWidth": base_width, + "baseHeight": base_height, + "outputWidth": out_width, + "outputHeight": out_height, + } + response = self.base_client.req("SetVideoSettings", payload) + return response + + def GetStreamServiceSettings(self): + """ + Gets the current stream service settings (stream destination). + + + """ + response = self.base_client.req("GetStreamServiceSettings") + return response + + def SetStreamServiceSettings(self, ss_type, ss_settings): + """ + Sets the current stream service settings (stream destination). + Note: Simple RTMP settings can be set with type rtmp_custom + and the settings fields server and key. + + :param ss_type: Type of stream service to apply. Example: rtmp_common or rtmp_custom + :type ss_type: string + :param ss_setting: Settings to apply to the service + :type ss_setting: dict + + + """ + payload = { + "streamServiceType": ss_type, + "streamServiceSettings": ss_settings, + } + response = self.base_client.req("SetStreamServiceSettings", payload) + return response + + def GetSourceActive(self, name): + """ + Gets the active and show state of a source + + :param name: Name of the source to get the active state of + :type name: str + + + """ + payload = {"sourceName": name} + response = self.base_client.req("GetSourceActive", payload) + return response + + def GetSourceScreenshot(self, name, img_format, width, height, quality): + """ + Gets a Base64-encoded screenshot of a source. + The imageWidth and imageHeight parameters are + treated as "scale to inner", meaning the smallest ratio + will be used and the aspect ratio of the original resolution is kept. + If imageWidth and imageHeight are not specified, the compressed image + will use the full resolution of the source. + + :param name: Name of the source to take a screenshot of + :type name: str + :param format: Image compression format to use. Use GetVersion to get compatible image formats + :type format: str + :param width: Width to scale the screenshot to (>= 8, <= 4096) + :type width: int + :param height: Height to scale the screenshot to (>= 8, <= 4096) + :type height: int + :param quality: Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" + :type quality: int + + + """ + payload = { + "sourceName": name, + "imageFormat": img_format, + "imageWidth": width, + "imageHeight": height, + "imageCompressionQuality": quality, + } + response = self.base_client.req("GetSourceScreenshot", payload) + return response + + def SaveSourceScreenshot(self, name, img_format, file_path, width, height, quality): + """ + Saves a Base64-encoded screenshot of a source. + The imageWidth and imageHeight parameters are + treated as "scale to inner", meaning the smallest ratio + will be used and the aspect ratio of the original resolution is kept. + If imageWidth and imageHeight are not specified, the compressed image + will use the full resolution of the source. + + :param name: Name of the source to take a screenshot of + :type name: str + :param format: Image compression format to use. Use GetVersion to get compatible image formats + :type format: str + :param file_path: Path to save the screenshot file to. Eg. C:\\Users\\user\\Desktop\\screenshot.png + :type file_path: str + :param width: Width to scale the screenshot to (>= 8, <= 4096) + :type width: int + :param height: Height to scale the screenshot to (>= 8, <= 4096) + :type height: int + :param quality: Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" + :type quality: int + + + """ + payload = { + "sourceName": name, + "imageFormat": img_format, + "imageFilePath": file_path, + "imageWidth": width, + "imageHeight": height, + "imageCompressionQuality": quality, + } + response = self.base_client.req("SaveSourceScreenshot", payload) + return response + + def GetSceneList(self): + """ + Gets a list of all scenes in OBS. + + + """ + response = self.base_client.req("GetSceneList") + return response + + def GetGroupList(self): + """ + Gets a list of all groups in OBS. + Groups in OBS are actually scenes, + but renamed and modified. In obs-websocket, + we treat them as scenes where we can.. + + + """ + response = self.base_client.req("GetSceneList") + return response + + def GetCurrentProgramScene(self): + """ + Gets the current program scene. + + + """ + response = self.base_client.req("GetCurrentProgramScene") + return response + + def SetCurrentProgramScene(self, name): + """ + Sets the current program scene + + :param name: Scene to set as the current program scene + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("SetCurrentProgramScene", payload) + return response + + def GetCurrentPreviewScene(self): + """ + Gets the current preview scene + + + """ + response = self.base_client.req("GetCurrentPreviewScene") + return response + + def SetCurrentPreviewScene(self, name): + """ + Sets the current program scene + + :param name: Scene to set as the current preview scene + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("SetCurrentPreviewScene", payload) + return response + + def CreateScene(self, name): + """ + Creates a new scene in OBS. + + :param name: Name for the new scene + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("CreateScene", payload) + return response + + def RemoveScene(self, name): + """ + Removes a scene from OBS + + :param name: Name of the scene to remove + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("RemoveScene", payload) + return response + + def SetSceneName(self, old_name, new_name): + """ + Sets the name of a scene (rename). + + :param old_name: Name of the scene to be renamed + :type old_name: str + :param new_name: New name for the scene + :type new_name: str + + + """ + payload = {"sceneName": old_name, "newSceneName": new_name} + response = self.base_client.req("SetSceneName", payload) + return response + + def GetSceneSceneTransitionOverride(self, name): + """ + Gets the scene transition overridden for a scene. + + :param name: Name of the scene + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("GetSceneSceneTransitionOverride", payload) + return response + + def SetSceneSceneTransitionOverride(self, scene_name, tr_name, tr_duration): + """ + Gets the scene transition overridden for a scene. + + :param scene_name: Name of the scene + :type scene_name: str + :param tr_name: Name of the scene transition to use as override. Specify null to remove + :type tr_name: str + :param tr_duration: Duration to use for any overridden transition. Specify null to remove (>= 50, <= 20000) + :type tr_duration: int + + + """ + payload = { + "sceneName": scene_name, + "transitionName": tr_name, + "transitionDuration": tr_duration, + } + response = self.base_client.req("SetSceneSceneTransitionOverride", payload) + return response + + def GetInputList(self, kind): + """ + Gets a list of all inputs in OBS. + + :param kind: Restrict the list to only inputs of the specified kind + :type kind: str + + + """ + payload = {"inputKind": kind} + response = self.base_client.req("GetInputList", payload) + return response + + def GetInputKindList(self, unversioned): + """ + Gets a list of all available input kinds in OBS. + + :param unversioned: True == Return all kinds as unversioned, False == Return with version suffixes (if available) + :type unversioned: bool + + + """ + payload = {"unversioned": unversioned} + response = self.base_client.req("GetInputKindList", payload) + return response + + def GetSpecialInputs(self): + """ + Gets the name of all special inputs. + + + """ + response = self.base_client.req("GetSpecialInputs") + return response + + def CreateInput( + self, sceneName, inputName, inputKind, inputSettings, sceneItemEnabled + ): + """ + Creates a new input, adding it as a scene item to the specified scene. + + :param sceneName: Name of the scene to add the input to as a scene item + :type sceneName: str + :param inputName Name of the new input to created + :type inputName: str + :param inputKind: The kind of input to be created + :type inputKind: str + :param inputSettings: Settings object to initialize the input with + :type inputSettings: object + :param sceneItemEnabled: Whether to set the created scene item to enabled or disabled + :type sceneItemEnabled: bool + + + """ + payload = { + "sceneName": sceneName, + "inputName": inputName, + "inputKind": inputKind, + "inputSettings": inputSettings, + "sceneItemEnabled": sceneItemEnabled, + } + response = self.base_client.req("CreateInput", payload) + return response + + def RemoveInput(self, name): + """ + Removes an existing input + + :param name: Name of the input to remove + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("RemoveInput", payload) + return response + + def SetInputName(self, old_name, new_name): + """ + Sets the name of an input (rename). + + :param old_name: Current input name + :type old_name: str + :param new_name: New name for the input + :type new_name: str + + + """ + payload = {"inputName": old_name, "newInputName": new_name} + response = self.base_client.req("SetInputName", payload) + return response + + def GetInputDefaultSettings(self, kind): + """ + Gets the default settings for an input kind. + + :param kind: Input kind to get the default settings for + :type kind: str + + + """ + payload = {"inputKind": kind} + response = self.base_client.req("GetInputDefaultSettings", payload) + return response + + def GetInputSettings(self, name): + """ + Gets the settings of an input. + Note: Does not include defaults. To create the entire settings object, + overlay inputSettings over the defaultInputSettings provided by GetInputDefaultSettings. + + :param name: Input kind to get the default settings for + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputSettings", payload) + return response + + def SetInputSettings(self, name, settings, overlay): + """ + Sets the settings of an input. + + :param name: Name of the input to set the settings of + :type name: str + :param settings: Object of settings to apply + :type settings: dict + :param overlay: True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. + :type overlay: bool + + + """ + payload = {"inputName": name, "inputSettings": settings, "overlay": overlay} + response = self.base_client.req("SetInputSettings", payload) + return response + + def GetInputMute(self, name): + """ + Gets the audio mute state of an input + + :param name: Name of input to get the mute state of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputMute", payload) + return response + + def SetInputMute(self, name, muted): + """ + Sets the audio mute state of an input. + + :param name: Name of the input to set the mute state of + :type name: str + :param muted: Whether to mute the input or not + :type muted: bool + + + """ + payload = {"inputName": name, "inputMuted": muted} + response = self.base_client.req("SetInputMute", payload) + return response + + def ToggleInputMute(self, name): + """ + Toggles the audio mute state of an input. + + :param name: Name of the input to toggle the mute state of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("ToggleInputMute", payload) + return response + + def GetInputVolume(self, name): + """ + Gets the current volume setting of an input. + + :param name: Name of the input to get the volume of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputVolume", payload) + return response + + def SetInputVolume(self, name, vol_mul=None, vol_db=None): + """ + Sets the volume setting of an input. + + :param name: Name of the input to set the volume of + :type name: str + :param vol_mul: Volume setting in mul (>= 0, <= 20) + :type vol_mul: int + :param vol_db: Volume setting in dB (>= -100, <= 26) + :type vol_db: int + + + """ + payload = { + "inputName": name, + "inputVolumeMul": vol_mul, + "inputVolumeDb": vol_db, + } + response = self.base_client.req("SetInputVolume", payload) + return response + + def GetInputAudioBalance(self, name): + """ + Gets the audio balance of an input. + + :param name: Name of the input to get the audio balance of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioBalance", payload) + return response + + def SetInputAudioBalance(self, name, balance): + """ + Sets the audio balance of an input. + + :param name: Name of the input to get the audio balance of + :type name: str + :param balance: New audio balance value (>= 0.0, <= 1.0) + :type balance: int + + + """ + payload = {"inputName": name, "inputAudioBalance": balance} + response = self.base_client.req("SetInputAudioBalance", payload) + return response + + def GetInputAudioOffset(self, name): + """ + Gets the audio sync offset of an input. + + :param name: Name of the input to get the audio sync offset of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioOffset", payload) + return response + + def SetInputAudioSyncOffset(self, name, offset): + """ + Sets the audio sync offset of an input. + + :param name: Name of the input to set the audio sync offset of + :type name: str + :param offset: New audio sync offset in milliseconds (>= -950, <= 20000) + :type offset: int + + + """ + payload = {"inputName": name, "inputAudioSyncOffset": offset} + response = self.base_client.req("SetInputAudioSyncOffset", payload) + return response + + def GetInputAudioMonitorType(self, name): + """ + Gets the audio monitor type of an input. + + The available audio monitor types are: + OBS_MONITORING_TYPE_NONE + OBS_MONITORING_TYPE_MONITOR_ONLY + OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT + + + :param name: Name of the input to get the audio monitor type of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioMonitorType", payload) + return response + + def SetInputAudioMonitorType(self, name, mon_type): + """ + Sets the audio monitor type of an input. + + :param name: Name of the input to set the audio monitor type of + :type name: str + :param mon_type: Audio monitor type + :type mon_type: int + + + """ + payload = {"inputName": name, "monitorType": mon_type} + response = self.base_client.req("SetInputAudioMonitorType", payload) + return response + + def GetInputAudioTracks(self, name): + """ + Gets the enable state of all audio tracks of an input. + + :param name: Name of the input + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioTracks", payload) + return response + + def SetInputAudioTracks(self, name, track): + """ + Sets the audio monitor type of an input. + + :param name: Name of the input + :type name: str + :param track: Track settings to apply + :type track: int + + + """ + payload = {"inputName": name, "inputAudioTracks": track} + response = self.base_client.req("SetInputAudioTracks", payload) + return response + + def GetInputPropertiesListPropertyItems(self, input_name, prop_name): + """ + Gets the items of a list property from an input's properties. + Note: Use this in cases where an input provides a dynamic, + selectable list of items. For example, display capture, + where it provides a list of available displays. + + :param input_name: Name of the input + :type input_name: str + :param prop_name: Name of the list property to get the items of + :type prop_name: str + + + """ + payload = {"inputName": input_name, "propertyName": prop_name} + response = self.base_client.req("GetInputPropertiesListPropertyItems", payload) + return response + + def PressInputPropertiesButton(self, input_name, prop_name): + """ + Presses a button in the properties of an input. + Note: Use this in cases where there is a button + in the properties of an input that cannot be accessed in any other way. + For example, browser sources, where there is a refresh button. + + :param input_name: Name of the input + :type input_name: str + :param prop_name: Name of the button property to press + :type prop_name: str + + + """ + payload = {"inputName": input_name, "propertyName": prop_name} + response = self.base_client.req("PressInputPropertiesButton", payload) + return response + + def GetTransitionKindList(self): + """ + Gets an array of all available transition kinds. + Similar to GetInputKindList + + + """ + response = self.base_client.req("GetTransitionKindList") + return response + + def GetSceneTransitionList(self): + """ + Gets an array of all scene transitions in OBS. + + + """ + response = self.base_client.req("GetSceneTransitionList") + return response + + def GetCurrentSceneTransition(self): + """ + Gets an array of all scene transitions in OBS. + + + """ + response = self.base_client.req("GetCurrentSceneTransition") + return response + + def SetCurrentSceneTransition(self, name): + """ + Sets the current scene transition. + Small note: While the namespace of scene transitions is generally unique, + that uniqueness is not a guarantee as it is with other resources like inputs. + + :param name: Name of the transition to make active + :type name: str + + + """ + payload = {"transitionName": name} + response = self.base_client.req("SetCurrentSceneTransition", payload) + return response + + def SetCurrentSceneTransitionDuration(self, duration): + """ + Sets the duration of the current scene transition, if it is not fixed. + + :param duration: Duration in milliseconds (>= 50, <= 20000) + :type duration: str + + + """ + payload = {"transitionDuration": duration} + response = self.base_client.req("SetCurrentSceneTransitionDuration", payload) + return response + + def SetCurrentSceneTransitionSettings(self, settings, overlay=None): + """ + Sets the settings of the current scene transition. + + :param settings: Settings object to apply to the transition. Can be {} + :type settings: dict + :param overlay: Whether to overlay over the current settings or replace them + :type overlay: bool + + + """ + payload = {"transitionSettings": settings, "overlay": overlay} + response = self.base_client.req("SetCurrentSceneTransitionSettings", payload) + return response + + def GetCurrentSceneTransitionCursor(self): + """ + Gets the cursor position of the current scene transition. + Note: transitionCursor will return 1.0 when the transition is inactive. + + + """ + response = self.base_client.req("GetCurrentSceneTransitionCursor") + return response + + def TriggerStudioModeTransition(self): + """ + Triggers the current scene transition. + Same functionality as the Transition button in studio mode. + Note: Studio mode should be active. if not throws an + RequestStatus::StudioModeNotActive (506) in response + + + """ + response = self.base_client.req("TriggerStudioModeTransition") + return response + + def SetTBarPosition(self, pos, release=None): + """ + Sets the position of the TBar. + Very important note: This will be deprecated + and replaced in a future version of obs-websocket. + + :param pos: New position (>= 0.0, <= 1.0) + :type pos: float + :param release: Whether to release the TBar. Only set false if you know that you will be sending another position update + :type release: bool + + + """ + payload = {"position": pos, "release": release} + response = self.base_client.req("SetTBarPosition", payload) + return response + + def GetSourceFilterList(self, name): + """ + Gets a list of all of a source's filters. + + :param name: Name of the source + :type name: str + + + """ + payload = {"sourceName": name} + response = self.base_client.req("GetSourceFilterList", payload) + return response + + def GetSourceFilterDefaultSettings(self, kind): + """ + Gets the default settings for a filter kind. + + :param kind: Filter kind to get the default settings for + :type kind: str + + + """ + payload = {"filterKind": kind} + response = self.base_client.req("GetSourceFilterDefaultSettings", payload) + return response + + def CreateSourceFilter( + self, source_name, filter_name, filter_kind, filter_settings=None + ): + """ + Gets the default settings for a filter kind. + + :param source_name: Name of the source to add the filter to + :type source_name: str + :param filter_name: Name of the new filter to be created + :type filter_name: str + :param filter_kind: The kind of filter to be created + :type filter_kind: str + :param filter_settings: Settings object to initialize the filter with + :type filter_settings: dict + + + """ + payload = { + "sourceName": source_name, + "filterName": filter_name, + "filterKind": filter_kind, + "filterSettings": filter_settings, + } + response = self.base_client.req("CreateSourceFilter", payload) + return response + + def RemoveSourceFilter(self, source_name, filter_name): + """ + Gets the default settings for a filter kind. + + :param source_name: Name of the source the filter is on + :type source_name: str + :param filter_name: Name of the filter to remove + :type filter_name: str + + + """ + payload = { + "sourceName": source_name, + "filterName": filter_name, + } + response = self.base_client.req("RemoveSourceFilter", payload) + return response + + def SetSourceFilterName(self, source_name, old_filter_name, new_filter_name): + """ + Sets the name of a source filter (rename). + + :param source_name: Name of the source the filter is on + :type source_name: str + :param old_filter_name: Current name of the filter + :type old_filter_name: str + :param new_filter_name: New name for the filter + :type new_filter_name: str + + + """ + payload = { + "sourceName": source_name, + "filterName": old_filter_name, + "newFilterName": new_filter_name, + } + response = self.base_client.req("SetSourceFilterName", payload) + return response + + def GetSourceFilter(self, source_name, filter_name): + """ + Gets the info for a specific source filter. + + :param source_name: Name of the source + :type source_name: str + :param filter_name: Name of the filter + :type filter_name: str + + + """ + payload = {"sourceName": source_name, "filterName": filter_name} + response = self.base_client.req("GetSourceFilter", payload) + return response + + def SetSourceFilterIndex(self, source_name, filter_name, filter_index): + """ + Gets the info for a specific source filter. + + :param source_name: Name of the source the filter is on + :type source_name: str + :param filter_name: Name of the filter + :type filter_name: str + :param filterIndex: New index position of the filter (>= 0) + :type filterIndex: int + + + """ + payload = { + "sourceName": source_name, + "filterName": filter_name, + "filterIndex": filter_index, + } + response = self.base_client.req("SetSourceFilterIndex", payload) + return response + + def SetSourceFilterSettings(self, source_name, filter_name, settings, overlay=None): + """ + Gets the info for a specific source filter. + + :param source_name: Name of the source the filter is on + :type source_name: str + :param filter_name: Name of the filter to set the settings of + :type filter_name: str + :param settings: Dictionary of settings to apply + :type settings: dict + :param overlay: True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. + :type overlay: bool + + + """ + payload = { + "sourceName": source_name, + "filterName": filter_name, + "filterSettings": settings, + "overlay": overlay, + } + response = self.base_client.req("SetSourceFilterSettings", payload) + return response + + def SetSourceFilterEnabled(self, source_name, filter_name, enabled): + """ + Gets the info for a specific source filter. + + :param source_name: Name of the source the filter is on + :type source_name: str + :param filter_name: Name of the filter + :type filter_name: str + :param enabled: New enable state of the filter + :type enabled: bool + + + """ + payload = { + "sourceName": source_name, + "filterName": filter_name, + "filterEnabled": enabled, + } + response = self.base_client.req("SetSourceFilterEnabled", payload) + return response + + def GetSceneItemList(self, name): + """ + Gets a list of all scene items in a scene. + + :param name: Name of the scene to get the items of + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("GetSceneItemList", payload) + return response + + def GetGroupItemList(self, name): + """ + Gets a list of all scene items in a scene. + + :param name: Name of the group to get the items of + :type name: str + + + """ + payload = {"sceneName": name} + response = self.base_client.req("GetGroupItemList", payload) + return response + + def GetSceneItemId(self, scene_name, source_name, offset=None): + """ + Searches a scene for a source, and returns its id. + + :param scene_name: Name of the scene or group to search in + :type scene_name: str + :param source_name: Name of the source to find + :type source_name: str + :param offset: Number of matches to skip during search. >= 0 means first forward. -1 means last (top) item (>= -1) + :type offset: int + + + """ + payload = { + "sceneName": scene_name, + "sourceName": source_name, + "searchOffset": offset, + } + response = self.base_client.req("GetSceneItemId", payload) + return response + + def CreateSceneItem(self, scene_name, source_name, enabled=None): + """ + Creates a new scene item using a source. + Scenes only + + :param scene_name: Name of the scene to create the new item in + :type scene_name: str + :param source_name: Name of the source to add to the scene + :type source_name: str + :param enabled: Enable state to apply to the scene item on creation + :type enabled: bool + + + """ + payload = { + "sceneName": scene_name, + "sourceName": source_name, + "sceneItemEnabled": enabled, + } + response = self.base_client.req("CreateSceneItem", payload) + return response + + def RemoveSceneItem(self, scene_name, item_id): + """ + Removes a scene item from a scene. + Scenes only + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item + :type item_id: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + } + response = self.base_client.req("RemoveSceneItem", payload) + return response + + def DuplicateSceneItem(self, scene_name, item_id, dest_scene_name=None): + """ + Duplicates a scene item, copying all transform and crop info. + Scenes only + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + :param dest_scene_name: Name of the scene to create the duplicated item in + :type dest_scene_name: str + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + "destinationSceneName": dest_scene_name, + } + response = self.base_client.req("DuplicateSceneItem", payload) + return response + + def GetSceneItemTransform(self, scene_name, item_id): + """ + Gets the transform and crop info of a scene item. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + } + response = self.base_client.req("GetSceneItemTransform", payload) + return response + + def SetSceneItemTransform(self, scene_name, item_id, transform): + """ + Sets the transform and crop info of a scene item. + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + :param transform: Dictionary containing scene item transform info to update + :type transform: dict + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemTransform": transform, + } + response = self.base_client.req("SetSceneItemTransform", payload) + return response + + def GetSceneItemEnabled(self, scene_name, item_id): + """ + Gets the enable state of a scene item. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + } + response = self.base_client.req("GetSceneItemEnabled", payload) + return response + + def SetSceneItemEnabled(self, scene_name, item_id, enabled): + """ + Sets the enable state of a scene item. + Scenes and Groups' + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + :param enabled: New enable state of the scene item + :type enabled: bool + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemEnabled": enabled, + } + response = self.base_client.req("SetSceneItemEnabled", payload) + return response + + def GetSceneItemLocked(self, scene_name, item_id): + """ + Gets the lock state of a scene item. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + } + response = self.base_client.req("GetSceneItemLocked", payload) + return response + + def SetSceneItemLocked(self, scene_name, item_id, locked): + """ + Sets the lock state of a scene item. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + :param locked: New lock state of the scene item + :type locked: bool + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemLocked": locked, + } + response = self.base_client.req("SetSceneItemLocked", payload) + return response + + def GetSceneItemIndex(self, scene_name, item_id): + """ + Gets the index position of a scene item in a scene. + An index of 0 is at the bottom of the source list in the UI. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + } + response = self.base_client.req("GetSceneItemIndex", payload) + return response + + def SetSceneItemIndex(self, scene_name, item_id, item_index): + """ + Sets the index position of a scene item in a scene. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + :param item_index: New index position of the scene item (>= 0) + :type item_index: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemLocked": item_index, + } + response = self.base_client.req("SetSceneItemIndex", payload) + return response + + def GetSceneItemBlendMode(self, scene_name, item_id): + """ + Gets the blend mode of a scene item. + Blend modes: + + OBS_BLEND_NORMAL + OBS_BLEND_ADDITIVE + OBS_BLEND_SUBTRACT + OBS_BLEND_SCREEN + OBS_BLEND_MULTIPLY + OBS_BLEND_LIGHTEN + OBS_BLEND_DARKEN + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + } + response = self.base_client.req("GetSceneItemBlendMode", payload) + return response + + def SetSceneItemBlendMode(self, scene_name, item_id, blend): + """ + Sets the blend mode of a scene item. + Scenes and Groups + + :param scene_name: Name of the scene the item is in + :type scene_name: str + :param item_id: Numeric ID of the scene item (>= 0) + :type item_id: int + :param blend: New blend mode + :type blend: str + + + """ + payload = { + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemBlendMode": blend, + } + response = self.base_client.req("SetSceneItemBlendMode", payload) + return response + + def GetVirtualCamStatus(self): + """ + Gets the status of the virtualcam output. + + + """ + response = self.base_client.req("GetVirtualCamStatus") + return response + + def ToggleVirtualCam(self): + """ + Toggles the state of the virtualcam output. + + + """ + response = self.base_client.req("ToggleVirtualCam") + return response + + def StartVirtualCam(self): + """ + Starts the virtualcam output. + + + """ + response = self.base_client.req("StartVirtualCam") + return response + + def StopVirtualCam(self): + """ + Stops the virtualcam output. + + + """ + response = self.base_client.req("StopVirtualCam") + return response + + def GetReplayBufferStatus(self): + """ + Gets the status of the replay buffer output. + + + """ + response = self.base_client.req("GetReplayBufferStatus") + return response + + def ToggleReplayBuffer(self): + """ + Toggles the state of the replay buffer output. + + + """ + response = self.base_client.req("ToggleReplayBuffer") + return response + + def StartReplayBuffer(self): + """ + Starts the replay buffer output. + + + """ + response = self.base_client.req("StartReplayBuffer") + return response + + def StopReplayBuffer(self): + """ + Stops the replay buffer output. + + + """ + response = self.base_client.req("StopReplayBuffer") + return response + + def SaveReplayBuffer(self): + """ + Saves the contents of the replay buffer output. + + + """ + response = self.base_client.req("SaveReplayBuffer") + return response + + def GetLastReplayBufferReplay(self): + """ + Gets the filename of the last replay buffer save file. + + + """ + response = self.base_client.req("GetLastReplayBufferReplay") + return response + + def GetStreamStatus(self): + """ + Gets the status of the stream output. + + + """ + response = self.base_client.req("GetStreamStatus") + return response + + def ToggleStream(self): + """ + Toggles the status of the stream output. + + + """ + response = self.base_client.req("ToggleStream") + return response + + def StartStream(self): + """ + Starts the stream output. + + + """ + response = self.base_client.req("StartStream") + return response + + def StopStream(self): + """ + Stops the stream output. + + + """ + response = self.base_client.req("StopStream") + return response + + def SendStreamCaption(self, caption): + """ + Sends CEA-608 caption text over the stream output. + + :param caption: Caption text + :type caption: str + + + """ + response = self.base_client.req("SendStreamCaption") + return response + + def GetRecordStatus(self): + """ + Gets the status of the record output. + + + """ + response = self.base_client.req("GetRecordStatus") + return response + + def ToggleRecord(self): + """ + Toggles the status of the record output. + + + """ + response = self.base_client.req("ToggleRecord") + return response + + def StartRecord(self): + """ + Starts the record output. + + + """ + response = self.base_client.req("StartRecord") + return response + + def StopRecord(self): + """ + Stops the record output. + + + """ + response = self.base_client.req("StopRecord") + return response + + def ToggleRecordPause(self): + """ + Toggles pause on the record output. + + + """ + response = self.base_client.req("ToggleRecordPause") + return response + + def PauseRecord(self): + """ + Pauses the record output. + + + """ + response = self.base_client.req("PauseRecord") + return response + + def ResumeRecord(self): + """ + Resumes the record output. + + + """ + response = self.base_client.req("ResumeRecord") + return response + + def GetMediaInputStatus(self, name): + """ + Gets the status of a media input. + + Media States: + OBS_MEDIA_STATE_NONE + OBS_MEDIA_STATE_PLAYING + OBS_MEDIA_STATE_OPENING + OBS_MEDIA_STATE_BUFFERING + OBS_MEDIA_STATE_PAUSED + OBS_MEDIA_STATE_STOPPED + OBS_MEDIA_STATE_ENDED + OBS_MEDIA_STATE_ERROR + + :param name: Name of the media input + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("GetMediaInputStatus", payload) + return response + + def SetMediaInputCursor(self, name, cursor): + """ + Sets the cursor position of a media input. + This request does not perform bounds checking of the cursor position. + + :param name: Name of the media input + :type name: str + :param cursor: New cursor position to set (>= 0) + :type cursor: int + + + """ + payload = {"inputName": name, "mediaCursor": cursor} + response = self.base_client.req("SetMediaInputCursor", payload) + return response + + def OffsetMediaInputCursor(self, name, offset): + """ + Offsets the current cursor position of a media input by the specified value. + This request does not perform bounds checking of the cursor position. + + :param name: Name of the media input + :type name: str + :param offset: Value to offset the current cursor position by + :type offset: int + + + """ + payload = {"inputName": name, "mediaCursorOffset": offset} + response = self.base_client.req("OffsetMediaInputCursor", payload) + return response + + def TriggerMediaInputAction(self, name, action): + """ + Triggers an action on a media input. + + :param name: Name of the media input + :type name: str + :param action: Identifier of the ObsMediaInputAction enum + :type action: str + + + """ + payload = {"inputName": name, "mediaAction": action} + response = self.base_client.req("TriggerMediaInputAction", payload) + return response + + def GetStudioModeEnabled(self): + """ + Gets whether studio is enabled. + + + """ + response = self.base_client.req("GetStudioModeEnabled") + return response + + def SetStudioModeEnabled(self, enabled): + """ + Enables or disables studio mode + + :param enabled: True == Enabled, False == Disabled + :type enabled: bool + + + """ + payload = {"studioModeEnabled": enabled} + response = self.base_client.req("SetStudioModeEnabled", payload) + return response + + def OpenInputPropertiesDialog(self, name): + """ + Opens the properties dialog of an input. + + :param name: Name of the input to open the dialog of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("OpenInputPropertiesDialog", payload) + return response + + def OpenInputFiltersDialog(self, name): + """ + Opens the filters dialog of an input. + + :param name: Name of the input to open the dialog of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("OpenInputFiltersDialog", payload) + return response + + def OpenInputInteractDialog(self, name): + """ + Opens the filters dialog of an input. + + :param name: Name of the input to open the dialog of + :type name: str + + + """ + payload = {"inputName": name} + response = self.base_client.req("OpenInputInteractDialog", payload) + return response + + def GetMonitorList(self, name): + """ + Gets a list of connected monitors and information about them. + + + """ + response = self.base_client.req("GetMonitorList") + return response diff --git a/build/lib/obsstudio_sdk/subject.py b/build/lib/obsstudio_sdk/subject.py new file mode 100644 index 0000000..24732c5 --- /dev/null +++ b/build/lib/obsstudio_sdk/subject.py @@ -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"(? 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() diff --git a/examples/events/__main__.py b/examples/events/__main__.py new file mode 100644 index 0000000..4f6c0c5 --- /dev/null +++ b/examples/events/__main__.py @@ -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(" to exit\n"): + if not cmd: + break diff --git a/examples/scene_rotate/__main__.py b/examples/scene_rotate/__main__.py new file mode 100644 index 0000000..23f71bb --- /dev/null +++ b/examples/scene_rotate/__main__.py @@ -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() diff --git a/obsstudio_sdk/__init__.py b/obsstudio_sdk/__init__.py index 8b13789..7c892fc 100644 --- a/obsstudio_sdk/__init__.py +++ b/obsstudio_sdk/__init__.py @@ -1 +1,4 @@ +from .events import EventsClient +from .reqs import ReqClient +__ALL__ = ["ReqClient", "EventsClient"] diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index 1326f16..809d207 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -1,53 +1,73 @@ -import websocket -import json -import hashlib 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, port, password): - self.host = host - self.port = port - self.password = password + def __init__(self, **kwargs): + defaultkwargs = {key: None for key in ["host", "port", "password"]} + kwargs = defaultkwargs | kwargs + 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.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()) - + ( + 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} - } - + 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 == None: - payload = { - "op": 6, - "d": { - "requestType": req_type, - "requestId": randint(1, 1000) - } - } - else: + if req_data: payload = { "op": 6, "d": { "requestType": req_type, "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)) return json.loads(self.ws.recv()) - \ No newline at end of file diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py new file mode 100644 index 0000000..24732c5 --- /dev/null +++ b/obsstudio_sdk/callback.py @@ -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"(? 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() diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py new file mode 100644 index 0000000..6c47dc6 --- /dev/null +++ b/obsstudio_sdk/events.py @@ -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 diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index 1c2571a..924682e 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -1,27 +1,29 @@ -from . import baseclient +from .baseclient import ObsClient """ A class to interact with obs-websocket requests defined in official github repo https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests """ + + class ReqClient(object): - def __init__(self, host, port, password): - self.base_client = baseclient.ObsClient(host, port, password) + def __init__(self, **kwargs): + self.base_client = ObsClient(**kwargs) self.base_client.authenticate() def GetVersion(self): """ Gets data about the current plugin and RPC version. - + :return: The version info as a dictionary :rtype: dict """ - response = self.base_client.req('GetVersion') + response = self.base_client.req("GetVersion") return response - + def GetStats(self): """ Gets statistics about OBS, obs-websocket, and the current session. @@ -31,7 +33,7 @@ class ReqClient(object): """ - response = self.base_client.req('GetStats') + response = self.base_client.req("GetStats") return response def BroadcastCustomEvent(self, eventData): @@ -46,20 +48,20 @@ class ReqClient(object): """ req_data = eventData - response = self.base_client.req('BroadcastCustomEvent', req_data) + response = self.base_client.req("BroadcastCustomEvent", req_data) return response def CallVendorRequest(self, vendorName, requestType, requestData=None): """ Call a request registered to a vendor. - - A vendor is a unique name registered by a - third-party plugin or script, which allows + + A vendor is a unique name registered by a + third-party plugin or script, which allows for custom requests and events to be added - to obs-websocket. If a plugin or script - implements vendor requests or events, + to obs-websocket. If a plugin or script + implements vendor requests or events, documentation is expected to be provided with them. - + :param vendorName: Name of the vendor to use :type vendorName: str :param requestType: The request type to call @@ -83,9 +85,9 @@ class ReqClient(object): """ - response = self.base_client.req('GetHotkeyList') + response = self.base_client.req("GetHotkeyList") return response - + def TriggerHotkeyByName(self, hotkeyName): """ Triggers a hotkey using its name. For hotkey names @@ -96,11 +98,13 @@ class ReqClient(object): """ - payload = {'hotkeyName': hotkeyName} - response = self.base_client.req('TriggerHotkeyByName', payload) + payload = {"hotkeyName": hotkeyName} + response = self.base_client.req("TriggerHotkeyByName", payload) return response - - def TriggerHotkeyByKeySequence(self,keyId, pressShift, pressCtrl, pressAlt, pressCmd): + + def TriggerHotkeyByKeySequence( + self, keyId, pressShift, pressCtrl, pressAlt, pressCmd + ): """ Triggers a hotkey using a sequence of keys. @@ -120,21 +124,21 @@ class ReqClient(object): """ payload = { - 'keyId': keyId, - 'keyModifiers': { - 'shift': pressShift, - 'control': pressCtrl, - 'alt': pressAlt, - 'cmd': pressCmd - } + "keyId": keyId, + "keyModifiers": { + "shift": pressShift, + "control": pressCtrl, + "alt": pressAlt, + "cmd": pressCmd, + }, } - - response = self.base_client.req('TriggerHotkeyByKeySequence', payload) + + response = self.base_client.req("TriggerHotkeyByKeySequence", payload) return response def Sleep(self, sleepMillis=None, sleepFrames=None): """ - Sleeps for a time duration or number of frames. + Sleeps for a time duration or number of frames. Only available in request batches with types SERIAL_REALTIME or SERIAL_FRAME :param sleepMillis: Number of milliseconds to sleep for (if SERIAL_REALTIME mode) 0 <= sleepMillis <= 50000 @@ -144,18 +148,15 @@ class ReqClient(object): """ - payload = { - 'sleepMillis': sleepMillis, - 'sleepFrames': sleepFrames - } - response = self.base_client.req('Sleep', payload) + payload = {"sleepMillis": sleepMillis, "sleepFrames": sleepFrames} + response = self.base_client.req("Sleep", payload) return response def GetPersistentData(self, realm, slotName): """ Gets the value of a "slot" from the selected persistent data realm. - :param realm: The data realm to select + :param realm: The data realm to select OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE :type realm: str :param slotName: The name of the slot to retrieve data from @@ -165,18 +166,15 @@ class ReqClient(object): """ - payload = { - 'realm': realm, - 'slotName': slotName - } - response = self.base_client.req('GetPersistentData', payload) + payload = {"realm": realm, "slotName": slotName} + response = self.base_client.req("GetPersistentData", payload) return response def SetPersistentData(self, realm, slotName, slotValue): """ Sets the value of a "slot" from the selected persistent data realm. - :param realm: The data realm to select. + :param realm: The data realm to select. OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE :type realm: str :param slotName: The name of the slot to retrieve data from @@ -186,12 +184,8 @@ class ReqClient(object): """ - payload = { - 'realm': realm, - 'slotName': slotName, - 'slotValue': slotValue - } - response = self.base_client.req('SetPersistentData', payload) + payload = {"realm": realm, "slotName": slotName, "slotValue": slotValue} + response = self.base_client.req("SetPersistentData", payload) return response def GetSceneCollectionList(self): @@ -203,7 +197,7 @@ class ReqClient(object): """ - response = self.base_client.req('GetSceneCollectionList') + response = self.base_client.req("GetSceneCollectionList") return response def SetCurrentSceneCollection(self, name): @@ -216,22 +210,22 @@ class ReqClient(object): """ - payload = {'sceneCollectionName': name} - response = self.base_client.req('SetCurrentSceneCollection', payload) + payload = {"sceneCollectionName": name} + response = self.base_client.req("SetCurrentSceneCollection", payload) return response def CreateSceneCollection(self, name): """ Creates a new scene collection, switching to it in the process. Note: This will block until the collection has finished changing. - + :param name: Name for the new scene collection :type name: str """ - payload = {'sceneCollectionName': name} - response = self.base_client.req('CreateSceneCollection', payload) + payload = {"sceneCollectionName": name} + response = self.base_client.req("CreateSceneCollection", payload) return response def GetProfileList(self): @@ -243,7 +237,7 @@ class ReqClient(object): """ - response = self.base_client.req('GetProfileList') + response = self.base_client.req("GetProfileList") return response def SetCurrentProfile(self, name): @@ -255,10 +249,10 @@ class ReqClient(object): """ - payload = {'profileName': name} - response = self.base_client.req('SetCurrentProfile', payload) + payload = {"profileName": name} + response = self.base_client.req("SetCurrentProfile", payload) return response - + def CreateProfile(self, name): """ Creates a new profile, switching to it in the process @@ -268,13 +262,13 @@ class ReqClient(object): """ - payload = {'profileName': name} - response = self.base_client.req('CreateProfile', payload) + payload = {"profileName": name} + response = self.base_client.req("CreateProfile", payload) return response def RemoveProfile(self, name): """ - Removes a profile. If the current profile is chosen, + Removes a profile. If the current profile is chosen, it will change to a different profile first. :param name: Name of the profile to remove @@ -282,8 +276,8 @@ class ReqClient(object): """ - payload = {'profileName': name} - response = self.base_client.req('RemoveProfile', payload) + payload = {"profileName": name} + response = self.base_client.req("RemoveProfile", payload) return response def GetProfileParameter(self, category, name): @@ -300,11 +294,8 @@ class ReqClient(object): """ - payload = { - 'parameterCategory': category, - 'parameterName': name - } - response = self.base_client.req('GetProfileParameter', payload) + payload = {"parameterCategory": category, "parameterName": name} + response = self.base_client.req("GetProfileParameter", payload) return response def SetProfileParameter(self, category, name, value): @@ -324,28 +315,30 @@ class ReqClient(object): """ payload = { - 'parameterCategory': category, - 'parameterName': name, - 'parameterValue': value + "parameterCategory": category, + "parameterName": name, + "parameterValue": value, } - response = self.base_client.req('SetProfileParameter', payload) + response = self.base_client.req("SetProfileParameter", payload) return response def GetVideoSettings(self): """ Gets the current video settings. - Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. + Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. Example: 60000/1001 - + """ - response = self.base_client.req('GetVideoSettings') + response = self.base_client.req("GetVideoSettings") return response - def SetVideoSettings(self, numerator, denominator, base_width, base_height, out_width, out_height): + def SetVideoSettings( + self, numerator, denominator, base_width, base_height, out_width, out_height + ): """ Sets the current video settings. - Note: Fields must be specified in pairs. + Note: Fields must be specified in pairs. For example, you cannot set only baseWidth without needing to specify baseHeight. :param numerator: Numerator of the fractional FPS value >=1 @@ -364,23 +357,23 @@ class ReqClient(object): """ payload = { - 'fpsNumerator': numerator, - 'fpsDenominator': denominator, - 'baseWidth': base_width, - 'baseHeight': base_height, - 'outputWidth': out_width, - 'outputHeight': out_height + "fpsNumerator": numerator, + "fpsDenominator": denominator, + "baseWidth": base_width, + "baseHeight": base_height, + "outputWidth": out_width, + "outputHeight": out_height, } - response = self.base_client.req('SetVideoSettings', payload) + response = self.base_client.req("SetVideoSettings", payload) return response def GetStreamServiceSettings(self): """ Gets the current stream service settings (stream destination). - + """ - response = self.base_client.req('GetStreamServiceSettings') + response = self.base_client.req("GetStreamServiceSettings") return response def SetStreamServiceSettings(self, ss_type, ss_settings): @@ -397,10 +390,10 @@ class ReqClient(object): """ payload = { - 'streamServiceType': ss_type, - 'streamServiceSettings': ss_settings, + "streamServiceType": ss_type, + "streamServiceSettings": ss_settings, } - response = self.base_client.req('SetStreamServiceSettings', payload) + response = self.base_client.req("SetStreamServiceSettings", payload) return response def GetSourceActive(self, name): @@ -412,17 +405,17 @@ class ReqClient(object): """ - payload = {'sourceName': name} - response = self.base_client.req('GetSourceActive', payload) + payload = {"sourceName": name} + response = self.base_client.req("GetSourceActive", payload) return response def GetSourceScreenshot(self, name, img_format, width, height, quality): """ Gets a Base64-encoded screenshot of a source. - The imageWidth and imageHeight parameters are - treated as "scale to inner", meaning the smallest ratio - will be used and the aspect ratio of the original resolution is kept. - If imageWidth and imageHeight are not specified, the compressed image + The imageWidth and imageHeight parameters are + treated as "scale to inner", meaning the smallest ratio + will be used and the aspect ratio of the original resolution is kept. + If imageWidth and imageHeight are not specified, the compressed image will use the full resolution of the source. :param name: Name of the source to take a screenshot of @@ -439,22 +432,22 @@ class ReqClient(object): """ payload = { - 'sourceName': name, - 'imageFormat': img_format, - 'imageWidth': width, - 'imageHeight': height, - 'imageCompressionQuality': quality + "sourceName": name, + "imageFormat": img_format, + "imageWidth": width, + "imageHeight": height, + "imageCompressionQuality": quality, } - response = self.base_client.req('GetSourceScreenshot', payload) + response = self.base_client.req("GetSourceScreenshot", payload) return response def SaveSourceScreenshot(self, name, img_format, file_path, width, height, quality): """ Saves a Base64-encoded screenshot of a source. - The imageWidth and imageHeight parameters are - treated as "scale to inner", meaning the smallest ratio - will be used and the aspect ratio of the original resolution is kept. - If imageWidth and imageHeight are not specified, the compressed image + The imageWidth and imageHeight parameters are + treated as "scale to inner", meaning the smallest ratio + will be used and the aspect ratio of the original resolution is kept. + If imageWidth and imageHeight are not specified, the compressed image will use the full resolution of the source. :param name: Name of the source to take a screenshot of @@ -469,39 +462,39 @@ class ReqClient(object): :type height: int :param quality: Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" :type quality: int - + """ payload = { - 'sourceName': name, - 'imageFormat': img_format, - 'imageFilePath': file_path, - 'imageWidth': width, - 'imageHeight': height, - 'imageCompressionQuality': quality + "sourceName": name, + "imageFormat": img_format, + "imageFilePath": file_path, + "imageWidth": width, + "imageHeight": height, + "imageCompressionQuality": quality, } - response = self.base_client.req('SaveSourceScreenshot', payload) + response = self.base_client.req("SaveSourceScreenshot", payload) return response def GetSceneList(self): """ Gets a list of all scenes in OBS. - + """ - response = self.base_client.req('GetSceneList') + response = self.base_client.req("GetSceneList") return response - + def GetGroupList(self): """ Gets a list of all groups in OBS. - Groups in OBS are actually scenes, - but renamed and modified. In obs-websocket, + Groups in OBS are actually scenes, + but renamed and modified. In obs-websocket, we treat them as scenes where we can.. - + """ - response = self.base_client.req('GetSceneList') + response = self.base_client.req("GetSceneList") return response def GetCurrentProgramScene(self): @@ -510,7 +503,7 @@ class ReqClient(object): """ - response = self.base_client.req('GetCurrentProgramScene') + response = self.base_client.req("GetCurrentProgramScene") return response def SetCurrentProgramScene(self, name): @@ -522,17 +515,17 @@ class ReqClient(object): """ - payload = {'sceneName': name} - response = self.base_client.req('SetCurrentProgramScene', payload) + payload = {"sceneName": name} + response = self.base_client.req("SetCurrentProgramScene", payload) return response def GetCurrentPreviewScene(self): """ Gets the current preview scene - + """ - response = self.base_client.req('GetCurrentPreviewScene') + response = self.base_client.req("GetCurrentPreviewScene") return response def SetCurrentPreviewScene(self, name): @@ -544,8 +537,8 @@ class ReqClient(object): """ - payload = {'sceneName': name} - response = self.base_client.req('SetCurrentPreviewScene', payload) + payload = {"sceneName": name} + response = self.base_client.req("SetCurrentPreviewScene", payload) return response def CreateScene(self, name): @@ -557,8 +550,8 @@ class ReqClient(object): """ - payload = {'sceneName': name } - response = self.base_client.req('CreateScene', payload) + payload = {"sceneName": name} + response = self.base_client.req("CreateScene", payload) return response def RemoveScene(self, name): @@ -570,8 +563,8 @@ class ReqClient(object): """ - payload = {'sceneName': name } - response = self.base_client.req('RemoveScene', payload) + payload = {"sceneName": name} + response = self.base_client.req("RemoveScene", payload) return response def SetSceneName(self, old_name, new_name): @@ -585,11 +578,8 @@ class ReqClient(object): """ - payload = { - 'sceneName': old_name, - 'newSceneName': new_name - } - response = self.base_client.req('SetSceneName', payload) + payload = {"sceneName": old_name, "newSceneName": new_name} + response = self.base_client.req("SetSceneName", payload) return response def GetSceneSceneTransitionOverride(self, name): @@ -598,11 +588,11 @@ class ReqClient(object): :param name: Name of the scene :type name: str - + """ - payload = {'sceneName': name} - response = self.base_client.req('GetSceneSceneTransitionOverride', payload) + payload = {"sceneName": name} + response = self.base_client.req("GetSceneSceneTransitionOverride", payload) return response def SetSceneSceneTransitionOverride(self, scene_name, tr_name, tr_duration): @@ -619,11 +609,11 @@ class ReqClient(object): """ payload = { - 'sceneName': scene_name, - 'transitionName': tr_name, - 'transitionDuration': tr_duration - } - response = self.base_client.req('SetSceneSceneTransitionOverride', payload) + "sceneName": scene_name, + "transitionName": tr_name, + "transitionDuration": tr_duration, + } + response = self.base_client.req("SetSceneSceneTransitionOverride", payload) return response def GetInputList(self, kind): @@ -632,11 +622,11 @@ class ReqClient(object): :param kind: Restrict the list to only inputs of the specified kind :type kind: str - + """ - payload = {'inputKind': kind} - response = self.base_client.req('GetInputList', payload) + payload = {"inputKind": kind} + response = self.base_client.req("GetInputList", payload) return response def GetInputKindList(self, unversioned): @@ -645,11 +635,11 @@ class ReqClient(object): :param unversioned: True == Return all kinds as unversioned, False == Return with version suffixes (if available) :type unversioned: bool - + """ - payload = {'unversioned': unversioned} - response = self.base_client.req('GetInputKindList', payload) + payload = {"unversioned": unversioned} + response = self.base_client.req("GetInputKindList", payload) return response def GetSpecialInputs(self): @@ -658,10 +648,12 @@ class ReqClient(object): """ - response = self.base_client.req('GetSpecialInputs') + response = self.base_client.req("GetSpecialInputs") return response - def CreateInput(self, sceneName, inputName, inputKind, inputSettings, sceneItemEnabled): + def CreateInput( + self, sceneName, inputName, inputKind, inputSettings, sceneItemEnabled + ): """ Creates a new input, adding it as a scene item to the specified scene. @@ -675,17 +667,17 @@ class ReqClient(object): :type inputSettings: object :param sceneItemEnabled: Whether to set the created scene item to enabled or disabled :type sceneItemEnabled: bool - + """ payload = { - 'sceneName': sceneName, - 'inputName': inputName, - 'inputKind': inputKind, - 'inputSettings': inputSettings, - 'sceneItemEnabled': sceneItemEnabled + "sceneName": sceneName, + "inputName": inputName, + "inputKind": inputKind, + "inputSettings": inputSettings, + "sceneItemEnabled": sceneItemEnabled, } - response = self.base_client.req('CreateInput', payload) + response = self.base_client.req("CreateInput", payload) return response def RemoveInput(self, name): @@ -697,8 +689,8 @@ class ReqClient(object): """ - payload = {'inputName': name} - response = self.base_client.req('RemoveInput', payload) + payload = {"inputName": name} + response = self.base_client.req("RemoveInput", payload) return response def SetInputName(self, old_name, new_name): @@ -712,11 +704,8 @@ class ReqClient(object): """ - payload = { - 'inputName': old_name, - 'newInputName': new_name - } - response = self.base_client.req('SetInputName', payload) + payload = {"inputName": old_name, "newInputName": new_name} + response = self.base_client.req("SetInputName", payload) return response def GetInputDefaultSettings(self, kind): @@ -725,26 +714,26 @@ class ReqClient(object): :param kind: Input kind to get the default settings for :type kind: str - + """ - payload = {'inputKind': kind} - response = self.base_client.req('GetInputDefaultSettings', payload) + payload = {"inputKind": kind} + response = self.base_client.req("GetInputDefaultSettings", payload) return response def GetInputSettings(self, name): """ Gets the settings of an input. - Note: Does not include defaults. To create the entire settings object, + Note: Does not include defaults. To create the entire settings object, overlay inputSettings over the defaultInputSettings provided by GetInputDefaultSettings. :param name: Input kind to get the default settings for :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputSettings', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputSettings", payload) return response def SetInputSettings(self, name, settings, overlay): @@ -757,15 +746,11 @@ class ReqClient(object): :type settings: dict :param overlay: True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. :type overlay: bool - + """ - payload = { - 'inputName': name, - 'inputSettings': settings, - 'overlay': overlay - } - response = self.base_client.req('SetInputSettings', payload) + payload = {"inputName": name, "inputSettings": settings, "overlay": overlay} + response = self.base_client.req("SetInputSettings", payload) return response def GetInputMute(self, name): @@ -774,11 +759,11 @@ class ReqClient(object): :param name: Name of input to get the mute state of :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputMute', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputMute", payload) return response def SetInputMute(self, name, muted): @@ -792,11 +777,8 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'inputMuted': muted - } - response = self.base_client.req('SetInputMute', payload) + payload = {"inputName": name, "inputMuted": muted} + response = self.base_client.req("SetInputMute", payload) return response def ToggleInputMute(self, name): @@ -805,11 +787,11 @@ class ReqClient(object): :param name: Name of the input to toggle the mute state of :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('ToggleInputMute', payload) + payload = {"inputName": name} + response = self.base_client.req("ToggleInputMute", payload) return response def GetInputVolume(self, name): @@ -818,11 +800,11 @@ class ReqClient(object): :param name: Name of the input to get the volume of :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputVolume', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputVolume", payload) return response def SetInputVolume(self, name, vol_mul=None, vol_db=None): @@ -839,30 +821,30 @@ class ReqClient(object): """ payload = { - 'inputName': name, - 'inputVolumeMul': vol_mul, - 'inputVolumeDb': vol_db + "inputName": name, + "inputVolumeMul": vol_mul, + "inputVolumeDb": vol_db, } - response = self.base_client.req('SetInputVolume', payload) + response = self.base_client.req("SetInputVolume", payload) return response def GetInputAudioBalance(self, name): """ Gets the audio balance of an input. - + :param name: Name of the input to get the audio balance of :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputAudioBalance', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioBalance", payload) return response def SetInputAudioBalance(self, name, balance): """ Sets the audio balance of an input. - + :param name: Name of the input to get the audio balance of :type name: str :param balance: New audio balance value (>= 0.0, <= 1.0) @@ -870,30 +852,27 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'inputAudioBalance': balance - } - response = self.base_client.req('SetInputAudioBalance', payload) + payload = {"inputName": name, "inputAudioBalance": balance} + response = self.base_client.req("SetInputAudioBalance", payload) return response def GetInputAudioOffset(self, name): """ Gets the audio sync offset of an input. - + :param name: Name of the input to get the audio sync offset of :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputAudioOffset', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioOffset", payload) return response def SetInputAudioSyncOffset(self, name, offset): """ Sets the audio sync offset of an input. - + :param name: Name of the input to set the audio sync offset of :type name: str :param offset: New audio sync offset in milliseconds (>= -950, <= 20000) @@ -901,11 +880,8 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'inputAudioSyncOffset': offset - } - response = self.base_client.req('SetInputAudioSyncOffset', payload) + payload = {"inputName": name, "inputAudioSyncOffset": offset} + response = self.base_client.req("SetInputAudioSyncOffset", payload) return response def GetInputAudioMonitorType(self, name): @@ -917,20 +893,20 @@ class ReqClient(object): OBS_MONITORING_TYPE_MONITOR_ONLY OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT - + :param name: Name of the input to get the audio monitor type of :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputAudioMonitorType', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioMonitorType", payload) return response def SetInputAudioMonitorType(self, name, mon_type): """ Sets the audio monitor type of an input. - + :param name: Name of the input to set the audio monitor type of :type name: str :param mon_type: Audio monitor type @@ -938,11 +914,8 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'monitorType': mon_type - } - response = self.base_client.req('SetInputAudioMonitorType', payload) + payload = {"inputName": name, "monitorType": mon_type} + response = self.base_client.req("SetInputAudioMonitorType", payload) return response def GetInputAudioTracks(self, name): @@ -951,17 +924,17 @@ class ReqClient(object): :param name: Name of the input :type name: str - + """ - payload = {'inputName': name} - response = self.base_client.req('GetInputAudioTracks', payload) + payload = {"inputName": name} + response = self.base_client.req("GetInputAudioTracks", payload) return response def SetInputAudioTracks(self, name, track): """ Sets the audio monitor type of an input. - + :param name: Name of the input :type name: str :param track: Track settings to apply @@ -969,39 +942,33 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'inputAudioTracks': track - } - response = self.base_client.req('SetInputAudioTracks', payload) + payload = {"inputName": name, "inputAudioTracks": track} + response = self.base_client.req("SetInputAudioTracks", payload) return response def GetInputPropertiesListPropertyItems(self, input_name, prop_name): """ Gets the items of a list property from an input's properties. - Note: Use this in cases where an input provides a dynamic, - selectable list of items. For example, display capture, + Note: Use this in cases where an input provides a dynamic, + selectable list of items. For example, display capture, where it provides a list of available displays. :param input_name: Name of the input :type input_name: str :param prop_name: Name of the list property to get the items of :type prop_name: str - + """ - payload = { - 'inputName': input_name, - 'propertyName': prop_name - } - response = self.base_client.req('GetInputPropertiesListPropertyItems', payload) + payload = {"inputName": input_name, "propertyName": prop_name} + response = self.base_client.req("GetInputPropertiesListPropertyItems", payload) return response def PressInputPropertiesButton(self, input_name, prop_name): """ Presses a button in the properties of an input. - Note: Use this in cases where there is a button - in the properties of an input that cannot be accessed in any other way. + Note: Use this in cases where there is a button + in the properties of an input that cannot be accessed in any other way. For example, browser sources, where there is a refresh button. :param input_name: Name of the input @@ -1011,39 +978,36 @@ class ReqClient(object): """ - payload = { - 'inputName': input_name, - 'propertyName': prop_name - } - response = self.base_client.req('PressInputPropertiesButton', payload) + payload = {"inputName": input_name, "propertyName": prop_name} + response = self.base_client.req("PressInputPropertiesButton", payload) return response def GetTransitionKindList(self): """ Gets an array of all available transition kinds. Similar to GetInputKindList - + """ - response = self.base_client.req('GetTransitionKindList') + response = self.base_client.req("GetTransitionKindList") return response def GetSceneTransitionList(self): """ Gets an array of all scene transitions in OBS. - + """ - response = self.base_client.req('GetSceneTransitionList') + response = self.base_client.req("GetSceneTransitionList") return response def GetCurrentSceneTransition(self): """ Gets an array of all scene transitions in OBS. - + """ - response = self.base_client.req('GetCurrentSceneTransition') + response = self.base_client.req("GetCurrentSceneTransition") return response def SetCurrentSceneTransition(self, name): @@ -1057,10 +1021,10 @@ class ReqClient(object): """ - payload = {'transitionName': name} - response = self.base_client.req('SetCurrentSceneTransition', payload) + payload = {"transitionName": name} + response = self.base_client.req("SetCurrentSceneTransition", payload) return response - + def SetCurrentSceneTransitionDuration(self, duration): """ Sets the duration of the current scene transition, if it is not fixed. @@ -1070,8 +1034,8 @@ class ReqClient(object): """ - payload = {'transitionDuration': duration} - response = self.base_client.req('SetCurrentSceneTransitionDuration', payload) + payload = {"transitionDuration": duration} + response = self.base_client.req("SetCurrentSceneTransitionDuration", payload) return response def SetCurrentSceneTransitionSettings(self, settings, overlay=None): @@ -1085,39 +1049,36 @@ class ReqClient(object): """ - payload = { - 'transitionSettings': settings, - 'overlay': overlay - } - response = self.base_client.req('SetCurrentSceneTransitionSettings', payload) + payload = {"transitionSettings": settings, "overlay": overlay} + response = self.base_client.req("SetCurrentSceneTransitionSettings", payload) return response def GetCurrentSceneTransitionCursor(self): """ Gets the cursor position of the current scene transition. Note: transitionCursor will return 1.0 when the transition is inactive. - + """ - response = self.base_client.req('GetCurrentSceneTransitionCursor') + response = self.base_client.req("GetCurrentSceneTransitionCursor") return response def TriggerStudioModeTransition(self): """ - Triggers the current scene transition. + Triggers the current scene transition. Same functionality as the Transition button in studio mode. - Note: Studio mode should be active. if not throws an + Note: Studio mode should be active. if not throws an RequestStatus::StudioModeNotActive (506) in response """ - response = self.base_client.req('TriggerStudioModeTransition') + response = self.base_client.req("TriggerStudioModeTransition") return response def SetTBarPosition(self, pos, release=None): """ Sets the position of the TBar. - Very important note: This will be deprecated + Very important note: This will be deprecated and replaced in a future version of obs-websocket. :param pos: New position (>= 0.0, <= 1.0) @@ -1127,11 +1088,8 @@ class ReqClient(object): """ - payload = { - 'position': pos, - 'release': release - } - response = self.base_client.req('SetTBarPosition', payload) + payload = {"position": pos, "release": release} + response = self.base_client.req("SetTBarPosition", payload) return response def GetSourceFilterList(self, name): @@ -1140,11 +1098,11 @@ class ReqClient(object): :param name: Name of the source :type name: str - + """ - payload = {'sourceName': name} - response = self.base_client.req('GetSourceFilterList', payload) + payload = {"sourceName": name} + response = self.base_client.req("GetSourceFilterList", payload) return response def GetSourceFilterDefaultSettings(self, kind): @@ -1153,14 +1111,16 @@ class ReqClient(object): :param kind: Filter kind to get the default settings for :type kind: str - + """ - payload = {'filterKind': kind} - response = self.base_client.req('GetSourceFilterDefaultSettings', payload) + payload = {"filterKind": kind} + response = self.base_client.req("GetSourceFilterDefaultSettings", payload) return response - def CreateSourceFilter(self, source_name, filter_name, filter_kind, filter_settings=None): + def CreateSourceFilter( + self, source_name, filter_name, filter_kind, filter_settings=None + ): """ Gets the default settings for a filter kind. @@ -1176,12 +1136,12 @@ class ReqClient(object): """ payload = { - 'sourceName': source_name, - 'filterName': filter_name, - 'filterKind': filter_kind, - 'filterSettings': filter_settings + "sourceName": source_name, + "filterName": filter_name, + "filterKind": filter_kind, + "filterSettings": filter_settings, } - response = self.base_client.req('CreateSourceFilter', payload) + response = self.base_client.req("CreateSourceFilter", payload) return response def RemoveSourceFilter(self, source_name, filter_name): @@ -1196,10 +1156,10 @@ class ReqClient(object): """ payload = { - 'sourceName': source_name, - 'filterName': filter_name, + "sourceName": source_name, + "filterName": filter_name, } - response = self.base_client.req('RemoveSourceFilter', payload) + response = self.base_client.req("RemoveSourceFilter", payload) return response def SetSourceFilterName(self, source_name, old_filter_name, new_filter_name): @@ -1216,11 +1176,11 @@ class ReqClient(object): """ payload = { - 'sourceName': source_name, - 'filterName': old_filter_name, - 'newFilterName': new_filter_name, + "sourceName": source_name, + "filterName": old_filter_name, + "newFilterName": new_filter_name, } - response = self.base_client.req('SetSourceFilterName', payload) + response = self.base_client.req("SetSourceFilterName", payload) return response def GetSourceFilter(self, source_name, filter_name): @@ -1231,14 +1191,11 @@ class ReqClient(object): :type source_name: str :param filter_name: Name of the filter :type filter_name: str - + """ - payload = { - 'sourceName': source_name, - 'filterName': filter_name - } - response = self.base_client.req('GetSourceFilter', payload) + payload = {"sourceName": source_name, "filterName": filter_name} + response = self.base_client.req("GetSourceFilter", payload) return response def SetSourceFilterIndex(self, source_name, filter_name, filter_index): @@ -1255,11 +1212,11 @@ class ReqClient(object): """ payload = { - 'sourceName': source_name, - 'filterName': filter_name, - 'filterIndex': filter_index + "sourceName": source_name, + "filterName": filter_name, + "filterIndex": filter_index, } - response = self.base_client.req('SetSourceFilterIndex', payload) + response = self.base_client.req("SetSourceFilterIndex", payload) return response def SetSourceFilterSettings(self, source_name, filter_name, settings, overlay=None): @@ -1278,12 +1235,12 @@ class ReqClient(object): """ payload = { - 'sourceName': source_name, - 'filterName': filter_name, - 'filterSettings': settings, - 'overlay': overlay + "sourceName": source_name, + "filterName": filter_name, + "filterSettings": settings, + "overlay": overlay, } - response = self.base_client.req('SetSourceFilterSettings', payload) + response = self.base_client.req("SetSourceFilterSettings", payload) return response def SetSourceFilterEnabled(self, source_name, filter_name, enabled): @@ -1300,11 +1257,11 @@ class ReqClient(object): """ payload = { - 'sourceName': source_name, - 'filterName': filter_name, - 'filterEnabled': enabled + "sourceName": source_name, + "filterName": filter_name, + "filterEnabled": enabled, } - response = self.base_client.req('SetSourceFilterEnabled', payload) + response = self.base_client.req("SetSourceFilterEnabled", payload) return response def GetSceneItemList(self, name): @@ -1313,11 +1270,11 @@ class ReqClient(object): :param name: Name of the scene to get the items of :type name: str - + """ - payload = {'sceneName': name} - response = self.base_client.req('GetSceneItemList', payload) + payload = {"sceneName": name} + response = self.base_client.req("GetSceneItemList", payload) return response def GetGroupItemList(self, name): @@ -1326,11 +1283,11 @@ class ReqClient(object): :param name: Name of the group to get the items of :type name: str - + """ - payload = {'sceneName': name} - response = self.base_client.req('GetGroupItemList', payload) + payload = {"sceneName": name} + response = self.base_client.req("GetGroupItemList", payload) return response def GetSceneItemId(self, scene_name, source_name, offset=None): @@ -1343,15 +1300,15 @@ class ReqClient(object): :type source_name: str :param offset: Number of matches to skip during search. >= 0 means first forward. -1 means last (top) item (>= -1) :type offset: int - + """ payload = { - 'sceneName': scene_name, - 'sourceName': source_name, - 'searchOffset': offset + "sceneName": scene_name, + "sourceName": source_name, + "searchOffset": offset, } - response = self.base_client.req('GetSceneItemId', payload) + response = self.base_client.req("GetSceneItemId", payload) return response def CreateSceneItem(self, scene_name, source_name, enabled=None): @@ -1365,15 +1322,15 @@ class ReqClient(object): :type source_name: str :param enabled: Enable state to apply to the scene item on creation :type enabled: bool - + """ payload = { - 'sceneName': scene_name, - 'sourceName': source_name, - 'sceneItemEnabled': enabled + "sceneName": scene_name, + "sourceName": source_name, + "sceneItemEnabled": enabled, } - response = self.base_client.req('CreateSceneItem', payload) + response = self.base_client.req("CreateSceneItem", payload) return response def RemoveSceneItem(self, scene_name, item_id): @@ -1389,10 +1346,10 @@ class ReqClient(object): """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, + "sceneName": scene_name, + "sceneItemId": item_id, } - response = self.base_client.req('RemoveSceneItem', payload) + response = self.base_client.req("RemoveSceneItem", payload) return response def DuplicateSceneItem(self, scene_name, item_id, dest_scene_name=None): @@ -1406,15 +1363,15 @@ class ReqClient(object): :type item_id: int :param dest_scene_name: Name of the scene to create the duplicated item in :type dest_scene_name: str - + """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, - 'destinationSceneName': dest_scene_name + "sceneName": scene_name, + "sceneItemId": item_id, + "destinationSceneName": dest_scene_name, } - response = self.base_client.req('DuplicateSceneItem', payload) + response = self.base_client.req("DuplicateSceneItem", payload) return response def GetSceneItemTransform(self, scene_name, item_id): @@ -1426,14 +1383,14 @@ class ReqClient(object): :type scene_name: str :param item_id: Numeric ID of the scene item (>= 0) :type item_id: int - + """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, + "sceneName": scene_name, + "sceneItemId": item_id, } - response = self.base_client.req('GetSceneItemTransform', payload) + response = self.base_client.req("GetSceneItemTransform", payload) return response def SetSceneItemTransform(self, scene_name, item_id, transform): @@ -1448,11 +1405,11 @@ class ReqClient(object): :type transform: dict """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, - 'sceneItemTransform': transform + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemTransform": transform, } - response = self.base_client.req('SetSceneItemTransform', payload) + response = self.base_client.req("SetSceneItemTransform", payload) return response def GetSceneItemEnabled(self, scene_name, item_id): @@ -1464,14 +1421,14 @@ class ReqClient(object): :type scene_name: str :param item_id: Numeric ID of the scene item (>= 0) :type item_id: int - + """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, + "sceneName": scene_name, + "sceneItemId": item_id, } - response = self.base_client.req('GetSceneItemEnabled', payload) + response = self.base_client.req("GetSceneItemEnabled", payload) return response def SetSceneItemEnabled(self, scene_name, item_id, enabled): @@ -1489,11 +1446,11 @@ class ReqClient(object): """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, - 'sceneItemEnabled': enabled + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemEnabled": enabled, } - response = self.base_client.req('SetSceneItemEnabled', payload) + response = self.base_client.req("SetSceneItemEnabled", payload) return response def GetSceneItemLocked(self, scene_name, item_id): @@ -1505,14 +1462,14 @@ class ReqClient(object): :type scene_name: str :param item_id: Numeric ID of the scene item (>= 0) :type item_id: int - + """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, + "sceneName": scene_name, + "sceneItemId": item_id, } - response = self.base_client.req('GetSceneItemLocked', payload) + response = self.base_client.req("GetSceneItemLocked", payload) return response def SetSceneItemLocked(self, scene_name, item_id, locked): @@ -1530,11 +1487,11 @@ class ReqClient(object): """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, - 'sceneItemLocked': locked + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemLocked": locked, } - response = self.base_client.req('SetSceneItemLocked', payload) + response = self.base_client.req("SetSceneItemLocked", payload) return response def GetSceneItemIndex(self, scene_name, item_id): @@ -1547,14 +1504,14 @@ class ReqClient(object): :type scene_name: str :param item_id: Numeric ID of the scene item (>= 0) :type item_id: int - + """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, + "sceneName": scene_name, + "sceneItemId": item_id, } - response = self.base_client.req('GetSceneItemIndex', payload) + response = self.base_client.req("GetSceneItemIndex", payload) return response def SetSceneItemIndex(self, scene_name, item_id, item_index): @@ -1572,18 +1529,18 @@ class ReqClient(object): """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, - 'sceneItemLocked': item_index + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemLocked": item_index, } - response = self.base_client.req('SetSceneItemIndex', payload) + response = self.base_client.req("SetSceneItemIndex", payload) return response def GetSceneItemBlendMode(self, scene_name, item_id): """ Gets the blend mode of a scene item. Blend modes: - + OBS_BLEND_NORMAL OBS_BLEND_ADDITIVE OBS_BLEND_SUBTRACT @@ -1597,14 +1554,14 @@ class ReqClient(object): :type scene_name: str :param item_id: Numeric ID of the scene item (>= 0) :type item_id: int - + """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, + "sceneName": scene_name, + "sceneItemId": item_id, } - response = self.base_client.req('GetSceneItemBlendMode', payload) + response = self.base_client.req("GetSceneItemBlendMode", payload) return response def SetSceneItemBlendMode(self, scene_name, item_id, blend): @@ -1622,29 +1579,29 @@ class ReqClient(object): """ payload = { - 'sceneName': scene_name, - 'sceneItemId': item_id, - 'sceneItemBlendMode': blend + "sceneName": scene_name, + "sceneItemId": item_id, + "sceneItemBlendMode": blend, } - response = self.base_client.req('SetSceneItemBlendMode', payload) + response = self.base_client.req("SetSceneItemBlendMode", payload) return response def GetVirtualCamStatus(self): """ Gets the status of the virtualcam output. - + """ - response = self.base_client.req('GetVirtualCamStatus') + response = self.base_client.req("GetVirtualCamStatus") return response def ToggleVirtualCam(self): """ Toggles the state of the virtualcam output. - + """ - response = self.base_client.req('ToggleVirtualCam') + response = self.base_client.req("ToggleVirtualCam") return response def StartVirtualCam(self): @@ -1653,7 +1610,7 @@ class ReqClient(object): """ - response = self.base_client.req('StartVirtualCam') + response = self.base_client.req("StartVirtualCam") return response def StopVirtualCam(self): @@ -1662,7 +1619,7 @@ class ReqClient(object): """ - response = self.base_client.req('StopVirtualCam') + response = self.base_client.req("StopVirtualCam") return response def GetReplayBufferStatus(self): @@ -1671,16 +1628,16 @@ class ReqClient(object): """ - response = self.base_client.req('GetReplayBufferStatus') + response = self.base_client.req("GetReplayBufferStatus") return response def ToggleReplayBuffer(self): """ Toggles the state of the replay buffer output. - + """ - response = self.base_client.req('ToggleReplayBuffer') + response = self.base_client.req("ToggleReplayBuffer") return response def StartReplayBuffer(self): @@ -1689,7 +1646,7 @@ class ReqClient(object): """ - response = self.base_client.req('StartReplayBuffer') + response = self.base_client.req("StartReplayBuffer") return response def StopReplayBuffer(self): @@ -1698,7 +1655,7 @@ class ReqClient(object): """ - response = self.base_client.req('StopReplayBuffer') + response = self.base_client.req("StopReplayBuffer") return response def SaveReplayBuffer(self): @@ -1707,34 +1664,34 @@ class ReqClient(object): """ - response = self.base_client.req('SaveReplayBuffer') + response = self.base_client.req("SaveReplayBuffer") return response def GetLastReplayBufferReplay(self): """ Gets the filename of the last replay buffer save file. - + """ - response = self.base_client.req('GetLastReplayBufferReplay') + response = self.base_client.req("GetLastReplayBufferReplay") return response def GetStreamStatus(self): """ Gets the status of the stream output. - + """ - response = self.base_client.req('GetStreamStatus') + response = self.base_client.req("GetStreamStatus") return response def ToggleStream(self): """ Toggles the status of the stream output. - + """ - response = self.base_client.req('ToggleStream') + response = self.base_client.req("ToggleStream") return response def StartStream(self): @@ -1743,7 +1700,7 @@ class ReqClient(object): """ - response = self.base_client.req('StartStream') + response = self.base_client.req("StartStream") return response def StopStream(self): @@ -1752,7 +1709,7 @@ class ReqClient(object): """ - response = self.base_client.req('StopStream') + response = self.base_client.req("StopStream") return response def SendStreamCaption(self, caption): @@ -1764,16 +1721,16 @@ class ReqClient(object): """ - response = self.base_client.req('SendStreamCaption') + response = self.base_client.req("SendStreamCaption") return response def GetRecordStatus(self): """ Gets the status of the record output. - + """ - response = self.base_client.req('GetRecordStatus') + response = self.base_client.req("GetRecordStatus") return response def ToggleRecord(self): @@ -1782,7 +1739,7 @@ class ReqClient(object): """ - response = self.base_client.req('ToggleRecord') + response = self.base_client.req("ToggleRecord") return response def StartRecord(self): @@ -1791,7 +1748,7 @@ class ReqClient(object): """ - response = self.base_client.req('StartRecord') + response = self.base_client.req("StartRecord") return response def StopRecord(self): @@ -1800,7 +1757,7 @@ class ReqClient(object): """ - response = self.base_client.req('StopRecord') + response = self.base_client.req("StopRecord") return response def ToggleRecordPause(self): @@ -1809,7 +1766,7 @@ class ReqClient(object): """ - response = self.base_client.req('ToggleRecordPause') + response = self.base_client.req("ToggleRecordPause") return response def PauseRecord(self): @@ -1818,7 +1775,7 @@ class ReqClient(object): """ - response = self.base_client.req('PauseRecord') + response = self.base_client.req("PauseRecord") return response def ResumeRecord(self): @@ -1827,7 +1784,7 @@ class ReqClient(object): """ - response = self.base_client.req('ResumeRecord') + response = self.base_client.req("ResumeRecord") return response def GetMediaInputStatus(self, name): @@ -1849,8 +1806,8 @@ class ReqClient(object): """ - payload = {'inputName': name} - response = self.base_client.req('GetMediaInputStatus', payload) + payload = {"inputName": name} + response = self.base_client.req("GetMediaInputStatus", payload) return response def SetMediaInputCursor(self, name, cursor): @@ -1865,11 +1822,8 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'mediaCursor': cursor - } - response = self.base_client.req('SetMediaInputCursor', payload) + payload = {"inputName": name, "mediaCursor": cursor} + response = self.base_client.req("SetMediaInputCursor", payload) return response def OffsetMediaInputCursor(self, name, offset): @@ -1884,11 +1838,8 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'mediaCursorOffset': offset - } - response = self.base_client.req('OffsetMediaInputCursor', payload) + payload = {"inputName": name, "mediaCursorOffset": offset} + response = self.base_client.req("OffsetMediaInputCursor", payload) return response def TriggerMediaInputAction(self, name, action): @@ -1902,20 +1853,17 @@ class ReqClient(object): """ - payload = { - 'inputName': name, - 'mediaAction': action - } - response = self.base_client.req('TriggerMediaInputAction', payload) + payload = {"inputName": name, "mediaAction": action} + response = self.base_client.req("TriggerMediaInputAction", payload) return response def GetStudioModeEnabled(self): """ Gets whether studio is enabled. - + """ - response = self.base_client.req('GetStudioModeEnabled') + response = self.base_client.req("GetStudioModeEnabled") return response def SetStudioModeEnabled(self, enabled): @@ -1927,8 +1875,8 @@ class ReqClient(object): """ - payload = {'studioModeEnabled': enabled} - response = self.base_client.req('SetStudioModeEnabled', payload) + payload = {"studioModeEnabled": enabled} + response = self.base_client.req("SetStudioModeEnabled", payload) return response def OpenInputPropertiesDialog(self, name): @@ -1940,8 +1888,8 @@ class ReqClient(object): """ - payload = {'inputName': name} - response = self.base_client.req('OpenInputPropertiesDialog', payload) + payload = {"inputName": name} + response = self.base_client.req("OpenInputPropertiesDialog", payload) return response def OpenInputFiltersDialog(self, name): @@ -1953,8 +1901,8 @@ class ReqClient(object): """ - payload = {'inputName': name} - response = self.base_client.req('OpenInputFiltersDialog', payload) + payload = {"inputName": name} + response = self.base_client.req("OpenInputFiltersDialog", payload) return response def OpenInputInteractDialog(self, name): @@ -1966,15 +1914,15 @@ class ReqClient(object): """ - payload = {'inputName': name} - response = self.base_client.req('OpenInputInteractDialog', payload) + payload = {"inputName": name} + response = self.base_client.req("OpenInputInteractDialog", payload) return response def GetMonitorList(self, name): """ Gets a list of connected monitors and information about them. - + """ - response = self.base_client.req('GetMonitorList') - return response \ No newline at end of file + response = self.base_client.req("GetMonitorList") + return response From 5399b66e4511229cc0d80abc25042ed02a3ac38e Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 00:56:07 +0100 Subject: [PATCH 02/27] upd gitignore --- .gitignore | 34 ++++++++++++++---- .../__pycache__/__main__.cpython-311.pyc | Bin 0 -> 1847 bytes .../__pycache__/__main__.cpython-311.pyc | Bin 0 -> 1267 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 300 bytes .../__pycache__/baseclient.cpython-311.pyc | Bin 0 -> 4405 bytes .../__pycache__/callback.cpython-311.pyc | Bin 0 -> 3874 bytes .../__pycache__/events.cpython-311.pyc | Bin 0 -> 2237 bytes .../__pycache__/reqs.cpython-311.pyc | Bin 0 -> 65867 bytes .../__pycache__/subject.cpython-311.pyc | Bin 0 -> 3873 bytes 9 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 examples/events/__pycache__/__main__.cpython-311.pyc create mode 100644 examples/scene_rotate/__pycache__/__main__.cpython-311.pyc create mode 100644 obsstudio_sdk/__pycache__/__init__.cpython-311.pyc create mode 100644 obsstudio_sdk/__pycache__/baseclient.cpython-311.pyc create mode 100644 obsstudio_sdk/__pycache__/callback.cpython-311.pyc create mode 100644 obsstudio_sdk/__pycache__/events.cpython-311.pyc create mode 100644 obsstudio_sdk/__pycache__/reqs.cpython-311.pyc create mode 100644 obsstudio_sdk/__pycache__/subject.cpython-311.pyc diff --git a/.gitignore b/.gitignore index 7e6985d..057f050 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,31 @@ -__pycache__ -obsstudio_sdk.egg-info -dist -docs -setup.py +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST -venv +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Test/config quick.py config.toml \ No newline at end of file diff --git a/examples/events/__pycache__/__main__.cpython-311.pyc b/examples/events/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d325b3d0c93ce31d6c5a504ad563a1f117342b67 GIT binary patch literal 1847 zcmaJBOKjsrbZp1zI8EA3s#2ngV6?e-EA0WwE^3z|mMvQ@S&2#=)TEZr}O5;hMOXIDj|KKJo z=CS4QFZRP!IPN0?kyxk-Rv)3N#N(-^ED5k|r8bc!2lzpq5&Do&Gl>u`%CIHa?B~!* zPv{{Xs0ey!J0{U~><=BBzMsfJm)i*e=p<7O@oZKEQ zY+DFo6)WAj4@tvj9-+ii$wT6LY(>?k0!cTW0Go#6)D3ed!1P!vP+P8Ub|?i+y-iyU zYBY6b5|`-amf<#t70iML*|j~Lc?R`>A-AdRdV$m+o+btMb9q1a`jdP9^2RXx!7~Y1&pCg>(cfvL%nWDV zKDhVIhF^a3X?gXiygDkc`sG#N!W!Wvp34`*&2EPprcUtb0a)(GGB@$F%}!k8;snm3~hkJ0rY13lWZIu56^y8cPW za3TrKfzdGSlt1Zv6kd+r$mf7^l!DFP1=LS|6H%7mnVn;rX8qXvj(2S-8xicddNN^0FLJRzEc0K$eyyw_3?v#Ix0t_1A$1#v? zx7~rUHk%fm2hmAHFYuix3v2Wh5b(ZZKLPr8h!&GIN#_K3`$hDO^PZmwRFujNx<^uR zg3>}^tP}@xzET`1i@vhhzdN2?80>y2e%{;b^*4UYzx2iYaN))%zvSnahMA?ob)Kxk kc(wppQVQd2ey~1~QDNaywNN)g3b+dWO)%UKd0~A20ZQJSs{jB1 literal 0 HcmV?d00001 diff --git a/examples/scene_rotate/__pycache__/__main__.cpython-311.pyc b/examples/scene_rotate/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7571f193ad6c60314449ec47d1f3a5c322caefe6 GIT binary patch literal 1267 zcmZWo&1(}u6o0#$k4c(Uv|25TGzby*5=47ZO2wvB!4kwkTT3*g*`eJwU(QVYC@HMa zLxmi?NDC_TR1_~BJoHcSn3OnO7}!9p*Lav$Pe@lNl;db=ltu-$CYMM}YYNTD5+i zf*DiZHGMy&XEN9IM<$3@!^^K?Wt$*+Y}=^$fAwcy1JSJoK*(%=m!+&j6)bhQqX0|m z;eE&aB=qOp!FNA)-|m;zl&Ufb4=2`C5$Yxe%U@OBz&o|ySLsL7Q|EDH_5T?C*t*=s zV+H=rT}xPy0yCJ*bxXxS*~)8p`6VQGLBW<*;bBCUZNV|*8Q4@5sOfSvrs{@xjuvS$ zTcF7gQdZ+vT*cyY;?d~g^{E@_+qPJ$ETl^doQsNOmonU1Nz-T9LOD-)+6&LHlE@00 z&SVN%yO_zu%g+x(V~d#hS(%Nmo|D@6SiY3Y=J|NMYapF!f4~oLpl;0KdH;Cp*4E0~ zH^KhN6c@dKU3}$z}H(ko Hm-qA^$c-O@ literal 0 HcmV?d00001 diff --git a/obsstudio_sdk/__pycache__/__init__.cpython-311.pyc b/obsstudio_sdk/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6cf5468f34ab673b96f8c9caf5b67e24a30d9143 GIT binary patch literal 300 zcmZ3+%ge<81os5*CuITY#~=<2FhLo`YCy(xh7^Vr#vF!R#wbQc5SuB7DVI5l8OUZ% zVM%9-Vo6~QX3%7N$p}=e$#{#$wJbHSq}Vwp6G#F1oI$CD5Ka*@P(=~Qa6e7fTWqOd zbzuH2mZH?cVzB%z_V{>5AD{U6l??1zXO?7?CdK3@6&IJ3rex;F7pG*S z@#5n%^D;}~V|(Y` zkm5Q~9(XV)Qem4a0bH%OLKH+Fc&sW_V&D2`qbsd-rD#)CRm+=EsX(X?J#(F0-^-7d zzI5*R%$b=pXa2r(&iIbc=S7enyz@=`uQEb^qm}I>Y%y9Km^FkEW>P3pFPR8q)!7IO zC7a^r_z2I?J}xE9i4oD_nUoZfEIBzMvuGS){xZUX>bb)qbRT~86!Bu_G*ZOJv^u3Q zCjXO|J_DshVriUA>xnHvt6;Y(+AuI{NJVrhunguhig1_(%3}_wfJH1^W+EcyLAhjl zPQ|s4Qb`rmF%Eqq@J;a3SWpbR1T~lqqdU-a-xgbB^K72UNeC@6d8TByI~n(gF#0rj z4m2)ui@a{{6rnuIb9vsqe+I2|=F#l-#t`vC%^pJ}sPWq~X-D#?w0$$%jaKQNe^JN_ znCrGB@+fNShaTS5W6uRWf~&`#SNGYf&@7xoJ-O$EIRHcT1Z%eWl4rZsSz^(-=UWKV zecCIzg?`K5fqS}d-xY)Oj>h;|dS_O0&D6Zz>MY%>bZdn*+dIPufFe)1;6V={06%R9 zkTsNq3;a}|Ai(I??233A-D9TiA%!*B(EBELLA|8WWA5*N@x6~n&y6Sbnfds+OkC6S zd7R8dHGJWvT0Ewy2@92>?4=jpF`P{3iOgK~Xl}2K*HAQ?OeghdG&GjVBw{J;Xs97a zSFiN|Sw`iq$$Bnn>w$G_SN{dQNa0L=CZp*lpUn{6l(R8STgVVh=#desh4_KwgavK% zz4u-)ql(`YlDaylnUbdJFlz1n!Ki_rcLp}LX4c(OdnRK zWAiC}tIE@bnB2u!%H*j|O@u}FTZvGZnez5w<=S68K)P4$KR}jIwSHKk2{@)o*QC)k zS@2cc`bxn{+fYHQcJKP4?`GePzG7buF`hjZxpKS^zP5X#IdCn!K3L)^&4WhsVA;ym z)*Y*!8sa_s*KsjvF?Yj%IB7A}KX?RzTiLq_{hfnSlTa`TGxwS4%Ie7M~8VTr%(fdVqjalFn|GJ1LR^8*t%*VD zrrqcO()b1T7u+Xkfnm@mh=I@PTbzCoES{grH35{SljlPgidhmjy<2!qX3~m8=t@m4 zv4BmU+NUX4z&H6!R!y4{9h6GOO`d{U;mA%ZEl(#?Y8Jwn$xLf>%>Ypgprjotxo!tG z=~N9PqNBB2K$g))VCU-L>!YiqC0`}5zaVdPbYDNadKLiT-SD+t8(t4rd;^AWfPzyg z{dA*yPqDWYyRoad3otZM?b*%)v4KU0!LK9{kyd%uz zU{uM5&z2IaaD&qgtCAhi-gX24-!=@Vk-T9?Ovw(0Z=T_;_UZ`S|F6Bi&Ds}P3{QtV zi^U>~r6Rk?!ES7$FIkUF=gujwcQ#sP?ZBPq+;`)yS%%!uYlrTJCiK$N%?jxzgH3;I zUY~*2UosKX!K@+7#8MEa5;L(>N==_vAJYqkK<*{k#95VSAVc;;(>;jonJn2yd;O+J z1NO*4(-R*#sHPJcteVnHOq-z&K-6Yp9~>I7;wYgZ*%Y@#M4UXYLhKc>N{0~okfCud zjR`#jQ^50cS&dLkD}3F7i102T5J-8tY*Wg{E?F^?S_Zh1x-T)C9eYx9!G=?DwtokP zXcFuWJWAVu(KfKr+_N&Zxua|KWGQS69(s^421mrOmo~i6O^G$2d=icky zRqqpt_xYb9AkSpv>%2N~WnyLGNt4*_eTsk-#DBM<)_$X9*OtznwNsy;x_)Z)R5jRR z1c&Z&MsQzw{|}7d(Lesu2u?oXn2r<7KWyzDfoccx%-hiJlUhMLkY|3>8n~)m(N?rt z3-b7C?Wkwx1LonG^1uYm?URrT(q=t4DvxC4H)mEOC8^ReY;+7)WW|t`vaD3WbuIjA z>FUyzrIn?!{Eij>rV@4_f#abb!}3r zm$z)gk_Owby+_FogRllW=?;(N@P^-F8?I5d-*nCPKl?`d-)ZVG9Y)>Ez3tZI6;D#x+i9**fT@&OD`Sn^{!_#?_ogr2eme{zI!6#Zn3k}gV|1|Wmd zs7}>e9Ht6?2tu0M1UL}O`5MTF>i4PCO(5{fYYNnm&*R?=c5d!|cVl4qZrj~(W#E7@ zaG<*T-I~M)+u@A{M36fDH3XzEUKp?KK%HIHJ^TMW{_xmCy)t&f7&}q!I0?BXa8wQm zNXdXn`Yd}R@-|w!A!rU3hrS5i4BZG7LzNcAXrY+2Fi7YX!9P=YvZiBmYBXwkqtUqx zo=;J}IU4PvfgA&=Gff0RstC^ZtT0Oa|yul`k$f7$w~sA<{ys;H~%&Q;V? fcIVB;sMa#t$~EyQ0~>smToa$r)~B!OviAO8W4w_W literal 0 HcmV?d00001 diff --git a/obsstudio_sdk/__pycache__/callback.cpython-311.pyc b/obsstudio_sdk/__pycache__/callback.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..77dad5cb1c9b3df1e492eb459979ef7d9a28760c GIT binary patch literal 3874 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 Date: Tue, 26 Jul 2022 01:03:57 +0100 Subject: [PATCH 03/27] refreshed ignored files --- .gitignore | 5 + build/lib/obsstudio_sdk/__init__.py | 4 - build/lib/obsstudio_sdk/baseclient.py | 71 - build/lib/obsstudio_sdk/events.py | 43 - build/lib/obsstudio_sdk/reqs.py | 1928 ----------------- build/lib/obsstudio_sdk/subject.py | 58 - .../__pycache__/__main__.cpython-311.pyc | Bin 1847 -> 0 bytes .../__pycache__/__main__.cpython-311.pyc | Bin 1267 -> 0 bytes .../__pycache__/__init__.cpython-311.pyc | Bin 300 -> 0 bytes .../__pycache__/baseclient.cpython-311.pyc | Bin 4405 -> 0 bytes .../__pycache__/callback.cpython-311.pyc | Bin 3874 -> 0 bytes .../__pycache__/events.cpython-311.pyc | Bin 2237 -> 0 bytes .../__pycache__/reqs.cpython-311.pyc | Bin 65867 -> 0 bytes .../__pycache__/subject.cpython-311.pyc | Bin 3873 -> 0 bytes 14 files changed, 5 insertions(+), 2104 deletions(-) delete mode 100644 build/lib/obsstudio_sdk/__init__.py delete mode 100644 build/lib/obsstudio_sdk/baseclient.py delete mode 100644 build/lib/obsstudio_sdk/events.py delete mode 100644 build/lib/obsstudio_sdk/reqs.py delete mode 100644 build/lib/obsstudio_sdk/subject.py delete mode 100644 examples/events/__pycache__/__main__.cpython-311.pyc delete mode 100644 examples/scene_rotate/__pycache__/__main__.cpython-311.pyc delete mode 100644 obsstudio_sdk/__pycache__/__init__.cpython-311.pyc delete mode 100644 obsstudio_sdk/__pycache__/baseclient.cpython-311.pyc delete mode 100644 obsstudio_sdk/__pycache__/callback.cpython-311.pyc delete mode 100644 obsstudio_sdk/__pycache__/events.cpython-311.pyc delete mode 100644 obsstudio_sdk/__pycache__/reqs.cpython-311.pyc delete mode 100644 obsstudio_sdk/__pycache__/subject.cpython-311.pyc diff --git a/.gitignore b/.gitignore index 057f050..a04522a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + # Distribution / packaging .Python build/ diff --git a/build/lib/obsstudio_sdk/__init__.py b/build/lib/obsstudio_sdk/__init__.py deleted file mode 100644 index 7c892fc..0000000 --- a/build/lib/obsstudio_sdk/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .events import EventsClient -from .reqs import ReqClient - -__ALL__ = ["ReqClient", "EventsClient"] diff --git a/build/lib/obsstudio_sdk/baseclient.py b/build/lib/obsstudio_sdk/baseclient.py deleted file mode 100644 index cc403f8..0000000 --- a/build/lib/obsstudio_sdk/baseclient.py +++ /dev/null @@ -1,71 +0,0 @@ -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()) diff --git a/build/lib/obsstudio_sdk/events.py b/build/lib/obsstudio_sdk/events.py deleted file mode 100644 index 3e007b0..0000000 --- a/build/lib/obsstudio_sdk/events.py +++ /dev/null @@ -1,43 +0,0 @@ -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 diff --git a/build/lib/obsstudio_sdk/reqs.py b/build/lib/obsstudio_sdk/reqs.py deleted file mode 100644 index 924682e..0000000 --- a/build/lib/obsstudio_sdk/reqs.py +++ /dev/null @@ -1,1928 +0,0 @@ -from .baseclient import ObsClient - -""" -A class to interact with obs-websocket requests -defined in official github repo -https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests -""" - - -class ReqClient(object): - def __init__(self, **kwargs): - self.base_client = ObsClient(**kwargs) - self.base_client.authenticate() - - def GetVersion(self): - """ - Gets data about the current plugin and RPC version. - - :return: The version info as a dictionary - :rtype: dict - - - """ - response = self.base_client.req("GetVersion") - return response - - def GetStats(self): - """ - Gets statistics about OBS, obs-websocket, and the current session. - - :return: The stats info as a dictionary - :rtype: dict - - - """ - response = self.base_client.req("GetStats") - return response - - def BroadcastCustomEvent(self, eventData): - """ - Broadcasts a CustomEvent to all WebSocket clients. Receivers are clients which are identified and subscribed. - - :param eventData: Data payload to emit to all receivers - :type eventData: object - :return: empty response - :rtype: str - - - """ - req_data = eventData - response = self.base_client.req("BroadcastCustomEvent", req_data) - return response - - def CallVendorRequest(self, vendorName, requestType, requestData=None): - """ - Call a request registered to a vendor. - - A vendor is a unique name registered by a - third-party plugin or script, which allows - for custom requests and events to be added - to obs-websocket. If a plugin or script - implements vendor requests or events, - documentation is expected to be provided with them. - - :param vendorName: Name of the vendor to use - :type vendorName: str - :param requestType: The request type to call - :type requestType: str - :param requestData: Object containing appropriate request data - :type requestData: dict, optional - :return: responseData - :rtype: dict - - - """ - response = self.base_client.req(req_type=requestType, req_data=requestData) - return response - - def GetHotkeyList(self): - """ - Gets an array of all hotkey names in OBS - - :return: hotkeys - :rtype: list[str] - - - """ - response = self.base_client.req("GetHotkeyList") - return response - - def TriggerHotkeyByName(self, hotkeyName): - """ - Triggers a hotkey using its name. For hotkey names - See GetHotkeyList - - :param hotkeyName: Name of the hotkey to trigger - :type hotkeyName: str - - - """ - payload = {"hotkeyName": hotkeyName} - response = self.base_client.req("TriggerHotkeyByName", payload) - return response - - def TriggerHotkeyByKeySequence( - self, keyId, pressShift, pressCtrl, pressAlt, pressCmd - ): - """ - 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 - :type keyId: str - :param keyModifiers: Object containing key modifiers to apply. - :type keyModifiers: dict - :param keyModifiers.shift: Press Shift - :type keyModifiers.shift: bool - :param keyModifiers.control: Press CTRL - :type keyModifiers.control: bool - :param keyModifiers.alt: Press ALT - :type keyModifiers.alt: bool - :param keyModifiers.cmd: Press CMD (Mac) - :type keyModifiers.cmd: bool - - - """ - payload = { - "keyId": keyId, - "keyModifiers": { - "shift": pressShift, - "control": pressCtrl, - "alt": pressAlt, - "cmd": pressCmd, - }, - } - - response = self.base_client.req("TriggerHotkeyByKeySequence", payload) - return response - - def Sleep(self, sleepMillis=None, sleepFrames=None): - """ - Sleeps for a time duration or number of frames. - Only available in request batches with types SERIAL_REALTIME or SERIAL_FRAME - - :param sleepMillis: Number of milliseconds to sleep for (if SERIAL_REALTIME mode) 0 <= sleepMillis <= 50000 - :type sleepMillis: int - :param sleepFrames: Number of frames to sleep for (if SERIAL_FRAME mode) 0 <= sleepFrames <= 10000 - :type sleepFrames: int - - - """ - payload = {"sleepMillis": sleepMillis, "sleepFrames": sleepFrames} - response = self.base_client.req("Sleep", payload) - return response - - def GetPersistentData(self, realm, slotName): - """ - Gets the value of a "slot" from the selected persistent data realm. - - :param realm: The data realm to select - OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE - :type realm: str - :param slotName: The name of the slot to retrieve data from - :type slotName: str - :return: slotValue Value associated with the slot - :rtype: any - - - """ - payload = {"realm": realm, "slotName": slotName} - response = self.base_client.req("GetPersistentData", payload) - return response - - def SetPersistentData(self, realm, slotName, slotValue): - """ - Sets the value of a "slot" from the selected persistent data realm. - - :param realm: The data realm to select. - OBS_WEBSOCKET_DATA_REALM_GLOBAL or OBS_WEBSOCKET_DATA_REALM_PROFILE - :type realm: str - :param slotName: The name of the slot to retrieve data from - :type slotName: str - :param slotValue: The value to apply to the slot - :type slotValue: any - - - """ - payload = {"realm": realm, "slotName": slotName, "slotValue": slotValue} - response = self.base_client.req("SetPersistentData", payload) - return response - - def GetSceneCollectionList(self): - """ - Gets an array of all scene collections - - :return: sceneCollections - :rtype: list[str] - - - """ - response = self.base_client.req("GetSceneCollectionList") - return response - - def SetCurrentSceneCollection(self, name): - """ - Creates a new scene collection, switching to it in the process - Note: This will block until the collection has finished changing - - :param name: Name of the scene collection to switch to - :type name: str - - - """ - payload = {"sceneCollectionName": name} - response = self.base_client.req("SetCurrentSceneCollection", payload) - return response - - def CreateSceneCollection(self, name): - """ - Creates a new scene collection, switching to it in the process. - Note: This will block until the collection has finished changing. - - :param name: Name for the new scene collection - :type name: str - - - """ - payload = {"sceneCollectionName": name} - response = self.base_client.req("CreateSceneCollection", payload) - return response - - def GetProfileList(self): - """ - Gets a list of all profiles - - :return: profiles (List of all profiles) - :rtype: list[str] - - - """ - response = self.base_client.req("GetProfileList") - return response - - def SetCurrentProfile(self, name): - """ - Switches to a profile - - :param name: Name of the profile to switch to - :type name: str - - - """ - payload = {"profileName": name} - response = self.base_client.req("SetCurrentProfile", payload) - return response - - def CreateProfile(self, name): - """ - Creates a new profile, switching to it in the process - - :param name: Name for the new profile - :type name: str - - - """ - payload = {"profileName": name} - response = self.base_client.req("CreateProfile", payload) - return response - - def RemoveProfile(self, name): - """ - Removes a profile. If the current profile is chosen, - it will change to a different profile first. - - :param name: Name of the profile to remove - :type name: str - - - """ - payload = {"profileName": name} - response = self.base_client.req("RemoveProfile", payload) - return response - - def GetProfileParameter(self, category, name): - """ - Gets a parameter from the current profile's configuration.. - - :param category: Category of the parameter to get - :type category: str - :param name: Name of the parameter to get - :type name: str - - :return: Value and default value for the parameter - :rtype: str - - - """ - payload = {"parameterCategory": category, "parameterName": name} - response = self.base_client.req("GetProfileParameter", payload) - return response - - def SetProfileParameter(self, category, name, value): - """ - Gets a parameter from the current profile's configuration.. - - :param category: Category of the parameter to set - :type category: str - :param name: Name of the parameter to set - :type name: str - :param value: Value of the parameter to set. Use null to delete - :type value: str - - :return: Value and default value for the parameter - :rtype: str - - - """ - payload = { - "parameterCategory": category, - "parameterName": name, - "parameterValue": value, - } - response = self.base_client.req("SetProfileParameter", payload) - return response - - def GetVideoSettings(self): - """ - Gets the current video settings. - Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. - Example: 60000/1001 - - - """ - response = self.base_client.req("GetVideoSettings") - return response - - def SetVideoSettings( - self, numerator, denominator, base_width, base_height, out_width, out_height - ): - """ - Sets the current video settings. - Note: Fields must be specified in pairs. - For example, you cannot set only baseWidth without needing to specify baseHeight. - - :param numerator: Numerator of the fractional FPS value >=1 - :type numerator: int - :param denominator: Denominator of the fractional FPS value >=1 - :type denominator: int - :param base_width: Width of the base (canvas) resolution in pixels (>= 1, <= 4096) - :type base_width: int - :param base_height: Height of the base (canvas) resolution in pixels (>= 1, <= 4096) - :type base_height: int - :param out_width: Width of the output resolution in pixels (>= 1, <= 4096) - :type out_width: int - :param out_height: Height of the output resolution in pixels (>= 1, <= 4096) - :type out_height: int - - - """ - payload = { - "fpsNumerator": numerator, - "fpsDenominator": denominator, - "baseWidth": base_width, - "baseHeight": base_height, - "outputWidth": out_width, - "outputHeight": out_height, - } - response = self.base_client.req("SetVideoSettings", payload) - return response - - def GetStreamServiceSettings(self): - """ - Gets the current stream service settings (stream destination). - - - """ - response = self.base_client.req("GetStreamServiceSettings") - return response - - def SetStreamServiceSettings(self, ss_type, ss_settings): - """ - Sets the current stream service settings (stream destination). - Note: Simple RTMP settings can be set with type rtmp_custom - and the settings fields server and key. - - :param ss_type: Type of stream service to apply. Example: rtmp_common or rtmp_custom - :type ss_type: string - :param ss_setting: Settings to apply to the service - :type ss_setting: dict - - - """ - payload = { - "streamServiceType": ss_type, - "streamServiceSettings": ss_settings, - } - response = self.base_client.req("SetStreamServiceSettings", payload) - return response - - def GetSourceActive(self, name): - """ - Gets the active and show state of a source - - :param name: Name of the source to get the active state of - :type name: str - - - """ - payload = {"sourceName": name} - response = self.base_client.req("GetSourceActive", payload) - return response - - def GetSourceScreenshot(self, name, img_format, width, height, quality): - """ - Gets a Base64-encoded screenshot of a source. - The imageWidth and imageHeight parameters are - treated as "scale to inner", meaning the smallest ratio - will be used and the aspect ratio of the original resolution is kept. - If imageWidth and imageHeight are not specified, the compressed image - will use the full resolution of the source. - - :param name: Name of the source to take a screenshot of - :type name: str - :param format: Image compression format to use. Use GetVersion to get compatible image formats - :type format: str - :param width: Width to scale the screenshot to (>= 8, <= 4096) - :type width: int - :param height: Height to scale the screenshot to (>= 8, <= 4096) - :type height: int - :param quality: Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" - :type quality: int - - - """ - payload = { - "sourceName": name, - "imageFormat": img_format, - "imageWidth": width, - "imageHeight": height, - "imageCompressionQuality": quality, - } - response = self.base_client.req("GetSourceScreenshot", payload) - return response - - def SaveSourceScreenshot(self, name, img_format, file_path, width, height, quality): - """ - Saves a Base64-encoded screenshot of a source. - The imageWidth and imageHeight parameters are - treated as "scale to inner", meaning the smallest ratio - will be used and the aspect ratio of the original resolution is kept. - If imageWidth and imageHeight are not specified, the compressed image - will use the full resolution of the source. - - :param name: Name of the source to take a screenshot of - :type name: str - :param format: Image compression format to use. Use GetVersion to get compatible image formats - :type format: str - :param file_path: Path to save the screenshot file to. Eg. C:\\Users\\user\\Desktop\\screenshot.png - :type file_path: str - :param width: Width to scale the screenshot to (>= 8, <= 4096) - :type width: int - :param height: Height to scale the screenshot to (>= 8, <= 4096) - :type height: int - :param quality: Compression quality to use. 0 for high compression, 100 for uncompressed. -1 to use "default" - :type quality: int - - - """ - payload = { - "sourceName": name, - "imageFormat": img_format, - "imageFilePath": file_path, - "imageWidth": width, - "imageHeight": height, - "imageCompressionQuality": quality, - } - response = self.base_client.req("SaveSourceScreenshot", payload) - return response - - def GetSceneList(self): - """ - Gets a list of all scenes in OBS. - - - """ - response = self.base_client.req("GetSceneList") - return response - - def GetGroupList(self): - """ - Gets a list of all groups in OBS. - Groups in OBS are actually scenes, - but renamed and modified. In obs-websocket, - we treat them as scenes where we can.. - - - """ - response = self.base_client.req("GetSceneList") - return response - - def GetCurrentProgramScene(self): - """ - Gets the current program scene. - - - """ - response = self.base_client.req("GetCurrentProgramScene") - return response - - def SetCurrentProgramScene(self, name): - """ - Sets the current program scene - - :param name: Scene to set as the current program scene - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("SetCurrentProgramScene", payload) - return response - - def GetCurrentPreviewScene(self): - """ - Gets the current preview scene - - - """ - response = self.base_client.req("GetCurrentPreviewScene") - return response - - def SetCurrentPreviewScene(self, name): - """ - Sets the current program scene - - :param name: Scene to set as the current preview scene - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("SetCurrentPreviewScene", payload) - return response - - def CreateScene(self, name): - """ - Creates a new scene in OBS. - - :param name: Name for the new scene - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("CreateScene", payload) - return response - - def RemoveScene(self, name): - """ - Removes a scene from OBS - - :param name: Name of the scene to remove - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("RemoveScene", payload) - return response - - def SetSceneName(self, old_name, new_name): - """ - Sets the name of a scene (rename). - - :param old_name: Name of the scene to be renamed - :type old_name: str - :param new_name: New name for the scene - :type new_name: str - - - """ - payload = {"sceneName": old_name, "newSceneName": new_name} - response = self.base_client.req("SetSceneName", payload) - return response - - def GetSceneSceneTransitionOverride(self, name): - """ - Gets the scene transition overridden for a scene. - - :param name: Name of the scene - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("GetSceneSceneTransitionOverride", payload) - return response - - def SetSceneSceneTransitionOverride(self, scene_name, tr_name, tr_duration): - """ - Gets the scene transition overridden for a scene. - - :param scene_name: Name of the scene - :type scene_name: str - :param tr_name: Name of the scene transition to use as override. Specify null to remove - :type tr_name: str - :param tr_duration: Duration to use for any overridden transition. Specify null to remove (>= 50, <= 20000) - :type tr_duration: int - - - """ - payload = { - "sceneName": scene_name, - "transitionName": tr_name, - "transitionDuration": tr_duration, - } - response = self.base_client.req("SetSceneSceneTransitionOverride", payload) - return response - - def GetInputList(self, kind): - """ - Gets a list of all inputs in OBS. - - :param kind: Restrict the list to only inputs of the specified kind - :type kind: str - - - """ - payload = {"inputKind": kind} - response = self.base_client.req("GetInputList", payload) - return response - - def GetInputKindList(self, unversioned): - """ - Gets a list of all available input kinds in OBS. - - :param unversioned: True == Return all kinds as unversioned, False == Return with version suffixes (if available) - :type unversioned: bool - - - """ - payload = {"unversioned": unversioned} - response = self.base_client.req("GetInputKindList", payload) - return response - - def GetSpecialInputs(self): - """ - Gets the name of all special inputs. - - - """ - response = self.base_client.req("GetSpecialInputs") - return response - - def CreateInput( - self, sceneName, inputName, inputKind, inputSettings, sceneItemEnabled - ): - """ - Creates a new input, adding it as a scene item to the specified scene. - - :param sceneName: Name of the scene to add the input to as a scene item - :type sceneName: str - :param inputName Name of the new input to created - :type inputName: str - :param inputKind: The kind of input to be created - :type inputKind: str - :param inputSettings: Settings object to initialize the input with - :type inputSettings: object - :param sceneItemEnabled: Whether to set the created scene item to enabled or disabled - :type sceneItemEnabled: bool - - - """ - payload = { - "sceneName": sceneName, - "inputName": inputName, - "inputKind": inputKind, - "inputSettings": inputSettings, - "sceneItemEnabled": sceneItemEnabled, - } - response = self.base_client.req("CreateInput", payload) - return response - - def RemoveInput(self, name): - """ - Removes an existing input - - :param name: Name of the input to remove - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("RemoveInput", payload) - return response - - def SetInputName(self, old_name, new_name): - """ - Sets the name of an input (rename). - - :param old_name: Current input name - :type old_name: str - :param new_name: New name for the input - :type new_name: str - - - """ - payload = {"inputName": old_name, "newInputName": new_name} - response = self.base_client.req("SetInputName", payload) - return response - - def GetInputDefaultSettings(self, kind): - """ - Gets the default settings for an input kind. - - :param kind: Input kind to get the default settings for - :type kind: str - - - """ - payload = {"inputKind": kind} - response = self.base_client.req("GetInputDefaultSettings", payload) - return response - - def GetInputSettings(self, name): - """ - Gets the settings of an input. - Note: Does not include defaults. To create the entire settings object, - overlay inputSettings over the defaultInputSettings provided by GetInputDefaultSettings. - - :param name: Input kind to get the default settings for - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputSettings", payload) - return response - - def SetInputSettings(self, name, settings, overlay): - """ - Sets the settings of an input. - - :param name: Name of the input to set the settings of - :type name: str - :param settings: Object of settings to apply - :type settings: dict - :param overlay: True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. - :type overlay: bool - - - """ - payload = {"inputName": name, "inputSettings": settings, "overlay": overlay} - response = self.base_client.req("SetInputSettings", payload) - return response - - def GetInputMute(self, name): - """ - Gets the audio mute state of an input - - :param name: Name of input to get the mute state of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputMute", payload) - return response - - def SetInputMute(self, name, muted): - """ - Sets the audio mute state of an input. - - :param name: Name of the input to set the mute state of - :type name: str - :param muted: Whether to mute the input or not - :type muted: bool - - - """ - payload = {"inputName": name, "inputMuted": muted} - response = self.base_client.req("SetInputMute", payload) - return response - - def ToggleInputMute(self, name): - """ - Toggles the audio mute state of an input. - - :param name: Name of the input to toggle the mute state of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("ToggleInputMute", payload) - return response - - def GetInputVolume(self, name): - """ - Gets the current volume setting of an input. - - :param name: Name of the input to get the volume of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputVolume", payload) - return response - - def SetInputVolume(self, name, vol_mul=None, vol_db=None): - """ - Sets the volume setting of an input. - - :param name: Name of the input to set the volume of - :type name: str - :param vol_mul: Volume setting in mul (>= 0, <= 20) - :type vol_mul: int - :param vol_db: Volume setting in dB (>= -100, <= 26) - :type vol_db: int - - - """ - payload = { - "inputName": name, - "inputVolumeMul": vol_mul, - "inputVolumeDb": vol_db, - } - response = self.base_client.req("SetInputVolume", payload) - return response - - def GetInputAudioBalance(self, name): - """ - Gets the audio balance of an input. - - :param name: Name of the input to get the audio balance of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputAudioBalance", payload) - return response - - def SetInputAudioBalance(self, name, balance): - """ - Sets the audio balance of an input. - - :param name: Name of the input to get the audio balance of - :type name: str - :param balance: New audio balance value (>= 0.0, <= 1.0) - :type balance: int - - - """ - payload = {"inputName": name, "inputAudioBalance": balance} - response = self.base_client.req("SetInputAudioBalance", payload) - return response - - def GetInputAudioOffset(self, name): - """ - Gets the audio sync offset of an input. - - :param name: Name of the input to get the audio sync offset of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputAudioOffset", payload) - return response - - def SetInputAudioSyncOffset(self, name, offset): - """ - Sets the audio sync offset of an input. - - :param name: Name of the input to set the audio sync offset of - :type name: str - :param offset: New audio sync offset in milliseconds (>= -950, <= 20000) - :type offset: int - - - """ - payload = {"inputName": name, "inputAudioSyncOffset": offset} - response = self.base_client.req("SetInputAudioSyncOffset", payload) - return response - - def GetInputAudioMonitorType(self, name): - """ - Gets the audio monitor type of an input. - - The available audio monitor types are: - OBS_MONITORING_TYPE_NONE - OBS_MONITORING_TYPE_MONITOR_ONLY - OBS_MONITORING_TYPE_MONITOR_AND_OUTPUT - - - :param name: Name of the input to get the audio monitor type of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputAudioMonitorType", payload) - return response - - def SetInputAudioMonitorType(self, name, mon_type): - """ - Sets the audio monitor type of an input. - - :param name: Name of the input to set the audio monitor type of - :type name: str - :param mon_type: Audio monitor type - :type mon_type: int - - - """ - payload = {"inputName": name, "monitorType": mon_type} - response = self.base_client.req("SetInputAudioMonitorType", payload) - return response - - def GetInputAudioTracks(self, name): - """ - Gets the enable state of all audio tracks of an input. - - :param name: Name of the input - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetInputAudioTracks", payload) - return response - - def SetInputAudioTracks(self, name, track): - """ - Sets the audio monitor type of an input. - - :param name: Name of the input - :type name: str - :param track: Track settings to apply - :type track: int - - - """ - payload = {"inputName": name, "inputAudioTracks": track} - response = self.base_client.req("SetInputAudioTracks", payload) - return response - - def GetInputPropertiesListPropertyItems(self, input_name, prop_name): - """ - Gets the items of a list property from an input's properties. - Note: Use this in cases where an input provides a dynamic, - selectable list of items. For example, display capture, - where it provides a list of available displays. - - :param input_name: Name of the input - :type input_name: str - :param prop_name: Name of the list property to get the items of - :type prop_name: str - - - """ - payload = {"inputName": input_name, "propertyName": prop_name} - response = self.base_client.req("GetInputPropertiesListPropertyItems", payload) - return response - - def PressInputPropertiesButton(self, input_name, prop_name): - """ - Presses a button in the properties of an input. - Note: Use this in cases where there is a button - in the properties of an input that cannot be accessed in any other way. - For example, browser sources, where there is a refresh button. - - :param input_name: Name of the input - :type input_name: str - :param prop_name: Name of the button property to press - :type prop_name: str - - - """ - payload = {"inputName": input_name, "propertyName": prop_name} - response = self.base_client.req("PressInputPropertiesButton", payload) - return response - - def GetTransitionKindList(self): - """ - Gets an array of all available transition kinds. - Similar to GetInputKindList - - - """ - response = self.base_client.req("GetTransitionKindList") - return response - - def GetSceneTransitionList(self): - """ - Gets an array of all scene transitions in OBS. - - - """ - response = self.base_client.req("GetSceneTransitionList") - return response - - def GetCurrentSceneTransition(self): - """ - Gets an array of all scene transitions in OBS. - - - """ - response = self.base_client.req("GetCurrentSceneTransition") - return response - - def SetCurrentSceneTransition(self, name): - """ - Sets the current scene transition. - Small note: While the namespace of scene transitions is generally unique, - that uniqueness is not a guarantee as it is with other resources like inputs. - - :param name: Name of the transition to make active - :type name: str - - - """ - payload = {"transitionName": name} - response = self.base_client.req("SetCurrentSceneTransition", payload) - return response - - def SetCurrentSceneTransitionDuration(self, duration): - """ - Sets the duration of the current scene transition, if it is not fixed. - - :param duration: Duration in milliseconds (>= 50, <= 20000) - :type duration: str - - - """ - payload = {"transitionDuration": duration} - response = self.base_client.req("SetCurrentSceneTransitionDuration", payload) - return response - - def SetCurrentSceneTransitionSettings(self, settings, overlay=None): - """ - Sets the settings of the current scene transition. - - :param settings: Settings object to apply to the transition. Can be {} - :type settings: dict - :param overlay: Whether to overlay over the current settings or replace them - :type overlay: bool - - - """ - payload = {"transitionSettings": settings, "overlay": overlay} - response = self.base_client.req("SetCurrentSceneTransitionSettings", payload) - return response - - def GetCurrentSceneTransitionCursor(self): - """ - Gets the cursor position of the current scene transition. - Note: transitionCursor will return 1.0 when the transition is inactive. - - - """ - response = self.base_client.req("GetCurrentSceneTransitionCursor") - return response - - def TriggerStudioModeTransition(self): - """ - Triggers the current scene transition. - Same functionality as the Transition button in studio mode. - Note: Studio mode should be active. if not throws an - RequestStatus::StudioModeNotActive (506) in response - - - """ - response = self.base_client.req("TriggerStudioModeTransition") - return response - - def SetTBarPosition(self, pos, release=None): - """ - Sets the position of the TBar. - Very important note: This will be deprecated - and replaced in a future version of obs-websocket. - - :param pos: New position (>= 0.0, <= 1.0) - :type pos: float - :param release: Whether to release the TBar. Only set false if you know that you will be sending another position update - :type release: bool - - - """ - payload = {"position": pos, "release": release} - response = self.base_client.req("SetTBarPosition", payload) - return response - - def GetSourceFilterList(self, name): - """ - Gets a list of all of a source's filters. - - :param name: Name of the source - :type name: str - - - """ - payload = {"sourceName": name} - response = self.base_client.req("GetSourceFilterList", payload) - return response - - def GetSourceFilterDefaultSettings(self, kind): - """ - Gets the default settings for a filter kind. - - :param kind: Filter kind to get the default settings for - :type kind: str - - - """ - payload = {"filterKind": kind} - response = self.base_client.req("GetSourceFilterDefaultSettings", payload) - return response - - def CreateSourceFilter( - self, source_name, filter_name, filter_kind, filter_settings=None - ): - """ - Gets the default settings for a filter kind. - - :param source_name: Name of the source to add the filter to - :type source_name: str - :param filter_name: Name of the new filter to be created - :type filter_name: str - :param filter_kind: The kind of filter to be created - :type filter_kind: str - :param filter_settings: Settings object to initialize the filter with - :type filter_settings: dict - - - """ - payload = { - "sourceName": source_name, - "filterName": filter_name, - "filterKind": filter_kind, - "filterSettings": filter_settings, - } - response = self.base_client.req("CreateSourceFilter", payload) - return response - - def RemoveSourceFilter(self, source_name, filter_name): - """ - Gets the default settings for a filter kind. - - :param source_name: Name of the source the filter is on - :type source_name: str - :param filter_name: Name of the filter to remove - :type filter_name: str - - - """ - payload = { - "sourceName": source_name, - "filterName": filter_name, - } - response = self.base_client.req("RemoveSourceFilter", payload) - return response - - def SetSourceFilterName(self, source_name, old_filter_name, new_filter_name): - """ - Sets the name of a source filter (rename). - - :param source_name: Name of the source the filter is on - :type source_name: str - :param old_filter_name: Current name of the filter - :type old_filter_name: str - :param new_filter_name: New name for the filter - :type new_filter_name: str - - - """ - payload = { - "sourceName": source_name, - "filterName": old_filter_name, - "newFilterName": new_filter_name, - } - response = self.base_client.req("SetSourceFilterName", payload) - return response - - def GetSourceFilter(self, source_name, filter_name): - """ - Gets the info for a specific source filter. - - :param source_name: Name of the source - :type source_name: str - :param filter_name: Name of the filter - :type filter_name: str - - - """ - payload = {"sourceName": source_name, "filterName": filter_name} - response = self.base_client.req("GetSourceFilter", payload) - return response - - def SetSourceFilterIndex(self, source_name, filter_name, filter_index): - """ - Gets the info for a specific source filter. - - :param source_name: Name of the source the filter is on - :type source_name: str - :param filter_name: Name of the filter - :type filter_name: str - :param filterIndex: New index position of the filter (>= 0) - :type filterIndex: int - - - """ - payload = { - "sourceName": source_name, - "filterName": filter_name, - "filterIndex": filter_index, - } - response = self.base_client.req("SetSourceFilterIndex", payload) - return response - - def SetSourceFilterSettings(self, source_name, filter_name, settings, overlay=None): - """ - Gets the info for a specific source filter. - - :param source_name: Name of the source the filter is on - :type source_name: str - :param filter_name: Name of the filter to set the settings of - :type filter_name: str - :param settings: Dictionary of settings to apply - :type settings: dict - :param overlay: True == apply the settings on top of existing ones, False == reset the input to its defaults, then apply settings. - :type overlay: bool - - - """ - payload = { - "sourceName": source_name, - "filterName": filter_name, - "filterSettings": settings, - "overlay": overlay, - } - response = self.base_client.req("SetSourceFilterSettings", payload) - return response - - def SetSourceFilterEnabled(self, source_name, filter_name, enabled): - """ - Gets the info for a specific source filter. - - :param source_name: Name of the source the filter is on - :type source_name: str - :param filter_name: Name of the filter - :type filter_name: str - :param enabled: New enable state of the filter - :type enabled: bool - - - """ - payload = { - "sourceName": source_name, - "filterName": filter_name, - "filterEnabled": enabled, - } - response = self.base_client.req("SetSourceFilterEnabled", payload) - return response - - def GetSceneItemList(self, name): - """ - Gets a list of all scene items in a scene. - - :param name: Name of the scene to get the items of - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("GetSceneItemList", payload) - return response - - def GetGroupItemList(self, name): - """ - Gets a list of all scene items in a scene. - - :param name: Name of the group to get the items of - :type name: str - - - """ - payload = {"sceneName": name} - response = self.base_client.req("GetGroupItemList", payload) - return response - - def GetSceneItemId(self, scene_name, source_name, offset=None): - """ - Searches a scene for a source, and returns its id. - - :param scene_name: Name of the scene or group to search in - :type scene_name: str - :param source_name: Name of the source to find - :type source_name: str - :param offset: Number of matches to skip during search. >= 0 means first forward. -1 means last (top) item (>= -1) - :type offset: int - - - """ - payload = { - "sceneName": scene_name, - "sourceName": source_name, - "searchOffset": offset, - } - response = self.base_client.req("GetSceneItemId", payload) - return response - - def CreateSceneItem(self, scene_name, source_name, enabled=None): - """ - Creates a new scene item using a source. - Scenes only - - :param scene_name: Name of the scene to create the new item in - :type scene_name: str - :param source_name: Name of the source to add to the scene - :type source_name: str - :param enabled: Enable state to apply to the scene item on creation - :type enabled: bool - - - """ - payload = { - "sceneName": scene_name, - "sourceName": source_name, - "sceneItemEnabled": enabled, - } - response = self.base_client.req("CreateSceneItem", payload) - return response - - def RemoveSceneItem(self, scene_name, item_id): - """ - Removes a scene item from a scene. - Scenes only - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item - :type item_id: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - } - response = self.base_client.req("RemoveSceneItem", payload) - return response - - def DuplicateSceneItem(self, scene_name, item_id, dest_scene_name=None): - """ - Duplicates a scene item, copying all transform and crop info. - Scenes only - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - :param dest_scene_name: Name of the scene to create the duplicated item in - :type dest_scene_name: str - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - "destinationSceneName": dest_scene_name, - } - response = self.base_client.req("DuplicateSceneItem", payload) - return response - - def GetSceneItemTransform(self, scene_name, item_id): - """ - Gets the transform and crop info of a scene item. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - } - response = self.base_client.req("GetSceneItemTransform", payload) - return response - - def SetSceneItemTransform(self, scene_name, item_id, transform): - """ - Sets the transform and crop info of a scene item. - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - :param transform: Dictionary containing scene item transform info to update - :type transform: dict - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - "sceneItemTransform": transform, - } - response = self.base_client.req("SetSceneItemTransform", payload) - return response - - def GetSceneItemEnabled(self, scene_name, item_id): - """ - Gets the enable state of a scene item. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - } - response = self.base_client.req("GetSceneItemEnabled", payload) - return response - - def SetSceneItemEnabled(self, scene_name, item_id, enabled): - """ - Sets the enable state of a scene item. - Scenes and Groups' - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - :param enabled: New enable state of the scene item - :type enabled: bool - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - "sceneItemEnabled": enabled, - } - response = self.base_client.req("SetSceneItemEnabled", payload) - return response - - def GetSceneItemLocked(self, scene_name, item_id): - """ - Gets the lock state of a scene item. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - } - response = self.base_client.req("GetSceneItemLocked", payload) - return response - - def SetSceneItemLocked(self, scene_name, item_id, locked): - """ - Sets the lock state of a scene item. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - :param locked: New lock state of the scene item - :type locked: bool - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - "sceneItemLocked": locked, - } - response = self.base_client.req("SetSceneItemLocked", payload) - return response - - def GetSceneItemIndex(self, scene_name, item_id): - """ - Gets the index position of a scene item in a scene. - An index of 0 is at the bottom of the source list in the UI. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - } - response = self.base_client.req("GetSceneItemIndex", payload) - return response - - def SetSceneItemIndex(self, scene_name, item_id, item_index): - """ - Sets the index position of a scene item in a scene. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - :param item_index: New index position of the scene item (>= 0) - :type item_index: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - "sceneItemLocked": item_index, - } - response = self.base_client.req("SetSceneItemIndex", payload) - return response - - def GetSceneItemBlendMode(self, scene_name, item_id): - """ - Gets the blend mode of a scene item. - Blend modes: - - OBS_BLEND_NORMAL - OBS_BLEND_ADDITIVE - OBS_BLEND_SUBTRACT - OBS_BLEND_SCREEN - OBS_BLEND_MULTIPLY - OBS_BLEND_LIGHTEN - OBS_BLEND_DARKEN - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - } - response = self.base_client.req("GetSceneItemBlendMode", payload) - return response - - def SetSceneItemBlendMode(self, scene_name, item_id, blend): - """ - Sets the blend mode of a scene item. - Scenes and Groups - - :param scene_name: Name of the scene the item is in - :type scene_name: str - :param item_id: Numeric ID of the scene item (>= 0) - :type item_id: int - :param blend: New blend mode - :type blend: str - - - """ - payload = { - "sceneName": scene_name, - "sceneItemId": item_id, - "sceneItemBlendMode": blend, - } - response = self.base_client.req("SetSceneItemBlendMode", payload) - return response - - def GetVirtualCamStatus(self): - """ - Gets the status of the virtualcam output. - - - """ - response = self.base_client.req("GetVirtualCamStatus") - return response - - def ToggleVirtualCam(self): - """ - Toggles the state of the virtualcam output. - - - """ - response = self.base_client.req("ToggleVirtualCam") - return response - - def StartVirtualCam(self): - """ - Starts the virtualcam output. - - - """ - response = self.base_client.req("StartVirtualCam") - return response - - def StopVirtualCam(self): - """ - Stops the virtualcam output. - - - """ - response = self.base_client.req("StopVirtualCam") - return response - - def GetReplayBufferStatus(self): - """ - Gets the status of the replay buffer output. - - - """ - response = self.base_client.req("GetReplayBufferStatus") - return response - - def ToggleReplayBuffer(self): - """ - Toggles the state of the replay buffer output. - - - """ - response = self.base_client.req("ToggleReplayBuffer") - return response - - def StartReplayBuffer(self): - """ - Starts the replay buffer output. - - - """ - response = self.base_client.req("StartReplayBuffer") - return response - - def StopReplayBuffer(self): - """ - Stops the replay buffer output. - - - """ - response = self.base_client.req("StopReplayBuffer") - return response - - def SaveReplayBuffer(self): - """ - Saves the contents of the replay buffer output. - - - """ - response = self.base_client.req("SaveReplayBuffer") - return response - - def GetLastReplayBufferReplay(self): - """ - Gets the filename of the last replay buffer save file. - - - """ - response = self.base_client.req("GetLastReplayBufferReplay") - return response - - def GetStreamStatus(self): - """ - Gets the status of the stream output. - - - """ - response = self.base_client.req("GetStreamStatus") - return response - - def ToggleStream(self): - """ - Toggles the status of the stream output. - - - """ - response = self.base_client.req("ToggleStream") - return response - - def StartStream(self): - """ - Starts the stream output. - - - """ - response = self.base_client.req("StartStream") - return response - - def StopStream(self): - """ - Stops the stream output. - - - """ - response = self.base_client.req("StopStream") - return response - - def SendStreamCaption(self, caption): - """ - Sends CEA-608 caption text over the stream output. - - :param caption: Caption text - :type caption: str - - - """ - response = self.base_client.req("SendStreamCaption") - return response - - def GetRecordStatus(self): - """ - Gets the status of the record output. - - - """ - response = self.base_client.req("GetRecordStatus") - return response - - def ToggleRecord(self): - """ - Toggles the status of the record output. - - - """ - response = self.base_client.req("ToggleRecord") - return response - - def StartRecord(self): - """ - Starts the record output. - - - """ - response = self.base_client.req("StartRecord") - return response - - def StopRecord(self): - """ - Stops the record output. - - - """ - response = self.base_client.req("StopRecord") - return response - - def ToggleRecordPause(self): - """ - Toggles pause on the record output. - - - """ - response = self.base_client.req("ToggleRecordPause") - return response - - def PauseRecord(self): - """ - Pauses the record output. - - - """ - response = self.base_client.req("PauseRecord") - return response - - def ResumeRecord(self): - """ - Resumes the record output. - - - """ - response = self.base_client.req("ResumeRecord") - return response - - def GetMediaInputStatus(self, name): - """ - Gets the status of a media input. - - Media States: - OBS_MEDIA_STATE_NONE - OBS_MEDIA_STATE_PLAYING - OBS_MEDIA_STATE_OPENING - OBS_MEDIA_STATE_BUFFERING - OBS_MEDIA_STATE_PAUSED - OBS_MEDIA_STATE_STOPPED - OBS_MEDIA_STATE_ENDED - OBS_MEDIA_STATE_ERROR - - :param name: Name of the media input - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("GetMediaInputStatus", payload) - return response - - def SetMediaInputCursor(self, name, cursor): - """ - Sets the cursor position of a media input. - This request does not perform bounds checking of the cursor position. - - :param name: Name of the media input - :type name: str - :param cursor: New cursor position to set (>= 0) - :type cursor: int - - - """ - payload = {"inputName": name, "mediaCursor": cursor} - response = self.base_client.req("SetMediaInputCursor", payload) - return response - - def OffsetMediaInputCursor(self, name, offset): - """ - Offsets the current cursor position of a media input by the specified value. - This request does not perform bounds checking of the cursor position. - - :param name: Name of the media input - :type name: str - :param offset: Value to offset the current cursor position by - :type offset: int - - - """ - payload = {"inputName": name, "mediaCursorOffset": offset} - response = self.base_client.req("OffsetMediaInputCursor", payload) - return response - - def TriggerMediaInputAction(self, name, action): - """ - Triggers an action on a media input. - - :param name: Name of the media input - :type name: str - :param action: Identifier of the ObsMediaInputAction enum - :type action: str - - - """ - payload = {"inputName": name, "mediaAction": action} - response = self.base_client.req("TriggerMediaInputAction", payload) - return response - - def GetStudioModeEnabled(self): - """ - Gets whether studio is enabled. - - - """ - response = self.base_client.req("GetStudioModeEnabled") - return response - - def SetStudioModeEnabled(self, enabled): - """ - Enables or disables studio mode - - :param enabled: True == Enabled, False == Disabled - :type enabled: bool - - - """ - payload = {"studioModeEnabled": enabled} - response = self.base_client.req("SetStudioModeEnabled", payload) - return response - - def OpenInputPropertiesDialog(self, name): - """ - Opens the properties dialog of an input. - - :param name: Name of the input to open the dialog of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("OpenInputPropertiesDialog", payload) - return response - - def OpenInputFiltersDialog(self, name): - """ - Opens the filters dialog of an input. - - :param name: Name of the input to open the dialog of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("OpenInputFiltersDialog", payload) - return response - - def OpenInputInteractDialog(self, name): - """ - Opens the filters dialog of an input. - - :param name: Name of the input to open the dialog of - :type name: str - - - """ - payload = {"inputName": name} - response = self.base_client.req("OpenInputInteractDialog", payload) - return response - - def GetMonitorList(self, name): - """ - Gets a list of connected monitors and information about them. - - - """ - response = self.base_client.req("GetMonitorList") - return response diff --git a/build/lib/obsstudio_sdk/subject.py b/build/lib/obsstudio_sdk/subject.py deleted file mode 100644 index 24732c5..0000000 --- a/build/lib/obsstudio_sdk/subject.py +++ /dev/null @@ -1,58 +0,0 @@ -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"(? 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() diff --git a/examples/events/__pycache__/__main__.cpython-311.pyc b/examples/events/__pycache__/__main__.cpython-311.pyc deleted file mode 100644 index d325b3d0c93ce31d6c5a504ad563a1f117342b67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1847 zcmaJBOKjsrbZp1zI8EA3s#2ngV6?e-EA0WwE^3z|mMvQ@S&2#=)TEZr}O5;hMOXIDj|KKJo z=CS4QFZRP!IPN0?kyxk-Rv)3N#N(-^ED5k|r8bc!2lzpq5&Do&Gl>u`%CIHa?B~!* zPv{{Xs0ey!J0{U~><=BBzMsfJm)i*e=p<7O@oZKEQ zY+DFo6)WAj4@tvj9-+ii$wT6LY(>?k0!cTW0Go#6)D3ed!1P!vP+P8Ub|?i+y-iyU zYBY6b5|`-amf<#t70iML*|j~Lc?R`>A-AdRdV$m+o+btMb9q1a`jdP9^2RXx!7~Y1&pCg>(cfvL%nWDV zKDhVIhF^a3X?gXiygDkc`sG#N!W!Wvp34`*&2EPprcUtb0a)(GGB@$F%}!k8;snm3~hkJ0rY13lWZIu56^y8cPW za3TrKfzdGSlt1Zv6kd+r$mf7^l!DFP1=LS|6H%7mnVn;rX8qXvj(2S-8xicddNN^0FLJRzEc0K$eyyw_3?v#Ix0t_1A$1#v? zx7~rUHk%fm2hmAHFYuix3v2Wh5b(ZZKLPr8h!&GIN#_K3`$hDO^PZmwRFujNx<^uR zg3>}^tP}@xzET`1i@vhhzdN2?80>y2e%{;b^*4UYzx2iYaN))%zvSnahMA?ob)Kxk kc(wppQVQd2ey~1~QDNaywNN)g3b+dWO)%UKd0~A20ZQJSs{jB1 diff --git a/examples/scene_rotate/__pycache__/__main__.cpython-311.pyc b/examples/scene_rotate/__pycache__/__main__.cpython-311.pyc deleted file mode 100644 index 7571f193ad6c60314449ec47d1f3a5c322caefe6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1267 zcmZWo&1(}u6o0#$k4c(Uv|25TGzby*5=47ZO2wvB!4kwkTT3*g*`eJwU(QVYC@HMa zLxmi?NDC_TR1_~BJoHcSn3OnO7}!9p*Lav$Pe@lNl;db=ltu-$CYMM}YYNTD5+i zf*DiZHGMy&XEN9IM<$3@!^^K?Wt$*+Y}=^$fAwcy1JSJoK*(%=m!+&j6)bhQqX0|m z;eE&aB=qOp!FNA)-|m;zl&Ufb4=2`C5$Yxe%U@OBz&o|ySLsL7Q|EDH_5T?C*t*=s zV+H=rT}xPy0yCJ*bxXxS*~)8p`6VQGLBW<*;bBCUZNV|*8Q4@5sOfSvrs{@xjuvS$ zTcF7gQdZ+vT*cyY;?d~g^{E@_+qPJ$ETl^doQsNOmonU1Nz-T9LOD-)+6&LHlE@00 z&SVN%yO_zu%g+x(V~d#hS(%Nmo|D@6SiY3Y=J|NMYapF!f4~oLpl;0KdH;Cp*4E0~ zH^KhN6c@dKU3}$z}H(ko Hm-qA^$c-O@ diff --git a/obsstudio_sdk/__pycache__/__init__.cpython-311.pyc b/obsstudio_sdk/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 6cf5468f34ab673b96f8c9caf5b67e24a30d9143..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 300 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 diff --git a/obsstudio_sdk/__pycache__/callback.cpython-311.pyc b/obsstudio_sdk/__pycache__/callback.cpython-311.pyc deleted file mode 100644 index 77dad5cb1c9b3df1e492eb459979ef7d9a28760c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3874 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 Date: Tue, 26 Jul 2022 03:31:32 +0100 Subject: [PATCH 04/27] send event name to callback add requirements to readme. --- README.md | 6 ++++++ obsstudio_sdk/callback.py | 11 ++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff0d837..16ec141 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ 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. [obs-websocket github page](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests) +## Requirements + +- [OBS Studio](https://obsproject.com/) +- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0) +- Python 3.11 or greater + ### How to install using pip ``` diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index 24732c5..fc2c445 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -1,4 +1,5 @@ import re +from typing import Callable, Iterable, Union class Callback: @@ -28,16 +29,16 @@ class Callback: for fn in self._callbacks: if fn.__name__ == self.to_snake_case(event): if "eventData" in data: - fn(data["eventData"]) + fn(event, data["eventData"]) else: - fn() + fn(event) - def register(self, fns): + def register(self, fns: Union[Iterable, Callable]): """registers callback functions""" try: - iter(fns) - for fn in fns: + iterator = iter(fns) + for fn in iterator: if fn not in self._callbacks: self._callbacks.append(fn) except TypeError as e: From c8f2b6419d237af4bb404399114528d5c3f39252 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 04:36:55 +0100 Subject: [PATCH 05/27] add subs intenum to baseclient expand events example --- examples/events/__main__.py | 21 +++++++++++++++++---- obsstudio_sdk/baseclient.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/examples/events/__main__.py b/examples/events/__main__.py index 4f6c0c5..a95770c 100644 --- a/examples/events/__main__.py +++ b/examples/events/__main__.py @@ -5,17 +5,30 @@ class Observer: def __init__(self, cl): self._cl = cl self._cl.callback.register( - [self.on_current_program_scene_changed, self.on_exit_started] + [ + self.on_current_program_scene_changed, + self.on_scene_created, + self.on_input_mute_state_changed, + self.on_exit_started, + ] ) print(f"Registered events: {self._cl.callback.get()}") + def on_current_program_scene_changed(self, data): + print(f"Switched to scene {data['sceneName']}") + + def on_scene_created(self, event, data): + """A new scene has been created.""" + print(f"{event}: {data}") + + def on_input_mute_state_changed(self, event, data): + """An input's mute state has changed.""" + print(f"{event}: {data}") + 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() diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index 809d207..da19ba3 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -1,12 +1,19 @@ import base64 import hashlib import json +from enum import IntEnum from pathlib import Path from random import randint import tomllib import websocket +Subs = IntEnum( + "Subs", + "general config scenes inputs transitions filters outputs sceneitems mediainputs vendors ui", + start=0, +) + class ObsClient(object): def __init__(self, **kwargs): @@ -49,7 +56,28 @@ class ObsClient(object): ).digest() ).decode() - payload = {"op": 1, "d": {"rpcVersion": 1, "authentication": auth}} + all_non_high_volume = ( + (1 << Subs.general) + | (1 << Subs.config) + | (1 << Subs.scenes) + | (1 << Subs.inputs) + | (1 << Subs.transitions) + | (1 << Subs.filters) + | (1 << Subs.outputs) + | (1 << Subs.sceneitems) + | (1 << Subs.mediainputs) + | (1 << Subs.vendors) + | (1 << Subs.ui) + ) + + payload = { + "op": 1, + "d": { + "rpcVersion": 1, + "authentication": auth, + "eventSubscriptions": (all_non_high_volume), + }, + } self.ws.send(json.dumps(payload)) return self.ws.recv() From 5532ecef03fc411c72ce133d65a8b27e3de581c2 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:46:59 +0100 Subject: [PATCH 06/27] check for response type in req --- obsstudio_sdk/baseclient.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index da19ba3..9a31c0a 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -1,6 +1,7 @@ import base64 import hashlib import json +import time from enum import IntEnum from pathlib import Path from random import randint @@ -16,6 +17,8 @@ Subs = IntEnum( class ObsClient(object): + DELAY = 0.001 + def __init__(self, **kwargs): defaultkwargs = {key: None for key in ["host", "port", "password"]} kwargs = defaultkwargs | kwargs @@ -75,7 +78,7 @@ class ObsClient(object): "d": { "rpcVersion": 1, "authentication": auth, - "eventSubscriptions": (all_non_high_volume), + "eventSubscriptions": all_non_high_volume, }, } @@ -98,4 +101,8 @@ class ObsClient(object): "d": {"requestType": req_type, "requestId": randint(1, 1000)}, } self.ws.send(json.dumps(payload)) - return json.loads(self.ws.recv()) + response = json.loads(self.ws.recv()) + while "requestId" not in response["d"]: + response = json.loads(self.ws.recv()) + time.sleep(self.DELAY) + return response["d"] From 362ec22257e9bbc1ae756eed54785d3814c7c790 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:47:41 +0100 Subject: [PATCH 07/27] no longer sending event name to callback update tests accordingly --- examples/events/__main__.py | 12 +++++++----- examples/scene_rotate/__main__.py | 6 +++--- obsstudio_sdk/callback.py | 7 ++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/examples/events/__main__.py b/examples/events/__main__.py index a95770c..83cdac1 100644 --- a/examples/events/__main__.py +++ b/examples/events/__main__.py @@ -15,17 +15,19 @@ class Observer: print(f"Registered events: {self._cl.callback.get()}") def on_current_program_scene_changed(self, data): + """The current program scene has changed.""" print(f"Switched to scene {data['sceneName']}") - def on_scene_created(self, event, data): + def on_scene_created(self, data): """A new scene has been created.""" - print(f"{event}: {data}") + print(f"scene {data['sceneName']} has been created") - def on_input_mute_state_changed(self, event, data): + def on_input_mute_state_changed(self, data): """An input's mute state has changed.""" - print(f"{event}: {data}") + print(f"{data['inputName']} mute toggled") - def on_exit_started(self): + def on_exit_started(self, data): + """OBS has begun the shutdown process.""" print(f"OBS closing!") self._cl.unsubscribe() diff --git a/examples/scene_rotate/__main__.py b/examples/scene_rotate/__main__.py index 23f71bb..9994d05 100644 --- a/examples/scene_rotate/__main__.py +++ b/examples/scene_rotate/__main__.py @@ -4,12 +4,12 @@ import obsstudio_sdk as obs def main(): - res = cl.GetSceneList() - scenes = reversed(tuple(d["sceneName"] for d in res["d"]["responseData"]["scenes"])) + resp = cl.get_scene_list() + scenes = reversed(tuple(di["sceneName"] for di in resp["scenes"])) for sc in scenes: print(f"Switching to scene {sc}") - cl.SetCurrentProgramScene(sc) + cl.set_current_program_scene(sc) time.sleep(0.5) diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index fc2c445..8cc4a61 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -23,15 +23,12 @@ class Callback: return [self.to_camel_case(fn.__name__) for fn in self._callbacks] - def trigger(self, event, data=None): + def trigger(self, event, data): """trigger callback on update""" for fn in self._callbacks: if fn.__name__ == self.to_snake_case(event): - if "eventData" in data: - fn(event, data["eventData"]) - else: - fn(event) + fn(data.get("eventData")) def register(self, fns: Union[Iterable, Callable]): """registers callback functions""" From d36b9cf7132cff24bf13af7f997a8f04e311f7be Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:47:50 +0100 Subject: [PATCH 08/27] add error class --- obsstudio_sdk/error.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 obsstudio_sdk/error.py diff --git a/obsstudio_sdk/error.py b/obsstudio_sdk/error.py new file mode 100644 index 0000000..67885a4 --- /dev/null +++ b/obsstudio_sdk/error.py @@ -0,0 +1,4 @@ +class OBSSDKError(Exception): + """general errors""" + + pass From eed83946c82c3d8f7fba23ea6556973821022f03 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 21:48:52 +0100 Subject: [PATCH 09/27] reqclient methods now snake cased. getter, setter added. initial test commit --- obsstudio_sdk/reqs.py | 341 +++++++++++++++++++++--------------------- tests/__init__.py | 11 ++ tests/test_request.py | 39 +++++ 3 files changed, 224 insertions(+), 167 deletions(-) create mode 100644 tests/__init__.py create mode 100644 tests/test_request.py diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index 924682e..40d0dc0 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -1,4 +1,8 @@ +import time +from re import S + from .baseclient import ObsClient +from .error import OBSSDKError """ A class to interact with obs-websocket requests @@ -12,7 +16,23 @@ class ReqClient(object): self.base_client = ObsClient(**kwargs) self.base_client.authenticate() - def GetVersion(self): + def getter(self, param): + response = self.base_client.req(param) + return response["responseData"] + + def setter(self, param, data): + response = self.base_client.req(param, data) + if not response["requestStatus"]["result"]: + error = ( + f"Request {response['requestType']} returned code {response['requestStatus']['code']}", + ) + if "comment" in response["requestStatus"]: + error += (f"With message: {response['requestStatus']['comment']}",) + raise OBSSDKError("\n".join(error)) + + action = setter + + def get_version(self): """ Gets data about the current plugin and RPC version. @@ -21,10 +41,9 @@ class ReqClient(object): """ - response = self.base_client.req("GetVersion") - return response + return self.getter("GetVersion") - def GetStats(self): + def get_stats(self): """ Gets statistics about OBS, obs-websocket, and the current session. @@ -33,10 +52,9 @@ class ReqClient(object): """ - response = self.base_client.req("GetStats") - return response + return self.getter("GetStats") - def BroadcastCustomEvent(self, eventData): + def broadcast_custom_event(self, eventData): """ Broadcasts a CustomEvent to all WebSocket clients. Receivers are clients which are identified and subscribed. @@ -47,11 +65,9 @@ class ReqClient(object): """ - req_data = eventData - response = self.base_client.req("BroadcastCustomEvent", req_data) - return response + self.action("BroadcastCustomEvent", eventData) - def CallVendorRequest(self, vendorName, requestType, requestData=None): + def call_vendor_request(self, vendorName, requestType, requestData=None): """ Call a request registered to a vendor. @@ -76,7 +92,7 @@ class ReqClient(object): response = self.base_client.req(req_type=requestType, req_data=requestData) return response - def GetHotkeyList(self): + def get_hot_key_list(self): """ Gets an array of all hotkey names in OBS @@ -85,10 +101,9 @@ class ReqClient(object): """ - response = self.base_client.req("GetHotkeyList") - return response + return self.getter("GetHotkeyList") - def TriggerHotkeyByName(self, hotkeyName): + def trigger_hot_key_by_name(self, hotkeyName): """ Triggers a hotkey using its name. For hotkey names See GetHotkeyList @@ -99,10 +114,9 @@ class ReqClient(object): """ payload = {"hotkeyName": hotkeyName} - response = self.base_client.req("TriggerHotkeyByName", payload) - return response + self.action("TriggerHotkeyByName", payload) - def TriggerHotkeyByKeySequence( + def trigger_hot_key_by_key_sequence( self, keyId, pressShift, pressCtrl, pressAlt, pressCmd ): """ @@ -136,7 +150,7 @@ class ReqClient(object): response = self.base_client.req("TriggerHotkeyByKeySequence", payload) return response - def Sleep(self, sleepMillis=None, sleepFrames=None): + def sleep(self, sleepMillis=None, sleepFrames=None): """ Sleeps for a time duration or number of frames. Only available in request batches with types SERIAL_REALTIME or SERIAL_FRAME @@ -149,10 +163,9 @@ class ReqClient(object): """ payload = {"sleepMillis": sleepMillis, "sleepFrames": sleepFrames} - response = self.base_client.req("Sleep", payload) - return response + self.action("Sleep", payload) - def GetPersistentData(self, realm, slotName): + def get_persistent_data(self, realm, slotName): """ Gets the value of a "slot" from the selected persistent data realm. @@ -170,7 +183,7 @@ class ReqClient(object): response = self.base_client.req("GetPersistentData", payload) return response - def SetPersistentData(self, realm, slotName, slotValue): + def set_persistent_data(self, realm, slotName, slotValue): """ Sets the value of a "slot" from the selected persistent data realm. @@ -188,7 +201,7 @@ class ReqClient(object): response = self.base_client.req("SetPersistentData", payload) return response - def GetSceneCollectionList(self): + def get_scene_collection_list(self): """ Gets an array of all scene collections @@ -197,13 +210,11 @@ class ReqClient(object): """ - response = self.base_client.req("GetSceneCollectionList") - return response + return self.getter("GetSceneCollectionList") - def SetCurrentSceneCollection(self, name): + def set_current_scene_collection(self, name): """ - Creates a new scene collection, switching to it in the process - Note: This will block until the collection has finished changing + Switches to a scene collection. :param name: Name of the scene collection to switch to :type name: str @@ -211,10 +222,9 @@ class ReqClient(object): """ payload = {"sceneCollectionName": name} - response = self.base_client.req("SetCurrentSceneCollection", payload) - return response + self.setter("SetCurrentSceneCollection", payload) - def CreateSceneCollection(self, name): + def create_scene_collection(self, name): """ Creates a new scene collection, switching to it in the process. Note: This will block until the collection has finished changing. @@ -225,10 +235,9 @@ class ReqClient(object): """ payload = {"sceneCollectionName": name} - response = self.base_client.req("CreateSceneCollection", payload) - return response + self.action("CreateSceneCollection", payload) - def GetProfileList(self): + def get_profile_list(self): """ Gets a list of all profiles @@ -237,10 +246,9 @@ class ReqClient(object): """ - response = self.base_client.req("GetProfileList") - return response + return self.getter("GetProfileList") - def SetCurrentProfile(self, name): + def set_current_profile(self, name): """ Switches to a profile @@ -250,10 +258,9 @@ class ReqClient(object): """ payload = {"profileName": name} - response = self.base_client.req("SetCurrentProfile", payload) - return response + self.setter("SetCurrentProfile", payload) - def CreateProfile(self, name): + def create_profile(self, name): """ Creates a new profile, switching to it in the process @@ -266,7 +273,7 @@ class ReqClient(object): response = self.base_client.req("CreateProfile", payload) return response - def RemoveProfile(self, name): + def remove_profile(self, name): """ Removes a profile. If the current profile is chosen, it will change to a different profile first. @@ -280,9 +287,9 @@ class ReqClient(object): response = self.base_client.req("RemoveProfile", payload) return response - def GetProfileParameter(self, category, name): + def get_profile_parameter(self, category, name): """ - Gets a parameter from the current profile's configuration.. + Gets a parameter from the current profile's configuration. :param category: Category of the parameter to get :type category: str @@ -298,9 +305,9 @@ class ReqClient(object): response = self.base_client.req("GetProfileParameter", payload) return response - def SetProfileParameter(self, category, name, value): + def set_profile_parameter(self, category, name, value): """ - Gets a parameter from the current profile's configuration.. + Sets the value of a parameter in the current profile's configuration. :param category: Category of the parameter to set :type category: str @@ -322,7 +329,7 @@ class ReqClient(object): response = self.base_client.req("SetProfileParameter", payload) return response - def GetVideoSettings(self): + def get_video_settings(self): """ Gets the current video settings. Note: To get the true FPS value, divide the FPS numerator by the FPS denominator. @@ -333,7 +340,7 @@ class ReqClient(object): response = self.base_client.req("GetVideoSettings") return response - def SetVideoSettings( + def set_video_settings( self, numerator, denominator, base_width, base_height, out_width, out_height ): """ @@ -367,7 +374,7 @@ class ReqClient(object): response = self.base_client.req("SetVideoSettings", payload) return response - def GetStreamServiceSettings(self): + def get_stream_service_settings(self): """ Gets the current stream service settings (stream destination). @@ -376,7 +383,7 @@ class ReqClient(object): response = self.base_client.req("GetStreamServiceSettings") return response - def SetStreamServiceSettings(self, ss_type, ss_settings): + def set_stream_service_settings(self, ss_type, ss_settings): """ Sets the current stream service settings (stream destination). Note: Simple RTMP settings can be set with type rtmp_custom @@ -396,7 +403,7 @@ class ReqClient(object): response = self.base_client.req("SetStreamServiceSettings", payload) return response - def GetSourceActive(self, name): + def get_source_active(self, name): """ Gets the active and show state of a source @@ -409,7 +416,7 @@ class ReqClient(object): response = self.base_client.req("GetSourceActive", payload) return response - def GetSourceScreenshot(self, name, img_format, width, height, quality): + def get_source_screenshot(self, name, img_format, width, height, quality): """ Gets a Base64-encoded screenshot of a source. The imageWidth and imageHeight parameters are @@ -441,7 +448,9 @@ class ReqClient(object): response = self.base_client.req("GetSourceScreenshot", payload) return response - def SaveSourceScreenshot(self, name, img_format, file_path, width, height, quality): + def save_source_screenshot( + self, name, img_format, file_path, width, height, quality + ): """ Saves a Base64-encoded screenshot of a source. The imageWidth and imageHeight parameters are @@ -476,16 +485,15 @@ class ReqClient(object): response = self.base_client.req("SaveSourceScreenshot", payload) return response - def GetSceneList(self): + def get_scene_list(self): """ Gets a list of all scenes in OBS. """ - response = self.base_client.req("GetSceneList") - return response + return self.getter("GetSceneList") - def GetGroupList(self): + def get_group_list(self): """ Gets a list of all groups in OBS. Groups in OBS are actually scenes, @@ -497,16 +505,15 @@ class ReqClient(object): response = self.base_client.req("GetSceneList") return response - def GetCurrentProgramScene(self): + def get_current_program_scene(self): """ Gets the current program scene. """ - response = self.base_client.req("GetCurrentProgramScene") - return response + return self.getter("GetCurrentProgramScene") - def SetCurrentProgramScene(self, name): + def set_current_program_scene(self, name): """ Sets the current program scene @@ -516,10 +523,9 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("SetCurrentProgramScene", payload) - return response + self.setter("SetCurrentProgramScene", payload) - def GetCurrentPreviewScene(self): + def get_current_preview_scene(self): """ Gets the current preview scene @@ -528,7 +534,7 @@ class ReqClient(object): response = self.base_client.req("GetCurrentPreviewScene") return response - def SetCurrentPreviewScene(self, name): + def set_current_preview_scene(self, name): """ Sets the current program scene @@ -541,7 +547,7 @@ class ReqClient(object): response = self.base_client.req("SetCurrentPreviewScene", payload) return response - def CreateScene(self, name): + def create_scene(self, name): """ Creates a new scene in OBS. @@ -554,7 +560,7 @@ class ReqClient(object): response = self.base_client.req("CreateScene", payload) return response - def RemoveScene(self, name): + def remove_scene(self, name): """ Removes a scene from OBS @@ -567,7 +573,7 @@ class ReqClient(object): response = self.base_client.req("RemoveScene", payload) return response - def SetSceneName(self, old_name, new_name): + def set_scene_name(self, old_name, new_name): """ Sets the name of a scene (rename). @@ -582,7 +588,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneName", payload) return response - def GetSceneSceneTransitionOverride(self, name): + def get_scene_scene_transition_override(self, name): """ Gets the scene transition overridden for a scene. @@ -595,7 +601,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneSceneTransitionOverride", payload) return response - def SetSceneSceneTransitionOverride(self, scene_name, tr_name, tr_duration): + def set_scene_scene_transition_override(self, scene_name, tr_name, tr_duration): """ Gets the scene transition overridden for a scene. @@ -616,7 +622,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneSceneTransitionOverride", payload) return response - def GetInputList(self, kind): + def get_input_list(self, kind): """ Gets a list of all inputs in OBS. @@ -629,7 +635,7 @@ class ReqClient(object): response = self.base_client.req("GetInputList", payload) return response - def GetInputKindList(self, unversioned): + def get_input_kind_list(self, unversioned): """ Gets a list of all available input kinds in OBS. @@ -642,7 +648,7 @@ class ReqClient(object): response = self.base_client.req("GetInputKindList", payload) return response - def GetSpecialInputs(self): + def get_special_inputs(self): """ Gets the name of all special inputs. @@ -651,7 +657,7 @@ class ReqClient(object): response = self.base_client.req("GetSpecialInputs") return response - def CreateInput( + def create_input( self, sceneName, inputName, inputKind, inputSettings, sceneItemEnabled ): """ @@ -680,7 +686,7 @@ class ReqClient(object): response = self.base_client.req("CreateInput", payload) return response - def RemoveInput(self, name): + def remove_input(self, name): """ Removes an existing input @@ -693,7 +699,7 @@ class ReqClient(object): response = self.base_client.req("RemoveInput", payload) return response - def SetInputName(self, old_name, new_name): + def set_input_name(self, old_name, new_name): """ Sets the name of an input (rename). @@ -708,7 +714,7 @@ class ReqClient(object): response = self.base_client.req("SetInputName", payload) return response - def GetInputDefaultSettings(self, kind): + def get_input_default_settings(self, kind): """ Gets the default settings for an input kind. @@ -721,7 +727,7 @@ class ReqClient(object): response = self.base_client.req("GetInputDefaultSettings", payload) return response - def GetInputSettings(self, name): + def get_input_settings(self, name): """ Gets the settings of an input. Note: Does not include defaults. To create the entire settings object, @@ -736,7 +742,7 @@ class ReqClient(object): response = self.base_client.req("GetInputSettings", payload) return response - def SetInputSettings(self, name, settings, overlay): + def set_input_settings(self, name, settings, overlay): """ Sets the settings of an input. @@ -753,7 +759,7 @@ class ReqClient(object): response = self.base_client.req("SetInputSettings", payload) return response - def GetInputMute(self, name): + def get_input_mute(self, name): """ Gets the audio mute state of an input @@ -766,7 +772,7 @@ class ReqClient(object): response = self.base_client.req("GetInputMute", payload) return response - def SetInputMute(self, name, muted): + def set_input_mute(self, name, muted): """ Sets the audio mute state of an input. @@ -781,7 +787,7 @@ class ReqClient(object): response = self.base_client.req("SetInputMute", payload) return response - def ToggleInputMute(self, name): + def toggle_input_mute(self, name): """ Toggles the audio mute state of an input. @@ -794,7 +800,7 @@ class ReqClient(object): response = self.base_client.req("ToggleInputMute", payload) return response - def GetInputVolume(self, name): + def get_input_volume(self, name): """ Gets the current volume setting of an input. @@ -807,7 +813,7 @@ class ReqClient(object): response = self.base_client.req("GetInputVolume", payload) return response - def SetInputVolume(self, name, vol_mul=None, vol_db=None): + def set_input_volume(self, name, vol_mul=None, vol_db=None): """ Sets the volume setting of an input. @@ -828,7 +834,7 @@ class ReqClient(object): response = self.base_client.req("SetInputVolume", payload) return response - def GetInputAudioBalance(self, name): + def get_input_audio_balance(self, name): """ Gets the audio balance of an input. @@ -841,7 +847,7 @@ class ReqClient(object): response = self.base_client.req("GetInputAudioBalance", payload) return response - def SetInputAudioBalance(self, name, balance): + def set_input_audio_balance(self, name, balance): """ Sets the audio balance of an input. @@ -856,7 +862,7 @@ class ReqClient(object): response = self.base_client.req("SetInputAudioBalance", payload) return response - def GetInputAudioOffset(self, name): + def get_input_audio_sync_offset(self, name): """ Gets the audio sync offset of an input. @@ -866,10 +872,10 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputAudioOffset", payload) + response = self.base_client.req("GetInputAudioSyncOffset", payload) return response - def SetInputAudioSyncOffset(self, name, offset): + def set_input_audio_sync_offset(self, name, offset): """ Sets the audio sync offset of an input. @@ -884,7 +890,7 @@ class ReqClient(object): response = self.base_client.req("SetInputAudioSyncOffset", payload) return response - def GetInputAudioMonitorType(self, name): + def get_input_audio_monitor_type(self, name): """ Gets the audio monitor type of an input. @@ -903,7 +909,7 @@ class ReqClient(object): response = self.base_client.req("GetInputAudioMonitorType", payload) return response - def SetInputAudioMonitorType(self, name, mon_type): + def set_input_audio_monitor_type(self, name, mon_type): """ Sets the audio monitor type of an input. @@ -918,7 +924,7 @@ class ReqClient(object): response = self.base_client.req("SetInputAudioMonitorType", payload) return response - def GetInputAudioTracks(self, name): + def get_input_audio_tracks(self, name): """ Gets the enable state of all audio tracks of an input. @@ -931,9 +937,9 @@ class ReqClient(object): response = self.base_client.req("GetInputAudioTracks", payload) return response - def SetInputAudioTracks(self, name, track): + def set_input_audio_tracks(self, name, track): """ - Sets the audio monitor type of an input. + Sets the enable state of audio tracks of an input. :param name: Name of the input :type name: str @@ -946,7 +952,7 @@ class ReqClient(object): response = self.base_client.req("SetInputAudioTracks", payload) return response - def GetInputPropertiesListPropertyItems(self, input_name, prop_name): + def get_input_properties_list_property_items(self, input_name, prop_name): """ Gets the items of a list property from an input's properties. Note: Use this in cases where an input provides a dynamic, @@ -964,7 +970,7 @@ class ReqClient(object): response = self.base_client.req("GetInputPropertiesListPropertyItems", payload) return response - def PressInputPropertiesButton(self, input_name, prop_name): + def press_input_properties_button(self, input_name, prop_name): """ Presses a button in the properties of an input. Note: Use this in cases where there is a button @@ -982,7 +988,7 @@ class ReqClient(object): response = self.base_client.req("PressInputPropertiesButton", payload) return response - def GetTransitionKindList(self): + def get_transition_kind_list(self): """ Gets an array of all available transition kinds. Similar to GetInputKindList @@ -992,7 +998,7 @@ class ReqClient(object): response = self.base_client.req("GetTransitionKindList") return response - def GetSceneTransitionList(self): + def get_scene_transition_list(self): """ Gets an array of all scene transitions in OBS. @@ -1001,7 +1007,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneTransitionList") return response - def GetCurrentSceneTransition(self): + def get_current_scene_transition(self): """ Gets an array of all scene transitions in OBS. @@ -1010,7 +1016,7 @@ class ReqClient(object): response = self.base_client.req("GetCurrentSceneTransition") return response - def SetCurrentSceneTransition(self, name): + def set_current_scene_transition(self, name): """ Sets the current scene transition. Small note: While the namespace of scene transitions is generally unique, @@ -1025,7 +1031,7 @@ class ReqClient(object): response = self.base_client.req("SetCurrentSceneTransition", payload) return response - def SetCurrentSceneTransitionDuration(self, duration): + def set_current_scene_transition_duration(self, duration): """ Sets the duration of the current scene transition, if it is not fixed. @@ -1038,7 +1044,7 @@ class ReqClient(object): response = self.base_client.req("SetCurrentSceneTransitionDuration", payload) return response - def SetCurrentSceneTransitionSettings(self, settings, overlay=None): + def set_current_scene_transition_settings(self, settings, overlay=None): """ Sets the settings of the current scene transition. @@ -1053,7 +1059,7 @@ class ReqClient(object): response = self.base_client.req("SetCurrentSceneTransitionSettings", payload) return response - def GetCurrentSceneTransitionCursor(self): + def get_current_scene_transition_cursor(self): """ Gets the cursor position of the current scene transition. Note: transitionCursor will return 1.0 when the transition is inactive. @@ -1063,7 +1069,7 @@ class ReqClient(object): response = self.base_client.req("GetCurrentSceneTransitionCursor") return response - def TriggerStudioModeTransition(self): + def trigger_studio_mode_transition(self): """ Triggers the current scene transition. Same functionality as the Transition button in studio mode. @@ -1075,7 +1081,7 @@ class ReqClient(object): response = self.base_client.req("TriggerStudioModeTransition") return response - def SetTBarPosition(self, pos, release=None): + def set_t_bar_position(self, pos, release=None): """ Sets the position of the TBar. Very important note: This will be deprecated @@ -1092,7 +1098,7 @@ class ReqClient(object): response = self.base_client.req("SetTBarPosition", payload) return response - def GetSourceFilterList(self, name): + def get_source_filter_list(self, name): """ Gets a list of all of a source's filters. @@ -1105,7 +1111,7 @@ class ReqClient(object): response = self.base_client.req("GetSourceFilterList", payload) return response - def GetSourceFilterDefaultSettings(self, kind): + def get_source_filter_default_settings(self, kind): """ Gets the default settings for a filter kind. @@ -1118,7 +1124,7 @@ class ReqClient(object): response = self.base_client.req("GetSourceFilterDefaultSettings", payload) return response - def CreateSourceFilter( + def create_source_filter( self, source_name, filter_name, filter_kind, filter_settings=None ): """ @@ -1144,7 +1150,7 @@ class ReqClient(object): response = self.base_client.req("CreateSourceFilter", payload) return response - def RemoveSourceFilter(self, source_name, filter_name): + def remove_source_filter(self, source_name, filter_name): """ Gets the default settings for a filter kind. @@ -1162,7 +1168,7 @@ class ReqClient(object): response = self.base_client.req("RemoveSourceFilter", payload) return response - def SetSourceFilterName(self, source_name, old_filter_name, new_filter_name): + def set_source_filter_name(self, source_name, old_filter_name, new_filter_name): """ Sets the name of a source filter (rename). @@ -1183,7 +1189,7 @@ class ReqClient(object): response = self.base_client.req("SetSourceFilterName", payload) return response - def GetSourceFilter(self, source_name, filter_name): + def get_source_filter(self, source_name, filter_name): """ Gets the info for a specific source filter. @@ -1198,9 +1204,9 @@ class ReqClient(object): response = self.base_client.req("GetSourceFilter", payload) return response - def SetSourceFilterIndex(self, source_name, filter_name, filter_index): + def set_source_filter_index(self, source_name, filter_name, filter_index): """ - Gets the info for a specific source filter. + Sets the index position of a filter on a source. :param source_name: Name of the source the filter is on :type source_name: str @@ -1219,9 +1225,11 @@ class ReqClient(object): response = self.base_client.req("SetSourceFilterIndex", payload) return response - def SetSourceFilterSettings(self, source_name, filter_name, settings, overlay=None): + def set_source_filter_settings( + self, source_name, filter_name, settings, overlay=None + ): """ - Gets the info for a specific source filter. + Sets the settings of a source filter. :param source_name: Name of the source the filter is on :type source_name: str @@ -1243,9 +1251,9 @@ class ReqClient(object): response = self.base_client.req("SetSourceFilterSettings", payload) return response - def SetSourceFilterEnabled(self, source_name, filter_name, enabled): + def set_source_filter_enabled(self, source_name, filter_name, enabled): """ - Gets the info for a specific source filter. + Sets the enable state of a source filter. :param source_name: Name of the source the filter is on :type source_name: str @@ -1264,7 +1272,7 @@ class ReqClient(object): response = self.base_client.req("SetSourceFilterEnabled", payload) return response - def GetSceneItemList(self, name): + def get_scene_item_list(self, name): """ Gets a list of all scene items in a scene. @@ -1277,7 +1285,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemList", payload) return response - def GetGroupItemList(self, name): + def get_group_item_list(self, name): """ Gets a list of all scene items in a scene. @@ -1290,7 +1298,7 @@ class ReqClient(object): response = self.base_client.req("GetGroupItemList", payload) return response - def GetSceneItemId(self, scene_name, source_name, offset=None): + def get_scene_item_id(self, scene_name, source_name, offset=None): """ Searches a scene for a source, and returns its id. @@ -1311,7 +1319,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemId", payload) return response - def CreateSceneItem(self, scene_name, source_name, enabled=None): + def create_scene_item(self, scene_name, source_name, enabled=None): """ Creates a new scene item using a source. Scenes only @@ -1333,7 +1341,7 @@ class ReqClient(object): response = self.base_client.req("CreateSceneItem", payload) return response - def RemoveSceneItem(self, scene_name, item_id): + def remove_scene_item(self, scene_name, item_id): """ Removes a scene item from a scene. Scenes only @@ -1352,7 +1360,7 @@ class ReqClient(object): response = self.base_client.req("RemoveSceneItem", payload) return response - def DuplicateSceneItem(self, scene_name, item_id, dest_scene_name=None): + def duplicate_scene_item(self, scene_name, item_id, dest_scene_name=None): """ Duplicates a scene item, copying all transform and crop info. Scenes only @@ -1374,7 +1382,7 @@ class ReqClient(object): response = self.base_client.req("DuplicateSceneItem", payload) return response - def GetSceneItemTransform(self, scene_name, item_id): + def get_scene_item_transform(self, scene_name, item_id): """ Gets the transform and crop info of a scene item. Scenes and Groups @@ -1393,7 +1401,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemTransform", payload) return response - def SetSceneItemTransform(self, scene_name, item_id, transform): + def set_scene_item_transform(self, scene_name, item_id, transform): """ Sets the transform and crop info of a scene item. @@ -1412,7 +1420,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneItemTransform", payload) return response - def GetSceneItemEnabled(self, scene_name, item_id): + def get_scene_item_enabled(self, scene_name, item_id): """ Gets the enable state of a scene item. Scenes and Groups @@ -1431,7 +1439,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemEnabled", payload) return response - def SetSceneItemEnabled(self, scene_name, item_id, enabled): + def set_scene_item_enabled(self, scene_name, item_id, enabled): """ Sets the enable state of a scene item. Scenes and Groups' @@ -1453,7 +1461,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneItemEnabled", payload) return response - def GetSceneItemLocked(self, scene_name, item_id): + def get_scene_item_locked(self, scene_name, item_id): """ Gets the lock state of a scene item. Scenes and Groups @@ -1472,7 +1480,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemLocked", payload) return response - def SetSceneItemLocked(self, scene_name, item_id, locked): + def set_scene_item_locked(self, scene_name, item_id, locked): """ Sets the lock state of a scene item. Scenes and Groups @@ -1494,7 +1502,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneItemLocked", payload) return response - def GetSceneItemIndex(self, scene_name, item_id): + def get_scene_item_index(self, scene_name, item_id): """ Gets the index position of a scene item in a scene. An index of 0 is at the bottom of the source list in the UI. @@ -1514,7 +1522,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemIndex", payload) return response - def SetSceneItemIndex(self, scene_name, item_id, item_index): + def set_scene_item_index(self, scene_name, item_id, item_index): """ Sets the index position of a scene item in a scene. Scenes and Groups @@ -1536,7 +1544,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneItemIndex", payload) return response - def GetSceneItemBlendMode(self, scene_name, item_id): + def get_scene_item_blend_mode(self, scene_name, item_id): """ Gets the blend mode of a scene item. Blend modes: @@ -1564,7 +1572,7 @@ class ReqClient(object): response = self.base_client.req("GetSceneItemBlendMode", payload) return response - def SetSceneItemBlendMode(self, scene_name, item_id, blend): + def set_scene_item_blend_mode(self, scene_name, item_id, blend): """ Sets the blend mode of a scene item. Scenes and Groups @@ -1586,7 +1594,7 @@ class ReqClient(object): response = self.base_client.req("SetSceneItemBlendMode", payload) return response - def GetVirtualCamStatus(self): + def get_virtual_cam_status(self): """ Gets the status of the virtualcam output. @@ -1595,7 +1603,7 @@ class ReqClient(object): response = self.base_client.req("GetVirtualCamStatus") return response - def ToggleVirtualCam(self): + def toggle_virtual_cam(self): """ Toggles the state of the virtualcam output. @@ -1604,7 +1612,7 @@ class ReqClient(object): response = self.base_client.req("ToggleVirtualCam") return response - def StartVirtualCam(self): + def start_virtual_cam(self): """ Starts the virtualcam output. @@ -1613,7 +1621,7 @@ class ReqClient(object): response = self.base_client.req("StartVirtualCam") return response - def StopVirtualCam(self): + def stop_virtual_cam(self): """ Stops the virtualcam output. @@ -1622,7 +1630,7 @@ class ReqClient(object): response = self.base_client.req("StopVirtualCam") return response - def GetReplayBufferStatus(self): + def get_replay_buffer_status(self): """ Gets the status of the replay buffer output. @@ -1631,7 +1639,7 @@ class ReqClient(object): response = self.base_client.req("GetReplayBufferStatus") return response - def ToggleReplayBuffer(self): + def toggle_replay_buffer(self): """ Toggles the state of the replay buffer output. @@ -1640,7 +1648,7 @@ class ReqClient(object): response = self.base_client.req("ToggleReplayBuffer") return response - def StartReplayBuffer(self): + def start_replay_buffer(self): """ Starts the replay buffer output. @@ -1649,7 +1657,7 @@ class ReqClient(object): response = self.base_client.req("StartReplayBuffer") return response - def StopReplayBuffer(self): + def stop_replay_buffer(self): """ Stops the replay buffer output. @@ -1658,7 +1666,7 @@ class ReqClient(object): response = self.base_client.req("StopReplayBuffer") return response - def SaveReplayBuffer(self): + def save_replay_buffer(self): """ Saves the contents of the replay buffer output. @@ -1667,7 +1675,7 @@ class ReqClient(object): response = self.base_client.req("SaveReplayBuffer") return response - def GetLastReplayBufferReplay(self): + def get_last_replay_buffer_replay(self): """ Gets the filename of the last replay buffer save file. @@ -1676,7 +1684,7 @@ class ReqClient(object): response = self.base_client.req("GetLastReplayBufferReplay") return response - def GetStreamStatus(self): + def get_stream_status(self): """ Gets the status of the stream output. @@ -1685,7 +1693,7 @@ class ReqClient(object): response = self.base_client.req("GetStreamStatus") return response - def ToggleStream(self): + def toggle_stream(self): """ Toggles the status of the stream output. @@ -1694,7 +1702,7 @@ class ReqClient(object): response = self.base_client.req("ToggleStream") return response - def StartStream(self): + def start_stream(self): """ Starts the stream output. @@ -1703,7 +1711,7 @@ class ReqClient(object): response = self.base_client.req("StartStream") return response - def StopStream(self): + def stop_stream(self): """ Stops the stream output. @@ -1712,7 +1720,7 @@ class ReqClient(object): response = self.base_client.req("StopStream") return response - def SendStreamCaption(self, caption): + def send_stream_caption(self, caption): """ Sends CEA-608 caption text over the stream output. @@ -1724,7 +1732,7 @@ class ReqClient(object): response = self.base_client.req("SendStreamCaption") return response - def GetRecordStatus(self): + def get_record_status(self): """ Gets the status of the record output. @@ -1733,7 +1741,7 @@ class ReqClient(object): response = self.base_client.req("GetRecordStatus") return response - def ToggleRecord(self): + def toggle_record(self): """ Toggles the status of the record output. @@ -1742,7 +1750,7 @@ class ReqClient(object): response = self.base_client.req("ToggleRecord") return response - def StartRecord(self): + def start_record(self): """ Starts the record output. @@ -1751,7 +1759,7 @@ class ReqClient(object): response = self.base_client.req("StartRecord") return response - def StopRecord(self): + def stop_record(self): """ Stops the record output. @@ -1760,7 +1768,7 @@ class ReqClient(object): response = self.base_client.req("StopRecord") return response - def ToggleRecordPause(self): + def toggle_record_pause(self): """ Toggles pause on the record output. @@ -1769,7 +1777,7 @@ class ReqClient(object): response = self.base_client.req("ToggleRecordPause") return response - def PauseRecord(self): + def pause_record(self): """ Pauses the record output. @@ -1778,7 +1786,7 @@ class ReqClient(object): response = self.base_client.req("PauseRecord") return response - def ResumeRecord(self): + def resume_record(self): """ Resumes the record output. @@ -1787,7 +1795,7 @@ class ReqClient(object): response = self.base_client.req("ResumeRecord") return response - def GetMediaInputStatus(self, name): + def get_media_input_status(self, name): """ Gets the status of a media input. @@ -1810,7 +1818,7 @@ class ReqClient(object): response = self.base_client.req("GetMediaInputStatus", payload) return response - def SetMediaInputCursor(self, name, cursor): + def set_media_input_cursor(self, name, cursor): """ Sets the cursor position of a media input. This request does not perform bounds checking of the cursor position. @@ -1826,7 +1834,7 @@ class ReqClient(object): response = self.base_client.req("SetMediaInputCursor", payload) return response - def OffsetMediaInputCursor(self, name, offset): + def offset_media_input_cursor(self, name, offset): """ Offsets the current cursor position of a media input by the specified value. This request does not perform bounds checking of the cursor position. @@ -1842,7 +1850,7 @@ class ReqClient(object): response = self.base_client.req("OffsetMediaInputCursor", payload) return response - def TriggerMediaInputAction(self, name, action): + def trigger_media_input_action(self, name, action): """ Triggers an action on a media input. @@ -1857,16 +1865,15 @@ class ReqClient(object): response = self.base_client.req("TriggerMediaInputAction", payload) return response - def GetStudioModeEnabled(self): + def get_studio_mode_enabled(self): """ Gets whether studio is enabled. """ - response = self.base_client.req("GetStudioModeEnabled") - return response + return self.getter("GetStudioModeEnabled") - def SetStudioModeEnabled(self, enabled): + def set_studio_mode_enabled(self, enabled): """ Enables or disables studio mode @@ -1879,7 +1886,7 @@ class ReqClient(object): response = self.base_client.req("SetStudioModeEnabled", payload) return response - def OpenInputPropertiesDialog(self, name): + def open_input_properties_dialog(self, name): """ Opens the properties dialog of an input. @@ -1892,7 +1899,7 @@ class ReqClient(object): response = self.base_client.req("OpenInputPropertiesDialog", payload) return response - def OpenInputFiltersDialog(self, name): + def open_input_filters_dialog(self, name): """ Opens the filters dialog of an input. @@ -1905,7 +1912,7 @@ class ReqClient(object): response = self.base_client.req("OpenInputFiltersDialog", payload) return response - def OpenInputInteractDialog(self, name): + def open_input_interact_dialog(self, name): """ Opens the filters dialog of an input. @@ -1918,7 +1925,7 @@ class ReqClient(object): response = self.base_client.req("OpenInputInteractDialog", payload) return response - def GetMonitorList(self, name): + def get_monitor_list(self, name): """ Gets a list of connected monitors and information about them. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..d341e9e --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,11 @@ +import obsstudio_sdk as obs + +req_cl = obs.ReqClient() + + +def setup_module(): + pass + + +def teardown_module(): + req_cl.base_client.ws.close() diff --git a/tests/test_request.py b/tests/test_request.py new file mode 100644 index 0000000..722748f --- /dev/null +++ b/tests/test_request.py @@ -0,0 +1,39 @@ +import time + +import pytest + +from tests import req_cl + + +class TestRequests: + __test__ = True + + def test_get_version(self): + resp = req_cl.get_version() + assert "obsVersion" in resp + assert "obsWebSocketVersion" in resp + + @pytest.mark.parametrize( + "scene", + [ + ("START"), + ("BRB"), + ("END"), + ], + ) + def test_current_program_scene(self, scene): + req_cl.set_current_program_scene(scene) + resp = req_cl.get_current_program_scene() + assert resp["currentProgramSceneName"] == scene + + @pytest.mark.parametrize( + "state", + [ + (False), + (True), + ], + ) + def test_set_studio_mode_enabled_true(self, state): + req_cl.set_studio_mode_enabled(state) + resp = req_cl.get_studio_mode_enabled() + assert resp["studioModeEnabled"] == state From 59d66d6ede3f6b919014dfef87cf39de3eef2cbe Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:05:09 +0100 Subject: [PATCH 10/27] md change in readme --- README.md | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 16ec141..18301e4 100644 --- a/README.md +++ b/README.md @@ -39,18 +39,14 @@ Otherwise: port: port to access server password: obs websocket server password -``` ->>>from obsstudio_sdk.reqs import ReqClient ->>> ->>>client = ReqClient('192.168.1.1', 4444, 'somepassword') -``` +Example `__main__.py` -Now you can make calls to OBS +```python +from obsstudio_sdk.reqs import ReqClient -Example: Toggle the mute state of your Mic input - -``` ->>>cl.ToggleInputMute('Mic/Aux') ->>> +# pass conn info if not in config.toml +cl = ReqClient('localhost', 4455, 'mystrongpass') +# Toggle the mute state of your Mic input +cl.ToggleInputMute('Mic/Aux') ``` From 1c24b4bc1e29b48ab3f70d5f0183f4cc12ae1151 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:06:06 +0100 Subject: [PATCH 11/27] snake case func name to match changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18301e4..6eb047c 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,5 @@ from obsstudio_sdk.reqs import ReqClient cl = ReqClient('localhost', 4455, 'mystrongpass') # Toggle the mute state of your Mic input -cl.ToggleInputMute('Mic/Aux') +cl.toggle_input_mute('Mic/Aux') ``` From 0819149d09bd1b71fb0eafd71490447be85fef15 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:18:32 +0100 Subject: [PATCH 12/27] move link to documentation into its own section --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6eb047c..e13d9e7 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,7 @@ -# 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. -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) +Not all endpoints in the official documentation are implemented. ## Requirements @@ -50,3 +47,7 @@ cl = ReqClient('localhost', 4455, 'mystrongpass') # Toggle the mute state of your Mic input cl.toggle_input_mute('Mic/Aux') ``` + +### Official Documentation + +- [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol) From 0819bb4342425b6b3a221146df5665e5efa7699f Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 22:25:31 +0100 Subject: [PATCH 13/27] upd import in readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e13d9e7..f46ab66 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,10 @@ Otherwise: Example `__main__.py` ```python -from obsstudio_sdk.reqs import ReqClient +import obsstudio_sdk as obs # pass conn info if not in config.toml -cl = ReqClient('localhost', 4455, 'mystrongpass') +cl = obs.ReqClient('localhost', 4455, 'mystrongpass') # Toggle the mute state of your Mic input cl.toggle_input_mute('Mic/Aux') From 35173733bc0a9ce8203432610242d4f6d1c3886a Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 23:08:07 +0100 Subject: [PATCH 14/27] md change --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f46ab66..c60b782 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ pip install obsstudio-sdk ### How to Use -- Load connection info from toml config. A valid `config.toml` might look like this: +Load connection info from toml config. A valid `config.toml` might look like this: ```toml [connection] @@ -28,13 +28,15 @@ password = "mystrongpass" It should be placed next to your `__main__.py` file. -Otherwise: +#### Otherwise: + +Import and start using -- Import and start using Parameters are as follows: - host: obs websocket server - port: port to access server - password: obs websocket server password + +- `host`: obs websocket server +- `port`: port to access server +- `password`: obs websocket server password Example `__main__.py` From ec048e1aefa33c21b5c2a974bb964ca9e684e35e Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 23:09:14 +0100 Subject: [PATCH 15/27] md change --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index c60b782..0af966f 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,7 @@ It should be placed next to your `__main__.py` file. #### Otherwise: -Import and start using - - Parameters are as follows: +Import and start using, parameters are as follows: - `host`: obs websocket server - `port`: port to access server From c71d7e4ea9582a1d1b49c62e568e963975e55e90 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Tue, 26 Jul 2022 23:12:34 +0100 Subject: [PATCH 16/27] remove redundant imports --- obsstudio_sdk/reqs.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index 40d0dc0..417d0a3 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -1,6 +1,3 @@ -import time -from re import S - from .baseclient import ObsClient from .error import OBSSDKError From 2a3a86c2778708a971bfbe7e9bac0973b54cfd5a Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Wed, 27 Jul 2022 19:39:33 +0100 Subject: [PATCH 17/27] EventsClient renamed to EventClient remove getter, setter for send. add persistend data unit test add hotkey example default event sub now 0. explicitly define subs in event class. now subs can be set as kwarg --- examples/events/__main__.py | 2 +- examples/hotkeys/__main__.py | 40 ++++ obsstudio_sdk/__init__.py | 2 +- obsstudio_sdk/baseclient.py | 26 +-- obsstudio_sdk/events.py | 22 +- obsstudio_sdk/reqs.py | 379 ++++++++++++----------------------- tests/test_request.py | 14 +- 7 files changed, 209 insertions(+), 276 deletions(-) create mode 100644 examples/hotkeys/__main__.py diff --git a/examples/events/__main__.py b/examples/events/__main__.py index 83cdac1..40b297d 100644 --- a/examples/events/__main__.py +++ b/examples/events/__main__.py @@ -33,7 +33,7 @@ class Observer: if __name__ == "__main__": - cl = obs.EventsClient() + cl = obs.EventClient() observer = Observer(cl) while cmd := input(" to exit\n"): diff --git a/examples/hotkeys/__main__.py b/examples/hotkeys/__main__.py new file mode 100644 index 0000000..f50211d --- /dev/null +++ b/examples/hotkeys/__main__.py @@ -0,0 +1,40 @@ +import inspect + +import keyboard +import obsstudio_sdk as obs + + +class Observer: + def __init__(self, cl): + self._cl = cl + self._cl.callback.register(self.on_current_program_scene_changed) + print(f"Registered events: {self._cl.callback.get()}") + + @property + def event_identifier(self): + return inspect.stack()[1].function + + def on_current_program_scene_changed(self, data): + """The current program scene has changed.""" + print(f"{self.event_identifier}: {data}") + + +def version(): + print(req_cl.get_version()) + + +def set_scene(scene, *args): + req_cl.set_current_program_scene(scene) + + +if __name__ == "__main__": + req_cl = obs.ReqClient() + req_ev = obs.EventClient() + observer = Observer(req_ev) + + 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") diff --git a/obsstudio_sdk/__init__.py b/obsstudio_sdk/__init__.py index 7c892fc..d0ba897 100644 --- a/obsstudio_sdk/__init__.py +++ b/obsstudio_sdk/__init__.py @@ -1,4 +1,4 @@ -from .events import EventsClient +from .events import EventClient from .reqs import ReqClient __ALL__ = ["ReqClient", "EventsClient"] diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index 9a31c0a..5419e1f 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -9,18 +9,13 @@ from random import randint import tomllib import websocket -Subs = IntEnum( - "Subs", - "general config scenes inputs transitions filters outputs sceneitems mediainputs vendors ui", - start=0, -) - class ObsClient(object): DELAY = 0.001 def __init__(self, **kwargs): defaultkwargs = {key: None for key in ["host", "port", "password"]} + defaultkwargs["subs"] = 0 kwargs = defaultkwargs | kwargs for attr, val in kwargs.items(): setattr(self, attr, val) @@ -59,26 +54,12 @@ class ObsClient(object): ).digest() ).decode() - all_non_high_volume = ( - (1 << Subs.general) - | (1 << Subs.config) - | (1 << Subs.scenes) - | (1 << Subs.inputs) - | (1 << Subs.transitions) - | (1 << Subs.filters) - | (1 << Subs.outputs) - | (1 << Subs.sceneitems) - | (1 << Subs.mediainputs) - | (1 << Subs.vendors) - | (1 << Subs.ui) - ) - payload = { "op": 1, "d": { "rpcVersion": 1, "authentication": auth, - "eventSubscriptions": all_non_high_volume, + "eventSubscriptions": self.subs, }, } @@ -102,7 +83,4 @@ class ObsClient(object): } self.ws.send(json.dumps(payload)) response = json.loads(self.ws.recv()) - while "requestId" not in response["d"]: - response = json.loads(self.ws.recv()) - time.sleep(self.DELAY) return response["d"] diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py index 6c47dc6..c38adec 100644 --- a/obsstudio_sdk/events.py +++ b/obsstudio_sdk/events.py @@ -1,5 +1,6 @@ import json import time +from enum import IntEnum from threading import Thread from .baseclient import ObsClient @@ -11,11 +12,30 @@ defined in official github repo https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events """ +Subs = IntEnum( + "Subs", + "general config scenes inputs transitions filters outputs sceneitems mediainputs vendors ui", + start=0, +) -class EventsClient(object): + +class EventClient(object): DELAY = 0.001 def __init__(self, **kwargs): + kwargs["subs"] = ( + (1 << Subs.general) + | (1 << Subs.config) + | (1 << Subs.scenes) + | (1 << Subs.inputs) + | (1 << Subs.transitions) + | (1 << Subs.filters) + | (1 << Subs.outputs) + | (1 << Subs.sceneitems) + | (1 << Subs.mediainputs) + | (1 << Subs.vendors) + | (1 << Subs.ui) + ) self.base_client = ObsClient(**kwargs) self.base_client.authenticate() self.callback = Callback() diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index 417d0a3..c685f11 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -13,11 +13,7 @@ class ReqClient(object): self.base_client = ObsClient(**kwargs) self.base_client.authenticate() - def getter(self, param): - response = self.base_client.req(param) - return response["responseData"] - - def setter(self, param, data): + def send(self, param, data=None): response = self.base_client.req(param, data) if not response["requestStatus"]["result"]: error = ( @@ -26,8 +22,8 @@ class ReqClient(object): if "comment" in response["requestStatus"]: error += (f"With message: {response['requestStatus']['comment']}",) raise OBSSDKError("\n".join(error)) - - action = setter + if "responseData" in response: + return response["responseData"] def get_version(self): """ @@ -38,7 +34,7 @@ class ReqClient(object): """ - return self.getter("GetVersion") + return self.send("GetVersion") def get_stats(self): """ @@ -49,7 +45,7 @@ class ReqClient(object): """ - return self.getter("GetStats") + return self.send("GetStats") def broadcast_custom_event(self, eventData): """ @@ -62,7 +58,7 @@ class ReqClient(object): """ - self.action("BroadcastCustomEvent", eventData) + self.send("BroadcastCustomEvent", eventData) def call_vendor_request(self, vendorName, requestType, requestData=None): """ @@ -86,8 +82,7 @@ class ReqClient(object): """ - response = self.base_client.req(req_type=requestType, req_data=requestData) - return response + self.send(requestType, requestData) def get_hot_key_list(self): """ @@ -98,7 +93,7 @@ class ReqClient(object): """ - return self.getter("GetHotkeyList") + return self.send("GetHotkeyList") def trigger_hot_key_by_name(self, hotkeyName): """ @@ -111,7 +106,7 @@ class ReqClient(object): """ payload = {"hotkeyName": hotkeyName} - self.action("TriggerHotkeyByName", payload) + self.send("TriggerHotkeyByName", payload) def trigger_hot_key_by_key_sequence( self, keyId, pressShift, pressCtrl, pressAlt, pressCmd @@ -143,9 +138,7 @@ class ReqClient(object): "cmd": pressCmd, }, } - - response = self.base_client.req("TriggerHotkeyByKeySequence", payload) - return response + self.send("TriggerHotkeyByKeySequence", payload) def sleep(self, sleepMillis=None, sleepFrames=None): """ @@ -160,7 +153,7 @@ class ReqClient(object): """ payload = {"sleepMillis": sleepMillis, "sleepFrames": sleepFrames} - self.action("Sleep", payload) + self.send("Sleep", payload) def get_persistent_data(self, realm, slotName): """ @@ -177,8 +170,7 @@ class ReqClient(object): """ payload = {"realm": realm, "slotName": slotName} - response = self.base_client.req("GetPersistentData", payload) - return response + return self.send("GetPersistentData", payload) def set_persistent_data(self, realm, slotName, slotValue): """ @@ -195,8 +187,7 @@ class ReqClient(object): """ payload = {"realm": realm, "slotName": slotName, "slotValue": slotValue} - response = self.base_client.req("SetPersistentData", payload) - return response + self.send("SetPersistentData", payload) def get_scene_collection_list(self): """ @@ -207,7 +198,7 @@ class ReqClient(object): """ - return self.getter("GetSceneCollectionList") + return self.send("GetSceneCollectionList") def set_current_scene_collection(self, name): """ @@ -219,7 +210,7 @@ class ReqClient(object): """ payload = {"sceneCollectionName": name} - self.setter("SetCurrentSceneCollection", payload) + self.send("SetCurrentSceneCollection", payload) def create_scene_collection(self, name): """ @@ -232,7 +223,7 @@ class ReqClient(object): """ payload = {"sceneCollectionName": name} - self.action("CreateSceneCollection", payload) + self.send("CreateSceneCollection", payload) def get_profile_list(self): """ @@ -243,7 +234,7 @@ class ReqClient(object): """ - return self.getter("GetProfileList") + return self.send("GetProfileList") def set_current_profile(self, name): """ @@ -255,7 +246,7 @@ class ReqClient(object): """ payload = {"profileName": name} - self.setter("SetCurrentProfile", payload) + self.send("SetCurrentProfile", payload) def create_profile(self, name): """ @@ -267,8 +258,7 @@ class ReqClient(object): """ payload = {"profileName": name} - response = self.base_client.req("CreateProfile", payload) - return response + self.send("CreateProfile", payload) def remove_profile(self, name): """ @@ -281,8 +271,7 @@ class ReqClient(object): """ payload = {"profileName": name} - response = self.base_client.req("RemoveProfile", payload) - return response + self.send("RemoveProfile", payload) def get_profile_parameter(self, category, name): """ @@ -299,8 +288,7 @@ class ReqClient(object): """ payload = {"parameterCategory": category, "parameterName": name} - response = self.base_client.req("GetProfileParameter", payload) - return response + return self.send("GetProfileParameter", payload) def set_profile_parameter(self, category, name, value): """ @@ -323,8 +311,7 @@ class ReqClient(object): "parameterName": name, "parameterValue": value, } - response = self.base_client.req("SetProfileParameter", payload) - return response + self.send("SetProfileParameter", payload) def get_video_settings(self): """ @@ -334,8 +321,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetVideoSettings") - return response + return self.send("GetVideoSettings") def set_video_settings( self, numerator, denominator, base_width, base_height, out_width, out_height @@ -368,8 +354,7 @@ class ReqClient(object): "outputWidth": out_width, "outputHeight": out_height, } - response = self.base_client.req("SetVideoSettings", payload) - return response + self.send("SetVideoSettings", payload) def get_stream_service_settings(self): """ @@ -377,8 +362,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetStreamServiceSettings") - return response + return self.send("GetStreamServiceSettings") def set_stream_service_settings(self, ss_type, ss_settings): """ @@ -397,8 +381,7 @@ class ReqClient(object): "streamServiceType": ss_type, "streamServiceSettings": ss_settings, } - response = self.base_client.req("SetStreamServiceSettings", payload) - return response + self.send("SetStreamServiceSettings", payload) def get_source_active(self, name): """ @@ -410,8 +393,7 @@ class ReqClient(object): """ payload = {"sourceName": name} - response = self.base_client.req("GetSourceActive", payload) - return response + return self.send("GetSourceActive", payload) def get_source_screenshot(self, name, img_format, width, height, quality): """ @@ -442,8 +424,7 @@ class ReqClient(object): "imageHeight": height, "imageCompressionQuality": quality, } - response = self.base_client.req("GetSourceScreenshot", payload) - return response + return self.send("GetSourceScreenshot", payload) def save_source_screenshot( self, name, img_format, file_path, width, height, quality @@ -479,8 +460,7 @@ class ReqClient(object): "imageHeight": height, "imageCompressionQuality": quality, } - response = self.base_client.req("SaveSourceScreenshot", payload) - return response + return self.send("SaveSourceScreenshot", payload) def get_scene_list(self): """ @@ -488,7 +468,7 @@ class ReqClient(object): """ - return self.getter("GetSceneList") + return self.send("GetSceneList") def get_group_list(self): """ @@ -499,8 +479,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetSceneList") - return response + return self.send("GetSceneList") def get_current_program_scene(self): """ @@ -508,7 +487,7 @@ class ReqClient(object): """ - return self.getter("GetCurrentProgramScene") + return self.send("GetCurrentProgramScene") def set_current_program_scene(self, name): """ @@ -520,7 +499,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - self.setter("SetCurrentProgramScene", payload) + self.send("SetCurrentProgramScene", payload) def get_current_preview_scene(self): """ @@ -528,8 +507,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetCurrentPreviewScene") - return response + return self.send("GetCurrentPreviewScene") def set_current_preview_scene(self, name): """ @@ -541,8 +519,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("SetCurrentPreviewScene", payload) - return response + self.send("SetCurrentPreviewScene", payload) def create_scene(self, name): """ @@ -554,8 +531,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("CreateScene", payload) - return response + self.send("CreateScene", payload) def remove_scene(self, name): """ @@ -567,8 +543,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("RemoveScene", payload) - return response + self.send("RemoveScene", payload) def set_scene_name(self, old_name, new_name): """ @@ -582,8 +557,7 @@ class ReqClient(object): """ payload = {"sceneName": old_name, "newSceneName": new_name} - response = self.base_client.req("SetSceneName", payload) - return response + self.send("SetSceneName", payload) def get_scene_scene_transition_override(self, name): """ @@ -595,8 +569,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("GetSceneSceneTransitionOverride", payload) - return response + return self.send("GetSceneSceneTransitionOverride", payload) def set_scene_scene_transition_override(self, scene_name, tr_name, tr_duration): """ @@ -616,8 +589,7 @@ class ReqClient(object): "transitionName": tr_name, "transitionDuration": tr_duration, } - response = self.base_client.req("SetSceneSceneTransitionOverride", payload) - return response + self.send("SetSceneSceneTransitionOverride", payload) def get_input_list(self, kind): """ @@ -629,8 +601,7 @@ class ReqClient(object): """ payload = {"inputKind": kind} - response = self.base_client.req("GetInputList", payload) - return response + return self.send("GetInputList", payload) def get_input_kind_list(self, unversioned): """ @@ -642,8 +613,7 @@ class ReqClient(object): """ payload = {"unversioned": unversioned} - response = self.base_client.req("GetInputKindList", payload) - return response + return self.send("GetInputKindList", payload) def get_special_inputs(self): """ @@ -651,8 +621,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetSpecialInputs") - return response + return self.send("GetSpecialInputs") def create_input( self, sceneName, inputName, inputKind, inputSettings, sceneItemEnabled @@ -680,8 +649,7 @@ class ReqClient(object): "inputSettings": inputSettings, "sceneItemEnabled": sceneItemEnabled, } - response = self.base_client.req("CreateInput", payload) - return response + self.send("CreateInput", payload) def remove_input(self, name): """ @@ -693,8 +661,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("RemoveInput", payload) - return response + self.send("RemoveInput", payload) def set_input_name(self, old_name, new_name): """ @@ -708,8 +675,7 @@ class ReqClient(object): """ payload = {"inputName": old_name, "newInputName": new_name} - response = self.base_client.req("SetInputName", payload) - return response + self.send("SetInputName", payload) def get_input_default_settings(self, kind): """ @@ -721,8 +687,7 @@ class ReqClient(object): """ payload = {"inputKind": kind} - response = self.base_client.req("GetInputDefaultSettings", payload) - return response + return self.send("GetInputDefaultSettings", payload) def get_input_settings(self, name): """ @@ -736,8 +701,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputSettings", payload) - return response + return self.send("GetInputSettings", payload) def set_input_settings(self, name, settings, overlay): """ @@ -753,8 +717,7 @@ class ReqClient(object): """ payload = {"inputName": name, "inputSettings": settings, "overlay": overlay} - response = self.base_client.req("SetInputSettings", payload) - return response + self.send("SetInputSettings", payload) def get_input_mute(self, name): """ @@ -766,8 +729,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputMute", payload) - return response + return self.send("GetInputMute", payload) def set_input_mute(self, name, muted): """ @@ -781,8 +743,7 @@ class ReqClient(object): """ payload = {"inputName": name, "inputMuted": muted} - response = self.base_client.req("SetInputMute", payload) - return response + self.send("SetInputMute", payload) def toggle_input_mute(self, name): """ @@ -794,8 +755,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("ToggleInputMute", payload) - return response + self.send("ToggleInputMute", payload) def get_input_volume(self, name): """ @@ -807,8 +767,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputVolume", payload) - return response + return self.send("GetInputVolume", payload) def set_input_volume(self, name, vol_mul=None, vol_db=None): """ @@ -828,8 +787,7 @@ class ReqClient(object): "inputVolumeMul": vol_mul, "inputVolumeDb": vol_db, } - response = self.base_client.req("SetInputVolume", payload) - return response + self.send("SetInputVolume", payload) def get_input_audio_balance(self, name): """ @@ -841,8 +799,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputAudioBalance", payload) - return response + return self.send("GetInputAudioBalance", payload) def set_input_audio_balance(self, name, balance): """ @@ -856,8 +813,7 @@ class ReqClient(object): """ payload = {"inputName": name, "inputAudioBalance": balance} - response = self.base_client.req("SetInputAudioBalance", payload) - return response + self.send("SetInputAudioBalance", payload) def get_input_audio_sync_offset(self, name): """ @@ -869,8 +825,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputAudioSyncOffset", payload) - return response + return self.send("GetInputAudioSyncOffset", payload) def set_input_audio_sync_offset(self, name, offset): """ @@ -884,8 +839,7 @@ class ReqClient(object): """ payload = {"inputName": name, "inputAudioSyncOffset": offset} - response = self.base_client.req("SetInputAudioSyncOffset", payload) - return response + self.send("SetInputAudioSyncOffset", payload) def get_input_audio_monitor_type(self, name): """ @@ -903,8 +857,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputAudioMonitorType", payload) - return response + return self.send("GetInputAudioMonitorType", payload) def set_input_audio_monitor_type(self, name, mon_type): """ @@ -918,8 +871,7 @@ class ReqClient(object): """ payload = {"inputName": name, "monitorType": mon_type} - response = self.base_client.req("SetInputAudioMonitorType", payload) - return response + self.send("SetInputAudioMonitorType", payload) def get_input_audio_tracks(self, name): """ @@ -931,8 +883,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetInputAudioTracks", payload) - return response + return self.send("GetInputAudioTracks", payload) def set_input_audio_tracks(self, name, track): """ @@ -946,8 +897,7 @@ class ReqClient(object): """ payload = {"inputName": name, "inputAudioTracks": track} - response = self.base_client.req("SetInputAudioTracks", payload) - return response + self.send("SetInputAudioTracks", payload) def get_input_properties_list_property_items(self, input_name, prop_name): """ @@ -964,8 +914,7 @@ class ReqClient(object): """ payload = {"inputName": input_name, "propertyName": prop_name} - response = self.base_client.req("GetInputPropertiesListPropertyItems", payload) - return response + return self.send("GetInputPropertiesListPropertyItems", payload) def press_input_properties_button(self, input_name, prop_name): """ @@ -982,8 +931,7 @@ class ReqClient(object): """ payload = {"inputName": input_name, "propertyName": prop_name} - response = self.base_client.req("PressInputPropertiesButton", payload) - return response + self.send("PressInputPropertiesButton", payload) def get_transition_kind_list(self): """ @@ -992,8 +940,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetTransitionKindList") - return response + return self.send("GetTransitionKindList") def get_scene_transition_list(self): """ @@ -1001,8 +948,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetSceneTransitionList") - return response + return self.send("GetSceneTransitionList") def get_current_scene_transition(self): """ @@ -1010,8 +956,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetCurrentSceneTransition") - return response + return self.send("GetCurrentSceneTransition") def set_current_scene_transition(self, name): """ @@ -1025,8 +970,7 @@ class ReqClient(object): """ payload = {"transitionName": name} - response = self.base_client.req("SetCurrentSceneTransition", payload) - return response + self.send("SetCurrentSceneTransition", payload) def set_current_scene_transition_duration(self, duration): """ @@ -1038,8 +982,7 @@ class ReqClient(object): """ payload = {"transitionDuration": duration} - response = self.base_client.req("SetCurrentSceneTransitionDuration", payload) - return response + self.send("SetCurrentSceneTransitionDuration", payload) def set_current_scene_transition_settings(self, settings, overlay=None): """ @@ -1053,8 +996,7 @@ class ReqClient(object): """ payload = {"transitionSettings": settings, "overlay": overlay} - response = self.base_client.req("SetCurrentSceneTransitionSettings", payload) - return response + self.send("SetCurrentSceneTransitionSettings", payload) def get_current_scene_transition_cursor(self): """ @@ -1063,8 +1005,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetCurrentSceneTransitionCursor") - return response + return self.send("GetCurrentSceneTransitionCursor") def trigger_studio_mode_transition(self): """ @@ -1075,8 +1016,7 @@ class ReqClient(object): """ - response = self.base_client.req("TriggerStudioModeTransition") - return response + self.send("TriggerStudioModeTransition") def set_t_bar_position(self, pos, release=None): """ @@ -1092,8 +1032,7 @@ class ReqClient(object): """ payload = {"position": pos, "release": release} - response = self.base_client.req("SetTBarPosition", payload) - return response + self.send("SetTBarPosition", payload) def get_source_filter_list(self, name): """ @@ -1105,8 +1044,7 @@ class ReqClient(object): """ payload = {"sourceName": name} - response = self.base_client.req("GetSourceFilterList", payload) - return response + return self.send("GetSourceFilterList", payload) def get_source_filter_default_settings(self, kind): """ @@ -1118,8 +1056,7 @@ class ReqClient(object): """ payload = {"filterKind": kind} - response = self.base_client.req("GetSourceFilterDefaultSettings", payload) - return response + return self.send("GetSourceFilterDefaultSettings", payload) def create_source_filter( self, source_name, filter_name, filter_kind, filter_settings=None @@ -1144,8 +1081,7 @@ class ReqClient(object): "filterKind": filter_kind, "filterSettings": filter_settings, } - response = self.base_client.req("CreateSourceFilter", payload) - return response + self.send("CreateSourceFilter", payload) def remove_source_filter(self, source_name, filter_name): """ @@ -1162,8 +1098,7 @@ class ReqClient(object): "sourceName": source_name, "filterName": filter_name, } - response = self.base_client.req("RemoveSourceFilter", payload) - return response + self.send("RemoveSourceFilter", payload) def set_source_filter_name(self, source_name, old_filter_name, new_filter_name): """ @@ -1183,8 +1118,7 @@ class ReqClient(object): "filterName": old_filter_name, "newFilterName": new_filter_name, } - response = self.base_client.req("SetSourceFilterName", payload) - return response + self.send("SetSourceFilterName", payload) def get_source_filter(self, source_name, filter_name): """ @@ -1198,8 +1132,7 @@ class ReqClient(object): """ payload = {"sourceName": source_name, "filterName": filter_name} - response = self.base_client.req("GetSourceFilter", payload) - return response + return self.send("GetSourceFilter", payload) def set_source_filter_index(self, source_name, filter_name, filter_index): """ @@ -1219,8 +1152,7 @@ class ReqClient(object): "filterName": filter_name, "filterIndex": filter_index, } - response = self.base_client.req("SetSourceFilterIndex", payload) - return response + self.send("SetSourceFilterIndex", payload) def set_source_filter_settings( self, source_name, filter_name, settings, overlay=None @@ -1245,8 +1177,7 @@ class ReqClient(object): "filterSettings": settings, "overlay": overlay, } - response = self.base_client.req("SetSourceFilterSettings", payload) - return response + self.send("SetSourceFilterSettings", payload) def set_source_filter_enabled(self, source_name, filter_name, enabled): """ @@ -1266,8 +1197,7 @@ class ReqClient(object): "filterName": filter_name, "filterEnabled": enabled, } - response = self.base_client.req("SetSourceFilterEnabled", payload) - return response + self.send("SetSourceFilterEnabled", payload) def get_scene_item_list(self, name): """ @@ -1279,8 +1209,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("GetSceneItemList", payload) - return response + return self.send("GetSceneItemList", payload) def get_group_item_list(self, name): """ @@ -1292,8 +1221,7 @@ class ReqClient(object): """ payload = {"sceneName": name} - response = self.base_client.req("GetGroupItemList", payload) - return response + return self.send("GetGroupItemList", payload) def get_scene_item_id(self, scene_name, source_name, offset=None): """ @@ -1313,8 +1241,7 @@ class ReqClient(object): "sourceName": source_name, "searchOffset": offset, } - response = self.base_client.req("GetSceneItemId", payload) - return response + return self.send("GetSceneItemId", payload) def create_scene_item(self, scene_name, source_name, enabled=None): """ @@ -1335,8 +1262,7 @@ class ReqClient(object): "sourceName": source_name, "sceneItemEnabled": enabled, } - response = self.base_client.req("CreateSceneItem", payload) - return response + self.send("CreateSceneItem", payload) def remove_scene_item(self, scene_name, item_id): """ @@ -1354,8 +1280,7 @@ class ReqClient(object): "sceneName": scene_name, "sceneItemId": item_id, } - response = self.base_client.req("RemoveSceneItem", payload) - return response + self.send("RemoveSceneItem", payload) def duplicate_scene_item(self, scene_name, item_id, dest_scene_name=None): """ @@ -1376,8 +1301,7 @@ class ReqClient(object): "sceneItemId": item_id, "destinationSceneName": dest_scene_name, } - response = self.base_client.req("DuplicateSceneItem", payload) - return response + self.send("DuplicateSceneItem", payload) def get_scene_item_transform(self, scene_name, item_id): """ @@ -1395,8 +1319,7 @@ class ReqClient(object): "sceneName": scene_name, "sceneItemId": item_id, } - response = self.base_client.req("GetSceneItemTransform", payload) - return response + return self.send("GetSceneItemTransform", payload) def set_scene_item_transform(self, scene_name, item_id, transform): """ @@ -1414,8 +1337,7 @@ class ReqClient(object): "sceneItemId": item_id, "sceneItemTransform": transform, } - response = self.base_client.req("SetSceneItemTransform", payload) - return response + self.send("SetSceneItemTransform", payload) def get_scene_item_enabled(self, scene_name, item_id): """ @@ -1433,8 +1355,7 @@ class ReqClient(object): "sceneName": scene_name, "sceneItemId": item_id, } - response = self.base_client.req("GetSceneItemEnabled", payload) - return response + return self.send("GetSceneItemEnabled", payload) def set_scene_item_enabled(self, scene_name, item_id, enabled): """ @@ -1455,8 +1376,7 @@ class ReqClient(object): "sceneItemId": item_id, "sceneItemEnabled": enabled, } - response = self.base_client.req("SetSceneItemEnabled", payload) - return response + self.send("SetSceneItemEnabled", payload) def get_scene_item_locked(self, scene_name, item_id): """ @@ -1474,8 +1394,7 @@ class ReqClient(object): "sceneName": scene_name, "sceneItemId": item_id, } - response = self.base_client.req("GetSceneItemLocked", payload) - return response + return self.send("GetSceneItemLocked", payload) def set_scene_item_locked(self, scene_name, item_id, locked): """ @@ -1496,8 +1415,7 @@ class ReqClient(object): "sceneItemId": item_id, "sceneItemLocked": locked, } - response = self.base_client.req("SetSceneItemLocked", payload) - return response + self.send("SetSceneItemLocked", payload) def get_scene_item_index(self, scene_name, item_id): """ @@ -1516,8 +1434,7 @@ class ReqClient(object): "sceneName": scene_name, "sceneItemId": item_id, } - response = self.base_client.req("GetSceneItemIndex", payload) - return response + return self.send("GetSceneItemIndex", payload) def set_scene_item_index(self, scene_name, item_id, item_index): """ @@ -1538,8 +1455,7 @@ class ReqClient(object): "sceneItemId": item_id, "sceneItemLocked": item_index, } - response = self.base_client.req("SetSceneItemIndex", payload) - return response + self.send("SetSceneItemIndex", payload) def get_scene_item_blend_mode(self, scene_name, item_id): """ @@ -1566,8 +1482,7 @@ class ReqClient(object): "sceneName": scene_name, "sceneItemId": item_id, } - response = self.base_client.req("GetSceneItemBlendMode", payload) - return response + return self.send("GetSceneItemBlendMode", payload) def set_scene_item_blend_mode(self, scene_name, item_id, blend): """ @@ -1588,8 +1503,7 @@ class ReqClient(object): "sceneItemId": item_id, "sceneItemBlendMode": blend, } - response = self.base_client.req("SetSceneItemBlendMode", payload) - return response + self.send("SetSceneItemBlendMode", payload) def get_virtual_cam_status(self): """ @@ -1597,8 +1511,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetVirtualCamStatus") - return response + return self.send("GetVirtualCamStatus") def toggle_virtual_cam(self): """ @@ -1606,8 +1519,7 @@ class ReqClient(object): """ - response = self.base_client.req("ToggleVirtualCam") - return response + self.send("ToggleVirtualCam") def start_virtual_cam(self): """ @@ -1615,8 +1527,7 @@ class ReqClient(object): """ - response = self.base_client.req("StartVirtualCam") - return response + self.send("StartVirtualCam") def stop_virtual_cam(self): """ @@ -1624,8 +1535,7 @@ class ReqClient(object): """ - response = self.base_client.req("StopVirtualCam") - return response + self.send("StopVirtualCam") def get_replay_buffer_status(self): """ @@ -1633,8 +1543,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetReplayBufferStatus") - return response + return self.send("GetReplayBufferStatus") def toggle_replay_buffer(self): """ @@ -1642,8 +1551,7 @@ class ReqClient(object): """ - response = self.base_client.req("ToggleReplayBuffer") - return response + self.send("ToggleReplayBuffer") def start_replay_buffer(self): """ @@ -1651,8 +1559,7 @@ class ReqClient(object): """ - response = self.base_client.req("StartReplayBuffer") - return response + self.send("StartReplayBuffer") def stop_replay_buffer(self): """ @@ -1660,8 +1567,7 @@ class ReqClient(object): """ - response = self.base_client.req("StopReplayBuffer") - return response + self.send("StopReplayBuffer") def save_replay_buffer(self): """ @@ -1669,8 +1575,7 @@ class ReqClient(object): """ - response = self.base_client.req("SaveReplayBuffer") - return response + self.send("SaveReplayBuffer") def get_last_replay_buffer_replay(self): """ @@ -1678,8 +1583,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetLastReplayBufferReplay") - return response + return self.send("GetLastReplayBufferReplay") def get_stream_status(self): """ @@ -1687,8 +1591,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetStreamStatus") - return response + return self.send("GetStreamStatus") def toggle_stream(self): """ @@ -1696,8 +1599,7 @@ class ReqClient(object): """ - response = self.base_client.req("ToggleStream") - return response + self.send("ToggleStream") def start_stream(self): """ @@ -1705,8 +1607,7 @@ class ReqClient(object): """ - response = self.base_client.req("StartStream") - return response + self.send("StartStream") def stop_stream(self): """ @@ -1714,8 +1615,7 @@ class ReqClient(object): """ - response = self.base_client.req("StopStream") - return response + self.send("StopStream") def send_stream_caption(self, caption): """ @@ -1726,8 +1626,7 @@ class ReqClient(object): """ - response = self.base_client.req("SendStreamCaption") - return response + self.send("SendStreamCaption") def get_record_status(self): """ @@ -1735,8 +1634,7 @@ class ReqClient(object): """ - response = self.base_client.req("GetRecordStatus") - return response + return self.send("GetRecordStatus") def toggle_record(self): """ @@ -1744,8 +1642,7 @@ class ReqClient(object): """ - response = self.base_client.req("ToggleRecord") - return response + self.send("ToggleRecord") def start_record(self): """ @@ -1753,8 +1650,7 @@ class ReqClient(object): """ - response = self.base_client.req("StartRecord") - return response + self.send("StartRecord") def stop_record(self): """ @@ -1762,8 +1658,7 @@ class ReqClient(object): """ - response = self.base_client.req("StopRecord") - return response + self.send("StopRecord") def toggle_record_pause(self): """ @@ -1771,8 +1666,7 @@ class ReqClient(object): """ - response = self.base_client.req("ToggleRecordPause") - return response + self.send("ToggleRecordPause") def pause_record(self): """ @@ -1780,8 +1674,7 @@ class ReqClient(object): """ - response = self.base_client.req("PauseRecord") - return response + self.send("PauseRecord") def resume_record(self): """ @@ -1789,8 +1682,7 @@ class ReqClient(object): """ - response = self.base_client.req("ResumeRecord") - return response + self.send("ResumeRecord") def get_media_input_status(self, name): """ @@ -1812,8 +1704,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("GetMediaInputStatus", payload) - return response + return self.send("GetMediaInputStatus", payload) def set_media_input_cursor(self, name, cursor): """ @@ -1828,8 +1719,7 @@ class ReqClient(object): """ payload = {"inputName": name, "mediaCursor": cursor} - response = self.base_client.req("SetMediaInputCursor", payload) - return response + self.send("SetMediaInputCursor", payload) def offset_media_input_cursor(self, name, offset): """ @@ -1844,8 +1734,7 @@ class ReqClient(object): """ payload = {"inputName": name, "mediaCursorOffset": offset} - response = self.base_client.req("OffsetMediaInputCursor", payload) - return response + self.send("OffsetMediaInputCursor", payload) def trigger_media_input_action(self, name, action): """ @@ -1859,8 +1748,7 @@ class ReqClient(object): """ payload = {"inputName": name, "mediaAction": action} - response = self.base_client.req("TriggerMediaInputAction", payload) - return response + self.send("TriggerMediaInputAction", payload) def get_studio_mode_enabled(self): """ @@ -1868,7 +1756,7 @@ class ReqClient(object): """ - return self.getter("GetStudioModeEnabled") + return self.send("GetStudioModeEnabled") def set_studio_mode_enabled(self, enabled): """ @@ -1880,8 +1768,7 @@ class ReqClient(object): """ payload = {"studioModeEnabled": enabled} - response = self.base_client.req("SetStudioModeEnabled", payload) - return response + self.send("SetStudioModeEnabled", payload) def open_input_properties_dialog(self, name): """ @@ -1893,8 +1780,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("OpenInputPropertiesDialog", payload) - return response + self.send("OpenInputPropertiesDialog", payload) def open_input_filters_dialog(self, name): """ @@ -1906,8 +1792,7 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("OpenInputFiltersDialog", payload) - return response + self.send("OpenInputFiltersDialog", payload) def open_input_interact_dialog(self, name): """ @@ -1919,14 +1804,12 @@ class ReqClient(object): """ payload = {"inputName": name} - response = self.base_client.req("OpenInputInteractDialog", payload) - return response + self.send("OpenInputInteractDialog", payload) - def get_monitor_list(self, name): + def get_monitor_list(self): """ Gets a list of connected monitors and information about them. """ - response = self.base_client.req("GetMonitorList") - return response + return self.send("GetMonitorList") diff --git a/tests/test_request.py b/tests/test_request.py index 722748f..d68feb9 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -33,7 +33,19 @@ class TestRequests: (True), ], ) - def test_set_studio_mode_enabled_true(self, state): + def test_studio_mode_enabled(self, state): req_cl.set_studio_mode_enabled(state) resp = req_cl.get_studio_mode_enabled() assert resp["studioModeEnabled"] == state + + @pytest.mark.parametrize( + "name,data", + [ + ("val1", 3), + ("val2", "hello"), + ], + ) + def test_persistent_data(self, name, data): + req_cl.set_persistent_data("OBS_WEBSOCKET_DATA_REALM_PROFILE", name, data) + resp = req_cl.get_persistent_data("OBS_WEBSOCKET_DATA_REALM_PROFILE", name) + assert resp["slotValue"] == data From b1c281e8a1938bcf406aa5ab61965dcd748ac35c Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Wed, 27 Jul 2022 19:43:34 +0100 Subject: [PATCH 18/27] fix event client name in example --- examples/hotkeys/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hotkeys/__main__.py b/examples/hotkeys/__main__.py index f50211d..413fe42 100644 --- a/examples/hotkeys/__main__.py +++ b/examples/hotkeys/__main__.py @@ -29,8 +29,8 @@ def set_scene(scene, *args): if __name__ == "__main__": req_cl = obs.ReqClient() - req_ev = obs.EventClient() - observer = Observer(req_ev) + ev_cl = obs.EventClient() + observer = Observer(ev_cl) keyboard.add_hotkey("1", set_scene, args=("START",)) keyboard.add_hotkey("2", set_scene, args=("BRB",)) From 95b1cb27da90b266b907aeac0f3110bb11ee963b Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Wed, 27 Jul 2022 19:49:37 +0100 Subject: [PATCH 19/27] add defaultkwarg into eventclient --- obsstudio_sdk/events.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py index c38adec..9379891 100644 --- a/obsstudio_sdk/events.py +++ b/obsstudio_sdk/events.py @@ -23,7 +23,8 @@ class EventClient(object): DELAY = 0.001 def __init__(self, **kwargs): - kwargs["subs"] = ( + defaultkwargs = dict() + defaultkwargs["subs"] = ( (1 << Subs.general) | (1 << Subs.config) | (1 << Subs.scenes) @@ -36,6 +37,7 @@ class EventClient(object): | (1 << Subs.vendors) | (1 << Subs.ui) ) + kwargs = defaultkwargs | kwargs self.base_client = ObsClient(**kwargs) self.base_client.authenticate() self.callback = Callback() From f5c2293dceac37ff2dd7fa647a1dc1772ca9eefd Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Wed, 27 Jul 2022 20:49:45 +0100 Subject: [PATCH 20/27] add callback unit tests. callback deregister now accepts iterable. --- obsstudio_sdk/callback.py | 12 +++++--- obsstudio_sdk/events.py | 4 ++- tests/test_callback.py | 59 +++++++++++++++++++++++++++++++++++++++ tests/test_request.py | 2 -- 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 tests/test_callback.py diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index 8cc4a61..a106c3a 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -42,13 +42,17 @@ class Callback: if fns not in self._callbacks: self._callbacks.append(fns) - def deregister(self, callback): + def deregister(self, fns: Union[Iterable, Callable]): """deregisters a callback from _callbacks""" try: - self._callbacks.remove(callback) - except ValueError: - print(f"Failed to remove: {callback}") + iterator = iter(fns) + for fn in iterator: + if fn in self._callbacks: + self._callbacks.remove(fn) + except TypeError as e: + if fns in self._callbacks: + self._callbacks.remove(fns) def clear(self): """clears the _callbacks list""" diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py index 9379891..8d62ab4 100644 --- a/obsstudio_sdk/events.py +++ b/obsstudio_sdk/events.py @@ -41,8 +41,9 @@ class EventClient(object): self.base_client = ObsClient(**kwargs) self.base_client.authenticate() self.callback = Callback() + self.subscribe() - self.running = True + def subscribe(self): worker = Thread(target=self.trigger, daemon=True) worker.start() @@ -52,6 +53,7 @@ class EventClient(object): Triggers a callback on event received. """ + self.running = True while self.running: self.data = json.loads(self.base_client.ws.recv()) event, data = (self.data["d"].get("eventType"), self.data["d"]) diff --git a/tests/test_callback.py b/tests/test_callback.py new file mode 100644 index 0000000..45d9329 --- /dev/null +++ b/tests/test_callback.py @@ -0,0 +1,59 @@ +import pytest +from obsstudio_sdk.callback import Callback + + +class TestCallbacks: + __test__ = True + + @classmethod + def setup_class(cls): + cls.callback = Callback() + + @pytest.fixture(autouse=True) + def wraps_tests(self): + yield + self.callback.clear() + + def test_register_callback(self): + def on_callback_method(): + pass + + self.callback.register(on_callback_method) + assert self.callback.get() == ["CallbackMethod"] + + def test_register_callbacks(self): + def on_callback_method_one(): + pass + + def on_callback_method_two(): + pass + + self.callback.register((on_callback_method_one, on_callback_method_two)) + assert self.callback.get() == ["CallbackMethodOne", "CallbackMethodTwo"] + + def test_deregister_callback(self): + def on_callback_method_one(): + pass + + def on_callback_method_two(): + pass + + self.callback.register((on_callback_method_one, on_callback_method_two)) + self.callback.deregister(on_callback_method_one) + assert self.callback.get() == ["CallbackMethodTwo"] + + def test_deregister_callbacks(self): + def on_callback_method_one(): + pass + + def on_callback_method_two(): + pass + + def on_callback_method_three(): + pass + + self.callback.register( + (on_callback_method_one, on_callback_method_two, on_callback_method_three) + ) + self.callback.deregister((on_callback_method_two, on_callback_method_three)) + assert self.callback.get() == ["CallbackMethodOne"] diff --git a/tests/test_request.py b/tests/test_request.py index d68feb9..70efc3e 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -1,5 +1,3 @@ -import time - import pytest from tests import req_cl From 20851c3880642a99a81d229501766df2d48234df Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Wed, 27 Jul 2022 22:44:40 +0100 Subject: [PATCH 21/27] request and event data now returned as dataclasses unit tests updated accordingly --- .gitignore | 13 +++++++++++++ examples/events/__main__.py | 6 +++--- examples/hotkeys/__main__.py | 2 +- examples/scene_rotate/__main__.py | 2 +- obsstudio_sdk/baseclient.py | 2 -- obsstudio_sdk/callback.py | 17 +++++------------ obsstudio_sdk/reqs.py | 3 ++- obsstudio_sdk/util.py | 22 ++++++++++++++++++++++ tests/test_request.py | 10 +++++----- 9 files changed, 52 insertions(+), 25 deletions(-) create mode 100644 obsstudio_sdk/util.py diff --git a/.gitignore b/.gitignore index a04522a..7b77f4a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,19 @@ wheels/ *.egg MANIFEST +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + # Environments .env .venv diff --git a/examples/events/__main__.py b/examples/events/__main__.py index 40b297d..214b371 100644 --- a/examples/events/__main__.py +++ b/examples/events/__main__.py @@ -16,15 +16,15 @@ class Observer: def on_current_program_scene_changed(self, data): """The current program scene has changed.""" - print(f"Switched to scene {data['sceneName']}") + print(f"Switched to scene {data.scene_name}") def on_scene_created(self, data): """A new scene has been created.""" - print(f"scene {data['sceneName']} has been created") + print(f"scene {data.scene_name} has been created") def on_input_mute_state_changed(self, data): """An input's mute state has changed.""" - print(f"{data['inputName']} mute toggled") + print(f"{data.input_name} mute toggled") def on_exit_started(self, data): """OBS has begun the shutdown process.""" diff --git a/examples/hotkeys/__main__.py b/examples/hotkeys/__main__.py index 413fe42..5ede72b 100644 --- a/examples/hotkeys/__main__.py +++ b/examples/hotkeys/__main__.py @@ -16,7 +16,7 @@ class Observer: def on_current_program_scene_changed(self, data): """The current program scene has changed.""" - print(f"{self.event_identifier}: {data}") + print(f"{self.event_identifier}: {data.scene_name}") def version(): diff --git a/examples/scene_rotate/__main__.py b/examples/scene_rotate/__main__.py index 9994d05..2454690 100644 --- a/examples/scene_rotate/__main__.py +++ b/examples/scene_rotate/__main__.py @@ -5,7 +5,7 @@ import obsstudio_sdk as obs def main(): resp = cl.get_scene_list() - scenes = reversed(tuple(di["sceneName"] for di in resp["scenes"])) + scenes = reversed(tuple(di.get("sceneName") for di in resp.scenes)) for sc in scenes: print(f"Switching to scene {sc}") diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index 5419e1f..50240e8 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -1,8 +1,6 @@ import base64 import hashlib import json -import time -from enum import IntEnum from pathlib import Path from random import randint diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index a106c3a..9015997 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -1,6 +1,7 @@ -import re from typing import Callable, Iterable, Union +from .util import as_dataclass, to_camel_case, to_snake_case + class Callback: """Adds support for callbacks""" @@ -10,25 +11,17 @@ class Callback: 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"(? list: """returns a list of registered events""" - return [self.to_camel_case(fn.__name__) for fn in self._callbacks] + return [to_camel_case(fn.__name__) for fn in self._callbacks] def trigger(self, event, data): """trigger callback on update""" for fn in self._callbacks: - if fn.__name__ == self.to_snake_case(event): - fn(data.get("eventData")) + if fn.__name__ == f"on_{to_snake_case(event)}": + fn(as_dataclass(event, data.get("eventData"))) def register(self, fns: Union[Iterable, Callable]): """registers callback functions""" diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index c685f11..2bf39f1 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -1,5 +1,6 @@ from .baseclient import ObsClient from .error import OBSSDKError +from .util import as_dataclass """ A class to interact with obs-websocket requests @@ -23,7 +24,7 @@ class ReqClient(object): error += (f"With message: {response['requestStatus']['comment']}",) raise OBSSDKError("\n".join(error)) if "responseData" in response: - return response["responseData"] + return as_dataclass(response["requestType"], response["responseData"]) def get_version(self): """ diff --git a/obsstudio_sdk/util.py b/obsstudio_sdk/util.py new file mode 100644 index 0000000..59dbcc3 --- /dev/null +++ b/obsstudio_sdk/util.py @@ -0,0 +1,22 @@ +import re +from dataclasses import dataclass + + +def to_camel_case(s): + s = "".join(word.title() for word in s.split("_")) + return s[2:] + + +def to_snake_case(s): + s = re.sub(r"(? Date: Wed, 27 Jul 2022 23:19:10 +0100 Subject: [PATCH 22/27] changes to to_camel_case and to_snake_case --- obsstudio_sdk/callback.py | 2 +- obsstudio_sdk/util.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index 9015997..8aa9a0c 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -14,7 +14,7 @@ class Callback: def get(self) -> list: """returns a list of registered events""" - return [to_camel_case(fn.__name__) for fn in self._callbacks] + return [to_camel_case(fn.__name__[2:]) for fn in self._callbacks] def trigger(self, event, data): """trigger callback on update""" diff --git a/obsstudio_sdk/util.py b/obsstudio_sdk/util.py index 59dbcc3..51fe39a 100644 --- a/obsstudio_sdk/util.py +++ b/obsstudio_sdk/util.py @@ -3,13 +3,11 @@ from dataclasses import dataclass def to_camel_case(s): - s = "".join(word.title() for word in s.split("_")) - return s[2:] + return "".join(word.title() for word in s.split("_")) def to_snake_case(s): - s = re.sub(r"(? Date: Wed, 27 Jul 2022 23:54:46 +0100 Subject: [PATCH 23/27] update version in hotkey example. add hotkey 0 --- examples/hotkeys/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/hotkeys/__main__.py b/examples/hotkeys/__main__.py index 5ede72b..b7b4a72 100644 --- a/examples/hotkeys/__main__.py +++ b/examples/hotkeys/__main__.py @@ -20,7 +20,10 @@ class Observer: def version(): - print(req_cl.get_version()) + resp = req_cl.get_version() + print( + f"Running OBS version:{resp.obs_version} with websocket version:{resp.obs_web_socket_version}" + ) def set_scene(scene, *args): @@ -32,6 +35,7 @@ if __name__ == "__main__": ev_cl = obs.EventClient() observer = Observer(ev_cl) + 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",)) From 051b5898a2812d5529837af17380abeffda41ebd Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Thu, 28 Jul 2022 00:12:45 +0100 Subject: [PATCH 24/27] change example in main to use kwargs. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0af966f..ada4121 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ It should be placed next to your `__main__.py` file. #### Otherwise: -Import and start using, parameters are as follows: +Import and start using, keyword arguments are as follows: - `host`: obs websocket server - `port`: port to access server @@ -42,7 +42,7 @@ Example `__main__.py` import obsstudio_sdk as obs # pass conn info if not in config.toml -cl = obs.ReqClient('localhost', 4455, 'mystrongpass') +cl = obs.ReqClient(host='localhost', port=4455, password='mystrongpass') # Toggle the mute state of your Mic input cl.toggle_input_mute('Mic/Aux') From 00a97b1d8b75729713e122101003cc7d6b3b0909 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Thu, 28 Jul 2022 10:00:24 +0100 Subject: [PATCH 25/27] dict expansion for defaultkwarg --- obsstudio_sdk/baseclient.py | 6 ++++-- obsstudio_sdk/events.py | 29 +++++++++++++++-------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index 50240e8..27a0c71 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -12,8 +12,10 @@ class ObsClient(object): DELAY = 0.001 def __init__(self, **kwargs): - defaultkwargs = {key: None for key in ["host", "port", "password"]} - defaultkwargs["subs"] = 0 + defaultkwargs = { + **{key: None for key in ["host", "port", "password"]}, + "subs": 0, + } kwargs = defaultkwargs | kwargs for attr, val in kwargs.items(): setattr(self, attr, val) diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py index 8d62ab4..fa34a13 100644 --- a/obsstudio_sdk/events.py +++ b/obsstudio_sdk/events.py @@ -23,20 +23,21 @@ class EventClient(object): DELAY = 0.001 def __init__(self, **kwargs): - defaultkwargs = dict() - defaultkwargs["subs"] = ( - (1 << Subs.general) - | (1 << Subs.config) - | (1 << Subs.scenes) - | (1 << Subs.inputs) - | (1 << Subs.transitions) - | (1 << Subs.filters) - | (1 << Subs.outputs) - | (1 << Subs.sceneitems) - | (1 << Subs.mediainputs) - | (1 << Subs.vendors) - | (1 << Subs.ui) - ) + defaultkwargs = { + "subs": ( + (1 << Subs.general) + | (1 << Subs.config) + | (1 << Subs.scenes) + | (1 << Subs.inputs) + | (1 << Subs.transitions) + | (1 << Subs.filters) + | (1 << Subs.outputs) + | (1 << Subs.sceneitems) + | (1 << Subs.mediainputs) + | (1 << Subs.vendors) + | (1 << Subs.ui) + ) + } kwargs = defaultkwargs | kwargs self.base_client = ObsClient(**kwargs) self.base_client.authenticate() From ce9bc7e8d65429b86d26cc353c2b9673662c91cc Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Thu, 28 Jul 2022 11:55:05 +0100 Subject: [PATCH 26/27] add attrs to dataclasses --- obsstudio_sdk/callback.py | 2 +- obsstudio_sdk/events.py | 5 ++++- obsstudio_sdk/util.py | 8 +++++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/obsstudio_sdk/callback.py b/obsstudio_sdk/callback.py index 8aa9a0c..1924848 100644 --- a/obsstudio_sdk/callback.py +++ b/obsstudio_sdk/callback.py @@ -21,7 +21,7 @@ class Callback: for fn in self._callbacks: if fn.__name__ == f"on_{to_snake_case(event)}": - fn(as_dataclass(event, data.get("eventData"))) + fn(as_dataclass(event, data)) def register(self, fns: Union[Iterable, Callable]): """registers callback functions""" diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py index fa34a13..6abc89d 100644 --- a/obsstudio_sdk/events.py +++ b/obsstudio_sdk/events.py @@ -57,7 +57,10 @@ class EventClient(object): self.running = True while self.running: self.data = json.loads(self.base_client.ws.recv()) - event, data = (self.data["d"].get("eventType"), self.data["d"]) + event, data = ( + self.data["d"].get("eventType"), + self.data["d"].get("eventData"), + ) self.callback.trigger(event, data) time.sleep(self.DELAY) diff --git a/obsstudio_sdk/util.py b/obsstudio_sdk/util.py index 51fe39a..7f1ec48 100644 --- a/obsstudio_sdk/util.py +++ b/obsstudio_sdk/util.py @@ -11,10 +11,16 @@ def to_snake_case(s): def as_dataclass(identifier, data): + def attrs(): + return list(data.keys()) + return dataclass( type( f"{identifier}Dataclass", (), - {**{to_snake_case(k): v for k, v in data.items()}}, + { + "attrs": attrs, + **{to_snake_case(k): v for k, v in data.items()}, + }, ) ) From e1453627263b53f60cd2ffd662b6c6dcb96dbc13 Mon Sep 17 00:00:00 2001 From: onyx-and-iris <75868496+onyx-and-iris@users.noreply.github.com> Date: Fri, 29 Jul 2022 02:42:44 +0100 Subject: [PATCH 27/27] added test_attrs added tests to test_request keys in attrs() list now snake cased --- obsstudio_sdk/baseclient.py | 2 +- obsstudio_sdk/events.py | 2 +- obsstudio_sdk/reqs.py | 2 +- obsstudio_sdk/util.py | 2 +- tests/test_attrs.py | 27 +++++++++++++++ tests/test_request.py | 69 +++++++++++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 tests/test_attrs.py diff --git a/obsstudio_sdk/baseclient.py b/obsstudio_sdk/baseclient.py index 27a0c71..0779a69 100644 --- a/obsstudio_sdk/baseclient.py +++ b/obsstudio_sdk/baseclient.py @@ -8,7 +8,7 @@ import tomllib import websocket -class ObsClient(object): +class ObsClient: DELAY = 0.001 def __init__(self, **kwargs): diff --git a/obsstudio_sdk/events.py b/obsstudio_sdk/events.py index 6abc89d..92f455b 100644 --- a/obsstudio_sdk/events.py +++ b/obsstudio_sdk/events.py @@ -19,7 +19,7 @@ Subs = IntEnum( ) -class EventClient(object): +class EventClient: DELAY = 0.001 def __init__(self, **kwargs): diff --git a/obsstudio_sdk/reqs.py b/obsstudio_sdk/reqs.py index 2bf39f1..46679f1 100644 --- a/obsstudio_sdk/reqs.py +++ b/obsstudio_sdk/reqs.py @@ -9,7 +9,7 @@ https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol. """ -class ReqClient(object): +class ReqClient: def __init__(self, **kwargs): self.base_client = ObsClient(**kwargs) self.base_client.authenticate() diff --git a/obsstudio_sdk/util.py b/obsstudio_sdk/util.py index 7f1ec48..26d3197 100644 --- a/obsstudio_sdk/util.py +++ b/obsstudio_sdk/util.py @@ -12,7 +12,7 @@ def to_snake_case(s): def as_dataclass(identifier, data): def attrs(): - return list(data.keys()) + return list(to_snake_case(k) for k in data.keys()) return dataclass( type( diff --git a/tests/test_attrs.py b/tests/test_attrs.py new file mode 100644 index 0000000..689ae8a --- /dev/null +++ b/tests/test_attrs.py @@ -0,0 +1,27 @@ +import pytest + +from tests import req_cl + + +class TestAttrs: + __test__ = True + + def test_get_version_attrs(self): + resp = req_cl.get_version() + assert resp.attrs() == [ + "available_requests", + "obs_version", + "obs_web_socket_version", + "platform", + "platform_description", + "rpc_version", + "supported_image_formats", + ] + + def test_get_current_program_scene_attrs(self): + resp = req_cl.get_current_program_scene() + assert resp.attrs() == ["current_program_scene_name"] + + def test_get_transition_kind_list_attrs(self): + resp = req_cl.get_transition_kind_list() + assert resp.attrs() == ["transition_kinds"] diff --git a/tests/test_request.py b/tests/test_request.py index bbea0f4..b7cc14a 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -36,6 +36,53 @@ class TestRequests: resp = req_cl.get_studio_mode_enabled() assert resp.studio_mode_enabled == state + def test_get_hot_key_list(self): + resp = req_cl.get_hot_key_list() + hotkey_list = [ + "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", + "libobs.mute", + "libobs.unmute", + "libobs.push-to-mute", + "libobs.push-to-talk", + "libobs.mute", + "libobs.unmute", + "libobs.push-to-mute", + "libobs.push-to-talk", + "OBSBasic.SelectScene", + "OBSBasic.SelectScene", + "OBSBasic.SelectScene", + "OBSBasic.SelectScene", + "libobs.show_scene_item.Colour Source 2", + "libobs.hide_scene_item.Colour Source 2", + "libobs.show_scene_item.Colour Source 3", + "libobs.hide_scene_item.Colour Source 3", + "libobs.show_scene_item.Colour Source", + "libobs.hide_scene_item.Colour Source", + "OBSBasic.QuickTransition.1", + "OBSBasic.QuickTransition.2", + "OBSBasic.QuickTransition.3", + ] + assert all(x in resp.hotkeys for x in hotkey_list) + @pytest.mark.parametrize( "name,data", [ @@ -47,3 +94,25 @@ class TestRequests: req_cl.set_persistent_data("OBS_WEBSOCKET_DATA_REALM_PROFILE", name, data) resp = req_cl.get_persistent_data("OBS_WEBSOCKET_DATA_REALM_PROFILE", name) assert resp.slot_value == data + + def test_profile_list(self): + req_cl.create_profile("test") + resp = req_cl.get_profile_list() + assert "test" in resp.profiles + req_cl.remove_profile("test") + resp = req_cl.get_profile_list() + assert "test" not in resp.profiles + + def test_source_filter(self): + req_cl.create_source_filter("START", "test", "color_key_filter_v2") + resp = req_cl.get_source_filter_list("START") + assert resp.filters == [ + { + "filterEnabled": True, + "filterIndex": 0, + "filterKind": "color_key_filter_v2", + "filterName": "test", + "filterSettings": {}, + } + ] + req_cl.remove_source_filter("START", "test")