@@ -208,6 +208,7 @@ def __init__(self, client, channel):
208208 self ._connected = threading .Event ()
209209
210210 self ._handshaking = False
211+ self ._potentially_reconnecting = False
211212 self ._voice_state_complete = asyncio .Event ()
212213 self ._voice_server_complete = asyncio .Event ()
213214
@@ -250,8 +251,10 @@ async def on_voice_state_update(self, data):
250251 self .session_id = data ['session_id' ]
251252 channel_id = data ['channel_id' ]
252253
253- if not self ._handshaking :
254+ if not self ._handshaking or self . _potentially_reconnecting :
254255 # 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
255258 if channel_id is None :
256259 # We're being disconnected so cleanup
257260 await self .disconnect ()
@@ -294,26 +297,39 @@ async def on_voice_server_update(self, data):
294297 self ._voice_server_complete .set ()
295298
296299 async def voice_connect (self ):
297- self ._connections += 1
298300 await self .channel .guild .change_voice_state (channel = self .channel )
299301
300302 async def voice_disconnect (self ):
301303 log .info ('The voice handshake is being terminated for Channel ID %s (Guild ID %s)' , self .channel .id , self .guild .id )
302304 await self .channel .guild .change_voice_state (channel = None )
303305
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+
304327 async def connect (self , * , reconnect , timeout ):
305328 log .info ('Connecting to voice...' )
306329 self .timeout = timeout
307- try :
308- del self .secret_key
309- except AttributeError :
310- pass
311-
312330
313331 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 ()
317333
318334 # This has to be created before we start the flow.
319335 futures = [
@@ -322,7 +338,6 @@ async def connect(self, *, reconnect, timeout):
322338 ]
323339
324340 # Start the connection flow
325- log .info ('Starting voice handshake... (connection attempt %d)' , self ._connections + 1 )
326341 await self .voice_connect ()
327342
328343 try :
@@ -331,17 +346,10 @@ async def connect(self, *, reconnect, timeout):
331346 await self .disconnect (force = True )
332347 raise
333348
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 ()
338350
339351 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 ()
345353 break
346354 except (ConnectionClosed , asyncio .TimeoutError ):
347355 if reconnect :
@@ -355,6 +363,26 @@ async def connect(self, *, reconnect, timeout):
355363 if self ._runner is None :
356364 self ._runner = self .loop .create_task (self .poll_voice_ws (reconnect ))
357365
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+
358386 @property
359387 def latency (self ):
360388 """:class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
@@ -387,10 +415,19 @@ async def poll_voice_ws(self, reconnect):
387415 # 1000 - normal closure (obviously)
388416 # 4014 - voice channel has been deleted.
389417 # 4015 - voice server has crashed
390- if exc .code in (1000 , 4014 , 4015 ):
418+ if exc .code in (1000 , 4015 ):
391419 log .info ('Disconnecting from voice normally, close code %d.' , exc .code )
392420 await self .disconnect ()
393421 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
394431
395432 if not reconnect :
396433 await self .disconnect ()
0 commit comments