From: Marcel van der Veldt Date: Mon, 15 Dec 2025 00:30:30 +0000 (+0100) Subject: Fix airplay player cleanup X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=822fabb1f1965b68e38efa7cc736f7d183b621ff;p=music-assistant-server.git Fix airplay player cleanup --- diff --git a/music_assistant/providers/airplay/stream_session.py b/music_assistant/providers/airplay/stream_session.py index b5d5ac78..2a81a7c9 100644 --- a/music_assistant/providers/airplay/stream_session.py +++ b/music_assistant/providers/airplay/stream_session.py @@ -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."""