From: Maxim Raznatovski Date: Tue, 16 Dec 2025 19:08:20 +0000 (+0100) Subject: Enable immediate Sendspin sync delay changes for Cast players (#2823) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=899bb38d8f69f85358ebcfb8ee042c630999b543;p=music-assistant-server.git Enable immediate Sendspin sync delay changes for Cast players (#2823) --- diff --git a/music_assistant/controllers/players/player_controller.py b/music_assistant/controllers/players/player_controller.py index 61164915..c4ca6255 100644 --- a/music_assistant/controllers/players/player_controller.py +++ b/music_assistant/controllers/players/player_controller.py @@ -1774,8 +1774,26 @@ class PlayerController(CoreController): await player.on_config_updated() player.update_state() # if the PlayerQueue was playing, restart playback - # TODO: add property to ConfigEntry if it requires a restart of playback on change + # TODO: add restart_stream property to ConfigEntry and use that instead of immediate_apply + # to check if we need to restart playback if not player_disabled and resume_queue and resume_queue.state == PlaybackState.PLAYING: + config_entries = await player.get_config_entries() + has_value_changes = False + all_immediate_apply = True + for key in changed_keys: + if not key.startswith("values/"): + continue # skip root values like "enabled", "name" + has_value_changes = True + actual_key = key.removeprefix("values/") + entry = next((e for e in config_entries if e.key == actual_key), None) + if entry is None or not entry.immediate_apply: + all_immediate_apply = False + break + + if has_value_changes and all_immediate_apply: + # All changed config entries have immediate_apply=True, so no need to restart + # the playback + return # always stop first to ensure the player uses the new config await self.mass.player_queues.stop(resume_queue.queue_id) self.mass.call_later(1, self.mass.player_queues.resume, resume_queue.queue_id, False) diff --git a/music_assistant/providers/chromecast/player.py b/music_assistant/providers/chromecast/player.py index dcdef068..ace7a5f1 100644 --- a/music_assistant/providers/chromecast/player.py +++ b/music_assistant/providers/chromecast/player.py @@ -223,7 +223,9 @@ class ChromecastPlayer(Player): ) ) if self._last_sent_sync_delay != current_sync_delay: - self.mass.create_task(self._send_sendspin_server_url()) + # Update immediately to prevent duplicate sends from concurrent events + self._last_sent_sync_delay = current_sync_delay + self.mass.create_task(self._send_sendspin_sync_delay(current_sync_delay)) async def get_config_entries( self, @@ -260,11 +262,13 @@ class ChromecastPlayer(Player): label="Sendspin sync delay (ms)", description="Static delay in milliseconds to adjust audio synchronization. " "Positive values delay playback, negative values advance it. " - "Use this to compensate for device-specific audio latency.", + "Use this to compensate for device-specific audio latency. " + "Changes take effect immediately.", required=False, default_value=DEFAULT_SENDSPIN_SYNC_DELAY, range=(-1000, 1000), hidden=not sendspin_available or self.type == PlayerType.GROUP, + immediate_apply=True, ) # Codec config entry (only visible when sendspin provider is available) @@ -319,20 +323,27 @@ class ChromecastPlayer(Player): ) ) - # Resend if either value changed - if ( - self._last_sent_sync_delay != current_sync_delay - or self._last_sent_codec != current_codec - ): - self.logger.debug( - "Sendspin config changed, resending (sync_delay: %s -> %s, codec: %s -> %s)", - self._last_sent_sync_delay, - current_sync_delay, - self._last_sent_codec, - current_codec, - ) + sync_delay_changed = self._last_sent_sync_delay != current_sync_delay + codec_changed = self._last_sent_codec != current_codec + + if sync_delay_changed or codec_changed: + # Store old values for logging before updating state + old_codec = self._last_sent_codec + # Update immediately to prevent duplicate sends from concurrent events + self._last_sent_sync_delay = current_sync_delay + self._last_sent_codec = current_codec try: - await self._send_sendspin_server_url() + if codec_changed: + # Codec changed - need full reconnection + self.logger.debug( + "Sendspin codec changed (%s -> %s), sending full config", + old_codec, + current_codec, + ) + await self._send_sendspin_server_url() + else: + # Only sync delay changed, don't reconnect, just send updated delay + await self._send_sendspin_sync_delay(current_sync_delay) except Exception as err: self.logger.warning("Failed to send updated Sendspin config to Chromecast: %s", err) @@ -895,6 +906,22 @@ class ChromecastPlayer(Player): self._last_sent_sync_delay = sync_delay self._last_sent_codec = codec + async def _send_sendspin_sync_delay(self, sync_delay: int) -> None: + """Send only the sync delay update to the Cast receiver (no reconnection).""" + + def send_message() -> None: + self.cc.socket_client.send_app_message( + SENDSPIN_CAST_NAMESPACE, + {"syncDelay": sync_delay}, + ) + + self.logger.debug( + "Sending Sendspin sync delay update to Cast receiver: syncDelay=%dms", + sync_delay, + ) + await self.mass.loop.run_in_executor(None, send_message) + self._last_sent_sync_delay = sync_delay + async def _wait_for_sendspin_player(self, timeout: float = 15.0) -> Player | None: """Wait for the Sendspin player to connect and become available.""" start_time = time.time()