From: Marcel van der Veldt Date: Sun, 22 Feb 2026 01:56:07 +0000 (+0100) Subject: Properly cleanup stream buffers X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=76721a8debc67169374ef6ab3aa3d589d4829c72;p=music-assistant-server.git Properly cleanup stream buffers --- diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index d3643026..f33da0ae 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -714,6 +714,7 @@ class PlayerQueuesController(CoreController): queue.elapsed_time = 0 queue.elapsed_time_last_updated = time.time() queue.index_in_buffer = None + self.mass.create_task(self.mass.streams.cleanup_queue_audio_data(queue_id)) self.update_items(queue_id, []) @api_command("player_queues/save_as_playlist") @@ -758,6 +759,7 @@ class PlayerQueuesController(CoreController): await self.mass.players.cmd_stop(queue_id) finally: IN_QUEUE_COMMAND.reset(token) + self.mass.create_task(self.mass.streams.cleanup_queue_audio_data(queue_id)) @api_command("player_queues/play") async def play(self, queue_id: str) -> None: @@ -1413,11 +1415,17 @@ class PlayerQueuesController(CoreController): raise PlayerUnavailableError(msg) # store the index of the item that is currently (being) loaded in the buffer # which helps us a bit to determine how far the player has buffered ahead - queue.index_in_buffer = self.index_by_id(queue_id, item_id) + current_index = self.index_by_id(queue_id, item_id) + queue.index_in_buffer = current_index self.logger.debug("PlayerQueue %s loaded item %s in buffer", queue.display_name, item_id) self.signal_update(queue_id) # preload next streamdetails self._preload_next_item(queue_id, item_id) + # clean up stale audio buffers for old queue items to prevent memory leaks + if current_index is not None: + self.mass.create_task( + self.mass.streams.cleanup_stale_queue_buffers(queue_id, current_index) + ) # Main queue manipulation methods diff --git a/music_assistant/controllers/streams/streams_controller.py b/music_assistant/controllers/streams/streams_controller.py index 6b439e0a..5d3611b1 100644 --- a/music_assistant/controllers/streams/streams_controller.py +++ b/music_assistant/controllers/streams/streams_controller.py @@ -2077,3 +2077,77 @@ class StreamsController(CoreController): else: self.smart_fades_analyzer.logger.setLevel(log_level) self.smart_fades_mixer.logger.setLevel(log_level) + + async def cleanup_stale_queue_buffers(self, queue_id: str, current_index: int) -> None: + """ + Clean up audio buffers for queue items that are no longer needed. + + This clears buffers for items at index <= current_index - 2, keeping only: + - The previous track (current_index - 1) + - The current track (current_index) + - The next track (current_index + 1, handled by preloading) + + :param queue_id: The queue ID to clean up buffers for. + :param current_index: The current playing index in the queue. + """ + if current_index < 2: + return # Nothing to clean up yet + + queue_items = self.mass.player_queues._queue_items.get(queue_id, []) + cleanup_threshold = current_index - 2 + buffers_cleared = 0 + + for idx, item in enumerate(queue_items): + if idx > cleanup_threshold: + break # No need to check further + if item.streamdetails and item.streamdetails.buffer: + self.logger.log( + VERBOSE_LOG_LEVEL, + "Clearing stale audio buffer for queue item %s (index %d) in queue %s", + item.name, + idx, + queue_id, + ) + await item.streamdetails.buffer.clear() + item.streamdetails.buffer = None + buffers_cleared += 1 + + if buffers_cleared > 0: + self.logger.debug( + "Cleared %d stale audio buffer(s) for queue %s (items before index %d)", + buffers_cleared, + queue_id, + cleanup_threshold + 1, + ) + + async def cleanup_queue_audio_data(self, queue_id: str) -> None: + """ + Clean up all audio-related data for a queue when it is stopped or cleared. + + This clears: + - All audio buffers attached to queue item streamdetails + - Any pending crossfade data for the queue + + :param queue_id: The queue ID to clean up. + """ + # Clear crossfade data for this queue + if queue_id in self._crossfade_data: + self.logger.debug("Clearing crossfade data for queue %s", queue_id) + del self._crossfade_data[queue_id] + + # Clear all audio buffers for queue items + queue_items = self.mass.player_queues._queue_items.get(queue_id, []) + buffers_cleared = 0 + + for item in queue_items: + if item.streamdetails and item.streamdetails.buffer: + await item.streamdetails.buffer.clear() + item.streamdetails.buffer = None + buffers_cleared += 1 + + if buffers_cleared > 0: + self.logger.debug( + "Cleared %d audio buffer(s) for stopped/cleared queue %s", + buffers_cleared, + queue_id, + )