# How sync_group Synchronizes Playback Across Multiple Players in Music Assistant

> Discover how sync_group in Music Assistant synchronizes playback across multiple players. Learn how a virtual SyncGroupPlayer ensures perfect audio streaming and synchronization.

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

---

**The `sync_group` feature in Music Assistant creates a virtual `SyncGroupPlayer` that delegates all media commands to a single sync leader, which streams audio to member players through protocol-level grouping to maintain perfect synchronization.**

The Music Assistant server implements multi-room audio synchronization through the `SyncGroupPlayer` class located in [`music_assistant/providers/sync_group/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/providers/sync_group/player.py). This virtual player abstracts multiple physical devices into a single logical entity, ensuring that playback commands execute simultaneously across all grouped speakers by forwarding control to a designated leader while managing protocol-level membership behind the scenes.

## The Virtual Player Architecture

The `SyncGroupPlayer` acts as a proxy rather than a physical playback device. It maintains a reference to a **sync leader**—one real player in the group that handles the actual media stream. All other members are attached to this leader's protocol-level group (such as AirPlay, Cast, or native speaker groups), allowing them to receive the identical audio stream with synchronized timing.

When you issue a command to the group, the virtual player intercepts it and forwards the instruction to the leader. The leader then distributes the audio to its attached members, creating the illusion of a single cohesive device to the Music Assistant core.

## Group Formation and Leader Selection

Synchronization begins when any command requiring a leader is issued, such as `play()`, `play_media()`, or `set_members()`. The group calls **`_form_syncgroup()`** to establish the leadership structure.

First, the method cancels any pending idle grace timer via **`_cancel_idle_grace_timer`** to prevent premature dissolution. If no leader exists, it invokes **`_select_sync_leader()`** (lines 91–95) to choose the most appropriate player based on current state and capabilities. The selected leader is then placed first in the `_attr_group_members` list (lines 100–103).

Before members can join, the leader must be "unsynced"—meaning it cannot already be part of another group. The code waits for `sync_leader.state.synced_to` to become `None` (lines 106–115). If the leader remains stuck in a synced state, formation aborts to prevent conflicting group memberships.

## Protocol-Level Member Synchronization

Once the leader is ready, the group determines which members need synchronization. The method calculates `already_synced` status and builds a `members_to_sync` list (lines 126–134). 

If the leader is currently playing, it is stopped first (lines 136–148) to ensure a clean state for group attachment. Then the group issues **`_handle_set_members`** directly on the leader, adding the missing members to its protocol-level group (lines 149–156). This low-level call ensures the underlying protocol (AirPlay, Cast, etc.) recognizes the grouped relationship.

## Executing Playback Commands

All playback flows through the leader to maintain synchronization. Both **`play()`** and **`play_media()`** call `_form_syncgroup()` to guarantee a leader is present (lines 98–102 and 120–124).

The actual command is sent to the leader via the internal player controller—either `mass.players.cmd_resume` or `_handle_play_media`—inside the **`_await_leader_playback`** context manager (lines 103–106 and 128–138). This context manager waits for the leader to report `PlaybackState.PLAYING` (lines 66–78), preventing race conditions where members might join before the stream is actually active.

## Dynamic Membership and Leader Switching

When **`set_members()`** is invoked on a dynamic group, the method handles additions and removals, potentially including the current leader itself (lines 48–73).

If the current leader is removed, the code evaluates whether to perform a **dynamic leader switch** using `_dynamic_leader_switch` or to completely dissolve and reform the group via `_dissolve_and_reform` (lines 150–166). This decision depends on whether the remaining members can support seamless leadership transfer without interrupting playback.

## Dissolution and Idle Grace Handling

When playback stops naturally, the group schedules an **idle grace timer**. If this timer elapses and the group is not "pinned" (lacking fake power control), **`_dissolve_syncgroup()`** is called (lines 18–28).

This dissolution method removes all members from the leader's protocol group by calling `_handle_set_members` with `player_ids_to_remove`, then clears the leader reference (lines 30–39). This teardown ensures resources are freed and players return to independent operation.

## Protocol Compatibility and Session Management

The group maintains awareness of which protocol carries the active stream to ensure compatibility when adding members. Helper methods such as **`_active_session_player()`**, **`active_protocol_domain`**, and **`_member_supports_protocol_domain()`** (lines 144–170) verify that the leader supports the required protocol domain for all members.

This protocol tracking enables seamless hand-offs when members requiring specific protocols join or leave, ensuring the leader selection remains compatible with the group's technical requirements.

## Practical Implementation Examples

### Creating a SyncGroup and Playing Media

```python

# Assume `mass` is the Music Assistant core instance.

# Create a SyncGroup provider (handled by the server automatically) and a player:

sg_player: SyncGroupPlayer = mass.players.get_player("syncgroup_mygroup")

# Add members (dynamic groups only)

await sg_player.set_members(
    player_ids_to_add=["sonos_livingroom", "cast_kitchen"]
)

# Play a track – the group will form, pick a leader, and start playback

await sg_player.play_media(
    PlayerMedia(
        uri="https://example.com/song.mp3",
        title="Example Song",
        album="Demo Album",
        artist="Demo Artist",
        duration=210,
        source_id=None,
    )
)

```

### Powering On a Static Group

```python
sg_player = mass.players.get_player("syncgroup_static")

# Turn the group on – this will capture the preset members (configured in the UI)

await sg_player.power(True)   # forms the syncgroup automatically

await sg_player.play()        # resumes playback on the current leader

```

### Handling Leader Removal

```python
sg_player = mass.players.get_player("syncgroup_dynamic")

# Remove the current leader; the group will switch to a new one if possible

await sg_player.set_members(player_ids_to_remove=[sg_player.sync_leader.player_id])

# The group will automatically reform and continue playback if supported

```

## Summary

- The `SyncGroupPlayer` acts as a virtual proxy that delegates all media control to a single sync leader, creating the illusion of a unified playback device.
- Group formation via `_form_syncgroup()` ensures the leader is unsynced before adding members, preventing conflicting group memberships.
- Playback commands utilize the `_await_leader_playback` context manager to wait for `PlaybackState.PLAYING`, eliminating race conditions during startup.
- Dynamic groups support runtime member changes and can switch leaders using `_dynamic_leader_switch` or `_dissolve_and_reform` without stopping playback when possible.
- Idle grace timers automatically dissolve inactive groups through `_dissolve_syncgroup()`, returning members to independent operation and freeing resources.

## Frequently Asked Questions

### What happens if the sync leader is removed from the group?

If the current leader is removed via `set_members()`, the group either performs a dynamic leader switch using `_dynamic_leader_switch()` or dissolves and reforms with `_dissolve_and_reform()` to select a new leader from the remaining members. This ensures playback can continue if supported by the underlying protocol.

### How does Music Assistant prevent race conditions during playback startup?

The `_await_leader_playback` context manager waits for the leader to report `PlaybackState.PLAYING` before proceeding. This ensures the leader is actively streaming audio before the virtual player considers the command complete, preventing members from joining a stream that hasn't started yet.

### Can a sync group contain players using different protocols?

The group tracks protocol compatibility through `active_protocol_domain` and `_member_supports_protocol_domain()`. While the implementation allows mixing protocols, the leader must support the protocol required by all members. The system typically selects leaders based on the most compatible protocol for the current group composition.

### What triggers the automatic dissolution of a sync group?

When playback stops naturally, an idle grace timer begins. If the timer elapses and the group is not "pinned" with fake power control, `_dissolve_syncgroup()` is automatically called. This removes all members from the leader's protocol group and clears the internal leader reference, returning players to standalone operation.