Re-implement voice sending.

This is a complete redesign of the old voice code.

A list of major changes is as follows:

* The voice websocket will now automatically reconnect with
  exponential back-off just like the regular Client does.
* Removal of the stream player concept.
* Audio now gracefully pauses and resumes when a disconnect is found.
* Introduce a discord.AudioSource concept to abstract streams
* Flatten previous stream player functionality with the
  VoiceClient, e.g. player.stop() is now voice_client.stop()
* With the above re-coupling this means you no longer have to
  store players anywhere.
* The after function now requires a single parameter, the error,
  if any existed. This will typically be None.

A lot of this design is experimental.
This commit is contained in:
Rapptz
2017-04-18 02:29:43 -04:00
parent 38fd0928df
commit 3b1b26ffb1
11 changed files with 609 additions and 537 deletions

View File

@ -109,7 +109,6 @@ class VoiceKeepAliveHandler(KeepAliveHandler):
self.msg = 'Keeping voice websocket alive with timestamp {0[d]}'
def get_payload(self):
self.ack()
return {
'op': self.ws.HEARTBEAT,
'd': int(time.time() * 1000)
@ -481,12 +480,9 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol):
}
}
log.debug('Updating our voice state to %s.', payload)
yield from self.send_as_json(payload)
# we're leaving a voice channel so remove it from the client list
if channel_id is None:
self._connection._remove_voice_client(guild_id)
@asyncio.coroutine
def close_connection(self, force=False):
if self._keep_alive:
@ -511,6 +507,14 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
Receive only. Gives you the secret key required for voice.
SPEAKING
Send only. Notifies the client if you are currently speaking.
HEARTBEAT_ACK
Receive only. Tells you your heartbeat has been acknowledged.
RESUME
Sent only. Tells the client to resume its session.
HELLO
Receive only. Tells you that your websocket connection was acknowledged.
INVALIDATE_SESSION
Sent only. Tells you that your RESUME request has failed and to re-IDENTIFY.
"""
IDENTIFY = 0
@ -519,6 +523,10 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
HEARTBEAT = 3
SESSION_DESCRIPTION = 4
SPEAKING = 5
HEARTBEAT_ACK = 6
RESUME = 7
HELLO = 8
INVALIDATE_SESSION = 9
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -527,28 +535,50 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
@asyncio.coroutine
def send_as_json(self, data):
log.debug('Sending voice websocket frame: %s.', data)
yield from self.send(utils.to_json(data))
@asyncio.coroutine
def resume(self):
state = self._connection
payload = {
'op': self.RESUME,
'd': {
'token': state.token,
'server_id': str(state.server_id),
'session_id': state.session_id
}
}
yield from self.send_as_json(payload)
@asyncio.coroutine
def identify(self):
state = self._connection
payload = {
'op': self.IDENTIFY,
'd': {
'server_id': str(state.server_id),
'user_id': str(state.user.id),
'session_id': state.session_id,
'token': state.token
}
}
yield from self.send_as_json(payload)
@classmethod
@asyncio.coroutine
def from_client(cls, client):
def from_client(cls, client, *, resume=False):
"""Creates a voice websocket for the :class:`VoiceClient`."""
gateway = 'wss://' + client.endpoint
gateway = 'wss://' + client.endpoint + '/?v=3'
ws = yield from websockets.connect(gateway, loop=client.loop, klass=cls)
ws.gateway = gateway
ws._connection = client
identify = {
'op': cls.IDENTIFY,
'd': {
'guild_id': client.guild_id,
'user_id': client.user.id,
'session_id': client.session_id,
'token': client.token
}
}
if resume:
yield from ws.resume()
else:
yield from ws.identify()
yield from ws.send_as_json(identify)
return ws
@asyncio.coroutine
@ -566,7 +596,6 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
}
yield from self.send_as_json(payload)
log.debug('Selected protocol as {}'.format(payload))
@asyncio.coroutine
def speak(self, is_speaking=True):
@ -579,12 +608,11 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
}
yield from self.send_as_json(payload)
log.debug('Voice speaking now set to {}'.format(is_speaking))
@asyncio.coroutine
def received_message(self, msg):
log.debug('Voice websocket frame received: {}'.format(msg))
op = msg.get('op')
op = msg['op']
data = msg.get('d')
if op == self.READY:
@ -592,14 +620,20 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol):
self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval)
self._keep_alive.start()
yield from self.initial_connection(data)
elif op == self.HEARTBEAT_ACK:
self._keep_alive.ack()
elif op == self.INVALIDATE_SESSION:
log.info('Voice RESUME failed.')
yield from self.identify()
elif op == self.SESSION_DESCRIPTION:
yield from self.load_secret_key(data)
@asyncio.coroutine
def initial_connection(self, data):
state = self._connection
state.ssrc = data.get('ssrc')
state.voice_port = data.get('port')
state.ssrc = data['ssrc']
state.voice_port = data['port']
packet = bytearray(70)
struct.pack_into('>I', packet, 0, state.ssrc)
state.socket.sendto(packet, (state.endpoint_ip, state.voice_port))