Prevent players buffering ahead too much
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 24 Oct 2024 10:32:15 +0000 (12:32 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 24 Oct 2024 10:32:15 +0000 (12:32 +0200)
music_assistant/server/controllers/player_queues.py
music_assistant/server/controllers/streams.py
music_assistant/server/helpers/ffmpeg.py
music_assistant/server/providers/player_group/ugp_stream.py

index addf8a0660dd9f66b738ad736d6fb8292baf4297..2a4d0d7a089c89f8708fc399dd62d03fbdb5d369 100644 (file)
@@ -1129,6 +1129,8 @@ class PlayerQueuesController(CoreController):
         if not queue:
             msg = f"PlayerQueue {queue_id} is not available"
             raise PlayerUnavailableError(msg)
+        # store the index of the item that is currently (being) loaded in the buffer
+        # which helps us a bit to determine how far the player has buffered ahead
         queue.index_in_buffer = self.index_by_id(queue_id, item_id)
         if queue.flow_mode:
             return  # nothing to do when flow mode is active
index 0b464bcab90db9bee07f293111d24f7c3023f42e..c13ab501feede8368f4a731f38b9e849079ebe5f 100644 (file)
@@ -383,6 +383,8 @@ class StreamsController(CoreController):
             input_format=pcm_format,
             output_format=output_format,
             filter_params=get_player_filter_params(self.mass, queue_player.player_id),
+            # we don't allow the player to buffer too much ahead so we use readrate limiting
+            extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"],
         ):
             try:
                 await resp.write(chunk)
@@ -472,6 +474,8 @@ class StreamsController(CoreController):
             output_format=output_format,
             filter_params=get_player_filter_params(self.mass, queue_player.player_id),
             chunk_size=icy_meta_interval if enable_icy else None,
+            # we don't allow the player to buffer too much ahead so we use readrate limiting
+            extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"],
         ):
             try:
                 await resp.write(chunk)
index 19a91a857c234dde36e264fabcaf4957242a6e2b..90e17ad8fef71f60b1344ce04e0bd95aea66e598 100644 (file)
@@ -17,6 +17,7 @@ from .process import AsyncProcess
 from .util import TimedAsyncGenerator, close_async_generator
 
 LOGGER = logging.getLogger("ffmpeg")
+MINIMAL_FFMPEG_VERSION = 6
 
 
 class FFMpeg(AsyncProcess):
@@ -175,7 +176,7 @@ async def get_ffmpeg_stream(
             yield chunk
 
 
-def get_ffmpeg_args(
+def get_ffmpeg_args(  # noqa: PLR0915
     input_format: AudioFormat,
     output_format: AudioFormat,
     filter_params: list[str],
@@ -199,6 +200,12 @@ def get_ffmpeg_args(
         )
 
     major_version = int("".join(char for char in version.split(".")[0] if not char.isalpha()))
+    if major_version < MINIMAL_FFMPEG_VERSION:
+        msg = (
+            f"FFmpeg version {version} is not supported. "
+            f"Minimal version required is {MINIMAL_FFMPEG_VERSION}."
+        )
+        raise AudioError(msg)
 
     # generic args
     generic_args = [
@@ -227,18 +234,14 @@ def get_ffmpeg_args(
             # If set then even streamed/non seekable streams will be reconnected on errors.
             "-reconnect_streamed",
             "1",
+            # Reconnect automatically in case of TCP/TLS errors during connect.
+            "-reconnect_on_network_error",
+            "1",
+            # A comma separated list of HTTP status codes to reconnect on.
+            # The list can include specific status codes (e.g. 503) or the strings 4xx / 5xx.
+            "-reconnect_on_http_error",
+            "5xx,4xx",
         ]
-        if major_version > 4:
-            # these options are only supported in ffmpeg > 5
-            input_args += [
-                # Reconnect automatically in case of TCP/TLS errors during connect.
-                "-reconnect_on_network_error",
-                "1",
-                # A comma separated list of HTTP status codes to reconnect on.
-                # The list can include specific status codes (e.g. 503) or the strings 4xx / 5xx.
-                "-reconnect_on_http_error",
-                "5xx,4xx",
-            ]
     if input_format.content_type.is_pcm():
         input_args += [
             "-ac",
index 2c7da2d715c6509305346fee5bb1b30b18f27919..281d80fb47aa5e20824a15125710e6f967eb53af 100644 (file)
@@ -83,9 +83,8 @@ class UGPStream:
             audio_input=self.audio_source,
             input_format=self.input_format,
             output_format=self.output_format,
-            # enable realtime to prevent too much buffering ahead
-            # TODO: enable initial burst once we have a newer ffmpeg version
-            extra_input_args=["-re"],
+            # we don't allow the player to buffer too much ahead so we use readrate limiting
+            extra_input_args=["-readrate", "1.1", "-readrate_initial_burst", "10"],
         ):
             await asyncio.gather(
                 *[sub(chunk) for sub in self.subscribers],