diff --git a/discord/gateway.py b/discord/gateway.py index 0e90c5f63..f484c8e5f 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. """ import asyncio -from collections import namedtuple +from collections import namedtuple, deque import concurrent.futures import json import logging @@ -132,9 +132,10 @@ class KeepAliveHandler(threading.Thread): class VoiceKeepAliveHandler(KeepAliveHandler): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.recent_ack_latencies = deque(maxlen=20) self.msg = 'Keeping voice websocket alive with timestamp %s.' self.block_msg = 'Voice heartbeat blocked for more than %s seconds' - self.behind_msg = 'Can\'t keep up, voice websocket is %.1fs behind' + self.behind_msg = 'High socket latency, heartbeat is %.1fs behind' def get_payload(self): return { @@ -142,6 +143,12 @@ class VoiceKeepAliveHandler(KeepAliveHandler): 'd': int(time.time() * 1000) } + def ack(self): + ack_time = time.perf_counter() + self._last_ack = ack_time + self.latency = ack_time - self._last_send + self.recent_ack_latencies.append(self.latency) + class DiscordWebSocket(websockets.client.WebSocketClientProtocol): """Implements a WebSocket for Discord's gateway v6. @@ -702,7 +709,7 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): await self.load_secret_key(data) elif op == self.HELLO: interval = data['heartbeat_interval'] / 1000.0 - self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval) + self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=min(interval, 5.0)) self._keep_alive.start() async def initial_connection(self, data): @@ -735,6 +742,19 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): await self.client_connect() + @property + def latency(self): + """:class:`float`: Latency between a HEARTBEAT and its HEARTBEAT_ACK in seconds.""" + heartbeat = self._keep_alive + return float('inf') if heartbeat is None else heartbeat.latency + + @property + def average_latency(self): + """:class:`list`: Average of last 20 HEARTBEAT latencies.""" + heartbeat = self._keep_alive + average_latency = sum(heartbeat.recent_ack_latencies)/len(heartbeat.recent_ack_latencies) + return float('inf') if heartbeat is None else average_latency + async def load_secret_key(self, data): log.info('received secret key for voice connection') self._connection.secret_key = data.get('secret_key') diff --git a/discord/voice_client.py b/discord/voice_client.py index 091fe2616..757f78588 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -57,7 +57,6 @@ try: except ImportError: has_nacl = False - log = logging.getLogger(__name__) class VoiceClient: @@ -207,6 +206,22 @@ class VoiceClient: self._handshake_complete.set() + @property + def latency(self): + """:class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. + + This could be referred to as the Discord Voice WebSocket latency and is + an analogue of user's voice latencies as seen in the Discord client. + """ + ws = self.ws + return float("inf") if not ws else ws.latency + + @property + def average_latency(self): + """:class:`float`: Average of most recent 20 HEARTBEAT latencies in seconds.""" + ws = self.ws + return float("inf") if not ws else ws.average_latency + async def connect(self, *, reconnect=True, _tries=0, do_handshake=True): log.info('Connecting to voice...') try: @@ -342,7 +357,6 @@ class VoiceClient: return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4] - def play(self, source, *, after=None): """Plays an :class:`AudioSource`.