How Music Assistant Player Sync Groups Enable Multi-Room Audio
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 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 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 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:
-
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. -
Member Ordering: The selected leader is moved to the front of the
_attr_group_memberslist to ensure it receives all streaming commands. -
Group Assembly: Compatible members are added to the leader via the internal
_handle_set_members()call.
# 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
stopcommand to the leader if currently playing - Removes all child members from the leader's group via
set_members(remove=...) - Clears the
sync_leaderreference and updates attributes
# 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.
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
POWERfeature appears insupported_features() - Powered-on groups remain formed after stopping
- The idle-grace timer is suppressed
# 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
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
await mass.players.set_members(
player_id="sg_x7b9a2c1",
player_ids_to_add=["sonos_office"],
)
Playing Media Through a Sync Group
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
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 inmusic_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_domainand_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.pywith provider instantiation inprovider.pyand controller integration inmusic_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 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, 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, while high-level orchestration happens in music_assistant/controllers/players/controller.py.
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 →