Skip to content

Commit fe88ee8

Browse files
committed
recv
1 parent d7be710 commit fe88ee8

File tree

6 files changed

+174
-25
lines changed

6 files changed

+174
-25
lines changed

discord/opus.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
from .sinks import RawData
4343

4444
if TYPE_CHECKING:
45+
from discord.voice.recorder import VoiceRecorderClient
46+
4547
T = TypeVar("T")
4648
APPLICATION_CTL = Literal["audio", "voip", "lowdelay"]
4749
BAND_CTL = Literal["narrow", "medium", "wide", "superwide", "full"]
@@ -548,17 +550,17 @@ def decode(self, data, *, fec=False):
548550

549551

550552
class DecodeManager(threading.Thread, _OpusStruct):
551-
def __init__(self, client):
553+
def __init__(self, client: VoiceRecorderClient):
552554
super().__init__(daemon=True, name="DecodeManager")
553555

554-
self.client = client
555-
self.decode_queue = []
556+
self.client: VoiceRecorderClient = client
557+
self.decode_queue: list[RawData] = []
556558

557-
self.decoder = {}
559+
self.decoder: dict[int, Decoder] = {}
558560

559561
self._end_thread = threading.Event()
560562

561-
def decode(self, opus_frame):
563+
def decode(self, opus_frame: RawData):
562564
if not isinstance(opus_frame, RawData):
563565
raise TypeError("opus_frame should be a RawData object.")
564566
self.decode_queue.append(opus_frame)
@@ -579,26 +581,26 @@ def run(self):
579581
data.decrypted_data
580582
)
581583
except OpusError:
582-
print("Error occurred while decoding opus frame.")
584+
_log.exception("Error occurred while decoding opus frame.", exc_info=True)
583585
continue
584586

585-
self.client.recv_decoded_audio(data)
587+
self.client.receive_audio(data)
586588

587-
def stop(self):
589+
def stop(self) -> None:
588590
while self.decoding:
589591
time.sleep(0.1)
590592
self.decoder = {}
591593
gc.collect()
592-
print("Decoder Process Killed")
594+
_log.debug("Decoder Process Killed")
593595
self._end_thread.set()
594596

595-
def get_decoder(self, ssrc):
597+
def get_decoder(self, ssrc: int) -> Decoder:
596598
d = self.decoder.get(ssrc)
597599
if d is not None:
598600
return d
599601
self.decoder[ssrc] = Decoder()
600602
return self.decoder[ssrc]
601603

602604
@property
603-
def decoding(self):
605+
def decoding(self) -> bool:
604606
return bool(self.decode_queue)

discord/sinks/core.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def __init__(self, *, filters=None):
224224
self.audio_data = {}
225225

226226
def init(self, vc): # called under listen
227-
self.vc: VoiceClient = vc
227+
self.vc = vc
228228
super().init()
229229

230230
@Filters.container

discord/voice/_types.py

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,34 @@
2525

2626
from __future__ import annotations
2727

28-
from typing import TYPE_CHECKING, Generic, TypeVar
28+
from collections.abc import Awaitable, Callable
29+
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union
2930

3031
if TYPE_CHECKING:
32+
from typing_extensions import ParamSpec
33+
3134
from discord import abc
3235
from discord.client import Client
3336
from discord.raw_models import (
3437
RawVoiceServerUpdateEvent,
3538
RawVoiceStateUpdateEvent,
3639
)
37-
from discord.voice.client import VoiceClient
40+
from discord.sinks import Sink
41+
42+
P = ParamSpec('P')
43+
R = TypeVar('R')
44+
RecordCallback = Union[Callable[P, R], Callable[P, Awaitable[R]]]
3845

3946
ClientT = TypeVar("ClientT", bound="Client", covariant=True)
40-
VoiceClientT = TypeVar('VoiceClientT', bound='VoiceClient', covariant=True)
47+
VoiceProtocolT = TypeVar('VoiceProtocolT', bound='VoiceProtocol', covariant=True)
4148

4249

4350
class VoiceProtocol(Generic[ClientT]):
4451
"""A class that represents the Discord voice protocol.
4552
4653
.. warning::
4754
48-
If you are a end user, you **should not construct this manually** but instead
55+
If you are an end user, you **should not construct this manually** but instead
4956
take it from the return type in :meth:`abc.Connectable.connect <VoiceChannel.connect>`.
5057
The parameters and methods being documented here is so third party libraries can refer to it
5158
when implementing their own VoiceProtocol types.
@@ -161,4 +168,99 @@ def cleanup(self) -> None:
161168
self.client._connection._remove_voice_client(key)
162169

163170

164-
class RecorderProtocol(Generic[VoiceClientT]):
171+
class VoiceRecorderProtocol(Generic[VoiceProtocolT]):
172+
"""A class that represents a Discord voice client recorder protocol.
173+
174+
.. warning::
175+
176+
If you are an end user, you **should not construct this manually** but instead
177+
take it from a :class:`VoiceProtocol` implementation, like :attr:`VoiceClient.recorder`.
178+
The parameters and methods being documented here is so third party libraries can refer to it
179+
when implementing their own RecorderProtocol types.
180+
181+
This is an abstract class. The library provides a concrete implementation under
182+
:class:`VoiceRecorderClient`.
183+
184+
This class allows you to implement a protocol to allow for an external
185+
method of receiving and handling voice data.
186+
187+
.. versionadded:: 2.7
188+
189+
Parameters
190+
----------
191+
client: :class:`VoiceProtocol`
192+
The voice client (or its subclasses) that are bound to this recorder.
193+
channel: :class:`abc.Connectable`
194+
The voice channel that is being recorder. If not provided, defaults to
195+
:attr:`VoiceProtocol.channel`
196+
"""
197+
198+
def __init__(self, client: VoiceProtocolT, channel: abc.Connectable | None = None) -> None:
199+
self.client: VoiceProtocolT = client
200+
self.channel: abc.Connectable = channel or client.channel
201+
202+
def get_ssrc(self, user_id: int) -> int:
203+
"""Gets the ssrc of a user.
204+
205+
Parameters
206+
----------
207+
user_id: :class:`int`
208+
The user ID to get the ssrc from.
209+
210+
Returns
211+
-------
212+
:class:`int`
213+
The ssrc for the provided user ID.
214+
"""
215+
raise NotImplementedError('subclasses must implement this')
216+
217+
def unpack(self, data: bytes) -> bytes | None:
218+
"""Takes an audio packet received from Discord and decodes it.
219+
220+
Parameters
221+
----------
222+
data: :class:`bytes`
223+
The bytes received by Discord.
224+
225+
Returns
226+
-------
227+
Optional[:class:`bytes`]
228+
The unpacked bytes, or ``None`` if they could not be unpacked.
229+
"""
230+
raise NotImplementedError('subclasses must implement this')
231+
232+
def record(
233+
self,
234+
sink: Sink,
235+
callback: RecordCallback[P, R],
236+
sync_start: bool,
237+
*callback_args: P.args,
238+
**callback_kwargs: P.kwargs,
239+
) -> None:
240+
"""Start recording audio from the current voice channel in the provided sink.
241+
242+
You must be in a voice channel.
243+
244+
Parameters
245+
----------
246+
sink: :class:`~discord.Sink`
247+
The sink to record to.
248+
callback: Callable[..., Any]
249+
The function called after the bot has stopped recording. This can take any arguments and
250+
can return an awaitable.
251+
sync_start: :class:`bool`
252+
Whether the subsequent recording users will start with silence. This is useful for recording
253+
audio just as it was heard.
254+
255+
Raises
256+
------
257+
RecordingException
258+
Not connected to a voice channel
259+
TypeError
260+
You did not pass a Sink object.
261+
"""
262+
raise NotImplementedError('subclasses must implement this')
263+
264+
def stop(self) -> None:
265+
"""Stops recording."""
266+
raise NotImplementedError('subclasses must implement this')

discord/voice/client.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
from ._types import VoiceProtocol
4040
from .player import AudioPlayer
41-
from .recorder import Recorder
41+
from .recorder import VoiceRecorderClient
4242
from .source import AudioSource
4343
from .state import VoiceConnectionState
4444

@@ -100,7 +100,13 @@ class VoiceClient(VoiceProtocol):
100100

101101
channel: VocalGuildChannel
102102

103-
def __init__(self, client: Client, channel: abc.Connectable) -> None:
103+
def __init__(
104+
self,
105+
client: Client,
106+
channel: abc.Connectable,
107+
*,
108+
use_recorder: bool = True,
109+
) -> None:
104110
if not has_nacl:
105111
raise RuntimeError(
106112
"PyNaCl library is needed in order to use voice related features, "
@@ -127,7 +133,9 @@ def __init__(self, client: Client, channel: abc.Connectable) -> None:
127133
self._connection: VoiceConnectionState = self.create_connection_state()
128134

129135
# voice recv things
130-
self._recorder: Recorder | None = None
136+
self._recorder: VoiceRecorderClient | None = None
137+
if use_recorder:
138+
self._recorder = VoiceRecorderClient(self)
131139

132140
warn_nacl: bool = not has_nacl
133141
supported_modes: tuple[SupportedModes, ...] = (
@@ -187,7 +195,7 @@ def checked_add(self, attr: str, value: int, limit: int) -> None:
187195
setattr(self, attr, val + value)
188196

189197
def create_connection_state(self) -> VoiceConnectionState:
190-
return VoiceConnectionState(self)
198+
return VoiceConnectionState(self, hook=self._recorder)
191199

192200
async def on_voice_state_update(self, data: RawVoiceStateUpdateEvent) -> None:
193201
await self._connection.voice_state_update(data)

discord/voice/gateway.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ async def received_message(self, msg: Any, /):
171171
)
172172
self._keep_alive.start()
173173

174-
await self._hook(self, msg)
174+
await utils.maybe_coroutine(self._hook, self, data)
175175

176176
async def ready(self, data: dict[str, Any]) -> None:
177177
state = self.state

discord/voice/recorder.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,52 @@
2525

2626
from __future__ import annotations
2727

28-
# TODO: finish this
28+
import asyncio
29+
import threading
30+
from typing import TYPE_CHECKING, Any, TypeVar
2931

32+
from discord.opus import DecodeManager
3033

31-
class Recorder:
34+
from ._types import VoiceRecorderProtocol
35+
36+
if TYPE_CHECKING:
37+
from discord.sinks import Sink
38+
39+
from .client import VoiceClient
40+
from .gateway import VoiceWebSocket
41+
42+
VoiceClientT = TypeVar('VoiceClientT', bound=VoiceClient, covariant=True)
43+
44+
45+
class VoiceRecorderClient(VoiceRecorderProtocol[VoiceClientT]):
3246
"""Represents a voice recorder for a voice client.
3347
3448
You should not construct this but instead obtain it from :attr:`VoiceClient.recorder`.
3549
3650
.. versionadded:: 2.7
3751
"""
3852

39-
def __init__(self, client: VoiceClient) -> None:
53+
def __init__(self, client: VoiceClientT) -> None:
54+
super().__init__(client)
55+
56+
self._paused: asyncio.Event = asyncio.Event()
57+
self._recording: asyncio.Event = asyncio.Event()
58+
self.decoder: DecodeManager = DecodeManager(self)
59+
self.sync_start: bool = False
60+
self.sinks: dict[int, tuple[Sink, threading.Thread]] = {}
61+
62+
def is_paused(self) -> bool:
63+
"""Whether the current recorder is paused."""
64+
return self._paused.is_set()
65+
66+
def is_recording(self) -> bool:
67+
"""Whether the current recording is actively recording."""
68+
return self._recording.is_set()
69+
70+
async def hook(self, ws: VoiceWebSocket, data: dict[str, Any]) -> None:
71+
...
72+
73+
def record(
74+
self,
75+
sink: Sink,
76+
)

0 commit comments

Comments
 (0)