Player
class
Player (Thread)
¶
Source code in naff/api/voice/player.py
class Player(threading.Thread):
def __init__(self, audio, v_state, loop) -> None:
super().__init__()
self.daemon = True
self.current_audio: Optional[BaseAudio] = audio
self.state: "ActiveVoiceState" = v_state
self.loop: AbstractEventLoop = loop
self._encoder: Encoder = Encoder()
self._resume: threading.Event = threading.Event()
self._stop_event: threading.Event = threading.Event()
self._stopped: asyncio.Event = asyncio.Event()
self._sent_payloads: int = 0
self._cond = threading.Condition()
if not shutil.which("ffmpeg"):
raise RuntimeError(
"Unable to start player. FFmpeg was not found. Please add it to your project directory or PATH. (https://ffmpeg.org/)"
)
def __enter__(self) -> "Player":
self.state.ws.cond = self._cond
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
try:
self.state.ws.cond = None
except AttributeError:
pass
def stop(self) -> None:
"""Stop playing completely."""
self._stop_event.set()
with self._cond:
self._cond.notify()
def resume(self) -> None:
"""Resume playing."""
self._resume.set()
with self._cond:
self._cond.notify()
@property
def paused(self) -> bool:
"""Is the player paused"""
return not self._resume.is_set()
def pause(self) -> None:
"""Pause the player."""
self._resume.clear()
@property
def stopped(self) -> bool:
"""Is the player currently stopped?"""
return self._stopped.is_set()
@property
def elapsed_time(self) -> float:
"""How many seconds of audio the player has sent."""
return self._sent_payloads * self._encoder.delay
def play(self) -> None:
"""Start playing."""
self._stop_event.clear()
self._resume.set()
self.start()
def run(self) -> None:
"""The main player loop to send audio to the voice websocket."""
loops = 0
if isinstance(self.current_audio, AudioVolume):
# noinspection PyProtectedMember
self.current_audio.volume = self.state._volume
self._encoder.set_bitrate(getattr(self.current_audio, "bitrate", self.state.channel.bitrate))
self._stopped.clear()
asyncio.run_coroutine_threadsafe(self.state.ws.speaking(True), self.loop)
logger.debug(f"Now playing {self.current_audio!r}")
start = None
try:
while not self._stop_event.is_set():
if not self.state.ws.ready.is_set() or not self._resume.is_set():
run_coroutine_threadsafe(self.state.ws.speaking(False), self.loop)
logger.debug("Voice playback has been suspended!")
wait_for = []
if not self.state.ws.ready.is_set():
wait_for.append(self.state.ws.ready)
if not self._resume.is_set():
wait_for.append(self._resume)
with self._cond:
while not (self._stop_event.is_set() or all(x.is_set() for x in wait_for)):
self._cond.wait()
if self._stop_event.is_set():
continue
run_coroutine_threadsafe(self.state.ws.speaking(), self.loop)
logger.debug("Voice playback has been resumed!")
start = None
loops = 0
if data := self.current_audio.read(self._encoder.frame_size):
self.state.ws.send_packet(data, self._encoder, needs_encode=self.current_audio.needs_encode)
else:
if self.current_audio.locked_stream or not self.current_audio.audio_complete:
# if more audio is expected
self.state.ws.send_packet(b"\xF8\xFF\xFE", self._encoder, needs_encode=False)
else:
break
if not start:
start = perf_counter()
loops += 1
self._sent_payloads += 1 # used for duration calc
sleep(max(0.0, start + (self._encoder.delay * loops) - perf_counter()))
finally:
asyncio.run_coroutine_threadsafe(self.state.ws.speaking(False), self.loop)
self.current_audio.cleanup()
self.loop.call_soon_threadsafe(self._stopped.set)
method
stop(self)
¶
Stop playing completely.
Source code in naff/api/voice/player.py
def stop(self) -> None:
"""Stop playing completely."""
self._stop_event.set()
with self._cond:
self._cond.notify()
method
resume(self)
¶
Resume playing.
Source code in naff/api/voice/player.py
def resume(self) -> None:
"""Resume playing."""
self._resume.set()
with self._cond:
self._cond.notify()
property
readonly
paused: bool
¶
Is the player paused
method
pause(self)
¶
Pause the player.
Source code in naff/api/voice/player.py
def pause(self) -> None:
"""Pause the player."""
self._resume.clear()
property
readonly
stopped: bool
¶
Is the player currently stopped?
property
readonly
elapsed_time: float
¶
How many seconds of audio the player has sent.
method
play(self)
¶
Start playing.
Source code in naff/api/voice/player.py
def play(self) -> None:
"""Start playing."""
self._stop_event.clear()
self._resume.set()
self.start()
method
run(self)
¶
The main player loop to send audio to the voice websocket.
Source code in naff/api/voice/player.py
def run(self) -> None:
"""The main player loop to send audio to the voice websocket."""
loops = 0
if isinstance(self.current_audio, AudioVolume):
# noinspection PyProtectedMember
self.current_audio.volume = self.state._volume
self._encoder.set_bitrate(getattr(self.current_audio, "bitrate", self.state.channel.bitrate))
self._stopped.clear()
asyncio.run_coroutine_threadsafe(self.state.ws.speaking(True), self.loop)
logger.debug(f"Now playing {self.current_audio!r}")
start = None
try:
while not self._stop_event.is_set():
if not self.state.ws.ready.is_set() or not self._resume.is_set():
run_coroutine_threadsafe(self.state.ws.speaking(False), self.loop)
logger.debug("Voice playback has been suspended!")
wait_for = []
if not self.state.ws.ready.is_set():
wait_for.append(self.state.ws.ready)
if not self._resume.is_set():
wait_for.append(self._resume)
with self._cond:
while not (self._stop_event.is_set() or all(x.is_set() for x in wait_for)):
self._cond.wait()
if self._stop_event.is_set():
continue
run_coroutine_threadsafe(self.state.ws.speaking(), self.loop)
logger.debug("Voice playback has been resumed!")
start = None
loops = 0
if data := self.current_audio.read(self._encoder.frame_size):
self.state.ws.send_packet(data, self._encoder, needs_encode=self.current_audio.needs_encode)
else:
if self.current_audio.locked_stream or not self.current_audio.audio_complete:
# if more audio is expected
self.state.ws.send_packet(b"\xF8\xFF\xFE", self._encoder, needs_encode=False)
else:
break
if not start:
start = perf_counter()
loops += 1
self._sent_payloads += 1 # used for duration calc
sleep(max(0.0, start + (self._encoder.delay * loops) - perf_counter()))
finally:
asyncio.run_coroutine_threadsafe(self.state.ws.speaking(False), self.loop)
self.current_audio.cleanup()
self.loop.call_soon_threadsafe(self._stopped.set)
inherited
method
start(self)
¶
Start the thread's activity.
It must be called at most once per thread object. It arranges for the object's run() method to be invoked in a separate thread of control.
This method will raise a RuntimeError if called more than once on the same thread object.
Source code in naff/api/voice/player.py
def start(self):
"""Start the thread's activity.
It must be called at most once per thread object. It arranges for the
object's run() method to be invoked in a separate thread of control.
This method will raise a RuntimeError if called more than once on the
same thread object.
"""
if not self._initialized:
raise RuntimeError("thread.__init__() not called")
if self._started.is_set():
raise RuntimeError("threads can only be started once")
with _active_limbo_lock:
_limbo[self] = self
try:
_start_new_thread(self._bootstrap, ())
except Exception:
with _active_limbo_lock:
del _limbo[self]
raise
self._started.wait()
inherited
method
join(self, timeout)
¶
Wait until the thread terminates.
This blocks the calling thread until the thread whose join() method is called terminates -- either normally or through an unhandled exception or until the optional timeout occurs.
When the timeout argument is present and not None, it should be a floating point number specifying a timeout for the operation in seconds (or fractions thereof). As join() always returns None, you must call is_alive() after join() to decide whether a timeout happened -- if the thread is still alive, the join() call timed out.
When the timeout argument is not present or None, the operation will block until the thread terminates.
A thread can be join()ed many times.
join() raises a RuntimeError if an attempt is made to join the current thread as that would cause a deadlock. It is also an error to join() a thread before it has been started and attempts to do so raises the same exception.
Source code in naff/api/voice/player.py
def join(self, timeout=None):
"""Wait until the thread terminates.
This blocks the calling thread until the thread whose join() method is
called terminates -- either normally or through an unhandled exception
or until the optional timeout occurs.
When the timeout argument is present and not None, it should be a
floating point number specifying a timeout for the operation in seconds
(or fractions thereof). As join() always returns None, you must call
is_alive() after join() to decide whether a timeout happened -- if the
thread is still alive, the join() call timed out.
When the timeout argument is not present or None, the operation will
block until the thread terminates.
A thread can be join()ed many times.
join() raises a RuntimeError if an attempt is made to join the current
thread as that would cause a deadlock. It is also an error to join() a
thread before it has been started and attempts to do so raises the same
exception.
"""
if not self._initialized:
raise RuntimeError("Thread.__init__() not called")
if not self._started.is_set():
raise RuntimeError("cannot join thread before it is started")
if self is current_thread():
raise RuntimeError("cannot join current thread")
if timeout is None:
self._wait_for_tstate_lock()
else:
# the behavior of a negative timeout isn't documented, but
# historically .join(timeout=x) for x<0 has acted as if timeout=0
self._wait_for_tstate_lock(timeout=max(timeout, 0))
inherited
property
writable
name
¶
A string used for identification purposes only.
It has no semantics. Multiple threads may be given the same name. The initial name is set by the constructor.
inherited
property
readonly
ident
¶
Thread identifier of this thread or None if it has not been started.
This is a nonzero integer. See the get_ident() function. Thread identifiers may be recycled when a thread exits and another thread is created. The identifier is available even after the thread has exited.
inherited
property
readonly
native_id
¶
Native integral thread ID of this thread, or None if it has not been started.
This is a non-negative integer. See the get_native_id() function. This represents the Thread ID as reported by the kernel.
inherited
method
is_alive(self)
¶
Return whether the thread is alive.
This method returns True just before the run() method starts until just after the run() method terminates. See also the module function enumerate().
Source code in naff/api/voice/player.py
def is_alive(self):
"""Return whether the thread is alive.
This method returns True just before the run() method starts until just
after the run() method terminates. See also the module function
enumerate().
"""
assert self._initialized, "Thread.__init__() not called"
if self._is_stopped or not self._started.is_set():
return False
self._wait_for_tstate_lock(False)
return not self._is_stopped
inherited
property
writable
daemon
¶
A boolean value indicating whether this thread is a daemon thread.
This must be set before start() is called, otherwise RuntimeError is raised. Its initial value is inherited from the creating thread; the main thread is not a daemon thread and therefore all threads created in the main thread default to daemon = False.
The entire Python program exits when only daemon threads are left.
inherited
method
isDaemon(self)
¶
Return whether this thread is a daemon.
This method is deprecated, use the daemon attribute instead.
Source code in naff/api/voice/player.py
def isDaemon(self):
"""Return whether this thread is a daemon.
This method is deprecated, use the daemon attribute instead.
"""
import warnings
warnings.warn('isDaemon() is deprecated, get the daemon attribute instead',
DeprecationWarning, stacklevel=2)
return self.daemon
inherited
method
setDaemon(self, daemonic)
¶
Set whether this thread is a daemon.
This method is deprecated, use the .daemon property instead.
Source code in naff/api/voice/player.py
def setDaemon(self, daemonic):
"""Set whether this thread is a daemon.
This method is deprecated, use the .daemon property instead.
"""
import warnings
warnings.warn('setDaemon() is deprecated, set the daemon attribute instead',
DeprecationWarning, stacklevel=2)
self.daemon = daemonic
inherited
method
getName(self)
¶
Return a string used for identification purposes only.
This method is deprecated, use the name attribute instead.
Source code in naff/api/voice/player.py
def getName(self):
"""Return a string used for identification purposes only.
This method is deprecated, use the name attribute instead.
"""
import warnings
warnings.warn('getName() is deprecated, get the name attribute instead',
DeprecationWarning, stacklevel=2)
return self.name
inherited
method
setName(self, name)
¶
Set the name string for this thread.
This method is deprecated, use the name attribute instead.
Source code in naff/api/voice/player.py
def setName(self, name):
"""Set the name string for this thread.
This method is deprecated, use the name attribute instead.
"""
import warnings
warnings.warn('setName() is deprecated, set the name attribute instead',
DeprecationWarning, stacklevel=2)
self.name = name