Type-hint voice_client / player
This commit is contained in:
parent
cd6b453cb3
commit
5acea453cc
@ -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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
if TYPE_CHECKING:
|
||||
from .voice_client import VoiceClient
|
||||
|
||||
|
||||
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:
|
||||
|
@ -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
|
||||
|
@ -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._connected: threading.Event = threading.Event()
|
||||
|
||||
self._handshaking = False
|
||||
self._potentially_reconnecting = False
|
||||
self._voice_state_complete = asyncio.Event()
|
||||
self._voice_server_complete = asyncio.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 = 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.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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user