Fix: ensure playback continues even after a stream error
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 15 Jan 2025 00:16:09 +0000 (01:16 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 15 Jan 2025 00:16:09 +0000 (01:16 +0100)
music_assistant/controllers/player_queues.py
music_assistant/controllers/streams.py
music_assistant/helpers/audio.py
music_assistant/helpers/ffmpeg.py

index 82cbc9a5c862961ea3cdf3fb77ea7ddbc977b4b7..0bcd4c0540f66e56075ad3a859b9bc72dc303908 100644 (file)
@@ -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
 
index e3eba3511636fcf827c7167ee2c8329628996944..139b196bc7db81deb6da492c6280b1daff7ab5b4 100644 (file)
@@ -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:
index 83b8052023198441c2e430f15f15685eb9b9797d..85aa5afe2aca6df07bd678964905978c720cb3ae 100644 (file)
@@ -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:
index 99e3d8970065ef093e45c665760ceda6ca150646..ebcd70bb7ea7aa492b2ff3d55e9eb5a310867a13 100644 (file)
@@ -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()