Fix airplay player cleanup
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 15 Dec 2025 00:30:30 +0000 (01:30 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Mon, 15 Dec 2025 00:30:30 +0000 (01:30 +0100)
music_assistant/providers/airplay/stream_session.py

index b5d5ac78f83c718d9a8795de39ab61c1e69216c2..2a81a7c9087062a1e470f253106c199306fd8b0e 100644 (file)
@@ -138,9 +138,8 @@ class AirPlayStreamSession:
             if airplay_player not in self.sync_clients:
                 return
             self.sync_clients.remove(airplay_player)
-        if not airplay_player.stream or airplay_player.stream.session != self:
-            return
-        await airplay_player.stream.stop()
+        if airplay_player.stream and airplay_player.stream.session == self:
+            await airplay_player.stream.stop()
         if ffmpeg := self._player_ffmpeg.pop(airplay_player.player_id, None):
             await ffmpeg.close()
         # If this was the last client, stop the session
@@ -257,14 +256,14 @@ class AirPlayStreamSession:
                     player = sync_clients[i]
 
                     if isinstance(result, asyncio.TimeoutError):
-                        self.prov.logger.error(
-                            "TIMEOUT writing chunk to player %s - REMOVING from sync group!",
+                        self.prov.logger.warning(
+                            "Removing player %s from session: stopped reading data (write timeout)",
                             player.player_id,
                         )
                         players_to_remove.append(player)
                     elif isinstance(result, Exception):
-                        self.prov.logger.error(
-                            ("Error writing chunk to player %s: %s - REMOVING from sync group!"),
+                        self.prov.logger.warning(
+                            "Removing player %s from session due to write error: %s",
                             player.player_id,
                             result,
                         )
@@ -272,15 +271,7 @@ class AirPlayStreamSession:
 
                 # Remove failed/timed-out players from sync group
                 for player in players_to_remove:
-                    if player in self.sync_clients:
-                        self.sync_clients.remove(player)
-                        self.prov.logger.warning(
-                            "Player %s removed from sync group due to write failure/timeout",
-                            player.player_id,
-                        )
-                        # Stop the player's stream
-                        if player.stream:
-                            self.mass.create_task(player.stream.stop())
+                    self.mass.create_task(self.remove_client(player))
 
                 # Update chunk counter (each chunk is exactly one second of audio)
                 chunk_seconds = len(chunk) / pcm_sample_size
@@ -327,7 +318,6 @@ class AirPlayStreamSession:
         For late joiners, compensates for chunks sent between join time and actual chunk delivery.
         Blocks (async) until the data has been written.
         """
-        write_start = time.time()
         player_id = airplay_player.player_id
 
         # don't write a chunk if we're paused
@@ -340,21 +330,10 @@ class AirPlayStreamSession:
         # to the named pipe associated with the player's stream
         if ffmpeg := self._player_ffmpeg.get(player_id):
             if ffmpeg.closed:
-                raise RuntimeError(f"FFMpeg process for player {player_id} is closed")
-            await ffmpeg.write(chunk)
-
-        stream_write_start = time.time()
-        stream_write_elapsed = time.time() - stream_write_start
-        total_elapsed = time.time() - write_start
-        # Log only truly abnormal writes (>5s indicates a real stall)
-        # Can take up to ~4s if player's latency buffer is being drained
-        if total_elapsed > 5.0:
-            self.prov.logger.error(
-                "!!! STALLED WRITE: Player %s writing chunk took %.3fs total (stream write: %.3fs)",
-                player_id,
-                total_elapsed,
-                stream_write_elapsed,
-            )
+                return
+            # Use a 10 second timeout - if the write takes longer, the player
+            # has stopped reading data and we're in a deadlock situation
+            await asyncio.wait_for(ffmpeg.write(chunk), timeout=10.0)
 
     async def _write_eof_to_player(self, airplay_player: AirPlayPlayer) -> None:
         """Write EOF to a specific player."""