Compare commits

..

No commits in common. "3c36619173a9cbb15a844af5697cbccaac8f67b9" and "4e45de17ea998c3eca0034f956c4ad63a1bcc16d" have entirely different histories.

9 changed files with 48 additions and 208 deletions

5
.gitignore vendored
View File

@ -45,11 +45,6 @@ env.bak/
venv.bak/
.hatch
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
.python-version
# Test/config
quick.py
config.toml

View File

@ -9,10 +9,10 @@ Not all endpoints in the official documentation are implemented.
## Requirements
- [OBS Studio](https://obsproject.com/)
- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0)
- With the release of OBS Studio version 28, Websocket plugin is included by default. But it should be manually installed for earlier versions of OBS.
- Python 3.9 or greater
- [OBS Studio](https://obsproject.com/)
- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0)
- With the release of OBS Studio version 28, Websocket plugin is included by default. But it should be manually installed for earlier versions of OBS.
- Python 3.9 or greater
### How to install using pip
@ -24,10 +24,10 @@ pip install obsws-python
By default the clients connect with parameters:
- `host`: "localhost"
- `port`: 4455
- `password`: ""
- `timeout`: None
- `host`: "localhost"
- `port`: 4455
- `password`: ""
- `timeout`: None
You may override these parameters by storing them in a toml config file or passing them as keyword arguments.
@ -62,7 +62,7 @@ cl.toggle_input_mute('Mic/Aux')
### Requests
Method names for requests match the API calls but snake cased. If a successful call is made with the Request client and the response is expected to contain fields then a response object will be returned. You may then access the response fields as class attributes. They will be snake cased.
Method names for requests match the API calls but snake cased.
example:
@ -70,28 +70,13 @@ example:
# load conn info from config.toml
cl = obs.ReqClient()
# GetVersion, returns a response object
# GetVersion
resp = cl.get_version()
# Access it's field as an attribute
print(f"OBS Version: {resp.obs_version}")
# SetCurrentProgramScene
cl.set_current_program_scene("BRB")
```
#### `send(param, data=None, raw=False)`
If you prefer to work with the JSON data directly the {ReqClient}.send() method accepts an argument, `raw`. If set to True the raw response data will be returned, instead of a response object.
example:
```python
resp = cl_req.send("GetVersion", raw=True)
print(f"response data: {resp}")
```
For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
### Events
@ -143,13 +128,11 @@ def on_scene_created(data):
### Errors
- `OBSSDKError`: Base error class.
- `OBSSDKTimeoutError`: Raised if a timeout occurs during sending/receiving a request or receiving an event
- `OBSSDKRequestError`: Raised when a request returns an error code.
- The following attributes are available:
- `req_name`: name of the request.
- `code`: request status code.
- For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
If a request fails an `OBSSDKError` will be raised with a status code.
For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
If a timeout occurs during sending/receiving a request or receiving an event an `OBSSDKTimeoutError` will be raised.
### Logging
@ -182,4 +165,4 @@ pytest -v
For the full documentation:
- [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol)
- [OBS Websocket SDK](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#obs-websocket-501-protocol)

View File

@ -11,12 +11,11 @@ from websocket import WebSocketTimeoutException
from .error import OBSSDKError, OBSSDKTimeoutError
logger = logging.getLogger(__name__)
class ObsClient:
logger = logging.getLogger("baseclient.obsclient")
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = {
"host": "localhost",
"port": 4455,
@ -43,7 +42,7 @@ class ObsClient:
except ValueError as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
except (ConnectionRefusedError, TimeoutError, WebSocketTimeoutException) as e:
except (ConnectionRefusedError, WebSocketTimeoutException) as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise
@ -97,9 +96,9 @@ class ObsClient:
auth = base64.b64encode(
hashlib.sha256(
(
secret
+ self.server_hello["d"]["authentication"]["challenge"].encode()
)
secret.decode()
+ self.server_hello["d"]["authentication"]["challenge"]
).encode()
).digest()
).decode()
@ -108,15 +107,9 @@ class ObsClient:
self.ws.send(json.dumps(payload))
try:
response = json.loads(self.ws.recv())
if response["op"] != 2:
raise OBSSDKError(
"failed to identify client with the server, expected response with OpCode 2"
)
return response["d"]
return response["op"] == 2
except json.decoder.JSONDecodeError:
raise OBSSDKError(
"failed to identify client with the server, please check connection settings"
)
raise OBSSDKError("failed to identify client with the server")
def req(self, req_type, req_data=None):
payload = {

View File

@ -1,18 +1,6 @@
class OBSSDKError(Exception):
"""Base class for OBSSDK errors"""
"""Exception raised when general errors occur"""
class OBSSDKTimeoutError(OBSSDKError):
class OBSSDKTimeoutError(Exception):
"""Exception raised when a connection times out"""
class OBSSDKRequestError(OBSSDKError):
"""Exception raised when a request returns an error code"""
def __init__(self, req_name, code, comment):
self.req_name = req_name
self.code = code
message = f"Request {self.req_name} returned code {self.code}."
if comment:
message += f" With message: {comment}"
super().__init__(message)

View File

@ -7,7 +7,7 @@ from websocket import WebSocketTimeoutException
from .baseclient import ObsClient
from .callback import Callback
from .error import OBSSDKError, OBSSDKTimeoutError
from .error import OBSSDKTimeoutError
from .subs import Subs
"""
@ -16,25 +16,17 @@ defined in official github repo
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
"""
logger = logging.getLogger(__name__)
class EventClient:
logger = logging.getLogger("events.eventclient")
DELAY = 0.001
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
defaultkwargs = {"subs": Subs.LOW_VOLUME}
kwargs = defaultkwargs | kwargs
self.base_client = ObsClient(**kwargs)
try:
success = self.base_client.authenticate()
self.logger.info(
f"Successfully identified {self} with the server using RPC version:{success['negotiatedRpcVersion']}"
)
except OBSSDKError as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
if self.base_client.authenticate():
self.logger.info(f"Successfully identified {self} with the server")
self.callback = Callback()
self.subscribe()

View File

@ -1,8 +1,7 @@
import logging
from warnings import warn
from .baseclient import ObsClient
from .error import OBSSDKError, OBSSDKRequestError
from .error import OBSSDKError
from .util import as_dataclass
"""
@ -11,21 +10,14 @@ defined in official github repo
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests
"""
logger = logging.getLogger(__name__)
class ReqClient:
logger = logging.getLogger("reqs.reqclient")
def __init__(self, **kwargs):
self.logger = logger.getChild(self.__class__.__name__)
self.base_client = ObsClient(**kwargs)
try:
success = self.base_client.authenticate()
self.logger.info(
f"Successfully identified {self} with the server using RPC version:{success['negotiatedRpcVersion']}"
)
except OBSSDKError as e:
self.logger.error(f"{type(e).__name__}: {e}")
raise
if self.base_client.authenticate():
self.logger.info(f"Successfully identified {self} with the server")
def __enter__(self):
return self
@ -44,17 +36,14 @@ class ReqClient:
return type(self).__name__
def send(self, param, data=None, raw=False):
try:
response = self.base_client.req(param, data)
if not response["requestStatus"]["result"]:
raise OBSSDKRequestError(
response["requestType"],
response["requestStatus"]["code"],
response["requestStatus"].get("comment"),
)
except OBSSDKRequestError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise
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))
if "responseData" in response:
if raw:
return response["responseData"]
@ -1939,66 +1928,3 @@ class ReqClient:
"""
return self.send("GetMonitorList")
def open_video_mix_projector(
self, video_mix_type, monitor_index=-1, projector_geometry=None
):
"""
Opens a projector for a specific output video mix.
The available mix types are:
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PROGRAM
OBS_WEBSOCKET_VIDEO_MIX_TYPE_MULTIVIEW
:param video_mix_type: Type of mix to open.
:type video_mix_type: str
:param monitor_index: Monitor index, use GetMonitorList to obtain index
:type monitor_index: int
:param projector_geometry:
Size/Position data for a windowed projector, in Qt Base64 encoded format.
Mutually exclusive with monitorIndex
:type projector_geometry: str
"""
warn(
"open_video_mix_projector request serves to provide feature parity with 4.x. "
"It is very likely to be changed/deprecated in a future release.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"videoMixType": video_mix_type,
"monitorIndex": monitor_index,
"projectorGeometry": projector_geometry,
}
self.send("OpenVideoMixProjector", payload)
def open_source_projector(
self, source_name, monitor_index=-1, projector_geometry=None
):
"""
Opens a projector for a source.
:param source_name: Name of the source to open a projector for
:type source_name: str
:param monitor_index: Monitor index, use GetMonitorList to obtain index
:type monitor_index: int
:param projector_geometry:
Size/Position data for a windowed projector, in Qt Base64 encoded format.
Mutually exclusive with monitorIndex
:type projector_geometry: str
"""
warn(
"open_source_projector request serves to provide feature parity with 4.x. "
"It is very likely to be changed/deprecated in a future release.",
DeprecationWarning,
stacklevel=2,
)
payload = {
"sourceName": source_name,
"monitorIndex": monitor_index,
"projectorGeometry": projector_geometry,
}
self.send("OpenSourceProjector", payload)

View File

@ -1 +1 @@
version = "1.6.1"
version = "1.5.0"

View File

@ -1,35 +0,0 @@
import pytest
import obsws_python as obsws
from tests import req_cl
class TestErrors:
__test__ = True
def test_it_raises_an_obssdk_error_on_incorrect_password(self):
bad_conn = {"host": "localhost", "port": 4455, "password": "incorrectpassword"}
with pytest.raises(
obsws.error.OBSSDKError,
match="failed to identify client with the server, please check connection settings",
):
obsws.ReqClient(**bad_conn)
def test_it_raises_an_obssdk_error_if_auth_enabled_but_no_password_provided(self):
bad_conn = {"host": "localhost", "port": 4455, "password": ""}
with pytest.raises(
obsws.error.OBSSDKError,
match="authentication enabled but no password provided",
):
obsws.ReqClient(**bad_conn)
def test_it_raises_a_request_error_on_invalid_request(self):
with pytest.raises(
obsws.error.OBSSDKRequestError,
match="Request SetCurrentProgramScene returned code 600. With message: No source was found by the name of `invalid`.",
) as exc_info:
req_cl.set_current_program_scene("invalid")
e = exc_info.value
assert e.req_name == "SetCurrentProgramScene"
assert e.code == 600

View File

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