Skip to content

Fix voice connection issues and upgrade to voice v8 #10210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions discord/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,9 @@ def ack(self) -> None:


class VoiceKeepAliveHandler(KeepAliveHandler):
if TYPE_CHECKING:
ws: DiscordVoiceWebSocket

def __init__(self, *args: Any, **kwargs: Any) -> None:
name: str = kwargs.pop('name', f'voice-keep-alive-handler:{id(self):#x}')
super().__init__(*args, name=name, **kwargs)
Expand All @@ -223,7 +226,10 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
def get_payload(self) -> Dict[str, Any]:
return {
'op': self.ws.HEARTBEAT,
'd': int(time.time() * 1000),
'd': {
't': int(time.time() * 1000),
'seq_ack': self.ws.seq_ack,
},
}

def ack(self) -> None:
Expand Down Expand Up @@ -830,6 +836,8 @@ def __init__(
self._keep_alive: Optional[VoiceKeepAliveHandler] = None
self._close_code: Optional[int] = None
self.secret_key: Optional[List[int]] = None
# defaulting to -1
self.seq_ack: int = -1
if hook:
self._hook = hook # type: ignore

Expand All @@ -850,6 +858,7 @@ async def resume(self) -> None:
'token': state.token,
'server_id': str(state.server_id),
'session_id': state.session_id,
'seq_ack': self.seq_ack,
},
}
await self.send_as_json(payload)
Expand All @@ -874,14 +883,16 @@ async def from_connection_state(
*,
resume: bool = False,
hook: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None,
seq_ack: int = -1,
) -> Self:
"""Creates a voice websocket for the :class:`VoiceClient`."""
gateway = f'wss://{state.endpoint}/?v=4'
gateway = f'wss://{state.endpoint}/?v=8'
client = state.voice_client
http = client._state.http
socket = await http.ws_connect(gateway, compress=15)
ws = cls(socket, loop=client.loop, hook=hook)
ws.gateway = gateway
ws.seq_ack = seq_ack
ws._connection = state
ws._max_heartbeat_timeout = 60.0
ws.thread_id = threading.get_ident()
Expand Down Expand Up @@ -934,6 +945,7 @@ async def received_message(self, msg: Dict[str, Any]) -> None:
_log.debug('Voice websocket frame received: %s', msg)
op = msg['op']
data = msg['d'] # According to Discord this key is always given
self.seq_ack = msg.get('seq', self.seq_ack) # this key could not be given

if op == self.READY:
await self.initial_connection(data)
Expand Down
40 changes: 35 additions & 5 deletions discord/voice_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ async def voice_server_update(self, data: VoiceServerUpdatePayload) -> None:
)
return

self.endpoint, _, _ = endpoint.rpartition(':')
self.endpoint = endpoint
if self.endpoint.startswith('wss://'):
# Just in case, strip it off since we're going to add it later
self.endpoint = self.endpoint[6:]
Expand Down Expand Up @@ -574,7 +574,10 @@ async def _voice_disconnect(self) -> None:
self._disconnected.clear()

async def _connect_websocket(self, resume: bool) -> DiscordVoiceWebSocket:
ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook)
seq_ack = -1
if self.ws is not MISSING:
seq_ack = self.ws.seq_ack
ws = await DiscordVoiceWebSocket.from_connection_state(self, resume=resume, hook=self.hook, seq_ack=seq_ack)
self.state = ConnectionFlowState.websocket_connected
return ws

Expand Down Expand Up @@ -603,15 +606,17 @@ async def _poll_voice_ws(self, reconnect: bool) -> None:
# The following close codes are undocumented so I will document them here.
# 1000 - normal closure (obviously)
# 4014 - we were externally disconnected (voice channel deleted, we were moved, etc)
# 4015 - voice server has crashed
if exc.code in (1000, 4015):
# 4015 - voice server has crashed, we should resume
# 4021 - rate limited, we should not reconnect
# 4022 - call terminated, similar to 4014
if exc.code == 1000:
# Don't call disconnect a second time if the websocket closed from a disconnect call
if not self._expecting_disconnect:
_log.info('Disconnecting from voice normally, close code %d.', exc.code)
await self.disconnect()
break

if exc.code == 4014:
if exc.code in (4014, 4022):
# We were disconnected by discord
# This condition is a race between the main ws event and the voice ws closing
if self._disconnected.is_set():
Expand All @@ -631,6 +636,31 @@ async def _poll_voice_ws(self, reconnect: bool) -> None:
else:
continue

if exc.code == 4021:
_log.warning('We are being ratelimited while trying to connect to voice. Disconnecting...')
if self.state is not ConnectionFlowState.disconnected:
await self.disconnect()
break

if exc.code == 4015:
_log.info('Disconnected from voice, attempting a resume...')
try:
await self._connect(
reconnect=reconnect,
timeout=self.timeout,
self_deaf=(self.self_voice_state or self).self_deaf,
self_mute=(self.self_voice_state or self).self_mute,
resume=True,
)
except asyncio.TimeoutError:
_log.info('Could not resume the voice connection... Disconnecting...')
if self.state is not ConnectionFlowState.disconnected:
await self.disconnect()
break
else:
_log.info('Successfully resumed voice connection')
continue

_log.debug('Not handling close code %s (%s)', exc.code, exc.reason or 'no reason')

if not reconnect:
Expand Down
Loading