# How Music Assistant Handles Music Streaming Protocols: Architecture and Implementation

> Discover how Music Assistant seamlessly manages diverse music streaming protocols like AirPlay and Chromecast. Explore its elegant architecture and implementation for unified playback.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: architecture
- Published: 2026-06-15

---

**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`](https://github.com/music-assistant/server/blob/main/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`](https://github.com/music-assistant/server/blob/main/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`](https://github.com/music-assistant/server/blob/main/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 processes `play_media` calls.
- 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`](https://github.com/music-assistant/server/blob/main/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:

```python

# 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:

```python

# 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:

```python
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`](https://github.com/music-assistant/server/blob/main/protocol_linking.py)) automatically discovers and links protocols to native hardware using device identifiers.
- **OutputProtocol** objects are prioritized using `PROTOCOL_PRIORITY` constants, 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_FEATURES` and `_check_feature_with_active_protocol`.
- Configuration keys `CONF_LINKED_PROTOCOL_IDS` and `CONF_PROTOCOL_PARENT_ID` persist 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`](https://github.com/music-assistant/server/blob/main/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.