yuntongzhang / test

MIT License
0 stars 2 forks source link

VoiceClient.play() doesn't pass FFMPEG error to 'after' function. #6

Open yuntongzhang opened 2 weeks ago

yuntongzhang commented 2 weeks ago

Summary

VoiceClient.play() doesn't pass FFMPEG error to 'after' function.

Reproduction Steps

I know from https://github.com/Rapptz/discord.py/issues/5131 that the session invalidation error is not a Discord.py error, however, the VoiceClient's play() function does not seem to be capturing and passing the FFMPEG error into my provided after function. I would like to notify when this error happens, so capturing this error would be very helpful. It also appears that Discord.py wrongly states that FFMPEG exited successfully, even when it exits with code 1, which by definition, is not successful.

Here is my current code and log results: Code:

def sync_playback_error(error: Exception = None):
    async def playback_error(error: Exception = None):
        has_error: bool = error != None
        if has_error:
            print('Sending error to chat...')
            await ctx.message.channel.send('A playback error has caused the audio stream to stop.')
        else:
            print('No error in playback!')
    print('Starting async callback...')
    asyncio.run_coroutine_threadsafe(playback_error(error), self.bot.loop)

# Attempt to play ffmpeg source
vc.play(ffmpeg_src, after=sync_playback_error)

Logs:

[tls @ 0x5632a384eb80] Error in the pull function.
[matroska,webm @ 0x5632a384a600] Read error
[tls @ 0x5632a384eb80] The specified session has been invalidated for some reason.
    Last message repeated 1 times
Starting async callback...
DEBUG:asyncio:Using selector: EpollSelector
No error in playback!
2024-06-15 16:58:28 INFO     discord.player ffmpeg process 2801802 successfully terminated with return code of 1.

Minimal Reproducible Code

No response

Expected Results

I expected the FFMPEG error to be passed to the after function

Actual Results

The error was not captured and passed to the after function

Intents

discord.Intents.default()

System Information

Python v3.10.12-final discord.py v2.3.2-final aiohttp v3.9.5 system info: Linux 5.15.0-112-generic https://github.com/Rapptz/discord.py/pull/122 SMP Thu May 23 07:48:21 UTC 2024

Checklist

Additional Context

No response

yuntongzhang commented 2 weeks ago

@acr-bot

github-actions[bot] commented 2 weeks ago

Here is a potential patch for reference:

diff --git a/discord/player.py b/discord/player.py
index 5b2c99d..9b90342 100644
--- a/discord/player.py
+++ b/discord/player.py
@@ -730,39 +730,44 @@ class AudioPlayer(threading.Thread):
         self._speak(SpeakingState.voice)

         while not self._end.is_set():
-            # are we paused?
-            if not self._resumed.is_set():
-                self.send_silence()
-                # wait until we aren't
-                self._resumed.wait()
-                continue
-
-            data = self.source.read()
-
-            if not data:
+            try:
+                # are we paused?
+                if not self._resumed.is_set():
+                    self.send_silence()
+                    # wait until we aren't
+                    self._resumed.wait()
+                    continue
+
+                data = self.source.read()
+
+                if not data:
+                    self.stop()
+                    break
+
+                # are we disconnected from voice?
+                if not client.is_connected():
+                    _log.debug('Not connected, waiting for %ss...', client.timeout)
+                    # wait until we are connected, but not forever
+                    connected = client.wait_until_connected(client.timeout)
+                    if self._end.is_set() or not connected:
+                        _log.debug('Aborting playback')
+                        return
+                    _log.debug('Reconnected, resuming playback')
+                    self._speak(SpeakingState.voice)
+                    # reset our internal data
+                    self.loops = 0
+                    self._start = time.perf_counter()
+
+                play_audio(data, encode=not self.source.is_opus())
+                self.loops += 1
+                next_time = self._start + self.DELAY * self.loops
+                delay = max(0, self.DELAY + (next_time - time.perf_counter()))
+                time.sleep(delay)
+            except Exception as e:
+                self._current_error = e
                 self.stop()
                 break

-            # are we disconnected from voice?
-            if not client.is_connected():
-                _log.debug('Not connected, waiting for %ss...', client.timeout)
-                # wait until we are connected, but not forever
-                connected = client.wait_until_connected(client.timeout)
-                if self._end.is_set() or not connected:
-                    _log.debug('Aborting playback')
-                    return
-                _log.debug('Reconnected, resuming playback')
-                self._speak(SpeakingState.voice)
-                # reset our internal data
-                self.loops = 0
-                self._start = time.perf_counter()
-
-            play_audio(data, encode=not self.source.is_opus())
-            self.loops += 1
-            next_time = self._start + self.DELAY * self.loops
-            delay = max(0, self.DELAY + (next_time - time.perf_counter()))
-            time.sleep(delay)
-
         if client.is_connected():
             self.send_silence()

This run costs 0.19 USD.

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

Anthropic API key is missing. Please set it up in the repository.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

I could not generate a patch for this issue.

yuntongzhang commented 2 weeks ago

@acr-bot sonnet

github-actions[bot] commented 2 weeks ago

Here is a potential patch for reference:

diff --git a/discord/player.py b/discord/player.py
index 5b2c99d..e0aec97 100644
--- a/discord/player.py
+++ b/discord/player.py
@@ -205,9 +205,13 @@ class FFmpegAudio(AudioSource):
             process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, **subprocess_kwargs)
         except FileNotFoundError:
             executable = args.partition(' ')[0] if isinstance(args, str) else args[0]
-            raise ClientException(executable + ' was not found.') from None
+            error = ClientException(executable + ' was not found.')
+            self._current_error = error
+            raise error from None
         except subprocess.SubprocessError as exc:
-            raise ClientException(f'Popen failed: {exc.__class__.__name__}: {exc}') from exc
+            error = ClientException(f'Popen failed: {exc.__class__.__name__}: {exc}')
+            self._current_error = error
+            raise error from exc
         else:
             return process

@@ -227,7 +231,10 @@ class FFmpegAudio(AudioSource):
         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)
+
+        if proc.returncode != 0:
+            _log.warning('ffmpeg process %s terminated with non-zero return code of %s.', proc.pid, proc.returncode)
+            self._current_error = Exception(f"FFmpeg exited with non-zero status {proc.returncode}")
         else:
             _log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode)

@@ -788,6 +795,9 @@ class AudioPlayer(threading.Thread):
         elif error:
             _log.exception('Exception in voice thread %s', self.name, exc_info=error)

+        # Reset the error after calling the after function
+        self._current_error = None
+
     def stop(self) -> None:
         self._end.set()
         self._resumed.set()

This run costs 0.14 USD.

yuntongzhang commented 2 weeks ago

@acr-bot open-pr

yuntongzhang commented 1 week ago

@acr-bot sonnet

code-rover-bot[bot] commented 1 week ago

Anthropic API key is missing. Please set it up in the repository.

github-actions[bot] commented 1 week ago

I could not generate a patch for this issue.

yuntongzhang commented 1 week ago

@acr-bot sonnet

code-rover-bot[bot] commented 1 week ago

Anthropic API key is missing. Please set it up in the repository.

yuntongzhang commented 1 week ago

@acr-bot sonnet

code-rover-bot[bot] commented 1 week ago

Anthropic API key is missing. Please set it up in the repository.

yuntongzhang commented 1 week ago

@acr-bot sonnet

code-rover-bot[bot] commented 1 week ago

Anthropic API key is missing. Please set it up in the repository.

github-actions[bot] commented 1 week ago

Here is a potential patch for reference:

diff --git a/discord/player.py b/discord/player.py
index 5b2c99d..dc24c30 100644
--- a/discord/player.py
+++ b/discord/player.py
@@ -723,48 +723,60 @@ class AudioPlayer(threading.Thread):
     def _do_run(self) -> None:
         self.loops = 0
         self._start = time.perf_counter()
+        self._current_error = None

         # getattr lookup speed ups
         client = self.client
         play_audio = client.send_audio_packet
         self._speak(SpeakingState.voice)

-        while not self._end.is_set():
-            # are we paused?
-            if not self._resumed.is_set():
+        try:
+            while not self._end.is_set():
+                # are we paused?
+                if not self._resumed.is_set():
+                    self.send_silence()
+                    # wait until we aren't
+                    self._resumed.wait()
+                    continue
+
+                try:
+                    data = self.source.read()
+                except Exception as e:
+                    self._current_error = e
+                    self.stop()
+                    break
+
+                if not data:
+                    self.stop()
+                    break
+
+                # are we disconnected from voice?
+                if not client.is_connected():
+                    _log.debug('Not connected, waiting for %ss...', client.timeout)
+                    # wait until we are connected, but not forever
+                    connected = client.wait_until_connected(client.timeout)
+                    if self._end.is_set() or not connected:
+                        _log.debug('Aborting playback')
+                        self._current_error = Exception("Voice client disconnected")
+                        return
+                    _log.debug('Reconnected, resuming playback')
+                    self._speak(SpeakingState.voice)
+                    # reset our internal data
+                    self.loops = 0
+                    self._start = time.perf_counter()
+
+                play_audio(data, encode=not self.source.is_opus())
+                self.loops += 1
+                next_time = self._start + self.DELAY * self.loops
+                delay = max(0, self.DELAY + (next_time - time.perf_counter()))
+                time.sleep(delay)
+
+        except Exception as e:
+            self._current_error = e
+        finally:
+            if client.is_connected():
                 self.send_silence()
-                # wait until we aren't
-                self._resumed.wait()
-                continue
-
-            data = self.source.read()
-
-            if not data:
-                self.stop()
-                break
-
-            # are we disconnected from voice?
-            if not client.is_connected():
-                _log.debug('Not connected, waiting for %ss...', client.timeout)
-                # wait until we are connected, but not forever
-                connected = client.wait_until_connected(client.timeout)
-                if self._end.is_set() or not connected:
-                    _log.debug('Aborting playback')
-                    return
-                _log.debug('Reconnected, resuming playback')
-                self._speak(SpeakingState.voice)
-                # reset our internal data
-                self.loops = 0
-                self._start = time.perf_counter()
-
-            play_audio(data, encode=not self.source.is_opus())
-            self.loops += 1
-            next_time = self._start + self.DELAY * self.loops
-            delay = max(0, self.DELAY + (next_time - time.perf_counter()))
-            time.sleep(delay)
-
-        if client.is_connected():
-            self.send_silence()
+            self._call_after()

     def run(self) -> None:
         try:
@@ -785,8 +797,11 @@ class AudioPlayer(threading.Thread):
             except Exception as exc:
                 exc.__context__ = error
                 _log.exception('Calling the after function failed.', exc_info=exc)
-        elif error:
+        
+        if error:
             _log.exception('Exception in voice thread %s', self.name, exc_info=error)
+        else:
+            _log.info('Voice thread %s completed successfully', self.name)

     def stop(self) -> None:
         self._end.set()

This run costs 0.21 USD.

github-actions[bot] commented 1 week ago

Here is a potential patch for reference:

diff --git a/discord/player.py b/discord/player.py
index 5b2c99d..ec1eff9 100644
--- a/discord/player.py
+++ b/discord/player.py
@@ -729,42 +729,61 @@ class AudioPlayer(threading.Thread):
         play_audio = client.send_audio_packet
         self._speak(SpeakingState.voice)

-        while not self._end.is_set():
-            # are we paused?
-            if not self._resumed.is_set():
-                self.send_silence()
-                # wait until we aren't
-                self._resumed.wait()
-                continue
+        try:
+            while not self._end.is_set():
+                # are we paused?
+                if not self._resumed.is_set():
+                    self.send_silence()
+                    # wait until we aren't
+                    self._resumed.wait()
+                    continue
+
+                try:
+                    data = self.source.read()
+                except Exception as e:
+                    _log.exception("Error reading from audio source")
+                    self.stop()
+                    if self.after is not None:
+                        self.after(e)
+                    return

-            data = self.source.read()
+                if not data:
+                    self.stop()
+                    break
+
+                # are we disconnected from voice?
+                if not client.is_connected():
+                    _log.debug('Not connected, waiting for %ss...', client.timeout)
+                    # wait until we are connected, but not forever
+                    connected = client.wait_until_connected(client.timeout)
+                    if self._end.is_set() or not connected:
+                        _log.debug('Aborting playback')
+                        return
+                    _log.debug('Reconnected, resuming playback')
+                    self._speak(SpeakingState.voice)
+                    # reset our internal data
+                    self.loops = 0
+                    self._start = time.perf_counter()
+
+                play_audio(data, encode=not self.source.is_opus())
+                self.loops += 1
+                next_time = self._start + self.DELAY * self.loops
+                delay = max(0, self.DELAY + (next_time - time.perf_counter()))
+                time.sleep(delay)
+
+        except Exception as e:
+            _log.exception("Unexpected error in audio playback")
+            self.stop()
+            if self.after is not None:
+                self.after(e)
+            return

-            if not data:
-                self.stop()
-                break
-
-            # are we disconnected from voice?
-            if not client.is_connected():
-                _log.debug('Not connected, waiting for %ss...', client.timeout)
-                # wait until we are connected, but not forever
-                connected = client.wait_until_connected(client.timeout)
-                if self._end.is_set() or not connected:
-                    _log.debug('Aborting playback')
-                    return
-                _log.debug('Reconnected, resuming playback')
-                self._speak(SpeakingState.voice)
-                # reset our internal data
-                self.loops = 0
-                self._start = time.perf_counter()
-
-            play_audio(data, encode=not self.source.is_opus())
-            self.loops += 1
-            next_time = self._start + self.DELAY * self.loops
-            delay = max(0, self.DELAY + (next_time - time.perf_counter()))
-            time.sleep(delay)
-
-        if client.is_connected():
-            self.send_silence()
+        finally:
+            if client.is_connected():
+                self.send_silence()
+
+        if self.after is not None:
+            self.after(None)

     def run(self) -> None:
         try:

This run costs 0.16 USD.

github-actions[bot] commented 1 week ago

Here is a potential patch for reference:

diff --git a/discord/player.py b/discord/player.py
index 5b2c99d..706ab8a 100644
--- a/discord/player.py
+++ b/discord/player.py
@@ -207,8 +207,10 @@ class FFmpegAudio(AudioSource):
             executable = args.partition(' ')[0] if isinstance(args, str) else args[0]
             raise ClientException(executable + ' was not found.') from None
         except subprocess.SubprocessError as exc:
+            _log.error(f'Failed to spawn FFmpeg process: {exc.__class__.__name__}: {exc}')
             raise ClientException(f'Popen failed: {exc.__class__.__name__}: {exc}') from exc
         else:
+            _log.info(f'FFmpeg process spawned with PID: {process.pid}')
             return process

     def _kill_process(self) -> None:
@@ -227,9 +229,14 @@ class FFmpegAudio(AudioSource):
         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)
+
+        if proc.returncode is not None:
+            if proc.returncode == 0:
+                _log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode)
+            else:
+                _log.warning('ffmpeg process %s terminated with non-zero return code %s.', proc.pid, proc.returncode)
         else:
-            _log.info('ffmpeg process %s successfully terminated with return code of %s.', proc.pid, proc.returncode)
+            _log.error('ffmpeg process %s failed to terminate.', proc.pid)

     def _pipe_writer(self, source: io.BufferedIOBase) -> None:
         while self._process:
@@ -729,42 +736,58 @@ class AudioPlayer(threading.Thread):
         play_audio = client.send_audio_packet
         self._speak(SpeakingState.voice)

-        while not self._end.is_set():
-            # are we paused?
-            if not self._resumed.is_set():
-                self.send_silence()
-                # wait until we aren't
-                self._resumed.wait()
-                continue
-
-            data = self.source.read()
-
-            if not data:
-                self.stop()
-                break
-
-            # are we disconnected from voice?
-            if not client.is_connected():
-                _log.debug('Not connected, waiting for %ss...', client.timeout)
-                # wait until we are connected, but not forever
-                connected = client.wait_until_connected(client.timeout)
-                if self._end.is_set() or not connected:
-                    _log.debug('Aborting playback')
+        try:
+            while not self._end.is_set():
+                # are we paused?
+                if not self._resumed.is_set():
+                    self.send_silence()
+                    # wait until we aren't
+                    self._resumed.wait()
+                    continue
+
+                try:
+                    data = self.source.read()
+                except Exception as e:
+                    _log.exception("Error reading from audio source")
+                    self.stop()
+                    if self.after is not None:
+                        self.after(e)
                     return
-                _log.debug('Reconnected, resuming playback')
-                self._speak(SpeakingState.voice)
-                # reset our internal data
-                self.loops = 0
-                self._start = time.perf_counter()
-
-            play_audio(data, encode=not self.source.is_opus())
-            self.loops += 1
-            next_time = self._start + self.DELAY * self.loops
-            delay = max(0, self.DELAY + (next_time - time.perf_counter()))
-            time.sleep(delay)
-
-        if client.is_connected():
-            self.send_silence()
+
+                if not data:
+                    self.stop()
+                    break
+
+                # are we disconnected from voice?
+                if not client.is_connected():
+                    _log.debug('Not connected, waiting for %ss...', client.timeout)
+                    # wait until we are connected, but not forever
+                    connected = client.wait_until_connected(client.timeout)
+                    if self._end.is_set() or not connected:
+                        _log.debug('Aborting playback')
+                        return
+                    _log.debug('Reconnected, resuming playback')
+                    self._speak(SpeakingState.voice)
+                    # reset our internal data
+                    self.loops = 0
+                    self._start = time.perf_counter()
+
+                play_audio(data, encode=not self.source.is_opus())
+                self.loops += 1
+                next_time = self._start + self.DELAY * self.loops
+                delay = max(0, self.DELAY + (next_time - time.perf_counter()))
+                time.sleep(delay)
+
+        except Exception as e:
+            _log.exception("Unexpected error in audio playback")
+            self.stop()
+            if self.after is not None:
+                self.after(e)
+        finally:
+            if client.is_connected():
+                self.send_silence()
+            if self.after is not None and not isinstance(getattr(self, '_error', None), Exception):
+                self.after(None)

     def run(self) -> None:
         try:

This run costs 0.17 USD.

yuntongzhang commented 1 week ago

@acr-bot sonnet

code-rover-bot[bot] commented 1 week ago

Anthropic API key is missing. Please set it up in the repository.

yuntongzhang commented 1 week ago

@acr-bot sonnet