Fix: Ensure we pad some silence before any live/radio stream to prevent jitter
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 10 Mar 2025 16:04:06 +0000 (17:04 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 10 Mar 2025 16:04:06 +0000 (17:04 +0100)
music_assistant/controllers/streams.py
music_assistant/helpers/audio.py

index d3b37837ac64cf85a424aae06aae39813457604a..f4f5f31e1836dab00b953f804245c1ce403b82aa 100644 (file)
@@ -949,25 +949,30 @@ class StreamsController(CoreController):
             filter_params.append(f"volume={gain_correct}dB")
         streamdetails.volume_normalization_gain_correct = gain_correct
 
-        if streamdetails.media_type == MediaType.RADIO:
-            # pad some silence before the radio stream starts to create some headroom
-            # for radio stations that do not provide any look ahead buffer
-            # without this, some radio streams jitter a lot, especially with dynamic normalization
-            pad_seconds = (
-                5
-                if streamdetails.volume_normalization_mode == VolumeNormalizationMode.DYNAMIC
-                else 2
-            )
-            async for chunk in get_silence(pad_seconds, pcm_format):
-                yield chunk
-
+        pad_silence_seconds = 0
+        if streamdetails.media_type == MediaType.RADIO or not streamdetails.duration:
+            # pad some silence before the radio/live stream starts to create some headroom
+            # for radio stations (or other live streams) that do not provide any look ahead buffer
+            # without this, some radio streams jitter a lot, especially with dynamic normalization,
+            # if the stream does not provide a look ahead buffer
+            pad_silence_seconds = 2
+
+        first_chunk_received = False
         async for chunk in get_media_stream(
             self.mass,
             streamdetails=streamdetails,
             pcm_format=pcm_format,
             filter_params=filter_params,
         ):
+            # yield silence when the chunk has been received from source but not yet sent to player
+            # so we have a bit of backpressure to prevent jittering
+            if not first_chunk_received and pad_silence_seconds:
+                first_chunk_received = True
+                async for silence in get_silence(pad_silence_seconds, pcm_format):
+                    yield silence
+                    del silence
             yield chunk
+            del chunk
 
     def _log_request(self, request: web.Request) -> None:
         """Log request."""
index 185642d3f64391a26d4e0d05f5d4a378fafd5e67..1dde1d2b4f8ae8123f237750dea397e5698e53ac 100644 (file)
@@ -597,7 +597,7 @@ async def get_media_stream(
         substream = await get_hls_substream(mass, streamdetails.path)
         audio_source = substream.path
         if streamdetails.media_type == MediaType.RADIO:
-            # Especially the BBC streams struggle when they're played directly
+            # HLS streams (especially the BBC) struggle when they're played directly
             # with ffmpeg, where they just stop after some minutes,
             # so we tell ffmpeg to loop around in this case.
             extra_input_args += ["-stream_loop", "-1", "-re"]