How Music Assistant Handles Music Streaming Protocols: Architecture and Implementation
Music Assistant abstracts every music streaming protocol (AirPlay, DLNA, Chromecast, etc.) as a protocol player linked to a native player or Universal Player, selecting the active protocol based on priority rankings and feature inheritance defined in the core Player model.
Music Assistant is an open-source media server that unifies diverse hardware and streaming technologies under a single interface. Understanding how it handles music streaming protocols reveals a sophisticated three-layer architecture that decouples physical devices from transport mechanisms. This article examines the implementation details found in the music-assistant/server repository, focusing on protocol discovery, linking, and runtime selection.
The Three-Layer Player Architecture
At the heart of Music Assistant's protocol handling sits the Player model defined in music_assistant/models/player.py. This model maintains three distinct concepts that work together to abstract hardware capabilities:
Native players represent physical devices capable of playing media independently, such as a Sonos speaker or a Raspberry Pi running a Snapcast client. These entities manage the actual audio output and hardware controls.
Protocol players act as thin wrappers implementing only the capabilities a specific streaming protocol provides, such as ENQUEUE or GAPLESS_PLAYBACK. These wrappers handle protocol-specific communication without managing hardware directly.
Universal Players serve as synthetic entities that group a native endpoint with one or more protocol players. This abstraction exposes a single coherent entity to the UI and Home Assistant integration, even when multiple transport protocols are available for the same hardware.
Protocol Discovery and Automatic Linking
The Protocol-Linking controller (music_assistant/controllers/players/protocol_linking.py) manages the lifecycle of protocol associations. When a new protocol player appears on the network, the controller attempts automatic matching through _try_link_protocols_to_native.
This matching process uses device identifiers such as MAC addresses and serial numbers to correlate protocol endpoints with their underlying native hardware. If the controller detects multiple native players sharing identifiers—common in complex setups—it creates a Universal Player via ensure_universal_player_for_protocols in the Universal Player provider.
The controller also handles maintenance operations including merging duplicate universal players through _check_merge_universal_players and migrating cached protocol identifiers when parent relationships change using _migrate_protocol_ids_to_parent.
Output Protocol Selection and Priority
Each Player instance maintains an output_protocols list containing OutputProtocol objects that define available streaming endpoints. The native endpoint always occupies the first position with output_protocol_id="native", followed by linked protocol players from the private attribute __attr_linked_protocols.
The system sorts these protocols according to PROTOCOL_PRIORITY defined in music_assistant/constants.py, where lower integer values indicate higher preference. Disabled protocols are reconstructed from the cached configuration key linked_protocol_ids.
The active output protocol determines which endpoint handles playback. The property active_output_protocol and method set_active_output_protocol manage this selection:
- When set to
"native", the native player directly processesplay_mediacalls. - When set to a protocol player ID, that protocol wrapper receives the call, while the native player reacts through
on_protocol_playback.
Feature Inheritance from Active Protocols
Music Assistant dynamically adjusts player capabilities based on the currently active protocol. The constant ACTIVE_PROTOCOL_FEATURES in music_assistant/constants.py enumerates which features may be inherited from the active protocol, including ENQUEUE, GAPLESS_PLAYBACK, and PAUSE.
Properties such as supports_enqueue and supports_gapless delegate to _check_feature_with_active_protocol to determine availability. This mechanism ensures that the UI only exposes controls that the current transport protocol actually supports, preventing user errors when switching between AirPlay, DLNA, or other protocols.
Configuration Persistence and Migration
Protocol relationships survive server restarts through centralized configuration storage. Two keys maintain these associations:
- CONF_LINKED_PROTOCOL_IDS: Stores the list of protocol player IDs belonging to a parent native player.
- CONF_PROTOCOL_PARENT_ID: Stores the parent ID on each protocol player instance.
This bidirectional mapping allows the linking controller to restore complex device relationships without requiring rediscovery. When hardware changes or players are reorganized, the migration logic in the Protocol-Linking controller updates these references to maintain consistency.
Working with Protocols in Code
The following examples demonstrate interacting with the protocol system programmatically.
List available output protocols for a player:
# Retrieve the Player object (mass is the top-level server instance)
player = mass.players.get_player("livingroom_sonos")
# Display all output protocols with priority and availability
for proto in player.output_protocols:
print(f"{proto.output_protocol_id}: {proto.name} "
f"(domain={proto.protocol_domain}, priority={proto.priority}, "
f"available={proto.available})")
Switch the active protocol to AirPlay:
# Activate a specific protocol player by ID
await player.set_active_output_protocol("ap_12345")
# Subsequent play_media calls route through the AirPlay protocol player
Programmatically add a protocol link:
from music_assistant.helpers.uri import OutputProtocol
new_proto = OutputProtocol(
output_protocol_id="ap_67890",
name="Living-Room AirPlay",
protocol_domain="airplay",
priority=constants.PROTOCOL_PRIORITY["airplay"],
available=True,
)
# Update the player's linked protocols
player.set_linked_output_protocols(
player.linked_output_protocols + [new_proto]
)
Summary
- Music Assistant implements a three-layer architecture separating native players, protocol players, and Universal Players to abstract streaming complexities.
- The Protocol-Linking controller (
protocol_linking.py) automatically discovers and links protocols to native hardware using device identifiers. - OutputProtocol objects are prioritized using
PROTOCOL_PRIORITYconstants, with the active output protocol determining the actual transport mechanism. - Features like enqueue and gapless playback are dynamically inherited from the active protocol via
ACTIVE_PROTOCOL_FEATURESand_check_feature_with_active_protocol. - Configuration keys
CONF_LINKED_PROTOCOL_IDSandCONF_PROTOCOL_PARENT_IDpersist protocol relationships across server restarts.
Frequently Asked Questions
What is the difference between a native player and a protocol player in Music Assistant?
A native player represents the physical hardware device that can render audio independently, such as a speaker or media renderer. A protocol player is a software wrapper that implements only the capabilities of a specific streaming protocol like AirPlay or DLNA. The protocol player handles the transport layer communication while delegating actual audio output to the linked native player.
How does Music Assistant decide which protocol to use for playback?
The system selects the active protocol based on the active_output_protocol property, which defaults according to PROTOCOL_PRIORITY values defined in constants.py. Lower priority numbers indicate preferred protocols. Users can override this selection programmatically using set_active_output_protocol(), or the system automatically chooses based on feature requirements and availability.
Where does Music Assistant store protocol relationships between restarts?
Protocol relationships persist in the central configuration using two keys: CONF_LINKED_PROTOCOL_IDS stores the list of protocols for each parent player, while CONF_PROTOCOL_PARENT_ID stores the reverse reference on each protocol player. The Protocol-Linking controller uses these keys to reconstruct the device topology during startup without requiring network rediscovery.
Can a single device support multiple streaming protocols simultaneously?
Yes. When a native player supports multiple protocols (such as AirPlay and Chromecast), Music Assistant creates a Universal Player that aggregates these protocol players. The Universal Player exposes a single entity to the UI while maintaining separate protocol players internally. Users can switch between protocols by changing the active_output_protocol, effectively changing the transport method while keeping the same physical endpoint.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →