Fix AirPlay late join sync
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 5 Nov 2025 20:37:34 +0000 (21:37 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 5 Nov 2025 20:37:34 +0000 (21:37 +0100)
music_assistant/providers/airplay/protocols/raop.py
music_assistant/providers/airplay/stream_session.py

index 91f91bfe295cd07a600e278670a73704a8427b03..c258e70b126055d8ed93e49dec8dc8e90ab9a67c 100644 (file)
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, cast
 from music_assistant_models.enums import PlaybackState
 from music_assistant_models.errors import PlayerCommandFailed
 
-from music_assistant.constants import CONF_SYNC_ADJUST, VERBOSE_LOG_LEVEL
+from music_assistant.constants import VERBOSE_LOG_LEVEL
 from music_assistant.helpers.process import AsyncProcess
 from music_assistant.providers.airplay.constants import (
     CONF_ALAC_ENCODE,
@@ -51,8 +51,6 @@ class RaopStream(AirPlayProtocol):
         for prop in ("et", "md", "am", "pk", "pw"):
             if prop_value := self.player.raop_discovery_info.decoded_properties.get(prop):
                 extra_args += [f"-{prop}", prop_value]
-        sync_adjust = self.player.config.get_value(CONF_SYNC_ADJUST, 0)
-        assert isinstance(sync_adjust, int)
         if device_password := self.mass.config.get_raw_player_config_value(
             player_id, CONF_PASSWORD, None
         ):
index 65c6cf1dc073f0f7a4b43af631d082e76506b29b..c7e72dba465f7f335d52f95574d51c873b4f4306 100644 (file)
@@ -10,10 +10,11 @@ from typing import TYPE_CHECKING
 
 from music_assistant_models.enums import PlaybackState
 
+from music_assistant.constants import CONF_SYNC_ADJUST
 from music_assistant.helpers.audio import get_player_filter_params
 from music_assistant.helpers.ffmpeg import FFMpeg
 from music_assistant.helpers.util import TaskManager
-from music_assistant.providers.airplay.helpers import unix_time_to_ntp
+from music_assistant.providers.airplay.helpers import ntp_to_unix_time, unix_time_to_ntp
 
 from .constants import CONF_ENABLE_LATE_JOIN, ENABLE_LATE_JOIN_DEFAULT, StreamingProtocol
 from .protocols.airplay2 import AirPlay2Stream
@@ -176,7 +177,7 @@ class AirPlayStreamSession:
             if airplay_player not in self.sync_clients:
                 self.sync_clients.append(airplay_player)
 
-        await self._start_client(airplay_player, start_ntp)
+            await self._start_client(airplay_player, start_ntp)
 
     async def replace_stream(self, audio_source: AsyncGenerator[bytes, None]) -> None:
         """Replace the audio source of the stream."""
@@ -380,6 +381,12 @@ class AirPlayStreamSession:
 
     async def _start_client(self, airplay_player: AirPlayPlayer, start_ntp: int) -> None:
         """Start stream for a single client."""
+        sync_adjust = airplay_player.config.get_value(CONF_SYNC_ADJUST, 0)
+        assert isinstance(sync_adjust, int)
+        if sync_adjust != 0:
+            # apply sync adjustment
+            start_ntp += sync_adjust * 1000  # sync_adjust is in seconds, NTP in milliseconds
+            start_ntp = unix_time_to_ntp(ntp_to_unix_time(start_ntp) + (sync_adjust / 1000))
         # start the stream
         assert airplay_player.stream  # for type checker
         await airplay_player.stream.start(start_ntp)
@@ -407,7 +414,7 @@ class AirPlayStreamSession:
             output_format=airplay_player.stream.pcm_format,
             filter_params=filter_params,
             audio_output=airplay_player.stream.audio_pipe.path,
-            extra_input_args=["-y", "-readrate", "1.0", "-readrate_initial_burst", "1.2"],
+            extra_input_args=["-y", "-re"],
         )
         await ffmpeg.start()
         self._player_ffmpeg[airplay_player.player_id] = ffmpeg