Skip to content

Commit 6aed072

Browse files
committed
Many changes + bump to 1.1.0
- Change `host` and `port` into `url` - Remove `use_ssl` parameter - General code cleanup - Update for latest v5.0.0 protocol changes - Add `emit_batch()`
1 parent cfef48a commit 6aed072

File tree

4 files changed

+80
-66
lines changed

4 files changed

+80
-66
lines changed

samples/sample_events.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import logging
2+
logging.basicConfig(level=logging.DEBUG)
13
import asyncio
24
import simpleobsws
35

46
parameters = simpleobsws.IdentificationParameters(ignoreInvalidMessages=False, ignoreNonFatalRequestChecks=False) # Create an IdentificationParameters object (optional for connecting)
5-
ws = simpleobsws.WebSocketClient(host='localhost', port=4444, password='test', identification_parameters=parameters, call_poll_delay=100) # Every possible argument has been passed, but none are required. See lib code for defaults.
7+
ws = simpleobsws.WebSocketClient(url='ws://localhost:4444', password='test', identification_parameters=parameters, call_poll_delay=100) # Every possible argument has been passed, but none are required. See lib code for defaults.
68

7-
async def on_event(eventData):
8-
print('New event! Type: {} | Raw Data: {}'.format(eventData['update-type'], eventData)) # Print the event data. Note that `update-type` is also provided in the data
9+
async def on_event(eventType, eventData):
10+
print('New event! Type: {} | Raw Data: {}'.format(eventType, eventData)) # Print the event data. Note that `update-type` is also provided in the data
911

1012
async def on_switchscenes(eventData):
1113
print('Scene switched to "{}".'.format(eventData['sceneName']))

samples/sample_request.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import logging
22
logging.basicConfig(level=logging.DEBUG)
33
import asyncio
4-
import json
54
import simpleobsws
65

76
parameters = simpleobsws.IdentificationParameters(ignoreInvalidMessages=False, ignoreNonFatalRequestChecks=False, eventSubscriptions=(1 << 0)) # Create an IdentificationParameters object (optional for connecting)
8-
ws = simpleobsws.WebSocketClient(host='localhost', port=4444, password='test', identification_parameters=parameters, call_poll_delay=100) # Every possible argument has been passed, but none are required. See lib code for defaults.
7+
ws = simpleobsws.WebSocketClient(url='ws://localhost:4444', password='test', identification_parameters=parameters, call_poll_delay=100) # Every possible argument has been passed, but none are required. See lib code for defaults.
98

109
async def make_request():
1110
await ws.connect() # Make the connection to obs-websocket

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
setuptools.setup(
99
name="simpleobsws",
10-
version="1.0.3",
10+
version="1.1.0",
1111
author="tt2468",
1212
author_email="tt2468@gmail.com",
1313
description="A simple obs-websocket library in async Python for people who just want JSON output.",

simpleobsws.py

Lines changed: 73 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -53,27 +53,23 @@ class NotIdentifiedError(Exception):
5353

5454
class WebSocketClient:
5555
def __init__(self,
56-
host: str = 'localhost',
57-
port: int = 4444,
56+
url: str = "ws://localhost:4444",
5857
password: str = '',
5958
identification_parameters: IdentificationParameters = IdentificationParameters(),
60-
call_poll_delay: int = 100,
61-
use_ssl: bool = False
59+
call_poll_delay: int = 100
6260
):
63-
self.host = host
64-
self.port = port
61+
self.url = url
6562
self.password = password
6663
self.identification_parameters = identification_parameters
6764
self.call_poll_delay = call_poll_delay / 1000
68-
self.use_ssl = use_ssl
6965
self.loop = asyncio.get_event_loop()
7066

7167
self.ws = None
7268
self.answers = {}
7369
self.identified = False
7470
self.recv_task = None
75-
self.event_callbacks = []
7671
self.hello_message = None
72+
self.event_callbacks = []
7773

7874
async def connect(self):
7975
if self.ws != None and self.ws.open:
@@ -83,8 +79,7 @@ async def connect(self):
8379
self.recv_task = None
8480
self.identified = False
8581
self.hello_message = None
86-
connect_method = 'wss' if self.use_ssl else 'ws'
87-
self.ws = await websockets.connect('{}://{}:{}'.format(connect_method, self.host, self.port), max_size=2**23)
82+
self.ws = await websockets.connect(self.url, max_size=2**23)
8883
self.recv_task = self.loop.create_task(self._ws_recv_task())
8984
return True
9085

@@ -112,8 +107,8 @@ async def disconnect(self):
112107
await self.ws.close()
113108
self.ws = None
114109
self.answers = {}
115-
self.recv_task = None
116110
self.identified = False
111+
self.recv_task = None
117112
self.hello_message = None
118113
return True
119114

@@ -122,35 +117,38 @@ async def call(self, request: Request, timeout: int = 15):
122117
raise NotIdentifiedError('Calls to requests cannot be made without being identified with obs-websocket.')
123118
request_id = str(uuid.uuid1())
124119
request_payload = {
125-
'messageType': 'Request',
126-
'requestType': request.requestType,
127-
'requestId': request_id
120+
'op': 6,
121+
'd': {
122+
'requestType': request.requestType,
123+
'requestId': request_id
124+
}
128125
}
129126
if request.requestData != None:
130-
request_payload['requestData'] = request.requestData
127+
request_payload['d']['requestData'] = request.requestData
131128
log.debug('Sending Request message:\n{}'.format(json.dumps(request_payload, indent=2)))
132129
await self.ws.send(json.dumps(request_payload))
133130
wait_timeout = time.time() + timeout
134131
await asyncio.sleep(self.call_poll_delay / 2)
135132
while time.time() < wait_timeout:
136133
if request_id in self.answers:
137134
ret = self.answers.pop(request_id)
138-
ret.pop('requestId')
139135
return self._build_request_response(ret)
140136
await asyncio.sleep(self.call_poll_delay)
141137
raise MessageTimeout('The request with type {} timed out after {} seconds.'.format(request.requestType, timeout))
142138

143139
async def emit(self, request: Request):
144140
if not self.identified:
145-
raise NotIdentifiedError('Calls to requests cannot be made without being identified with obs-websocket.')
141+
raise NotIdentifiedError('Emits to requests cannot be made without being identified with obs-websocket.')
146142
request_id = str(uuid.uuid1())
147143
request_payload = {
148-
'messageType': 'Request',
149-
'requestType': request.requestType,
150-
'requestId': 'emit_{}'.format(request_id)
144+
'op': 6,
145+
'd': {
146+
'requestType': request.requestType,
147+
'requestId': 'emit_{}'.format(request_id)
148+
}
151149
}
152150
if request.requestData != None:
153-
request_payload['requestData'] = request.requestData
151+
request_payload['d']['requestData'] = request.requestData
154152
log.debug('Sending Request message:\n{}'.format(json.dumps(request_payload, indent=2)))
155153
await self.ws.send(json.dumps(request_payload))
156154

@@ -159,20 +157,20 @@ async def call_batch(self, requests: list, timeout: int = 15, halt_on_failure: b
159157
raise NotIdentifiedError('Calls to requests cannot be made without being identified with obs-websocket.')
160158
request_batch_id = str(uuid.uuid1())
161159
request_batch_payload = {
162-
'messageType': 'RequestBatch',
163-
'requestId': request_batch_id,
164-
'haltOnFailure': halt_on_failure,
165-
'requests': []
160+
'op': 8,
161+
'd': {
162+
'requestId': request_batch_id,
163+
'haltOnFailure': halt_on_failure,
164+
'requests': []
165+
}
166166
}
167167
for request in requests:
168168
request_payload = {
169-
'messageType': 'Request',
170-
'requestType': request.requestType,
171-
'requestId': '0'
169+
'requestType': request.requestType
172170
}
173171
if request.requestData != None:
174172
request_payload['requestData'] = request.requestData
175-
request_batch_payload['requests'].append(request_payload)
173+
request_batch_payload['d']['requests'].append(request_payload)
176174
log.debug('Sending Request batch message:\n{}'.format(json.dumps(request_batch_payload, indent=2)))
177175
await self.ws.send(json.dumps(request_batch_payload))
178176
wait_timeout = time.time() + timeout
@@ -187,6 +185,28 @@ async def call_batch(self, requests: list, timeout: int = 15, halt_on_failure: b
187185
await asyncio.sleep(self.call_poll_delay)
188186
raise MessageTimeout('The batch request timed out after {} seconds.'.format(request, timeout))
189187

188+
async def emit_batch(self, requests: list, halt_on_failure: bool = False):
189+
if not self.identified:
190+
raise NotIdentifiedError('Emits to requests cannot be made without being identified with obs-websocket.')
191+
request_batch_id = str(uuid.uuid1())
192+
request_batch_payload = {
193+
'op': 8,
194+
'd': {
195+
'requestId': 'emit_{}'.format(request_batch_id),
196+
'haltOnFailure': halt_on_failure,
197+
'requests': []
198+
}
199+
}
200+
for request in requests:
201+
request_payload = {
202+
'requestType': request.requestType
203+
}
204+
if request.requestData != None:
205+
request_payload['requestData'] = request.requestData
206+
request_batch_payload['d']['requests'].append(request_payload)
207+
log.debug('Sending Request batch message:\n{}'.format(json.dumps(request_batch_payload, indent=2)))
208+
await self.ws.send(json.dumps(request_batch_payload))
209+
190210
def register_event_callback(self, callback, event = None):
191211
if not inspect.iscoroutinefunction(callback):
192212
raise EventRegistrationError('Registered functions must be async')
@@ -198,35 +218,31 @@ def deregister_event_callback(self, callback, event = None):
198218
if (c == callback) and (event == None or t == event):
199219
self.event_callbacks.remove((c, t))
200220

201-
def _get_hello(self):
221+
def _get_hello_data(self):
202222
return self.hello_message
203223

204224
def _build_request_response(self, response: dict):
205-
if 'responseData' in response:
206-
ret = RequestResponse(response['requestType'], responseData=response['responseData'])
207-
else:
208-
ret = RequestResponse(response['requestType'])
225+
ret = RequestResponse(response['requestType'], responseData = response.get('responseData'))
209226
ret.requestStatus.result = response['requestStatus']['result']
210227
ret.requestStatus.code = response['requestStatus']['code']
211-
if 'comment' in response['requestStatus']:
212-
ret.requestStatus.comment = response['requestStatus']['comment']
228+
ret.requestStatus.comment = response['requestStatus'].get('comment')
213229
return ret
214230

215231
async def _send_identify(self, password, identification_parameters):
216232
if self.hello_message == None:
217233
return
218-
identify_message = {'messageType': 'Identify'}
219-
identify_message['rpcVersion'] = RPC_VERSION
234+
identify_message = {'op': 1, 'd': {}}
235+
identify_message['d']['rpcVersion'] = RPC_VERSION
220236
if 'authentication' in self.hello_message:
221237
secret = base64.b64encode(hashlib.sha256((self.password + self.hello_message['authentication']['salt']).encode('utf-8')).digest())
222238
authentication_string = base64.b64encode(hashlib.sha256(secret + (self.hello_message['authentication']['challenge'].encode('utf-8'))).digest()).decode('utf-8')
223-
identify_message['authentication'] = authentication_string
239+
identify_message['d']['authentication'] = authentication_string
224240
if self.identification_parameters.ignoreInvalidMessages != None:
225-
identify_message['ignoreInvalidMessages'] = self.identification_parameters.ignoreInvalidMessages
241+
identify_message['d']['ignoreInvalidMessages'] = self.identification_parameters.ignoreInvalidMessages
226242
if self.identification_parameters.ignoreNonFatalRequestChecks != None:
227-
identify_message['ignoreNonFatalRequestChecks'] = self.identification_parameters.ignoreNonFatalRequestChecks
243+
identify_message['d']['ignoreNonFatalRequestChecks'] = self.identification_parameters.ignoreNonFatalRequestChecks
228244
if self.identification_parameters.eventSubscriptions != None:
229-
identify_message['eventSubscriptions'] = self.identification_parameters.eventSubscriptions
245+
identify_message['d']['eventSubscriptions'] = self.identification_parameters.eventSubscriptions
230246
log.debug('Sending Identify message:\n{}'.format(json.dumps(identify_message, indent=2)))
231247
await self.ws.send(json.dumps(identify_message))
232248

@@ -237,32 +253,29 @@ async def _ws_recv_task(self):
237253
message = await self.ws.recv()
238254
if not message:
239255
continue
240-
incoming_message = json.loads(message)
241-
242-
log.debug('Received message:\n{}'.format(json.dumps(incoming_message, indent=2)))
256+
incoming_payload = json.loads(message)
243257

244-
if 'messageType' not in incoming_message:
245-
log.warning('Received a message which is missing a `messageType`. Will ignore: {}'.format(incoming_message))
246-
continue
258+
log.debug('Received message:\n{}'.format(json.dumps(incoming_payload, indent=2)))
247259

248-
message_type = incoming_message['messageType']
249-
if message_type == 'RequestResponse' or message_type == 'RequestBatchResponse':
250-
if incoming_message['requestId'].startswith('emit_'):
260+
op_code = incoming_payload['op']
261+
data_payload = incoming_payload['d']
262+
if op_code == 7 or op_code == 9: # RequestResponse or RequestBatchResponse
263+
if data_payload['requestId'].startswith('emit_'):
251264
continue
252-
self.answers[incoming_message['requestId']] = incoming_message
253-
elif message_type == 'Event':
265+
self.answers[data_payload['requestId']] = data_payload
266+
elif op_code == 5: # Event
254267
for callback, trigger in self.event_callbacks:
255268
if trigger == None:
256-
self.loop.create_task(callback(incoming_message['eventType'], incoming_message['eventData']))
257-
elif trigger == incoming_message['eventType']:
258-
self.loop.create_task(callback(incoming_message['eventData']))
259-
elif message_type == 'Hello':
260-
self.hello_message = incoming_message
269+
self.loop.create_task(callback(data_payload['eventType'], data_payload.get('eventData')))
270+
elif trigger == data_payload['eventType']:
271+
self.loop.create_task(callback(data_payload.get('eventData')))
272+
elif op_code == 0: # Hello
273+
self.hello_message = data_payload
261274
await self._send_identify(self.password, self.identification_parameters)
262-
elif message_type == 'Identified':
275+
elif op_code == 3: # Identified
263276
self.identified = True
264277
else:
265-
log.warning('Unknown message type: {}'.format(incoming_message))
278+
log.warning('Unknown OpCode: {}'.format(op_code))
266279
except (websockets.exceptions.ConnectionClosed, websockets.exceptions.ConnectionClosedError, websockets.exceptions.ConnectionClosedOK):
267280
log.debug('The WebSocket connection was closed. Code: {} | Reason: {}'.format(self.ws.close_code, self.ws.close_reason))
268281
break

0 commit comments

Comments
 (0)