mirror of
https://github.com/Rapptz/discord.py.git
synced 2026-03-05 03:02:49 +00:00
Fix FFmpeg errors not sent to after callback
This commit is contained in:
@@ -48,6 +48,7 @@ __all__ = (
|
|||||||
'PrivilegedIntentsRequired',
|
'PrivilegedIntentsRequired',
|
||||||
'InteractionResponded',
|
'InteractionResponded',
|
||||||
'MissingApplicationID',
|
'MissingApplicationID',
|
||||||
|
'FFmpegProcessError',
|
||||||
)
|
)
|
||||||
|
|
||||||
APP_ID_NOT_FOUND = (
|
APP_ID_NOT_FOUND = (
|
||||||
@@ -74,6 +75,15 @@ class ClientException(DiscordException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FFmpegProcessError(ClientException):
|
||||||
|
"""Exception that's raised when an FFmpeg process fails.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class GatewayNotFound(DiscordException):
|
class GatewayNotFound(DiscordException):
|
||||||
"""An exception that is raised when the gateway for Discord could not be found"""
|
"""An exception that is raised when the gateway for Discord could not be found"""
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import io
|
|||||||
from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
|
from typing import Any, Callable, Generic, IO, Optional, TYPE_CHECKING, Tuple, TypeVar, Union
|
||||||
|
|
||||||
from .enums import SpeakingState
|
from .enums import SpeakingState
|
||||||
from .errors import ClientException
|
from .errors import ClientException, FFmpegProcessError
|
||||||
from .opus import Encoder as OpusEncoder, OPUS_SILENCE
|
from .opus import Encoder as OpusEncoder, OPUS_SILENCE
|
||||||
from .oggparse import OggStream
|
from .oggparse import OggStream
|
||||||
from .utils import MISSING
|
from .utils import MISSING
|
||||||
@@ -186,6 +186,8 @@ class FFmpegAudio(AudioSource):
|
|||||||
self._stderr: Optional[IO[bytes]] = None
|
self._stderr: Optional[IO[bytes]] = None
|
||||||
self._pipe_writer_thread: Optional[threading.Thread] = None
|
self._pipe_writer_thread: Optional[threading.Thread] = None
|
||||||
self._pipe_reader_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:
|
if piping_stdin:
|
||||||
n = f'popen-stdin-writer:pid-{self._process.pid}'
|
n = f'popen-stdin-writer:pid-{self._process.pid}'
|
||||||
@@ -212,25 +214,72 @@ class FFmpegAudio(AudioSource):
|
|||||||
else:
|
else:
|
||||||
return process
|
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 = '<failed to read stderr>'
|
||||||
|
|
||||||
|
stderr_info = stderr_text if stderr_text else '<no stderr>'
|
||||||
|
self._current_error = FFmpegProcessError(f'FFmpeg exited with code {ret}. Stderr: {stderr_info}')
|
||||||
|
|
||||||
def _kill_process(self) -> None:
|
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
|
# this function gets called in __del__ so instance attributes might not even exist
|
||||||
proc = getattr(self, '_process', MISSING)
|
proc = getattr(self, '_process', MISSING)
|
||||||
|
# Only proceed if proc is a subprocess.Popen instance
|
||||||
if proc is MISSING:
|
if proc is MISSING:
|
||||||
return
|
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:
|
try:
|
||||||
proc.kill()
|
proc.kill()
|
||||||
except Exception:
|
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:
|
try:
|
||||||
_log.info('ffmpeg process %s has not terminated. Waiting to terminate...', proc.pid)
|
still_running = proc.poll() is None
|
||||||
proc.communicate()
|
except Exception:
|
||||||
_log.info('ffmpeg process %s should have terminated with a return code of %s.', proc.pid, proc.returncode)
|
_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:
|
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:
|
def _pipe_writer(self, source: io.BufferedIOBase) -> None:
|
||||||
while self._process:
|
while self._process:
|
||||||
@@ -267,6 +316,7 @@ class FFmpegAudio(AudioSource):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def cleanup(self) -> None:
|
def cleanup(self) -> None:
|
||||||
|
self._stopped = True
|
||||||
self._kill_process()
|
self._kill_process()
|
||||||
self._process = self._stdout = self._stdin = self._stderr = MISSING
|
self._process = self._stdout = self._stdin = self._stderr = MISSING
|
||||||
|
|
||||||
@@ -348,6 +398,8 @@ class FFmpegPCMAudio(FFmpegAudio):
|
|||||||
def read(self) -> bytes:
|
def read(self) -> bytes:
|
||||||
ret = self._stdout.read(OpusEncoder.FRAME_SIZE)
|
ret = self._stdout.read(OpusEncoder.FRAME_SIZE)
|
||||||
if len(ret) != 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 b''
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@@ -646,7 +698,11 @@ class FFmpegOpusAudio(FFmpegAudio):
|
|||||||
return codec, bitrate
|
return codec, bitrate
|
||||||
|
|
||||||
def read(self) -> bytes:
|
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:
|
def is_opus(self) -> bool:
|
||||||
return True
|
return True
|
||||||
@@ -745,6 +801,11 @@ class AudioPlayer(threading.Thread):
|
|||||||
data = self.source.read()
|
data = self.source.read()
|
||||||
|
|
||||||
if not data:
|
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()
|
self.stop()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|||||||
@@ -6216,6 +6216,8 @@ The following exceptions are thrown by the library.
|
|||||||
|
|
||||||
.. autoexception:: MissingApplicationID
|
.. autoexception:: MissingApplicationID
|
||||||
|
|
||||||
|
.. autoexception:: FFmpegProcessError
|
||||||
|
|
||||||
.. autoexception:: discord.opus.OpusError
|
.. autoexception:: discord.opus.OpusError
|
||||||
|
|
||||||
.. autoexception:: discord.opus.OpusNotLoaded
|
.. autoexception:: discord.opus.OpusNotLoaded
|
||||||
@@ -6234,6 +6236,7 @@ Exception Hierarchy
|
|||||||
- :exc:`PrivilegedIntentsRequired`
|
- :exc:`PrivilegedIntentsRequired`
|
||||||
- :exc:`InteractionResponded`
|
- :exc:`InteractionResponded`
|
||||||
- :exc:`MissingApplicationID`
|
- :exc:`MissingApplicationID`
|
||||||
|
- :exc:`FFmpegProcessError`
|
||||||
- :exc:`GatewayNotFound`
|
- :exc:`GatewayNotFound`
|
||||||
- :exc:`HTTPException`
|
- :exc:`HTTPException`
|
||||||
- :exc:`Forbidden`
|
- :exc:`Forbidden`
|
||||||
|
|||||||
Reference in New Issue
Block a user