@@ -53,27 +53,23 @@ class NotIdentifiedError(Exception):
5353
5454class 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