How the PlayerQueuesController Manages Playback Queues in Music Assistant

The PlayerQueuesController in the music-assistant/server repository handles every aspect of playback queue management, from lifecycle persistence and API manipulation to real-time playback processing and error recovery.

The PlayerQueuesController is the central orchestrator for all playback queues in Music Assistant. Located in music_assistant/controllers/player_queues/controller.py, this component creates and maintains PlayerQueue instances for every registered player, exposes a comprehensive API for queue manipulation, and drives the actual playback flow by translating high-level commands into low-level player instructions.

Queue Lifecycle and Persistence

The controller automatically manages the creation and destruction of queue instances. When a player registers with the system, on_player_register (lines 10-30) instantiates a new PlayerQueue and attempts to restore its previous state from the cache.

The restoration process reads from two cache categories: CACHE_CATEGORY_PLAYER_QUEUE_STATE and CACHE_CATEGORY_PLAYER_QUEUE_ITEMS. Using PlayerQueue.from_dict and QueueItem.from_cache, the controller rebuilds the queue exactly as it existed before the player disconnected. When a player disappears, on_player_remove (lines 32-57) executes a complete cleanup, cancelling pending timers, removing cache entries, and purging internal dictionaries to prevent memory leaks.

Queue Manipulation API

All public queue operations are exposed through @api_command decorators, providing a consistent interface for enqueueing, reordering, and configuring playback behavior.

Key manipulation methods include:

  • play_media – Resolves media into QueueItem instances and initiates playback (lines 24-48)
  • move_item – Repositions items within the queue using absolute or relative shifts (lines 57-90)
  • clear – Removes all items from the queue while preserving settings (lines 37-51)
  • set_shuffle – Toggles shuffle mode and triggers re-shuffling of existing items (lines 95-122)
  • set_repeat – Configures repeat mode (off, one, all) for the queue (lines 124-136)
  • set_playback_speed – Adjusts playback speed with time-base correction (lines 75-84)
  • save_as_playlist – Exports the current queue to a permanent playlist (lines 54-73)

Playback Processing Engine

The core playback flow follows a strict pipeline: play_media → _handle_play_media → play_index. The _handle_play_media method is decorated with @handle_play_action to serialize concurrent play requests and prevent race conditions.

When play_index executes (lines 84-94), it sets queue.index_in_buffer, generates a fresh session_id, and attempts to load the target item via _load_item. If the item fails to load (e.g., MediaNotFoundError), the controller automatically selects the next playable index using _get_next_index and retries up to five times. Upon successful loading, the controller calls self.mass.players.play_media to dispatch the actual command to the underlying player, then updates queue.current_index, queue.current_item, and emits a UI update via signal_update.

Navigation commands (next, previous, skip, seek) update the queue state immediately, then schedule a debounced play_index call to handle the actual transition (lines 73-81, 93-101). For pause, stop, and resume operations, the controller cancels pending timers, adjusts queue.resume_pos and queue.elapsed_time, and invokes internal handlers (_handle_cmd_pause, _handle_cmd_stop) to avoid circular redirects between the queue and player controllers (lines 23-33, 84-92).

Handling Dynamic Queues and Radio Sources

The controller supports two advanced playback modes that require continuous queue management.

Radio Mode: When dont_stop_the_music_enabled is active and the queue approaches depletion, the controller treats the current track list as a radio source. The _fill_radio_tracks method periodically queries the music provider for similar tracks and appends them to the queue, ensuring uninterrupted playback.

Flow Mode: For live streaming providers, on_player_elapsed_time_corrected (lines 97-125) reconciles the difference between media time and stream time. Using the get_current_playback_speed helper from music_assistant/controllers/player_queues/helpers.py, it keeps queue.elapsed_time accurate even when the player reports stream-relative positions, essential for variable-speed playback and precise seeking.

Concurrency and Safety Mechanisms

The controller implements several safeguards to prevent race conditions during state transitions.

The @handle_play_action decorator maintains a reference count (_play_action_refcount) that prevents overlapping play actions on the same queue. Before executing any state-changing command, the controller cancels pending timers to avoid stale play_index calls.

During track transitions, the controller tracks transitioning players in _transitioning_players and temporarily ignores player-reported updates to prevent state corruption. This ensures that queue.current_index remains synchronized with the actual audio being heard, even when network latency or provider delays occur.

Practical Implementation Examples

The following examples demonstrate common queue operations using the Music Assistant API:


# Enqueue a single track and start playback immediately

await mass.player_queues.play_media(
    queue_id="myplayer",
    media="spotify:track:6rqhFgbbKwnb9MLmUQDhG6",
    option=QueueOption.PLAY,
)

# Replace the current queue with an album, shuffle it, and start playing

await mass.player_queues.play_media(
    queue_id="myplayer",
    media="spotify:album:1ATL5GLyefJaxhQzSPVrLX",
    option=QueueOption.REPLACE,
    radio_mode=False,
    sort_by="random",  # Controller handles shuffling internally

)

# Skip forward 30 seconds in the current track

await mass.player_queues.skip(queue_id="myplayer", seconds=30)

# Change repeat mode to repeat a single track

await mass.player_queues.set_repeat(
    queue_id="myplayer", 
    repeat_mode=RepeatMode.ONE
)

# Pause playback, then resume with a fade-in after 2 seconds

await mass.player_queues.pause(queue_id="myplayer")
await asyncio.sleep(2)
await mass.player_queues.resume(queue_id="myplayer", fade_in=True)

# Move the third item up by one position

await mass.player_queues.move_item(
    queue_id="myplayer", 
    queue_item_id="item-3", 
    pos_shift=-1
)

# Transfer the entire queue from one player to another

await mass.player_queues.transfer_queue(
    source_queue_id="livingroom",
    target_queue_id="bedroom",
    auto_play=True,
)

Summary

  • Centralized Management: The PlayerQueuesController in music_assistant/controllers/player_queues/controller.py serves as the single source of truth for all playback queue state, handling everything from player registration to queue persistence via CACHE_CATEGORY_PLAYER_QUEUE_STATE.
  • Robust Playback Flow: Playback processing uses a serialized pipeline (play_media → _handle_play_media → play_index) with automatic retry logic for failed tracks and strict concurrency controls via @handle_play_action.
  • Dynamic Content Support: The controller automatically refills queues using radio sources when dont_stop_the_music_enabled is active, and accurately tracks elapsed time for variable-speed playback using on_player_elapsed_time_corrected.
  • Safety Mechanisms: Race conditions are prevented through reference counting (_play_action_refcount), timer cancellation, and transition tracking (_transitioning_players), ensuring the queue state remains synchronized with actual player behavior.

Frequently Asked Questions

How does the PlayerQueuesController handle corrupted or missing media files?

When play_index encounters a MediaNotFoundError or other loading failure, it automatically calls _get_next_index to select the next playable item and retries up to five times. This ensures that a single missing track does not halt playback, allowing the queue to continue with available content while updating queue.current_index accordingly.

What happens to my queue when a player disconnects and reconnects?

The controller persists queue state using CACHE_CATEGORY_PLAYER_QUEUE_STATE and CACHE_CATEGORY_PLAYER_QUEUE_ITEMS. When the player re-registers via on_player_register, it reconstructs the PlayerQueue using PlayerQueue.from_dict and QueueItem.from_cache, restoring your exact position, shuffle state, and track list from the music_assistant/models/player_queue.py data model.

Can I transfer an active queue between different players?

Yes, the transfer_queue method allows you to move the entire queue state—including the current index, playback position, and all items—from one player to another. When auto_play=True is specified, playback resumes immediately on the target player without interruption, making it seamless to move music from one room to another.

How does the controller prevent conflicts when multiple commands are issued simultaneously?

The @handle_play_action decorator serializes concurrent play actions using an internal reference count (_play_action_refcount). This ensures that play_media, next, previous, and other state-changing operations execute atomically, preventing race conditions that could corrupt the queue state or cause playback errors during rapid successive commands.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →