Passed
Pull Request — main (#228)
by
unknown
02:10
created

VoiceClient.on_voice_server_update()   B

Complexity

Conditions 6

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 32
rs 8.4426
c 0
b 0
f 0
cc 6
nop 2
1
"""
2
This is a modified version of discord.py's voice_client module
3
Copyright (c) 2015-2021 Rapptz
4
Copyright (c) 2021-present Pincer
5
:license: MIT, see LICENSE for details
6
"""
7
8
9
from __future__ import annotations
10
11
import asyncio
12
import logging
13
import socket
14
import struct
15
import threading
16
from typing import Any, Callable, List, Optional, TYPE_CHECKING, Tuple
17
18
from . import opus, utils
19
from .backoff import ExponentialBackoff
0 ignored issues
show
introduced by
Unable to import 'pincer.backoff'
Loading history...
20
from .exceptions import ClientException, ConnectionClosed
0 ignored issues
show
Bug introduced by
The name ConnectionClosed does not seem to exist in module pincer.exceptions.
Loading history...
21
from .core.gateway import *
0 ignored issues
show
Coding Style introduced by
The usage of wildcard imports like core.gateway should generally be avoided.
Loading history...
introduced by
Cannot import 'core.gateway' due to syntax error 'invalid syntax (<unknown>, line 282)'
Loading history...
Bug introduced by
The name gateway does not seem to exist in module pincer.core.
Loading history...
22
from .player import AudioPlayer, AudioSource
23
from .utils import MISSING
24
25
if TYPE_CHECKING:
26
    from .client import Client
0 ignored issues
show
introduced by
Cannot import 'client' due to syntax error 'invalid syntax (<unknown>, line 354)'
Loading history...
27
    from .objects.guild import Guild
28
    from .state import ConnectionState
0 ignored issues
show
introduced by
Unable to import 'pincer.state'
Loading history...
29
    from .user import ClientUser
0 ignored issues
show
introduced by
Unable to import 'pincer.user'
Loading history...
30
    from .opus import Encoder
31
    from . import abc
32
33
    from .types.voice import (
34
        GuildVoiceState as GuildVoiceStatePayload,
35
        VoiceServerUpdate as VoiceServerUpdatePayload,
36
        SupportedModes,
37
    )
38
39
has_nacl: bool
40
41
try:
42
    import nacl.secret  # type: ignore
43
44
    has_nacl = True
45
except ImportError:
46
    has_nacl = False
47
48
__all__ = (
49
    'VoiceProtocol',
50
    'VoiceClient',
51
)
52
53
_log = logging.getLogger(__name__)
54
55
56
class VoiceProtocol:
57
    """A class that represents the Discord voice protocol.
58
    This is an abstract class. The library provides a concrete implementation
59
    under :class:`VoiceClient`.
60
    This class allows you to implement a protocol to allow for an external
61
    method of sending voice, such as Lavalink_ or a native library implementation.
62
    These classes are passed to :meth:`abc.Connectable.connect <VoiceChannel.connect>`.
63
    .. _Lavalink: https://github.com/freyacodes/Lavalink
64
    Parameters
65
    ------------
66
    client: :class:`Client`
67
        The client (or its subclasses) that started the connection request.
68
    channel: :class:`abc.Connectable`
69
        The voice channel that is being connected to.
70
    """
71
72
    def __init__(self, client: Client, channel: abc.Connectable) -> None:
73
        self.client: Client = client
74
        self.channel: abc.Connectable = channel
75
76
    async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None:
77
        """|coro|
78
        An abstract method that is called when the client's voice state
79
        has changed. This corresponds to ``VOICE_STATE_UPDATE``.
80
        Parameters
81
        ------------
82
        data: :class:`dict`
83
            The raw `voice state payload`__.
84
            .. _voice_state_update_payload: https://discord.com/developers/docs/resources/voice#voice-state-object
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (114/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
85
            __ voice_state_update_payload_
86
        """
87
        raise NotImplementedError
88
89
    async def on_voice_server_update(self,
90
                                     data: VoiceServerUpdatePayload) -> None:
91
        """|coro|
92
        An abstract method that is called when initially connecting to voice.
93
        This corresponds to ``VOICE_SERVER_UPDATE``.
94
        Parameters
95
        ------------
96
        data: :class:`dict`
97
            The raw `voice server update payload`__.
98
            .. _voice_server_update_payload: https://discord.com/developers/docs/topics/gateway#voice-server-update-voice-server-update-event-fields
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (148/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
99
            __ voice_server_update_payload_
100
        """
101
        raise NotImplementedError
102
103
    async def connect(self, *, timeout: float, reconnect: bool) -> None:
104
        """|coro|
105
        An abstract method called when the client initiates the connection request.
106
        When a connection is requested initially, the library calls the constructor
107
        under ``__init__`` and then calls :meth:`connect`. If :meth:`connect` fails at
108
        some point then :meth:`disconnect` is called.
109
        Within this method, to start the voice connection flow it is recommended to
110
        use :meth:`Guild.change_voice_state` to start the flow. After which,
111
        :meth:`on_voice_server_update` and :meth:`on_voice_state_update` will be called.
112
        The order that these two are called is unspecified.
113
        Parameters
114
        ------------
115
        timeout: :class:`float`
116
            The timeout for the connection.
117
        reconnect: :class:`bool`
118
            Whether reconnection is expected.
119
        """
120
        raise NotImplementedError
121
122
    async def disconnect(self, *, force: bool) -> None:
123
        """|coro|
124
        An abstract method called when the client terminates the connection.
125
        See :meth:`cleanup`.
126
        Parameters
127
        ------------
128
        force: :class:`bool`
129
            Whether the disconnection was forced.
130
        """
131
        raise NotImplementedError
132
133
    def cleanup(self) -> None:
134
        """This method *must* be called to ensure proper clean-up during a disconnect.
135
        It is advisable to call this from within :meth:`disconnect` when you are
136
        completely done with the voice protocol instance.
137
        This method removes it from the internal state cache that keeps track of
138
        currently alive voice clients. Failure to clean-up will cause subsequent
139
        connections to report that it's still connected.
140
        """
141
        key_id, _ = self.channel._get_voice_client_key()
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _get_voice_client_key was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
142
        self.client._connection._remove_voice_client(key_id)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _connection was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
Coding Style Best Practice introduced by
It seems like _remove_voice_client was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
143
144
145
class VoiceClient(VoiceProtocol):
0 ignored issues
show
best-practice introduced by
Too many public methods (26/20)
Loading history...
best-practice introduced by
Too many instance attributes (24/7)
Loading history...
146
    """Represents a Discord voice connection.
147
    You do not create these, you typically get them from
148
    e.g. :meth:`VoiceChannel.connect`.
149
    Warning
150
    --------
151
    In order to use PCM based AudioSources, you must have the opus library
152
    installed on your system and loaded through :func:`opus.load_opus`.
153
    Otherwise, your AudioSources must be opus encoded (e.g. using :class:`FFmpegOpusAudio`)
154
    or the library will not be able to transmit audio.
155
    Attributes
156
    -----------
157
    session_id: :class:`str`
158
        The voice connection session ID.
159
    token: :class:`str`
160
        The voice connection token.
161
    endpoint: :class:`str`
162
        The endpoint we are connecting to.
163
    channel: :class:`abc.Connectable`
164
        The voice channel connected to.
165
    loop: :class:`asyncio.AbstractEventLoop`
166
        The event loop that the voice client is running on.
167
    """
168
    endpoint_ip: str
169
    voice_port: int
170
    secret_key: List[int]
171
    ssrc: int
172
173
    def __init__(self, client: Client, channel: abc.Connectable):
174
        if not has_nacl:
175
            raise RuntimeError("PyNaCl library needed in order to use voice")
176
177
        super().__init__(client, channel)
178
        state = client._connection
179
        self.token: str = MISSING
180
        self.socket = MISSING
181
        self.loop: asyncio.AbstractEventLoop = state.loop
182
        self._state: ConnectionState = state
183
        # this will be used in the AudioPlayer thread
184
        self._connected: threading.Event = threading.Event()
185
186
        self._handshaking: bool = False
187
        self._potentially_reconnecting: bool = False
188
        self._voice_state_complete: asyncio.Event = asyncio.Event()
189
        self._voice_server_complete: asyncio.Event = asyncio.Event()
190
191
        self.mode: str = MISSING
192
        self._connections: int = 0
193
        self.sequence: int = 0
194
        self.timestamp: int = 0
195
        self.timeout: float = 0
196
        self._runner: asyncio.Task = MISSING
197
        self._player: Optional[AudioPlayer] = None
198
        self.encoder: Encoder = MISSING
199
        self._lite_nonce: int = 0
200
        self.ws: DiscordVoiceWebSocket = MISSING
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'DiscordVoiceWebSocket'
Loading history...
201
202
    warn_nacl = not has_nacl
203
    supported_modes: Tuple[SupportedModes, ...] = (
204
        'xsalsa20_poly1305_lite',
205
        'xsalsa20_poly1305_suffix',
206
        'xsalsa20_poly1305',
207
    )
208
209
    @property
210
    def guild(self) -> Optional[Guild]:
211
        """Optional[:class:`Guild`]: The guild we're connected to, if applicable."""
212
        return getattr(self.channel, 'guild', None)
213
214
    @property
215
    def user(self) -> ClientUser:
216
        """:class:`ClientUser`: The user connected to voice (i.e. ourselves)."""
217
        return self._state.user
218
219
    def checked_add(self, attr, value, limit):
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
220
        val = getattr(self, attr)
221
        if val + value > limit:
222
            setattr(self, attr, 0)
223
        else:
224
            setattr(self, attr, val + value)
225
226
    # connection related
227
228
    async def on_voice_state_update(self, data: GuildVoiceStatePayload) -> None:
229
        self.session_id = data['session_id']
0 ignored issues
show
Coding Style introduced by
The attribute session_id was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
230
        channel_id = data['channel_id']
231
232
        if not self._handshaking or self._potentially_reconnecting:
233
            # If we're done handshaking then we just need to update ourselves
234
            # If we're potentially reconnecting due to a 4014, then we need to differentiate
235
            # a channel move and an actual force disconnect
236
            if channel_id is None:
237
                # We're being disconnected so cleanup
238
                await self.disconnect()
239
            else:
240
                guild = self.guild
241
                self.channel = channel_id and guild and guild.get_channel(
242
                    int(channel_id))  # type: ignore
243
        else:
244
            self._voice_state_complete.set()
245
246
    async def on_voice_server_update(self,
247
                                     data: VoiceServerUpdatePayload) -> None:
248
        if self._voice_server_complete.is_set():
249
            _log.info('Ignoring extraneous voice server update.')
250
            return
251
252
        self.token = data.get('token')
253
        self.server_id = int(data['guild_id'])
0 ignored issues
show
Coding Style introduced by
The attribute server_id was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
254
        endpoint = data.get('endpoint')
255
256
        if endpoint is None or self.token is None:
257
            _log.warning('Awaiting endpoint... This requires waiting. ' \
258
                         'If timeout occurred considering raising the timeout and reconnecting.')
259
            return
260
261
        self.endpoint, _, _ = endpoint.rpartition(':')
0 ignored issues
show
Coding Style introduced by
The attribute endpoint was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
262
        if self.endpoint.startswith('wss://'):
263
            # Just in case, strip it off since we're going to add it later
264
            self.endpoint = self.endpoint[6:]
0 ignored issues
show
Coding Style introduced by
The attribute endpoint was defined outside __init__.

It is generally a good practice to initialize all attributes to default values in the __init__ method:

class Foo:
    def __init__(self, x=None):
        self.x = x
Loading history...
265
266
        # This gets set later
267
        self.endpoint_ip = MISSING
268
269
        self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
270
        self.socket.setblocking(False)
271
272
        if not self._handshaking:
273
            # If we're not handshaking then we need to terminate our previous connection in the websocket
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (105/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
274
            await self.ws.close(4000)
275
            return
276
277
        self._voice_server_complete.set()
278
279
    async def voice_connect(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
280
        await self.channel.guild.change_voice_state(channel=self.channel)
281
282
    async def voice_disconnect(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
283
        _log.info(
284
            'The voice handshake is being terminated for Channel ID %s (Guild ID %s)',
285
            self.channel.id, self.guild.id)
286
        await self.channel.guild.change_voice_state(channel=None)
287
288
    def prepare_handshake(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
289
        self._voice_state_complete.clear()
290
        self._voice_server_complete.clear()
291
        self._handshaking = True
292
        _log.info('Starting voice handshake... (connection attempt %d)',
293
                  self._connections + 1)
294
        self._connections += 1
295
296
    def finish_handshake(self) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
297
        _log.info('Voice handshake complete. Endpoint found %s', self.endpoint)
298
        self._handshaking = False
299
        self._voice_server_complete.clear()
300
        self._voice_state_complete.clear()
301
302
    async def connect_websocket(self) -> DiscordVoiceWebSocket:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
Comprehensibility Best Practice introduced by
Undefined variable 'DiscordVoiceWebSocket'
Loading history...
303
        ws = await DiscordVoiceWebSocket.from_client(self)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'DiscordVoiceWebSocket'
Loading history...
304
        self._connected.clear()
305
        while ws.secret_key is None:
306
            await ws.poll_event()
307
        self._connected.set()
308
        return ws
309
310
    async def connect(self, *, reconnect: bool, timeout: float) -> None:
0 ignored issues
show
Bug introduced by
Parameters differ from overridden 'connect' method
Loading history...
311
        _log.info('Connecting to voice...')
312
        self.timeout = timeout
313
314
        for i in range(5):
315
            self.prepare_handshake()
316
317
            # This has to be created before we start the flow.
318
            futures = [
319
                self._voice_state_complete.wait(),
320
                self._voice_server_complete.wait(),
321
            ]
322
323
            # Start the connection flow
324
            await self.voice_connect()
325
326
            try:
327
                await utils.sane_wait_for(futures, timeout=timeout)
328
            except asyncio.TimeoutError:
329
                await self.disconnect(force=True)
330
                raise
331
332
            self.finish_handshake()
333
334
            try:
335
                self.ws = await self.connect_websocket()
336
                break
337
            except (ConnectionClosed, asyncio.TimeoutError):
338
                if reconnect:
0 ignored issues
show
Unused Code introduced by
Unnecessary "else" after "continue"
Loading history...
339
                    _log.exception('Failed to connect to voice... Retrying...')
340
                    await asyncio.sleep(1 + i * 2.0)
341
                    await self.voice_disconnect()
342
                    continue
343
                else:
344
                    raise
345
346
        if self._runner is MISSING:
347
            self._runner = self.loop.create_task(self.poll_voice_ws(reconnect))
348
349
    async def potential_reconnect(self) -> bool:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
350
        # Attempt to stop the player thread from playing early
351
        self._connected.clear()
352
        self.prepare_handshake()
353
        self._potentially_reconnecting = True
354
        try:
355
            # We only care about VOICE_SERVER_UPDATE since VOICE_STATE_UPDATE can come before we get disconnected
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (113/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
356
            await asyncio.wait_for(self._voice_server_complete.wait(),
357
                                   timeout=self.timeout)
358
        except asyncio.TimeoutError:
359
            self._potentially_reconnecting = False
360
            await self.disconnect(force=True)
361
            return False
362
363
        self.finish_handshake()
364
        self._potentially_reconnecting = False
365
        try:
366
            self.ws = await self.connect_websocket()
367
        except (ConnectionClosed, asyncio.TimeoutError):
368
            return False
369
        else:
370
            return True
371
372
    @property
373
    def latency(self) -> float:
374
        """:class:`float`: Latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds.
375
        This could be referred to as the Discord Voice WebSocket latency and is
376
        an analogue of user's voice latencies as seen in the Discord client.
377
        .. versionadded:: 1.4
378
        """
379
        ws = self.ws
380
        return float("inf") if not ws else ws.latency
381
382
    @property
383
    def average_latency(self) -> float:
384
        """:class:`float`: Average of most recent 20 HEARTBEAT latencies in seconds.
385
        .. versionadded:: 1.4
386
        """
387
        ws = self.ws
388
        return float("inf") if not ws else ws.average_latency
389
390
    async def poll_voice_ws(self, reconnect: bool) -> None:
0 ignored issues
show
introduced by
Missing function or method docstring
Loading history...
391
        backoff = ExponentialBackoff()
392
        while True:
393
            try:
394
                await self.ws.poll_event()
395
            except (ConnectionClosed, asyncio.TimeoutError) as exc:
396
                if isinstance(exc, ConnectionClosed):
397
                    # The following close codes are undocumented so I will document them here.
398
                    # 1000 - normal closure (obviously)
399
                    # 4014 - voice channel has been deleted.
400
                    # 4015 - voice server has crashed
401
                    if exc.code in (1000, 4015):
402
                        _log.info(
403
                            'Disconnecting from voice normally, close code %d.',
404
                            exc.code)
405
                        await self.disconnect()
406
                        break
407
                    if exc.code == 4014:
408
                        _log.info(
409
                            'Disconnected from voice by force... potentially reconnecting.')
410
                        successful = await self.potential_reconnect()
411
                        if not successful:
0 ignored issues
show
Unused Code introduced by
Unnecessary "else" after "break"
Loading history...
412
                            _log.info(
413
                                'Reconnect was unsuccessful, disconnecting from voice normally...')
414
                            await self.disconnect()
415
                            break
416
                        else:
417
                            continue
418
419
                if not reconnect:
420
                    await self.disconnect()
421
                    raise
422
423
                retry = backoff.delay()
424
                _log.exception(
425
                    'Disconnected from voice... Reconnecting in %.2fs.', retry)
426
                self._connected.clear()
427
                await asyncio.sleep(retry)
428
                await self.voice_disconnect()
429
                try:
430
                    await self.connect(reconnect=True, timeout=self.timeout)
431
                except asyncio.TimeoutError:
432
                    # at this point we've retried 5 times... let's continue the loop.
433
                    _log.warning('Could not connect to voice... Retrying...')
434
                    continue
435
436
    async def disconnect(self, *, force: bool = False) -> None:
437
        """|coro|
438
        Disconnects this voice client from voice.
439
        """
440
        if not force and not self.is_connected():
441
            return
442
443
        self.stop()
444
        self._connected.clear()
445
446
        try:
447
            if self.ws:
448
                await self.ws.close()
449
450
            await self.voice_disconnect()
451
        finally:
452
            self.cleanup()
453
            if self.socket:
454
                self.socket.close()
455
456
    async def move_to(self, channel: abc.Snowflake) -> None:
457
        """|coro|
458
        Moves you to a different voice channel.
459
        Parameters
460
        -----------
461
        channel: :class:`abc.Snowflake`
462
            The channel to move to. Must be a voice channel.
463
        """
464
        await self.channel.guild.change_voice_state(channel=channel)
465
466
    def is_connected(self) -> bool:
467
        """Indicates if the voice client is connected to voice."""
468
        return self._connected.is_set()
469
470
    # audio related
471
472
    def _get_voice_packet(self, data):
473
        header = bytearray(12)
474
475
        # Formulate rtp header
476
        header[0] = 0x80
477
        header[1] = 0x78
478
        struct.pack_into('>H', header, 2, self.sequence)
479
        struct.pack_into('>I', header, 4, self.timestamp)
480
        struct.pack_into('>I', header, 8, self.ssrc)
481
482
        encrypt_packet = getattr(self, '_encrypt_' + self.mode)
483
        return encrypt_packet(header, data)
484
485
    def _encrypt_xsalsa20_poly1305(self, header: bytes, data) -> bytes:
486
        box = nacl.secret.SecretBox(bytes(self.secret_key))
487
        nonce = bytearray(24)
488
        nonce[:12] = header
489
490
        return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext
491
492
    def _encrypt_xsalsa20_poly1305_suffix(self, header: bytes, data) -> bytes:
493
        box = nacl.secret.SecretBox(bytes(self.secret_key))
494
        nonce = nacl.utils.random(nacl.secret.SecretBox.NONCE_SIZE)
495
496
        return header + box.encrypt(bytes(data), nonce).ciphertext + nonce
497
498
    def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes:
499
        box = nacl.secret.SecretBox(bytes(self.secret_key))
500
        nonce = bytearray(24)
501
502
        nonce[:4] = struct.pack('>I', self._lite_nonce)
503
        self.checked_add('_lite_nonce', 1, 4294967295)
504
505
        return header + box.encrypt(bytes(data),
506
                                    bytes(nonce)).ciphertext + nonce[:4]
507
508
    def play(self, source: AudioSource, *,
509
             after: Callable[[Optional[Exception]], Any] = None) -> None:
510
        """Plays an :class:`AudioSource`.
511
        The finalizer, ``after`` is called after the source has been exhausted
512
        or an error occurred.
513
        If an error happens while the audio player is running, the exception is
514
        caught and the audio player is then stopped.  If no after callback is
515
        passed, any caught exception will be displayed as if it were raised.
516
        Parameters
517
        -----------
518
        source: :class:`AudioSource`
519
            The audio source we're reading from.
520
        after: Callable[[Optional[:class:`Exception`]], Any]
521
            The finalizer that is called after the stream is exhausted.
522
            This function must have a single parameter, ``error``, that
523
            denotes an optional exception that was raised during playing.
524
        Raises
525
        -------
526
        ClientException
527
            Already playing audio or not connected.
528
        TypeError
529
            Source is not a :class:`AudioSource` or after is not a callable.
530
        OpusNotLoaded
531
            Source is not opus encoded and opus is not loaded.
532
        """
533
534
        if not self.is_connected():
535
            raise ClientException('Not connected to voice.')
536
537
        if self.is_playing():
538
            raise ClientException('Already playing audio.')
539
540
        if not isinstance(source, AudioSource):
541
            raise TypeError(
542
                f'source must be an AudioSource not {source.__class__.__name__}')
543
544
        if not self.encoder and not source.is_opus():
545
            self.encoder = opus.Encoder()
546
547
        self._player = AudioPlayer(source, self, after=after)
548
        self._player.start()
549
550
    def is_playing(self) -> bool:
551
        """Indicates if we're currently playing audio."""
552
        return self._player is not None and self._player.is_playing()
553
554
    def is_paused(self) -> bool:
555
        """Indicates if we're playing audio, but if we're paused."""
556
        return self._player is not None and self._player.is_paused()
557
558
    def stop(self) -> None:
559
        """Stops playing audio."""
560
        if self._player:
561
            self._player.stop()
562
            self._player = None
563
564
    def pause(self) -> None:
565
        """Pauses the audio playing."""
566
        if self._player:
567
            self._player.pause()
568
569
    def resume(self) -> None:
570
        """Resumes the audio playing."""
571
        if self._player:
572
            self._player.resume()
573
574
    @property
575
    def source(self) -> Optional[AudioSource]:
576
        """Optional[:class:`AudioSource`]: The audio source being played, if playing.
577
        This property can also be used to change the audio source currently being played.
578
        """
579
        return self._player.source if self._player else None
580
581
    @source.setter
582
    def source(self, value: AudioSource) -> None:
583
        if not isinstance(value, AudioSource):
584
            raise TypeError(
585
                f'expected AudioSource not {value.__class__.__name__}.')
586
587
        if self._player is None:
588
            raise ValueError('Not playing anything.')
589
590
        self._player._set_source(value)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like _set_source was declared protected and should not be accessed from this context.

Prefixing a member variable _ is usually regarded as the equivalent of declaring it with protected visibility that exists in other languages. Consequentially, such a member should only be accessed from the same class or a child class:

class MyParent:
    def __init__(self):
        self._x = 1;
        self.y = 2;

class MyChild(MyParent):
    def some_method(self):
        return self._x    # Ok, since accessed from a child class

class AnotherClass:
    def some_method(self, instance_of_my_child):
        return instance_of_my_child._x   # Would be flagged as AnotherClass is not
                                         # a child class of MyParent
Loading history...
591
592
    def send_audio_packet(self, data: bytes, *, encode: bool = True) -> None:
593
        """Sends an audio packet composed of the data.
594
        You must be connected to play audio.
595
        Parameters
596
        ----------
597
        data: :class:`bytes`
598
            The :term:`py:bytes-like object` denoting PCM or Opus voice data.
599
        encode: :class:`bool`
600
            Indicates if ``data`` should be encoded into Opus.
601
        Raises
602
        -------
603
        ClientException
604
            You are not connected.
605
        opus.OpusError
606
            Encoding the data failed.
607
        """
608
609
        self.checked_add('sequence', 1, 65535)
610
        if encode:
611
            encoded_data = self.encoder.encode(data,
612
                                               self.encoder.SAMPLES_PER_FRAME)
613
        else:
614
            encoded_data = data
615
        packet = self._get_voice_packet(encoded_data)
616
        try:
617
            self.socket.sendto(packet, (self.endpoint_ip, self.voice_port))
618
        except BlockingIOError:
619
            _log.warning('A packet has been dropped (seq: %s, timestamp: %s)',
620
                         self.sequence, self.timestamp)
621
622
        self.checked_add('timestamp', opus.Encoder.SAMPLES_PER_FRAME,
623
                         4294967295)
624