Skip to content

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