mirror of
https://github.com/onyx-and-iris/obsws-python.git
synced 2025-04-20 02:23:45 +01:00
Compare commits
22 Commits
4e45de17ea
...
3c36619173
Author | SHA1 | Date | |
---|---|---|---|
3c36619173 | |||
c4cf817042 | |||
ba5da8dfef | |||
83577e2d61 | |||
|
8aa2e78ba6 | ||
780f07e25f | |||
70a422696e | |||
a7ef61018b | |||
013cf15024 | |||
f88e8ee3a6 | |||
6fa24fe609 | |||
ffd215aadf | |||
f3e75c0ddf | |||
5db7a705c5 | |||
ca72b92eb3 | |||
98b17b6749 | |||
5462c47b65 | |||
126e5cb0a4 | |||
|
4ced7193df | ||
|
468c63f697 | ||
|
24f8487d93 | ||
2c07f242ad |
5
.gitignore
vendored
5
.gitignore
vendored
@ -45,6 +45,11 @@ env.bak/
|
|||||||
venv.bak/
|
venv.bak/
|
||||||
.hatch
|
.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
|
# Test/config
|
||||||
quick.py
|
quick.py
|
||||||
config.toml
|
config.toml
|
||||||
|
31
README.md
31
README.md
@ -62,7 +62,7 @@ cl.toggle_input_mute('Mic/Aux')
|
|||||||
|
|
||||||
### Requests
|
### Requests
|
||||||
|
|
||||||
Method names for requests match the API calls but snake cased.
|
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.
|
||||||
|
|
||||||
example:
|
example:
|
||||||
|
|
||||||
@ -70,13 +70,28 @@ example:
|
|||||||
# load conn info from config.toml
|
# load conn info from config.toml
|
||||||
cl = obs.ReqClient()
|
cl = obs.ReqClient()
|
||||||
|
|
||||||
# GetVersion
|
# GetVersion, returns a response object
|
||||||
resp = cl.get_version()
|
resp = cl.get_version()
|
||||||
|
# Access it's field as an attribute
|
||||||
|
print(f"OBS Version: {resp.obs_version}")
|
||||||
|
|
||||||
|
|
||||||
# SetCurrentProgramScene
|
# SetCurrentProgramScene
|
||||||
cl.set_current_program_scene("BRB")
|
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)
|
For a full list of requests refer to [Requests](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)
|
||||||
|
|
||||||
### Events
|
### Events
|
||||||
@ -128,11 +143,13 @@ def on_scene_created(data):
|
|||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
|
|
||||||
If a request fails an `OBSSDKError` will be raised with a status code.
|
- `OBSSDKError`: Base error class.
|
||||||
|
- `OBSSDKTimeoutError`: Raised if a timeout occurs during sending/receiving a request or receiving an event
|
||||||
For a full list of status codes refer to [Codes](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requeststatus)
|
- `OBSSDKRequestError`: Raised when a request returns an error code.
|
||||||
|
- The following attributes are available:
|
||||||
If a timeout occurs during sending/receiving a request or receiving an event an `OBSSDKTimeoutError` will be raised.
|
- `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)
|
||||||
|
|
||||||
### Logging
|
### Logging
|
||||||
|
|
||||||
|
@ -11,11 +11,12 @@ from websocket import WebSocketTimeoutException
|
|||||||
|
|
||||||
from .error import OBSSDKError, OBSSDKTimeoutError
|
from .error import OBSSDKError, OBSSDKTimeoutError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ObsClient:
|
class ObsClient:
|
||||||
logger = logging.getLogger("baseclient.obsclient")
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
defaultkwargs = {
|
defaultkwargs = {
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 4455,
|
"port": 4455,
|
||||||
@ -42,7 +43,7 @@ class ObsClient:
|
|||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
self.logger.error(f"{type(e).__name__}: {e}")
|
self.logger.error(f"{type(e).__name__}: {e}")
|
||||||
raise
|
raise
|
||||||
except (ConnectionRefusedError, WebSocketTimeoutException) as e:
|
except (ConnectionRefusedError, TimeoutError, WebSocketTimeoutException) as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@ -96,9 +97,9 @@ class ObsClient:
|
|||||||
auth = base64.b64encode(
|
auth = base64.b64encode(
|
||||||
hashlib.sha256(
|
hashlib.sha256(
|
||||||
(
|
(
|
||||||
secret.decode()
|
secret
|
||||||
+ self.server_hello["d"]["authentication"]["challenge"]
|
+ self.server_hello["d"]["authentication"]["challenge"].encode()
|
||||||
).encode()
|
)
|
||||||
).digest()
|
).digest()
|
||||||
).decode()
|
).decode()
|
||||||
|
|
||||||
@ -107,9 +108,15 @@ class ObsClient:
|
|||||||
self.ws.send(json.dumps(payload))
|
self.ws.send(json.dumps(payload))
|
||||||
try:
|
try:
|
||||||
response = json.loads(self.ws.recv())
|
response = json.loads(self.ws.recv())
|
||||||
return response["op"] == 2
|
if response["op"] != 2:
|
||||||
|
raise OBSSDKError(
|
||||||
|
"failed to identify client with the server, expected response with OpCode 2"
|
||||||
|
)
|
||||||
|
return response["d"]
|
||||||
except json.decoder.JSONDecodeError:
|
except json.decoder.JSONDecodeError:
|
||||||
raise OBSSDKError("failed to identify client with the server")
|
raise OBSSDKError(
|
||||||
|
"failed to identify client with the server, please check connection settings"
|
||||||
|
)
|
||||||
|
|
||||||
def req(self, req_type, req_data=None):
|
def req(self, req_type, req_data=None):
|
||||||
payload = {
|
payload = {
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
class OBSSDKError(Exception):
|
class OBSSDKError(Exception):
|
||||||
"""Exception raised when general errors occur"""
|
"""Base class for OBSSDK errors"""
|
||||||
|
|
||||||
|
|
||||||
class OBSSDKTimeoutError(Exception):
|
class OBSSDKTimeoutError(OBSSDKError):
|
||||||
"""Exception raised when a connection times out"""
|
"""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)
|
||||||
|
@ -7,7 +7,7 @@ from websocket import WebSocketTimeoutException
|
|||||||
|
|
||||||
from .baseclient import ObsClient
|
from .baseclient import ObsClient
|
||||||
from .callback import Callback
|
from .callback import Callback
|
||||||
from .error import OBSSDKTimeoutError
|
from .error import OBSSDKError, OBSSDKTimeoutError
|
||||||
from .subs import Subs
|
from .subs import Subs
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -16,17 +16,25 @@ defined in official github repo
|
|||||||
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
|
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#events
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class EventClient:
|
class EventClient:
|
||||||
logger = logging.getLogger("events.eventclient")
|
|
||||||
DELAY = 0.001
|
DELAY = 0.001
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
defaultkwargs = {"subs": Subs.LOW_VOLUME}
|
defaultkwargs = {"subs": Subs.LOW_VOLUME}
|
||||||
kwargs = defaultkwargs | kwargs
|
kwargs = defaultkwargs | kwargs
|
||||||
self.base_client = ObsClient(**kwargs)
|
self.base_client = ObsClient(**kwargs)
|
||||||
if self.base_client.authenticate():
|
try:
|
||||||
self.logger.info(f"Successfully identified {self} with the server")
|
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
|
||||||
self.callback = Callback()
|
self.callback = Callback()
|
||||||
self.subscribe()
|
self.subscribe()
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
from .baseclient import ObsClient
|
from .baseclient import ObsClient
|
||||||
from .error import OBSSDKError
|
from .error import OBSSDKError, OBSSDKRequestError
|
||||||
from .util import as_dataclass
|
from .util import as_dataclass
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@ -10,14 +11,21 @@ defined in official github repo
|
|||||||
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests
|
https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#Requests
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ReqClient:
|
class ReqClient:
|
||||||
logger = logging.getLogger("reqs.reqclient")
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.base_client = ObsClient(**kwargs)
|
self.base_client = ObsClient(**kwargs)
|
||||||
if self.base_client.authenticate():
|
try:
|
||||||
self.logger.info(f"Successfully identified {self} with the server")
|
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
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
return self
|
return self
|
||||||
@ -36,14 +44,17 @@ class ReqClient:
|
|||||||
return type(self).__name__
|
return type(self).__name__
|
||||||
|
|
||||||
def send(self, param, data=None, raw=False):
|
def send(self, param, data=None, raw=False):
|
||||||
|
try:
|
||||||
response = self.base_client.req(param, data)
|
response = self.base_client.req(param, data)
|
||||||
if not response["requestStatus"]["result"]:
|
if not response["requestStatus"]["result"]:
|
||||||
error = (
|
raise OBSSDKRequestError(
|
||||||
f"Request {response['requestType']} returned code {response['requestStatus']['code']}",
|
response["requestType"],
|
||||||
|
response["requestStatus"]["code"],
|
||||||
|
response["requestStatus"].get("comment"),
|
||||||
)
|
)
|
||||||
if "comment" in response["requestStatus"]:
|
except OBSSDKRequestError as e:
|
||||||
error += (f"With message: {response['requestStatus']['comment']}",)
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise OBSSDKError("\n".join(error))
|
raise
|
||||||
if "responseData" in response:
|
if "responseData" in response:
|
||||||
if raw:
|
if raw:
|
||||||
return response["responseData"]
|
return response["responseData"]
|
||||||
@ -1928,3 +1939,66 @@ class ReqClient:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
return self.send("GetMonitorList")
|
return self.send("GetMonitorList")
|
||||||
|
|
||||||
|
def open_video_mix_projector(
|
||||||
|
self, video_mix_type, monitor_index=-1, projector_geometry=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Opens a projector for a specific output video mix.
|
||||||
|
|
||||||
|
The available mix types are:
|
||||||
|
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PREVIEW
|
||||||
|
OBS_WEBSOCKET_VIDEO_MIX_TYPE_PROGRAM
|
||||||
|
OBS_WEBSOCKET_VIDEO_MIX_TYPE_MULTIVIEW
|
||||||
|
|
||||||
|
:param video_mix_type: Type of mix to open.
|
||||||
|
:type video_mix_type: str
|
||||||
|
:param monitor_index: Monitor index, use GetMonitorList to obtain index
|
||||||
|
:type monitor_index: int
|
||||||
|
:param projector_geometry:
|
||||||
|
Size/Position data for a windowed projector, in Qt Base64 encoded format.
|
||||||
|
Mutually exclusive with monitorIndex
|
||||||
|
:type projector_geometry: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
warn(
|
||||||
|
"open_video_mix_projector request serves to provide feature parity with 4.x. "
|
||||||
|
"It is very likely to be changed/deprecated in a future release.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"videoMixType": video_mix_type,
|
||||||
|
"monitorIndex": monitor_index,
|
||||||
|
"projectorGeometry": projector_geometry,
|
||||||
|
}
|
||||||
|
self.send("OpenVideoMixProjector", payload)
|
||||||
|
|
||||||
|
def open_source_projector(
|
||||||
|
self, source_name, monitor_index=-1, projector_geometry=None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Opens a projector for a source.
|
||||||
|
|
||||||
|
:param source_name: Name of the source to open a projector for
|
||||||
|
:type source_name: str
|
||||||
|
:param monitor_index: Monitor index, use GetMonitorList to obtain index
|
||||||
|
:type monitor_index: int
|
||||||
|
:param projector_geometry:
|
||||||
|
Size/Position data for a windowed projector, in Qt Base64 encoded format.
|
||||||
|
Mutually exclusive with monitorIndex
|
||||||
|
:type projector_geometry: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
warn(
|
||||||
|
"open_source_projector request serves to provide feature parity with 4.x. "
|
||||||
|
"It is very likely to be changed/deprecated in a future release.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
payload = {
|
||||||
|
"sourceName": source_name,
|
||||||
|
"monitorIndex": monitor_index,
|
||||||
|
"projectorGeometry": projector_geometry,
|
||||||
|
}
|
||||||
|
self.send("OpenSourceProjector", payload)
|
||||||
|
@ -1 +1 @@
|
|||||||
version = "1.5.0"
|
version = "1.6.1"
|
||||||
|
35
tests/test_error.py
Normal file
35
tests/test_error.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
|
@ -15,9 +15,9 @@ class TestRequests:
|
|||||||
resp = req_cl.get_hot_key_list()
|
resp = req_cl.get_hot_key_list()
|
||||||
obsbasic_hotkey_list = [
|
obsbasic_hotkey_list = [
|
||||||
"OBSBasic.SelectScene",
|
"OBSBasic.SelectScene",
|
||||||
"OBSBasic.SelectScene",
|
"OBSBasic.QuickTransition.1",
|
||||||
"OBSBasic.SelectScene",
|
"OBSBasic.QuickTransition.2",
|
||||||
"OBSBasic.SelectScene",
|
"OBSBasic.QuickTransition.3",
|
||||||
"OBSBasic.StartStreaming",
|
"OBSBasic.StartStreaming",
|
||||||
"OBSBasic.StopStreaming",
|
"OBSBasic.StopStreaming",
|
||||||
"OBSBasic.ForceStopStreaming",
|
"OBSBasic.ForceStopStreaming",
|
||||||
@ -25,15 +25,17 @@ class TestRequests:
|
|||||||
"OBSBasic.StopRecording",
|
"OBSBasic.StopRecording",
|
||||||
"OBSBasic.PauseRecording",
|
"OBSBasic.PauseRecording",
|
||||||
"OBSBasic.UnpauseRecording",
|
"OBSBasic.UnpauseRecording",
|
||||||
|
"OBSBasic.SplitFile",
|
||||||
"OBSBasic.StartReplayBuffer",
|
"OBSBasic.StartReplayBuffer",
|
||||||
"OBSBasic.StopReplayBuffer",
|
"OBSBasic.StopReplayBuffer",
|
||||||
"OBSBasic.StartVirtualCam",
|
"OBSBasic.StartVirtualCam",
|
||||||
"OBSBasic.StopVirtualCam",
|
"OBSBasic.StopVirtualCam",
|
||||||
"OBSBasic.EnablePreview",
|
"OBSBasic.EnablePreview",
|
||||||
"OBSBasic.DisablePreview",
|
"OBSBasic.DisablePreview",
|
||||||
|
"OBSBasic.EnablePreviewProgram",
|
||||||
|
"OBSBasic.DisablePreviewProgram",
|
||||||
"OBSBasic.ShowContextBar",
|
"OBSBasic.ShowContextBar",
|
||||||
"OBSBasic.HideContextBar",
|
"OBSBasic.HideContextBar",
|
||||||
"OBSBasic.TogglePreviewProgram",
|
|
||||||
"OBSBasic.Transition",
|
"OBSBasic.Transition",
|
||||||
"OBSBasic.ResetStats",
|
"OBSBasic.ResetStats",
|
||||||
"OBSBasic.Screenshot",
|
"OBSBasic.Screenshot",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user