diff --git a/discord/player.py b/discord/player.py index 2ea5308c5..1e08faf27 100644 --- a/discord/player.py +++ b/discord/player.py @@ -21,6 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +from __future__ import annotations import threading import traceback @@ -33,12 +34,23 @@ import time import json import sys import re +import io + +from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, Type, TypeVar, Union from .errors import ClientException from .opus import Encoder as OpusEncoder from .oggparse import OggStream +from .utils import MISSING + +if TYPE_CHECKING: + from .voice_client import VoiceClient + -log = logging.getLogger(__name__) +AT = TypeVar('AT', bound='AudioSource') +FT = TypeVar('FT', bound='FFmpegOpusAudio') + +log: logging.Logger = logging.getLogger(__name__) __all__ = ( 'AudioSource', @@ -49,6 +61,8 @@ __all__ = ( 'PCMVolumeTransformer', ) +CREATE_NO_WINDOW: int + if sys.platform != 'win32': CREATE_NO_WINDOW = 0 else: @@ -65,7 +79,7 @@ class AudioSource: The audio source reads are done in a separate thread. """ - def read(self): + def read(self) -> bytes: """Reads 20ms worth of audio. Subclasses must implement this. @@ -85,11 +99,11 @@ class AudioSource: """ raise NotImplementedError - def is_opus(self): + def is_opus(self) -> bool: """Checks if the audio source is already encoded in Opus.""" return False - def cleanup(self): + def cleanup(self) -> None: """Called when clean-up is needed to be done. Useful for clearing buffer data or processes after @@ -97,7 +111,7 @@ class AudioSource: """ pass - def __del__(self): + def __del__(self) -> None: self.cleanup() class PCMAudio(AudioSource): @@ -108,10 +122,10 @@ class PCMAudio(AudioSource): stream: :term:`py:file object` A file-like object that reads byte data representing raw PCM. """ - def __init__(self, stream): - self.stream = stream + def __init__(self, stream: io.BufferedIOBase) -> None: + self.stream: io.BufferedIOBase = stream - def read(self): + def read(self) -> bytes: ret = self.stream.read(OpusEncoder.FRAME_SIZE) if len(ret) != OpusEncoder.FRAME_SIZE: return b'' @@ -126,17 +140,15 @@ class FFmpegAudio(AudioSource): .. versionadded:: 1.3 """ - def __init__(self, source, *, executable='ffmpeg', args, **subprocess_kwargs): - self._process = self._stdout = None - + def __init__(self, source: str, *, executable: str = 'ffmpeg', args: Any, **subprocess_kwargs: Any): args = [executable, *args] kwargs = {'stdout': subprocess.PIPE} kwargs.update(subprocess_kwargs) - self._process = self._spawn_process(args, **kwargs) - self._stdout = self._process.stdout + self._process: subprocess.Popen = self._spawn_process(args, **kwargs) + self._stdout: IO[bytes] = self._process.stdout # type: ignore - def _spawn_process(self, args, **subprocess_kwargs): + def _spawn_process(self, args: Any, **subprocess_kwargs: Any) -> subprocess.Popen: process = None try: process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs) @@ -148,9 +160,9 @@ class FFmpegAudio(AudioSource): else: return process - def cleanup(self): + def cleanup(self) -> None: proc = self._process - if proc is None: + if proc is MISSING: return log.info('Preparing to terminate ffmpeg process %s.', proc.pid) @@ -167,7 +179,7 @@ class FFmpegAudio(AudioSource): else: log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode) - self._process = self._stdout = None + self._process = self._stdout = MISSING class FFmpegPCMAudio(FFmpegAudio): """An audio source from FFmpeg (or AVConv). @@ -204,7 +216,16 @@ class FFmpegPCMAudio(FFmpegAudio): The subprocess failed to be created. """ - def __init__(self, source, *, executable='ffmpeg', pipe=False, stderr=None, before_options=None, options=None): + def __init__( + self, + source: str, + *, + executable: str = 'ffmpeg', + pipe: bool = False, + stderr: Optional[IO[str]] = None, + before_options: Optional[str] = None, + options: Optional[str] = None + ) -> None: args = [] subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr} @@ -222,13 +243,13 @@ class FFmpegPCMAudio(FFmpegAudio): super().__init__(source, executable=executable, args=args, **subprocess_kwargs) - def read(self): + def read(self) -> bytes: ret = self._stdout.read(OpusEncoder.FRAME_SIZE) if len(ret) != OpusEncoder.FRAME_SIZE: return b'' return ret - def is_opus(self): + def is_opus(self) -> bool: return False class FFmpegOpusAudio(FFmpegAudio): @@ -292,8 +313,18 @@ class FFmpegOpusAudio(FFmpegAudio): The subprocess failed to be created. """ - def __init__(self, source, *, bitrate=128, codec=None, executable='ffmpeg', - pipe=False, stderr=None, before_options=None, options=None): + def __init__( + self, + source: str, + *, + bitrate: int = 128, + codec: Optional[str] = None, + executable: str = 'ffmpeg', + pipe=False, + stderr=None, + before_options=None, + options=None, + ) -> None: args = [] subprocess_kwargs = {'stdin': source if pipe else subprocess.DEVNULL, 'stderr': stderr} @@ -323,7 +354,13 @@ class FFmpegOpusAudio(FFmpegAudio): self._packet_iter = OggStream(self._stdout).iter_packets() @classmethod - async def from_probe(cls, source, *, method=None, **kwargs): + async def from_probe( + cls: Type[FT], + source: str, + *, + method: Optional[Union[str, Callable[[str, str], Tuple[Optional[str], Optional[int]]]]] = None, + **kwargs: Any, + ) -> FT: """|coro| A factory method that creates a :class:`FFmpegOpusAudio` after probing @@ -382,10 +419,16 @@ class FFmpegOpusAudio(FFmpegAudio): executable = kwargs.get('executable') codec, bitrate = await cls.probe(source, method=method, executable=executable) - return cls(source, bitrate=bitrate, codec=codec, **kwargs) + return cls(source, bitrate=bitrate, codec=codec, **kwargs) # type: ignore @classmethod - async def probe(cls, source, *, method=None, executable=None): + async def probe( + cls, + source: str, + *, + method: Optional[Union[str, Callable[[str, str], Tuple[Optional[str], Optional[int]]]]] = None, + executable: Optional[str] = None, + ) -> Tuple[Optional[str], Optional[int]]: """|coro| Probes the input source for bitrate and codec information. @@ -408,7 +451,7 @@ class FFmpegOpusAudio(FFmpegAudio): Returns --------- - Tuple[Optional[:class:`str`], Optional[:class:`int`]] + Optional[Tuple[Optional[:class:`str`], Optional[:class:`int`]]] A 2-tuple with the codec and bitrate of the input source. """ @@ -434,15 +477,15 @@ class FFmpegOpusAudio(FFmpegAudio): codec = bitrate = None loop = asyncio.get_event_loop() try: - codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) + codec, bitrate = await loop.run_in_executor(None, lambda: probefunc(source, executable)) # type: ignore except Exception: if not fallback: log.exception("Probe '%s' using '%s' failed", method, executable) - return + return # type: ignore log.exception("Probe '%s' using '%s' failed, trying fallback", method, executable) try: - codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) + codec, bitrate = await loop.run_in_executor(None, lambda: fallback(source, executable)) # type: ignore except Exception: log.exception("Fallback probe using '%s' failed", executable) else: @@ -453,7 +496,7 @@ class FFmpegOpusAudio(FFmpegAudio): return codec, bitrate @staticmethod - def _probe_codec_native(source, executable='ffmpeg'): + def _probe_codec_native(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]: exe = executable[:2] + 'probe' if executable in ('ffmpeg', 'avconv') else executable args = [exe, '-v', 'quiet', '-print_format', 'json', '-show_streams', '-select_streams', 'a:0', source] output = subprocess.check_output(args, timeout=20) @@ -465,12 +508,12 @@ class FFmpegOpusAudio(FFmpegAudio): codec = streamdata.get('codec_name') bitrate = int(streamdata.get('bit_rate', 0)) - bitrate = max(round(bitrate/1000, 0), 512) + bitrate = max(round(bitrate/1000), 512) return codec, bitrate @staticmethod - def _probe_codec_fallback(source, executable='ffmpeg'): + def _probe_codec_fallback(source, executable: str = 'ffmpeg') -> Tuple[Optional[str], Optional[int]]: args = [executable, '-hide_banner', '-i', source] proc = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out, _ = proc.communicate(timeout=20) @@ -487,13 +530,13 @@ class FFmpegOpusAudio(FFmpegAudio): return codec, bitrate - def read(self): + def read(self) -> bytes: return next(self._packet_iter, b'') - def is_opus(self): + def is_opus(self) -> bool: return True -class PCMVolumeTransformer(AudioSource): +class PCMVolumeTransformer(AudioSource, Generic[AT]): """Transforms a previous :class:`AudioSource` to have volume controls. This does not work on audio sources that have :meth:`AudioSource.is_opus` @@ -515,53 +558,53 @@ class PCMVolumeTransformer(AudioSource): The audio source is opus encoded. """ - def __init__(self, original, volume=1.0): + def __init__(self, original: AT, volume: float = 1.0): if not isinstance(original, AudioSource): raise TypeError(f'expected AudioSource not {original.__class__.__name__}.') if original.is_opus(): raise ClientException('AudioSource must not be Opus encoded.') - self.original = original + self.original: AT = original self.volume = volume @property - def volume(self): + def volume(self) -> float: """Retrieves or sets the volume as a floating point percentage (e.g. ``1.0`` for 100%).""" return self._volume @volume.setter - def volume(self, value): + def volume(self, value: float) -> None: self._volume = max(value, 0.0) - def cleanup(self): + def cleanup(self) -> None: self.original.cleanup() - def read(self): + def read(self) -> bytes: ret = self.original.read() return audioop.mul(ret, 2, min(self._volume, 2.0)) class AudioPlayer(threading.Thread): - DELAY = OpusEncoder.FRAME_LENGTH / 1000.0 + DELAY: float = OpusEncoder.FRAME_LENGTH / 1000.0 - def __init__(self, source, client, *, after=None): + def __init__(self, source: AudioSource, client: VoiceClient, *, after=None): threading.Thread.__init__(self) - self.daemon = True - self.source = source - self.client = client - self.after = after + self.daemon: bool = True + self.source: AudioSource = source + self.client: VoiceClient = client + self.after: Optional[Callable[[Optional[Exception]], Any]] = after - self._end = threading.Event() - self._resumed = threading.Event() + self._end: threading.Event = threading.Event() + self._resumed: threading.Event = threading.Event() self._resumed.set() # we are not paused - self._current_error = None - self._connected = client._connected - self._lock = threading.Lock() + self._current_error: Optional[Exception] = None + self._connected: threading.Event = client._connected + self._lock: threading.Lock = threading.Lock() if after is not None and not callable(after): raise TypeError('Expected a callable for the "after" parameter.') - def _do_run(self): + def _do_run(self) -> None: self.loops = 0 self._start = time.perf_counter() @@ -596,7 +639,7 @@ class AudioPlayer(threading.Thread): delay = max(0, self.DELAY + (next_time - time.perf_counter())) time.sleep(delay) - def run(self): + def run(self) -> None: try: self._do_run() except Exception as exc: @@ -606,7 +649,7 @@ class AudioPlayer(threading.Thread): self.source.cleanup() self._call_after() - def _call_after(self): + def _call_after(self) -> None: error = self._current_error if self.after is not None: @@ -622,36 +665,36 @@ class AudioPlayer(threading.Thread): print(msg, file=sys.stderr) traceback.print_exception(type(error), error, error.__traceback__) - def stop(self): + def stop(self) -> None: self._end.set() self._resumed.set() self._speak(False) - def pause(self, *, update_speaking=True): + def pause(self, *, update_speaking: bool = True) -> None: self._resumed.clear() if update_speaking: self._speak(False) - def resume(self, *, update_speaking=True): + def resume(self, *, update_speaking: bool = True) -> None: self.loops = 0 self._start = time.perf_counter() self._resumed.set() if update_speaking: self._speak(True) - def is_playing(self): + def is_playing(self) -> bool: return self._resumed.is_set() and not self._end.is_set() - def is_paused(self): + def is_paused(self) -> bool: return not self._end.is_set() and not self._resumed.is_set() - def _set_source(self, source): + def _set_source(self, source: AudioSource) -> None: with self._lock: self.pause(update_speaking=False) self.source = source self.resume(update_speaking=False) - def _speak(self, speaking): + def _speak(self, speaking: bool) -> None: try: asyncio.run_coroutine_threadsafe(self.client.ws.speak(speaking), self.client.loop) except Exception as e: diff --git a/discord/types/voice.py b/discord/types/voice.py index 06bd25a9b..b29288d45 100644 --- a/discord/types/voice.py +++ b/discord/types/voice.py @@ -22,11 +22,14 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -from typing import Optional, TypedDict +from typing import Optional, TypedDict, List, Literal from .snowflake import Snowflake from .member import Member +SupportedModes = Literal['xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305'] + + class _PartialVoiceStateOptional(TypedDict, total=False): member: Member self_stream: bool @@ -59,3 +62,24 @@ class VoiceRegion(TypedDict): optimal: bool deprecated: bool custom: bool + + +class VoiceServerUpdate(TypedDict): + token: str + guild_id: Snowflake + endpoint: Optional[str] + + +class VoiceIdentify(TypedDict): + server_id: Snowflake + user_id: Snowflake + session_id: str + token: str + + +class VoiceReady(TypedDict): + ssrc: int + ip: str + port: int + modes: List[SupportedModes] + heartbeat_interval: int diff --git a/discord/voice_client.py b/discord/voice_client.py index 2ae2a8b12..18cbb7320 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -20,9 +20,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" -"""Some documentation to refer to: + +Some documentation to refer to: - Our main web socket (mWS) sends opcode 4 with a guild ID and channel ID. - The mWS receives VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE. @@ -37,21 +37,41 @@ DEALINGS IN THE SOFTWARE. - Finally we can transmit data to endpoint:port. """ +from __future__ import annotations + import asyncio import socket import logging import struct import threading -from typing import Any, Callable +from typing import Any, Callable, List, Optional, TYPE_CHECKING, Tuple from . import opus, utils from .backoff import ExponentialBackoff from .gateway import * from .errors import ClientException, ConnectionClosed from .player import AudioPlayer, AudioSource +from .utils import MISSING + +if TYPE_CHECKING: + from .client import Client + from .guild import Guild + from .state import ConnectionState + from .user import ClientUser + from .opus import Encoder + from . import abc + + from .types.voice import ( + GuildVoiceState as GuildVoiceStatePayload, + VoiceServerUpdate as VoiceServerUpdatePayload, + SupportedModes, + ) + + +has_nacl: bool try: - import nacl.secret + import nacl.secret # type: ignore has_nacl = True except ImportError: has_nacl = False @@ -61,7 +81,10 @@ __all__ = ( 'VoiceClient', ) -log = logging.getLogger(__name__) + + + +log: logging.Logger = logging.getLogger(__name__) class VoiceProtocol: """A class that represents the Discord voice protocol. @@ -84,11 +107,11 @@ class VoiceProtocol: The voice channel that is being connected to. """ - def __init__(self, client, channel): - self.client = client - self.channel = channel + def __init__(self, client: Client, channel: abc.Connectable) -> None: + self.client: Client = client + self.channel: abc.Connectable = channel - async def on_voice_state_update(self, data): + async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None: """|coro| An abstract method that is called when the client's voice state @@ -105,7 +128,7 @@ class VoiceProtocol: """ raise NotImplementedError - async def on_voice_server_update(self, data): + async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None: """|coro| An abstract method that is called when initially connecting to voice. @@ -122,7 +145,7 @@ class VoiceProtocol: """ raise NotImplementedError - async def connect(self, *, timeout: float, reconnect: bool): + async def connect(self, *, timeout: float, reconnect: bool) -> None: """|coro| An abstract method called when the client initiates the connection request. @@ -145,7 +168,7 @@ class VoiceProtocol: """ raise NotImplementedError - async def disconnect(self, *, force: bool): + async def disconnect(self, *, force: bool) -> None: """|coro| An abstract method called when the client terminates the connection. @@ -159,7 +182,7 @@ class VoiceProtocol: """ raise NotImplementedError - def cleanup(self): + def cleanup(self) -> None: """This method *must* be called to ensure proper clean-up during a disconnect. It is advisable to call this from within :meth:`disconnect` when you are @@ -198,48 +221,55 @@ class VoiceClient(VoiceProtocol): loop: :class:`asyncio.AbstractEventLoop` The event loop that the voice client is running on. """ - def __init__(self, client, channel): + endpoint_ip: str + voice_port: int + secret_key: List[int] + ssrc: int + + + def __init__(self, client: Client, channel: abc.Connectable): if not has_nacl: raise RuntimeError("PyNaCl library needed in order to use voice") super().__init__(client, channel) state = client._connection - self.token = None - self.socket = None - self.loop = state.loop - self._state = state + self.token: str = MISSING + self.socket = MISSING + self.loop: asyncio.AbstractEventLoop = state.loop + self._state: ConnectionState = state # this will be used in the AudioPlayer thread - self._connected = threading.Event() - - self._handshaking = False - self._potentially_reconnecting = False - self._voice_state_complete = asyncio.Event() - self._voice_server_complete = asyncio.Event() - - self.mode = None - self._connections = 0 - self.sequence = 0 - self.timestamp = 0 - self._runner = None - self._player = None - self.encoder = None - self._lite_nonce = 0 - self.ws = None + self._connected: threading.Event = threading.Event() + + self._handshaking: bool = False + self._potentially_reconnecting: bool = False + self._voice_state_complete: asyncio.Event = asyncio.Event() + self._voice_server_complete: asyncio.Event = asyncio.Event() + + self.mode: str = MISSING + self._connections: int = 0 + self.sequence: int = 0 + self.timestamp: int = 0 + self.timeout: float = 0 + self._runner: asyncio.Task = MISSING + self._player: Optional[AudioPlayer] = None + self.encoder: Encoder = MISSING + self._lite_nonce: int = 0 + self.ws: DiscordVoiceWebSocket = MISSING warn_nacl = not has_nacl - supported_modes = ( + supported_modes: Tuple[SupportedModes, ...] = ( 'xsalsa20_poly1305_lite', 'xsalsa20_poly1305_suffix', 'xsalsa20_poly1305', ) @property - def guild(self): + def guild(self) -> Optional[Guild]: """Optional[:class:`Guild`]: The guild we're connected to, if applicable.""" return getattr(self.channel, 'guild', None) @property - def user(self): + def user(self) -> ClientUser: """:class:`ClientUser`: The user connected to voice (i.e. ourselves).""" return self._state.user @@ -252,7 +282,7 @@ class VoiceClient(VoiceProtocol): # connection related - async def on_voice_state_update(self, data): + async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None: self.session_id = data['session_id'] channel_id = data['channel_id'] @@ -265,11 +295,11 @@ class VoiceClient(VoiceProtocol): await self.disconnect() else: guild = self.guild - self.channel = channel_id and guild and guild.get_channel(int(channel_id)) + self.channel = channel_id and guild and guild.get_channel(int(channel_id)) # type: ignore else: self._voice_state_complete.set() - async def on_voice_server_update(self, data): + async def on_voice_server_update(self, data: VoiceServerUpdatePayload) -> None: if self._voice_server_complete.is_set(): log.info('Ignoring extraneous voice server update.') return @@ -289,7 +319,7 @@ class VoiceClient(VoiceProtocol): self.endpoint = self.endpoint[6:] # This gets set later - self.endpoint_ip = None + self.endpoint_ip = MISSING self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self.socket.setblocking(False) @@ -301,27 +331,27 @@ class VoiceClient(VoiceProtocol): self._voice_server_complete.set() - async def voice_connect(self): + async def voice_connect(self) -> None: await self.channel.guild.change_voice_state(channel=self.channel) - async def voice_disconnect(self): + async def voice_disconnect(self) -> None: log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', self.channel.id, self.guild.id) await self.channel.guild.change_voice_state(channel=None) - def prepare_handshake(self): + def prepare_handshake(self) -> None: self._voice_state_complete.clear() self._voice_server_complete.clear() self._handshaking = True log.info('Starting voice handshake... (connection attempt %d)', self._connections + 1) self._connections += 1 - def finish_handshake(self): + def finish_handshake(self) -> None: log.info('Voice handshake complete. Endpoint found %s', self.endpoint) self._handshaking = False self._voice_server_complete.clear() self._voice_state_complete.clear() - async def connect_websocket(self): + async def connect_websocket(self) -> DiscordVoiceWebSocket: ws = await DiscordVoiceWebSocket.from_client(self) self._connected.clear() while ws.secret_key is None: @@ -329,7 +359,7 @@ class VoiceClient(VoiceProtocol): self._connected.set() return ws - async def connect(self, *, reconnect: bool, timeout: bool): + async def connect(self, *, reconnect: bool, timeout: float) ->None: log.info('Connecting to voice...') self.timeout = timeout @@ -365,10 +395,10 @@ class VoiceClient(VoiceProtocol): else: raise - if self._runner is None: + if self._runner is MISSING: self._runner = self.loop.create_task(self.poll_voice_ws(reconnect)) - async def potential_reconnect(self): + async def potential_reconnect(self) -> bool: # Attempt to stop the player thread from playing early self._connected.clear() self.prepare_handshake() @@ -391,7 +421,7 @@ class VoiceClient(VoiceProtocol): return True @property - def latency(self): + def latency(self) -> float: """: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 @@ -403,7 +433,7 @@ class VoiceClient(VoiceProtocol): return float("inf") if not ws else ws.latency @property - def average_latency(self): + def average_latency(self) -> float: """:class:`float`: Average of most recent 20 HEARTBEAT latencies in seconds. .. versionadded:: 1.4 @@ -411,7 +441,7 @@ class VoiceClient(VoiceProtocol): ws = self.ws return float("inf") if not ws else ws.average_latency - async def poll_voice_ws(self, reconnect): + async def poll_voice_ws(self, reconnect: bool) -> None: backoff = ExponentialBackoff() while True: try: @@ -452,7 +482,7 @@ class VoiceClient(VoiceProtocol): log.warning('Could not connect to voice... Retrying...') continue - async def disconnect(self, *, force: bool = False): + async def disconnect(self, *, force: bool = False) -> None: """|coro| Disconnects this voice client from voice. @@ -473,7 +503,7 @@ class VoiceClient(VoiceProtocol): if self.socket: self.socket.close() - async def move_to(self, channel): + async def move_to(self, channel: abc.Snowflake) -> None: """|coro| Moves you to a different voice channel. @@ -485,7 +515,7 @@ class VoiceClient(VoiceProtocol): """ await self.channel.guild.change_voice_state(channel=channel) - def is_connected(self): + def is_connected(self) -> bool: """Indicates if the voice client is connected to voice.""" return self._connected.is_set() @@ -504,20 +534,20 @@ class VoiceClient(VoiceProtocol): encrypt_packet = getattr(self, '_encrypt_' + self.mode) return encrypt_packet(header, data) - def _encrypt_xsalsa20_poly1305(self, header, data): + def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes: box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = bytearray(24) nonce[:12] = header return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext - def _encrypt_xsalsa20_poly1305_suffix(self, header, data): + def _encrypt_xsalsa20_poly1305_suffix(self, header: bytes, data) -> bytes: box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE) return header + box.encrypt(bytes(data), nonce).ciphertext + nonce - def _encrypt_xsalsa20_poly1305_lite(self, header, data): + def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes: box = nacl.secret.SecretBox(bytes(self.secret_key)) nonce = bytearray(24) @@ -526,7 +556,7 @@ class VoiceClient(VoiceProtocol): return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4] - def play(self, source: AudioSource, *, after: Callable[[Exception], Any]=None): + def play(self, source: AudioSource, *, after: Callable[[Optional[Exception]], Any]=None) -> None: """Plays an :class:`AudioSource`. The finalizer, ``after`` is called after the source has been exhausted @@ -570,32 +600,32 @@ class VoiceClient(VoiceProtocol): self._player = AudioPlayer(source, self, after=after) self._player.start() - def is_playing(self): + def is_playing(self) -> bool: """Indicates if we're currently playing audio.""" return self._player is not None and self._player.is_playing() - def is_paused(self): + def is_paused(self) -> bool: """Indicates if we're playing audio, but if we're paused.""" return self._player is not None and self._player.is_paused() - def stop(self): + def stop(self) -> None: """Stops playing audio.""" if self._player: self._player.stop() self._player = None - def pause(self): + def pause(self) -> None: """Pauses the audio playing.""" if self._player: self._player.pause() - def resume(self): + def resume(self) -> None: """Resumes the audio playing.""" if self._player: self._player.resume() @property - def source(self): + def source(self) -> Optional[AudioSource]: """Optional[:class:`AudioSource`]: The audio source being played, if playing. This property can also be used to change the audio source currently being played. @@ -603,7 +633,7 @@ class VoiceClient(VoiceProtocol): return self._player.source if self._player else None @source.setter - def source(self, value): + def source(self, value: AudioSource) -> None: if not isinstance(value, AudioSource): raise TypeError(f'expected AudioSource not {value.__class__.__name__}.') @@ -612,7 +642,7 @@ class VoiceClient(VoiceProtocol): self._player._set_source(value) - def send_audio_packet(self, data, *, encode=True): + def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None: """Sends an audio packet composed of the data. You must be connected to play audio.