@@ -208,6 +208,7 @@ def __init__(self, client, channel):
208
208
self ._connected = threading .Event ()
209
209
210
210
self ._handshaking = False
211
+ self ._potentially_reconnecting = False
211
212
self ._voice_state_complete = asyncio .Event ()
212
213
self ._voice_server_complete = asyncio .Event ()
213
214
@@ -250,8 +251,10 @@ async def on_voice_state_update(self, data):
250
251
self .session_id = data ['session_id' ]
251
252
channel_id = data ['channel_id' ]
252
253
253
- if not self ._handshaking :
254
+ if not self ._handshaking or self . _potentially_reconnecting :
254
255
# If we're done handshaking then we just need to update ourselves
256
+ # If we're potentially reconnecting due to a 4014, then we need to differentiate
257
+ # a channel move and an actual force disconnect
255
258
if channel_id is None :
256
259
# We're being disconnected so cleanup
257
260
await self .disconnect ()
@@ -294,26 +297,39 @@ async def on_voice_server_update(self, data):
294
297
self ._voice_server_complete .set ()
295
298
296
299
async def voice_connect (self ):
297
- self ._connections += 1
298
300
await self .channel .guild .change_voice_state (channel = self .channel )
299
301
300
302
async def voice_disconnect (self ):
301
303
log .info ('The voice handshake is being terminated for Channel ID %s (Guild ID %s)' , self .channel .id , self .guild .id )
302
304
await self .channel .guild .change_voice_state (channel = None )
303
305
306
+ def prepare_handshake (self ):
307
+ self ._voice_state_complete .clear ()
308
+ self ._voice_server_complete .clear ()
309
+ self ._handshaking = True
310
+ log .info ('Starting voice handshake... (connection attempt %d)' , self ._connections + 1 )
311
+ self ._connections += 1
312
+
313
+ def finish_handshake (self ):
314
+ log .info ('Voice handshake complete. Endpoint found %s' , self .endpoint )
315
+ self ._handshaking = False
316
+ self ._voice_server_complete .clear ()
317
+ self ._voice_state_complete .clear ()
318
+
319
+ async def connect_websocket (self ):
320
+ ws = await DiscordVoiceWebSocket .from_client (self )
321
+ self ._connected .clear ()
322
+ while ws .secret_key is None :
323
+ await ws .poll_event ()
324
+ self ._connected .set ()
325
+ return ws
326
+
304
327
async def connect (self , * , reconnect , timeout ):
305
328
log .info ('Connecting to voice...' )
306
329
self .timeout = timeout
307
- try :
308
- del self .secret_key
309
- except AttributeError :
310
- pass
311
-
312
330
313
331
for i in range (5 ):
314
- self ._voice_state_complete .clear ()
315
- self ._voice_server_complete .clear ()
316
- self ._handshaking = True
332
+ self .prepare_handshake ()
317
333
318
334
# This has to be created before we start the flow.
319
335
futures = [
@@ -322,7 +338,6 @@ async def connect(self, *, reconnect, timeout):
322
338
]
323
339
324
340
# Start the connection flow
325
- log .info ('Starting voice handshake... (connection attempt %d)' , self ._connections + 1 )
326
341
await self .voice_connect ()
327
342
328
343
try :
@@ -331,17 +346,10 @@ async def connect(self, *, reconnect, timeout):
331
346
await self .disconnect (force = True )
332
347
raise
333
348
334
- log .info ('Voice handshake complete. Endpoint found %s' , self .endpoint )
335
- self ._handshaking = False
336
- self ._voice_server_complete .clear ()
337
- self ._voice_state_complete .clear ()
349
+ self .finish_handshake ()
338
350
339
351
try :
340
- self .ws = await DiscordVoiceWebSocket .from_client (self )
341
- self ._connected .clear ()
342
- while not hasattr (self , 'secret_key' ):
343
- await self .ws .poll_event ()
344
- self ._connected .set ()
352
+ self .ws = await self .connect_websocket ()
345
353
break
346
354
except (ConnectionClosed , asyncio .TimeoutError ):
347
355
if reconnect :
@@ -355,6 +363,26 @@ async def connect(self, *, reconnect, timeout):
355
363
if self ._runner is None :
356
364
self ._runner = self .loop .create_task (self .poll_voice_ws (reconnect ))
357
365
366
+ async def potential_reconnect (self ):
367
+ self .prepare_handshake ()
368
+ self ._potentially_reconnecting = True
369
+ try :
370
+ # We only care about VOICE_SERVER_UPDATE since VOICE_STATE_UPDATE can come before we get disconnected
371
+ await asyncio .wait_for (self ._voice_server_complete .wait (), timeout = self .timeout )
372
+ except asyncio .TimeoutError :
373
+ self ._potentially_reconnecting = False
374
+ await self .disconnect (force = True )
375
+ return False
376
+
377
+ self .finish_handshake ()
378
+ self ._potentially_reconnecting = False
379
+ try :
380
+ self .ws = await self .connect_websocket ()
381
+ except (ConnectionClosed , asyncio .TimeoutError ):
382
+ return False
383
+ else :
384
+ return True
385
+
358
386
@property
359
387
def latency (self ):
360
388
""":class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
@@ -387,10 +415,19 @@ async def poll_voice_ws(self, reconnect):
387
415
# 1000 - normal closure (obviously)
388
416
# 4014 - voice channel has been deleted.
389
417
# 4015 - voice server has crashed
390
- if exc .code in (1000 , 4014 , 4015 ):
418
+ if exc .code in (1000 , 4015 ):
391
419
log .info ('Disconnecting from voice normally, close code %d.' , exc .code )
392
420
await self .disconnect ()
393
421
break
422
+ if exc .code == 4014 :
423
+ log .info ('Disconnected from voice by force... potentially reconnecting.' )
424
+ successful = await self .potential_reconnect ()
425
+ if not successful :
426
+ log .info ('Reconnect was unsuccessful, disconnecting from voice normally...' )
427
+ await self .disconnect ()
428
+ break
429
+ else :
430
+ continue
394
431
395
432
if not reconnect :
396
433
await self .disconnect ()
0 commit comments