Fixes for Sonos AirPlay mode
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 27 Apr 2025 00:36:54 +0000 (02:36 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Sun, 27 Apr 2025 00:36:54 +0000 (02:36 +0200)
music_assistant/providers/airplay/provider.py
music_assistant/providers/sonos/provider.py

index eea6346a1df46e6bc8e93d63586a3811c90928d5..1312ab437e9085490e01dbfab4e7fa3cbd12f033 100644 (file)
@@ -353,10 +353,15 @@ class AirPlayProvider(PlayerProvider):
                 output_format=AIRPLAY_PCM_FORMAT,
             )
 
-        # if an existing stream session is running, replace it with the new stream
+        # if an existing stream session is running, we could replace it with the new stream
         if airplay_player.raop_stream and airplay_player.raop_stream.running:
-            await airplay_player.raop_stream.session.replace_stream(audio_source)
-            return
+            # check if we need to replace the stream
+            if airplay_player.raop_stream.prevent_playback:
+                # player is in prevent playback mode, we need to stop the stream
+                await airplay_player.cmd_stop(False)
+            else:
+                await airplay_player.raop_stream.session.replace_stream(audio_source)
+                return
 
         # setup RaopStreamSession for player (and its sync childs if any)
         sync_clients = self._get_sync_clients(player_id)
index 2481cc6eb97223f8e3e6303c4a69885b267a73d4..d25c3b9d3ad5fa265dc1a527b24ed30061b031de 100644 (file)
@@ -18,7 +18,7 @@ from aiosonos.utils import get_discovery_info
 from music_assistant_models.config_entries import ConfigEntry, PlayerConfig
 from music_assistant_models.enums import ConfigEntryType, MediaType, PlayerState, ProviderFeature
 from music_assistant_models.errors import PlayerCommandFailed
-from music_assistant_models.player import DeviceInfo, PlayerMedia
+from music_assistant_models.player import DeviceInfo, Player, PlayerMedia
 from zeroconf import ServiceStateChange
 
 from music_assistant.constants import (
@@ -299,24 +299,10 @@ class SonosPlayerProvider(PlayerProvider):
             raise PlayerCommandFailed(msg)
         # for now always reset the active session
         sonos_player.client.player.group.active_session_id = None
-        if airplay := sonos_player.get_linked_airplay_player(True):
+        if airplay_player := sonos_player.get_linked_airplay_player(True):
             # airplay mode is enabled, redirect the command
             self.logger.debug("Redirecting PLAY_MEDIA command to linked airplay player.")
-            mass_player.active_source = airplay.active_source
-            # Sonos has an annoying bug (for years already, and they dont seem to care),
-            # where it looses its sync childs when airplay playback is (re)started.
-            # Try to handle it here with this workaround.
-            group_childs = [
-                x for x in sonos_player.client.player.group.player_ids if x != player_id
-            ]
-            if group_childs:
-                await self.mass.players.cmd_ungroup_many(group_childs)
-            await self.mass.players.play_media(airplay.player_id, media)
-            if group_childs:
-                # ensure master player is first in the list
-                group_childs = [sonos_player.player_id, *group_childs]
-                await asyncio.sleep(5)
-                await sonos_player.client.player.group.set_group_members(group_childs)
+            await self._play_media_airplay(sonos_player, airplay_player, media)
             return
 
         if media.media_type in (
@@ -324,7 +310,7 @@ class SonosPlayerProvider(PlayerProvider):
             MediaType.FLOW_STREAM,
         ) or media.queue_id.startswith("ugp_"):
             # flow stream or plugin source playback
-            # use the legacy playback method for this as it also
+            # always use the legacy (UPNP) playback method for this
             await self._play_media_legacy(sonos_player, media)
             return
 
@@ -676,3 +662,35 @@ class SonosPlayerProvider(PlayerProvider):
         # crossfading is handled by our streams controller
         if sonos_player.client.player.group.play_modes.crossfade:
             await sonos_player.client.player.group.set_play_modes(crossfade=False)
+
+    async def _play_media_airplay(
+        self,
+        sonos_player: SonosPlayer,
+        airplay_player: Player,
+        media: PlayerMedia,
+    ) -> None:
+        """Handle PLAY MEDIA using the legacy upnp api."""
+        player_id = sonos_player.player_id
+        mass_player = self.mass.players.get(player_id)
+        mass_player.active_source = airplay_player.active_source
+        if (
+            airplay_player.state == PlayerState.PLAYING
+            and airplay_player.active_source == media.queue_id
+        ):
+            # if the airplay player is already playing,
+            # the stream will be reused so no need to do the whole grouping thing below
+            await self.mass.players.play_media(airplay_player.player_id, media)
+            return
+
+        # Sonos has an annoying bug (for years already, and they dont seem to care),
+        # where it looses its sync childs when airplay playback is (re)started.
+        # Try to handle it here with this workaround.
+        group_childs = [x for x in sonos_player.client.player.group.player_ids if x != player_id]
+        if group_childs:
+            await self.mass.players.cmd_ungroup_many(group_childs)
+        await self.mass.players.play_media(airplay_player.player_id, media)
+        if group_childs:
+            # ensure master player is first in the list
+            group_childs = [sonos_player.player_id, *group_childs]
+            await asyncio.sleep(5)
+            await sonos_player.client.player.group.set_group_members(group_childs)