From 7f9c3b1b4044dbea1a3abeb3e5b23fc308dae26d Mon Sep 17 00:00:00 2001 From: levin <123833241+levinismynameirl@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:03:02 +0100 Subject: [PATCH] Fix FFmpeg errors not sent to after callback --- discord/errors.py | 10 ++++++ discord/player.py | 79 +++++++++++++++++++++++++++++++++++++++++------ docs/api.rst | 3 ++ 3 files changed, 83 insertions(+), 9 deletions(-) diff --git a/discord/errors.py b/discord/errors.py index c07a7ed15..11f5cfaa2 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -48,6 +48,7 @@ __all__ = ( 'PrivilegedIntentsRequired', 'InteractionResponded', 'MissingApplicationID', + 'FFmpegProcessError', ) APP_ID_NOT_FOUND = ( @@ -74,6 +75,15 @@ class ClientException(DiscordException): pass +class FFmpegProcessError(ClientException): + """Exception that's raised when an FFmpeg process fails. + + .. versionadded:: 2.7 + """ + + pass + + class GatewayNotFound(DiscordException): """An exception that is raised when the gateway for Discord could not be found""" diff --git a/discord/player.py b/discord/player.py index 6243c0417..2a1fdf95d 100644 --- a/discord/player.py +++ b/discord/player.py @@ -40,7 +40,7 @@ import io from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, TypeVar, Union from .enums import SpeakingState -from .errors import ClientException +from .errors import ClientException, FFmpegProcessError from .opus import Encoder as OpusEncoder, OPUS_SILENCE from .oggparse import OggStream from .utils import MISSING @@ -186,6 +186,8 @@ class FFmpegAudio(AudioSource): self._stderr: Optional[IO[bytes]] = None self._pipe_writer_thread: Optional[threading.Thread] = None self._pipe_reader_thread: Optional[threading.Thread] = None + self._current_error: Optional[Exception] = None + self._stopped: bool = False if piping_stdin: n = f'popen-stdin-writer:pid-{self._process.pid}' @@ -212,25 +214,72 @@ class FFmpegAudio(AudioSource): else: return process + def _check_process_returncode(self) -> None: + """Set _current_error if FFmpeg exited with a non-zero code.""" + if self._process is MISSING: + return + + ret = self._process.poll() + if ret is None: + return # still running + + if self._stopped: + return # intentionally stopped + + if ret != 0 and self._current_error is None: + # Only set error once, on first detection + # read stderr if available + stderr_text = None + if self._stderr: + try: + stderr_text = self._stderr.read(8192).decode(errors='ignore') + except Exception: + stderr_text = '' + + stderr_info = stderr_text if stderr_text else '' + self._current_error = FFmpegProcessError(f'FFmpeg exited with code {ret}. Stderr: {stderr_info}') + def _kill_process(self) -> None: + # check if FFmpeg process failed + self._check_process_returncode() + # this function gets called in __del__ so instance attributes might not even exist proc = getattr(self, '_process', MISSING) + # Only proceed if proc is a subprocess.Popen instance if proc is MISSING: return - _log.debug('Preparing to terminate ffmpeg process %s.', proc.pid) + pid = getattr(proc, 'pid', 'unknown') + _log.debug('Preparing to terminate ffmpeg process %s.', pid) try: proc.kill() except Exception: - _log.exception('Ignoring error attempting to kill ffmpeg process %s', proc.pid) + _log.exception('Ignoring error attempting to kill ffmpeg process %s', pid) - if proc.poll() is None: - _log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid) - proc.communicate() - _log.info('ffmpeg process %s should have terminated with a return code of %s.', proc.pid, proc.returncode) + try: + still_running = proc.poll() is None + except Exception: + _log.exception('Error checking poll() on ffmpeg process %s', pid) + still_running = False + + if still_running: + _log.info('ffmpeg process %s has not terminated. Waiting to terminate...', pid) + try: + proc.communicate() + except Exception: + pass + _log.info( + 'ffmpeg process %s should have terminated with a return code of %s.', + pid, + getattr(proc, 'returncode', 'unknown'), + ) else: - _log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode) + _log.info( + 'ffmpeg process %s successfully terminated with return code of %s.', + pid, + getattr(proc, 'returncode', 'unknown'), + ) def _pipe_writer(self, source: io.BufferedIOBase) -> None: while self._process: @@ -267,6 +316,7 @@ class FFmpegAudio(AudioSource): return def cleanup(self) -> None: + self._stopped = True self._kill_process() self._process = self._stdout = self._stdin = self._stderr = MISSING @@ -348,6 +398,8 @@ class FFmpegPCMAudio(FFmpegAudio): def read(self) -> bytes: ret = self._stdout.read(OpusEncoder.FRAME_SIZE) if len(ret) != OpusEncoder.FRAME_SIZE: + # Check for FFmpeg process failure when read returns incomplete data + self._check_process_returncode() return b'' return ret @@ -646,7 +698,11 @@ class FFmpegOpusAudio(FFmpegAudio): return codec, bitrate def read(self) -> bytes: - return next(self._packet_iter, b'') + data = next(self._packet_iter, b'') + if not data: + # Check for FFmpeg process failure when read returns empty + self._check_process_returncode() + return data def is_opus(self) -> bool: return True @@ -745,6 +801,11 @@ class AudioPlayer(threading.Thread): data = self.source.read() if not data: + # Check if the source has an error (e.g., from FFmpegAudio process failure) + if self._current_error is None: + source_error = getattr(self.source, '_current_error', None) + if source_error: + self._current_error = source_error self.stop() break diff --git a/docs/api.rst b/docs/api.rst index ad86df8a2..5d816087e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6216,6 +6216,8 @@ The following exceptions are thrown by the library. .. autoexception:: MissingApplicationID +.. autoexception:: FFmpegProcessError + .. autoexception:: discord.opus.OpusError .. autoexception:: discord.opus.OpusNotLoaded @@ -6234,6 +6236,7 @@ Exception Hierarchy - :exc:`PrivilegedIntentsRequired` - :exc:`InteractionResponded` - :exc:`MissingApplicationID` + - :exc:`FFmpegProcessError` - :exc:`GatewayNotFound` - :exc:`HTTPException` - :exc:`Forbidden`