Fix WiiM devices not starting as part of a group (#2855)
authorMarvin Schenkel <marvinschenkel@gmail.com>
Fri, 19 Dec 2025 14:29:58 +0000 (15:29 +0100)
committerGitHub <noreply@github.com>
Fri, 19 Dec 2025 14:29:58 +0000 (15:29 +0100)
music_assistant/providers/squeezelite/player.py

index b68ada644af7667bcca125eb7ca661ee20538a54..9613fe83eb9cbfa12508a3da4fc98fc318667a2a 100644 (file)
@@ -282,6 +282,7 @@ class SqueezelitePlayer(Player):
                         media=media,
                         send_flush=True,
                         auto_play=False,
+                        is_group_playback=True,
                     )
                 )
 
@@ -408,6 +409,7 @@ class SqueezelitePlayer(Player):
         enqueue: bool = False,
         send_flush: bool = True,
         auto_play: bool = False,
+        is_group_playback: bool = False,
     ) -> None:
         """Handle playback of an url on slimproto player(s)."""
         metadata = {
@@ -435,6 +437,12 @@ class SqueezelitePlayer(Player):
             # to coordinate a start of multiple synced players
             autostart=auto_play,
         )
+        # TODO: When we implement server clock sync, we can remove the pause here
+        # and rely on unpause_at + HEADROOM in the buffer_ready handler. LMS
+        # also does NOT use an explicit pause. For now, we pause here to avoid
+        # WiiM devices starting playback too early, causing huge initial drift.
+        if is_group_playback:
+            await slimplayer.pause()
         # if queue is set to single track repeat,
         # immediately set this track as the next
         # this prevents race conditions with super short audio clips (on single repeat)
@@ -499,7 +507,7 @@ class SqueezelitePlayer(Player):
                 # but I did not have any good results with that.
                 # Instead just start playback on all players and let the sync logic work out
                 # the delays etc.
-                tg.create_task(sync_client.pause_for(200))
+                tg.create_task(pause_and_unpause(sync_client, 200))
 
     async def _handle_player_cli_event(self, event: SlimEvent) -> None:
         """Process CLI Event."""
@@ -607,7 +615,7 @@ class SqueezelitePlayer(Player):
             # player lagging behind more than MAX_SKIP_AHEAD_MS,
             # we need to correct the sync_master
             self.logger.debug("%s resync: pauseFor %sms", sync_master.name, delta)
-            self.mass.create_task(sync_master.pause_for(delta))
+            self.mass.create_task(pause_and_unpause(sync_master, delta))
         elif avg_diff > 0:
             # handle player lagging behind, fix with skip_ahead
             self.logger.debug("%s resync: skipAhead %sms", self.display_name, delta)
@@ -615,7 +623,7 @@ class SqueezelitePlayer(Player):
         else:
             # handle player is drifting too far ahead, use pause_for to adjust
             self.logger.debug("%s resync: pauseFor %sms", self.display_name, delta)
-            self.mass.create_task(self.client.pause_for(delta))
+            self.mass.create_task(pause_and_unpause(self.client, delta))
 
     async def _set_preset_items(self) -> None:
         """Set the presets for a player."""
@@ -678,6 +686,17 @@ class SqueezelitePlayer(Player):
                 yield slimplayer
 
 
+async def pause_and_unpause(slim_client: SlimClient, pause_duration_ms: int) -> None:
+    """Pause player and schedule unpause after specified duration.
+
+    This is used instead of pause_for because WiiM devices
+    don't properly auto-unpause after pause_for interval.
+    """
+    await slim_client.pause()
+    unpause_timestamp = slim_client.jiffies + pause_duration_ms
+    await slim_client.unpause_at(unpause_timestamp)
+
+
 async def _patched_send_strm(  # noqa: PLR0913
     self: SlimClient,
     command: bytes = b"q",