Add voice playback control (#111)
Some checks are pending
Deploy MkDocs / deploy (push) Waiting to run
Some checks are pending
Deploy MkDocs / deploy (push) Waiting to run
This commit is contained in:
parent
506adeca20
commit
d710487fc2
@ -77,11 +77,14 @@ class VoiceClient:
|
||||
self.secret_key: Optional[Sequence[int]] = None
|
||||
self._server_ip: Optional[str] = None
|
||||
self._server_port: Optional[int] = None
|
||||
self._current_source: Optional[AudioSource] = None
|
||||
self._play_task: Optional[asyncio.Task] = None
|
||||
self._sink: Optional[AudioSink] = None
|
||||
self._ssrc_map: dict[int, int] = {}
|
||||
self._ssrc_lock = threading.Lock()
|
||||
self._current_source: Optional[AudioSource] = None
|
||||
self._play_task: Optional[asyncio.Task] = None
|
||||
self._pause_event = asyncio.Event()
|
||||
self._pause_event.set()
|
||||
self._is_playing = False
|
||||
self._sink: Optional[AudioSink] = None
|
||||
self._ssrc_map: dict[int, int] = {}
|
||||
self._ssrc_lock = threading.Lock()
|
||||
|
||||
async def connect(self) -> None:
|
||||
if self._ws is None:
|
||||
@ -189,31 +192,37 @@ class VoiceClient:
|
||||
raise RuntimeError("UDP socket not initialised")
|
||||
self._udp.send(frame)
|
||||
|
||||
async def _play_loop(self) -> None:
|
||||
assert self._current_source is not None
|
||||
try:
|
||||
while True:
|
||||
data = await self._current_source.read()
|
||||
if not data:
|
||||
break
|
||||
volume = getattr(self._current_source, "volume", 1.0)
|
||||
if volume != 1.0:
|
||||
data = _apply_volume(data, volume)
|
||||
await self.send_audio_frame(data)
|
||||
finally:
|
||||
await self._current_source.close()
|
||||
self._current_source = None
|
||||
self._play_task = None
|
||||
async def _play_loop(self) -> None:
|
||||
assert self._current_source is not None
|
||||
self._is_playing = True
|
||||
try:
|
||||
while True:
|
||||
await self._pause_event.wait()
|
||||
data = await self._current_source.read()
|
||||
if not data:
|
||||
break
|
||||
volume = getattr(self._current_source, "volume", 1.0)
|
||||
if volume != 1.0:
|
||||
data = _apply_volume(data, volume)
|
||||
await self.send_audio_frame(data)
|
||||
finally:
|
||||
await self._current_source.close()
|
||||
self._current_source = None
|
||||
self._play_task = None
|
||||
self._is_playing = False
|
||||
self._pause_event.set()
|
||||
|
||||
async def stop(self) -> None:
|
||||
if self._play_task:
|
||||
self._play_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._play_task
|
||||
self._play_task = None
|
||||
if self._current_source:
|
||||
await self._current_source.close()
|
||||
self._current_source = None
|
||||
async def stop(self) -> None:
|
||||
if self._play_task:
|
||||
self._play_task.cancel()
|
||||
self._pause_event.set()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await self._play_task
|
||||
self._play_task = None
|
||||
self._is_playing = False
|
||||
if self._current_source:
|
||||
await self._current_source.close()
|
||||
self._current_source = None
|
||||
|
||||
async def play(self, source: AudioSource, *, wait: bool = True) -> None:
|
||||
"""|coro| Play an :class:`AudioSource` on the voice connection."""
|
||||
@ -224,10 +233,31 @@ class VoiceClient:
|
||||
if wait:
|
||||
await self._play_task
|
||||
|
||||
async def play_file(self, filename: str, *, wait: bool = True) -> None:
|
||||
"""|coro| Stream an audio file or URL using FFmpeg."""
|
||||
|
||||
await self.play(FFmpegAudioSource(filename), wait=wait)
|
||||
async def play_file(self, filename: str, *, wait: bool = True) -> None:
|
||||
"""|coro| Stream an audio file or URL using FFmpeg."""
|
||||
|
||||
await self.play(FFmpegAudioSource(filename), wait=wait)
|
||||
|
||||
def pause(self) -> None:
|
||||
"""Pause the current audio source."""
|
||||
|
||||
if self._play_task and not self._play_task.done():
|
||||
self._pause_event.clear()
|
||||
|
||||
def resume(self) -> None:
|
||||
"""Resume playback of a paused source."""
|
||||
|
||||
if self._play_task and not self._play_task.done():
|
||||
self._pause_event.set()
|
||||
|
||||
def is_paused(self) -> bool:
|
||||
"""Return ``True`` if playback is currently paused."""
|
||||
|
||||
return bool(self._play_task and not self._pause_event.is_set())
|
||||
|
||||
def is_playing(self) -> bool:
|
||||
"""Return ``True`` if audio is actively being played."""
|
||||
return self._is_playing and self._pause_event.is_set()
|
||||
|
||||
def listen(self, sink: AudioSink) -> None:
|
||||
"""Start listening to voice and routing to a sink."""
|
||||
|
@ -6,6 +6,10 @@ Disagreement includes experimental support for connecting to voice channels. You
|
||||
voice = await client.join_voice(guild_id, channel_id)
|
||||
await voice.play_file("welcome.mp3")
|
||||
await voice.play_file("another.mp3") # switch sources while connected
|
||||
voice.pause()
|
||||
voice.resume()
|
||||
if voice.is_playing():
|
||||
print("audio is playing")
|
||||
await voice.close()
|
||||
```
|
||||
|
||||
|
@ -59,6 +59,17 @@ class DummySource(AudioSource):
|
||||
return b""
|
||||
|
||||
|
||||
class SlowSource(AudioSource):
|
||||
def __init__(self, chunks):
|
||||
self.chunks = list(chunks)
|
||||
|
||||
async def read(self) -> bytes:
|
||||
await asyncio.sleep(0)
|
||||
if self.chunks:
|
||||
return self.chunks.pop(0)
|
||||
return b""
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_voice_client_handshake():
|
||||
hello = {"d": {"heartbeat_interval": 50}}
|
||||
@ -205,3 +216,49 @@ async def test_voice_client_volume_scaling(monkeypatch):
|
||||
samples[1] = int(samples[1] * 0.5)
|
||||
expected = samples.tobytes()
|
||||
assert udp.sent == [expected]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_pause_resume_and_status():
|
||||
ws = DummyWebSocket(
|
||||
[
|
||||
{"d": {"heartbeat_interval": 50}},
|
||||
{"d": {"ssrc": 1, "ip": "127.0.0.1", "port": 4000}},
|
||||
{"d": {"secret_key": []}},
|
||||
]
|
||||
)
|
||||
udp = DummyUDP()
|
||||
vc = VoiceClient(
|
||||
client=DummyVoiceClient(),
|
||||
endpoint="ws://localhost",
|
||||
session_id="sess",
|
||||
token="tok",
|
||||
guild_id=1,
|
||||
user_id=2,
|
||||
ws=ws,
|
||||
udp=udp,
|
||||
)
|
||||
await vc.connect()
|
||||
vc._heartbeat_task.cancel()
|
||||
|
||||
src = SlowSource([b"a", b"b", b"c"])
|
||||
await vc.play(src, wait=False)
|
||||
|
||||
while not udp.sent:
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert vc.is_playing()
|
||||
vc.pause()
|
||||
assert vc.is_paused()
|
||||
await asyncio.sleep(0)
|
||||
sent = len(udp.sent)
|
||||
await asyncio.sleep(0.01)
|
||||
assert len(udp.sent) == sent
|
||||
assert not vc.is_playing()
|
||||
|
||||
vc.resume()
|
||||
assert not vc.is_paused()
|
||||
|
||||
await vc._play_task
|
||||
assert udp.sent == [b"a", b"b", b"c"]
|
||||
assert not vc.is_playing()
|
||||
|
Loading…
x
Reference in New Issue
Block a user