From fd9f1a0fb7fa2aa31ba8a2f8794980cdfd4fbddd Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 15 Jan 2025 01:16:09 +0100 Subject: [PATCH] Fix: ensure playback continues even after a stream error --- music_assistant/controllers/player_queues.py | 2 +- music_assistant/controllers/streams.py | 4 +++ music_assistant/helpers/audio.py | 37 +++++++++++--------- music_assistant/helpers/ffmpeg.py | 13 +++---- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 82cbc9a5..0bcd4c05 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -1204,7 +1204,7 @@ class PlayerQueuesController(CoreController): return # enqueue next track on the player if we're not in flow mode task_id = f"enqueue_next_item_{queue_id}" - self.mass.call_later(5, self._enqueue_next_item, queue_id, item_id, task_id=task_id) + self.mass.call_later(2, self._enqueue_next_item, queue_id, item_id, task_id=task_id) # Main queue manipulation methods diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index e3eba351..139b196b 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -403,6 +403,10 @@ class StreamsController(CoreController): queue_item.uri, queue.display_name, ) + # some players do not like it when we dont return anything after an error + # so we send some silence so they move on to the next track on their own (hopefully) + async for chunk in get_silence(10, output_format): + await resp.write(chunk) return resp async def serve_queue_flow_stream(self, request: web.Request) -> web.Response: diff --git a/music_assistant/helpers/audio.py b/music_assistant/helpers/audio.py index 83b80520..85aa5afe 100644 --- a/music_assistant/helpers/audio.py +++ b/music_assistant/helpers/audio.py @@ -22,6 +22,7 @@ from music_assistant_models.enums import ( VolumeNormalizationMode, ) from music_assistant_models.errors import ( + AudioError, InvalidDataError, MediaNotFoundError, MusicAssistantError, @@ -364,6 +365,10 @@ async def get_media_stream( buffer = buffer[pcm_format.pcm_sample_size :] # end of audio/track reached + if bytes_sent == 0: + # edge case: no audio data was sent + raise AudioError("No audio was received") + logger.log(VERBOSE_LOG_LEVEL, "End of stream reached.") if strip_silence_end and buffer: # strip silence from end of audio @@ -378,27 +383,25 @@ async def get_media_stream( yield buffer del buffer finished = True - + except Exception as err: + if isinstance(err, asyncio.CancelledError): + # we were cancelled, just raise + raise + logger.error("Error while streaming %s: %s", streamdetails.uri, err) + streamdetails.stream_error = True finally: logger.log(VERBOSE_LOG_LEVEL, "Closing ffmpeg...") await ffmpeg_proc.close() - if bytes_sent == 0: - # edge case: no audio data was sent - streamdetails.stream_error = True - seconds_streamed = 0 - logger.warning("Stream error on %s", streamdetails.uri) - else: - # try to determine how many seconds we've streamed - seconds_streamed = bytes_sent / pcm_format.pcm_sample_size if bytes_sent else 0 - logger.debug( - "stream %s (with code %s) for %s - seconds streamed: %s", - "finished" if finished else "aborted", - ffmpeg_proc.returncode, - streamdetails.uri, - seconds_streamed, - ) - + # try to determine how many seconds we've streamed + seconds_streamed = bytes_sent / pcm_format.pcm_sample_size if bytes_sent else 0 + logger.debug( + "stream %s (with code %s) for %s - seconds streamed: %s", + "finished" if finished else "aborted", + ffmpeg_proc.returncode, + streamdetails.uri, + seconds_streamed, + ) streamdetails.seconds_streamed = seconds_streamed # store accurate duration if finished and not streamdetails.seek_position and seconds_streamed: diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py index 99e3d897..ebcd70bb 100644 --- a/music_assistant/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -141,12 +141,13 @@ class FFMpeg(AsyncProcess): generator_exhausted = True except Exception as err: cancelled = isinstance(err, asyncio.CancelledError) - if not cancelled: - self.logger.error( - "Stream error: %s", - str(err) or err.__class__.__name__, - exc_info=err if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else None, - ) + if cancelled: + raise + self.logger.error( + "Stream error: %s", + str(err) or err.__class__.__name__, + exc_info=err if self.logger.isEnabledFor(VERBOSE_LOG_LEVEL) else None, + ) finally: if not cancelled: await self.write_eof() -- 2.34.1