# How Music Assistant Player Sync Groups Enable Multi-Room Audio

> Learn how Music Assistant player sync groups enable seamless multi-room audio by aggregating speakers, designating a sync leader, and mirroring playback across all devices.

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

---

**Music Assistant implements multi-room audio by creating a virtual sync-group player that aggregates compatible speakers, designates a single "sync leader" to handle streaming, and mirrors playback state across all members while managing protocol compatibility and automatic group lifecycle.**

The open-source `music-assistant/server` repository provides a sophisticated multi-room audio implementation that abstracts collections of speakers into unified virtual players. Player sync groups in Music Assistant handle the complexity of synchronizing heterogeneous protocols like AirPlay and Sonos while presenting a simple, single-player interface to users and controllers.

## Architecture of Player Sync Groups

The implementation spans four distinct architectural layers that coordinate between provider registration, player logic, controller commands, and runtime state management.

### Provider Layer

The `SyncGroupProvider` class in [`music_assistant/providers/sync_group/provider.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/provider.py) instantiates virtual group players. It creates player IDs prefixed with `sg_`, registers them with the system, and persists configuration including `CONF_GROUP_MEMBERS` and the `CONF_DYNAMIC_GROUP_MEMBERS` flag.

### Player Implementation

The `SyncGroupPlayer` class in [`music_assistant/providers/sync_group/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/player.py) contains the core group logic. Key methods include `_form_syncgroup()` for assembly, `_dissolve_syncgroup()` for teardown, `_select_sync_leader()` for leadership election, and `_update_attributes()` for state synchronization.

### Controller Integration

The players controller at [`music_assistant/controllers/players/controller.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/players/controller.py) treats sync groups as standard players. It forwards UI commands such as `join_group()`, `leave_group()`, `play()`, and `stop()` directly to the group player instance.

### Runtime State Management

The group continuously mirrors the leader's playback state, current media, and elapsed time. An **idle-grace timer** automatically dissolves the group after playback stops unless **fake power control** pins the group in an active state.

## Forming a Sync Group

Formation occurs when a `play` command is received or `power(True)` is invoked on the group player.

The process follows these sequential steps:

1. **Leader Selection**: The `_select_sync_leader()` method identifies an available member that prefers the previous protocol domain to maintain seamless transitions. For static groups, it prioritizes permanent members over dynamic additions.

2. **Member Ordering**: The selected leader is moved to the front of the `_attr_group_members` list to ensure it receives all streaming commands.

3. **Group Assembly**: Compatible members are added to the leader via the internal `_handle_set_members()` call.

```python

# Excerpt from _form_syncgroup in player.py

if not self.sync_leader:
    self.sync_leader = self._select_sync_leader()
if self.sync_leader:
    self._attr_group_members = [
        self.sync_leader.player_id,
        *[x for x in self._attr_group_members if x != self.sync_leader.player_id],
    ]
    await self.mass.players._handle_set_members(
        self.sync_leader, player_ids_to_add=members_to_sync
    )

```

## Dissolving a Sync Group

Groups dissolve automatically when playback stops or manually via `power(False)`.

The `_dissolve_syncgroup()` method performs the following actions:

- Cancels any pending idle-grace timer
- Sends a `stop` command to the leader if currently playing
- Removes all child members from the leader's group via `set_members(remove=...)`
- Clears the `sync_leader` reference and updates attributes

```python

# From player.py lines 69-78

if sync_leader := self.sync_leader:
    sync_children = [
        x for x in sync_leader.state.group_members if x != sync_leader.player_id
    ]
    if sync_children:
        await self.mass.players._handle_set_members(
            sync_leader, player_ids_to_remove=sync_children
        )
self.sync_leader = None
self._update_attributes()

```

Dynamic groups support **leader switching** or full re-formation via `_dissolve_and_reform()` when members leave during active playback.

## Multi-Protocol Handling

Sync groups support heterogeneous speaker protocols through the `active_protocol_domain` property.

The system tracks protocol domains (e.g., AirPlay versus Sonos native) and only maintains non-native protocols while required by current members. When the last AirPlay-only member departs, the group automatically down-shifts to the leader's native protocol.

```python
def active_protocol_domain(self) -> str | None:
    session_player = self._active_session_player()
    if session_player is None or self.sync_leader is None:
        return None
    domain = session_player.provider.domain
    native_domain = self.sync_leader.provider.domain
    if domain != native_domain and not self._any_member_requires_protocol_domain(domain):
        return native_domain
    return domain

```

## Power Control and Group Persistence

By default, sync groups lack power switches and dissolve automatically after playback stops through the idle-grace mechanism.

**Fake Power Control** (`CONF_POWER_CONTROL == PLAYER_CONTROL_FAKE`) enables explicit power management:

- When enabled, the `POWER` feature appears in `supported_features()`
- Powered-on groups remain formed after stopping
- The idle-grace timer is suppressed

```python

# From supported_features() in player.py

raw_power_conf = self.mass.config.get_raw_player_config_value(
    self.player_id, CONF_POWER_CONTROL
)
if raw_power_conf == PLAYER_CONTROL_FAKE:
    base_features.add(PlayerFeature.POWER)

```

## Practical Implementation Examples

### Creating a Dynamic Group

```python
from music_assistant import MusicAssistant

async def make_group(mass: MusicAssistant):
    group = await mass.providers.get("sync_group").create_group_player(
        name="Living-Room",
        members=["sonos_kitchen", "sonos_bedroom"],
        dynamic=True,
    )
    print(f"Group created: {group.player_id}")

```

### Adding Members to an Existing Group

```python
await mass.players.set_members(
    player_id="sg_x7b9a2c1",
    player_ids_to_add=["sonos_office"],
)

```

### Playing Media Through a Sync Group

```python
media = await mass.metadata.get_media_item(
    item_id="spotify:track:6rqhFgbbKwnb9MLmUQDw6w"
)
await mass.players.play_media(
    player_id="sg_x7b9a2c1", 
    media=media
)

```

### Configuring Fake Power Control

```yaml
player_id: sg_x7b9a2c1
power_control: fake

```

## Summary

- **Virtual Player Abstraction**: Music Assistant creates virtual players with `sg_` prefixed IDs to represent speaker collections as single entities in [`music_assistant/providers/sync_group/provider.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/provider.py).
- **Leader-Based Architecture**: The sync group designates one member as the leader to handle actual audio streaming while mirroring state to followers through `_update_attributes()`.
- **Dynamic Protocol Management**: The system automatically switches between transport protocols based on member requirements via `active_protocol_domain` and `_any_member_requires_protocol_domain()`.
- **Automatic Lifecycle**: Groups form on demand when playback starts and dissolve when idle via `_dissolve_syncgroup()`, unless pinned via fake power control.
- **Repository Locations**: Core logic resides in [`music_assistant/providers/sync_group/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/player.py) with provider instantiation in [`provider.py`](https://github.com/music-assistant/server/blob/main/provider.py) and controller integration in [`music_assistant/controllers/players/controller.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/players/controller.py).

## Frequently Asked Questions

### How does Music Assistant select which speaker becomes the sync leader?

The `_select_sync_leader()` method in [`music_assistant/providers/sync_group/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/player.py) prioritizes available members that maintain the previous protocol domain for seamless transitions. For static groups, it prefers permanent members over temporary additions. The leader must be compatible with all intended group members through the `can_group_with` check.

### Can I mix AirPlay and Sonos speakers in the same sync group?

Yes. The `active_protocol_domain` property tracks which transport protocol is currently active. The system maintains non-native protocols only while required by specific members, automatically down-shifting to the leader's native protocol when AirPlay-only members leave the group.

### Why does my sync group disappear after I stop playback?

Default behavior automatically dissolves groups through the idle-grace timer when playback stops. To keep speakers grouped, enable **fake power control** by setting `CONF_POWER_CONTROL` to `PLAYER_CONTROL_FAKE` in the player configuration. This advertises a `POWER` feature in `supported_features()` and suppresses automatic dissolution.

### Where is the sync group configuration stored in the Music Assistant codebase?

Configuration persists in the provider layer at [`music_assistant/providers/sync_group/provider.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/provider.py), which stores `CONF_GROUP_MEMBERS` and `CONF_DYNAMIC_GROUP_MEMBERS` settings. Runtime state and leadership decisions occur in [`music_assistant/providers/sync_group/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/player.py), while high-level orchestration happens in [`music_assistant/controllers/players/controller.py`](https://github.com/music-assistant/server/blob/main/music_assistant/controllers/players/controller.py).