music-assistant-server

15.7 KBMD
README.md
15.7 KB323 lines • markdown
1# Player Controller Architecture
2
3This document provides an overview of the Music Assistant Player Controller architecture, including the Player/PlayerState model, multi-protocol player system, and universal player concept.
4
5## Table of Contents
6
7- [Overview](#overview)
8- [Player vs PlayerState](#player-vs-playerstate)
9- [Core Components](#core-components)
10- [Player Types](#player-types)
11- [Multi-Protocol Player System](#multi-protocol-player-system)
12- [Universal Player](#universal-player)
13- [Protocol Linking](#protocol-linking)
14- [Development Guide](#development-guide)
15
16## Overview
17
18The Player Controller is a core controller that manages all connected audio players from various providers. It provides:
19- Unified control interface for all players (play, pause, volume, etc.)
20- Multi-protocol player linking (combining AirPlay, Chromecast, DLNA for the same device)
21- Universal Player wrapping for devices without native vendor support
22- Sync group management for synchronized playback
23- Player state management and event broadcasting
24- User access control and permissions
25
26## Player vs PlayerState
27
28The Player Controller distinguishes between two key concepts:
29
30### Player (Internal Model)
31
32The `Player` class is the actual object provided by a Player Provider. It:
33- Incorporates the actual state of the player (volume, playback state, etc.)
34- Contains methods for controlling the player (play, pause, volume, etc.)
35- Is used internally by providers and the controller
36- May contain provider-specific implementation details
37
38### PlayerState (API Model)
39
40The `PlayerState` is a dataclass representing the final state of the player. It:
41- Includes any user customizations (custom name, hidden status, etc.)
42- Applies transformations (e.g., fake power/volume controls)
43- Is the object exposed to the outside world via the API
44- Is a snapshot created when `player.update_state()` is called
45- Contains only serializable data suitable for API consumers
46
47```
48┌─────────────────────────────────────────────────────────────────┐
49│                     Player (Internal)                           │
50│  - Provider-specific implementation                             │
51│  - Control methods (play, pause, volume_set, etc.)              │
52│  - Raw state (_attr_volume_level, _attr_playback_state, etc.)   │
53│  - Device info and identifiers                                  │
54└─────────────────────────────────┬───────────────────────────────┘
55                                  │
56                                  │ update_state()
57                                  ▼
58┌─────────────────────────────────────────────────────────────────┐
59│                   PlayerState (API)                             │
60│  - Final display name (with user customizations)                │
61│  - Transformed state (fake controls applied)                    │
62│  - Player controls configuration                                │
63│  - Serializable for API/WebSocket                               │
64└─────────────────────────────────────────────────────────────────┘
65```
66
67## Core Components
68
69### 1. PlayerController ([controller.py](controller.py))
70
71The main orchestrator that manages:
72- Player registration and lifecycle
73- Player commands (play, pause, stop, volume, etc.)
74- Protocol linking and evaluation
75- Universal player creation
76- Sync group coordination
77
78**Key responsibilities:**
79- Routes commands to appropriate players or protocol players
80- Manages player availability and state
81- Handles announcements and TTS playback
82- Coordinates sync groups and grouped playback
83
84### 2. ProtocolLinkingMixin ([protocol_linking.py](protocol_linking.py))
85
86Mixin class containing all protocol linking logic:
87- Matching protocol players to native players via device identifiers
88- Creating and managing Universal Players
89- Protocol link lifecycle (add, remove, cleanup)
90- Output protocol selection for playback
91
92### 3. Helper Utilities ([helpers.py](helpers.py))
93
94Contains standalone helper functions and decorators:
95- `handle_player_command` decorator for command validation
96- `AnnounceData` type definition
97
98## Player Types
99
100Players in Music Assistant have different types based on their capabilities:
101
102### PlayerType.PLAYER
103
104A regular player with native (vendor-specific) support. Examples:
105- Sonos speakers via the Sonos provider
106- Apple devices via the AirPlay provider (HomePod, Apple TV)
107- Google devices via the Chromecast provider (Nest Audio, Google Home)
108
109### PlayerType.PROTOCOL
110
111A generic protocol player without native vendor support. These are streaming endpoints discovered via generic protocols but manufactured by third parties. Examples:
112- Samsung TV discovered via AirPlay (not an Apple device)
113- Sony speaker discovered via Chromecast (not a Google device)
114- Any DLNA/UPnP device (always PROTOCOL type)
115
116**Important:** Protocol players with `PlayerType.PROTOCOL` are hidden from the UI and wrapped in a Universal Player or attached to an existing native player.
117
118### PlayerType.GROUP
119
120A group player that represents (synchronized) playback across multiple physical speakers.
121
122### PlayerType.STEREO_PAIR
123
124A dedicated stereo pair of two speakers acting as one player.
125
126## Multi-Protocol Player System
127
128Modern audio devices often support multiple streaming protocols (AirPlay, Chromecast, DLNA). The Player Controller automatically detects and links these protocols to provide a unified experience.
129
130### How It Works
131
132```
133┌─────────────────────────────────────────────────────────────────────┐
134│                     Physical Device                                 │
135│                  (e.g., Samsung Soundbar)                           │
136├─────────────────────────────────────────────────────────────────────┤
137│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐               │
138│  │   AirPlay    │  │  Chromecast  │  │    DLNA      │               │
139│  │   Protocol   │  │   Protocol   │  │   Protocol   │               │
140│  │   Player     │  │   Player     │  │   Player     │               │
141│  │  (hidden)    │  │  (hidden)    │  │  (hidden)    │               │
142│  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘               │
143│         │                 │                 │                       │
144│         └─────────────────┼─────────────────┘                       │
145│                           │                                         │
146│                           ▼                                         │
147│              ┌─────────────────────────┐                            │
148│              │    Universal Player     │                            │
149│              │  (visible in UI)        │                            │
150│              │  - Aggregates protocols │                            │
151│              │  - Selects best output  │                            │
152│              │  - Unified control      │                            │
153│              └─────────────────────────┘                            │
154└─────────────────────────────────────────────────────────────────────┘
155```
156
157### Device Identifier Matching
158
159Protocol players are matched to the same physical device using identifiers in order of reliability:
160
1611. **MAC_ADDRESS** - Most reliable, unique to the network interface
1622. **SERIAL_NUMBER** - Unique device serial number
1633. **UUID** - Universally unique identifier
1644. **player_id** - Fallback for players without identifiers (e.g., Sendspin)
165
166**Note:** IP_ADDRESS is intentionally NOT used for matching as it can change with DHCP and cause incorrect matches between different devices.
167
168**Important:** Protocol players from the **same protocol domain** (same provider.domain) will NOT be matched together, even if they share the same MAC/IP address. This is intentional to handle multiple software player instances (e.g., multiple Snapcast clients, multiple SendSpin web players) running on the same host. These are separate logical players, not multiple protocols of the same physical device.
169
170**Fallback behavior:** Protocol players that don't expose any identifiers (like Sendspin clients) will still get wrapped in a Universal Player using their player_id as the device key. This ensures all protocol players get a consistent user-facing interface.
171
172### Output Protocol Selection
173
174When playing media, the controller selects the best output protocol:
175
1761. **Grouped protocol** - If a protocol is actively grouped/synced, use it
1772. **User preference** - Honor user's configured preferred protocol
1783. **Native playback** - Use native PLAY_MEDIA if available
1794. **Best available** - Select by protocol priority (AirPlay > Chromecast > DLNA)
180
181## Universal Player
182
183The Universal Player is a virtual player that wraps one or more protocol players when no native vendor support exists.
184
185### When Created
186
187A Universal Player is created when:
1881. A device is discovered via a protocol but has no native provider
1892. The device's protocol player has `PlayerType.PROTOCOL`
1903. There is no existing native player that matches the device identifiers
191
192### Features
193
194- **Aggregates Features** - Combines capabilities from all linked protocols
195- **No PLAY_MEDIA** - Delegates playback to protocol players
196- **Unified Control** - Single point of control for volume, power, etc.
197- **Protocol Selection** - Automatically selects best protocol for playback
198
199### Lifecycle
200
201```
2021. Protocol player registered with PlayerType.PROTOCOL
2032. Controller checks for cached parent_id from previous session:
204   - If found, restores link immediately (skips evaluation)
205   - If parent not yet registered, waits without creating universal player
2063. If no cached parent, checks for matching native player (links immediately if found)
2074. If no native player, schedules delayed evaluation:
208   - 10 seconds standard delay (allows other protocols to register)
209   - 30 seconds if previously linked to a native player (allows native provider to start)
2105. After delay, finds all matching protocol players by identifiers
2116. Creates UniversalPlayer and links all protocols
2127. Protocol players become hidden, Universal Player visible
213```
214
215## Protocol Linking
216
217### Native Player Linking
218
219When a native player (e.g., Sonos) is registered, the controller:
2201. Searches for protocol players with matching identifiers
2212. Links matching protocols to the native player
2223. Protocol players become hidden, native player gains `output_protocols`
223
224### Protocol to Universal
225
226When protocol players are registered without a native match:
2271. Each protocol player schedules a delayed evaluation
2282. After the delay, matching protocols are grouped
2293. A Universal Player is created to wrap them all
2304. All protocol players link to the Universal Player
231
232### Universal to Native Promotion
233
234When a native player appears for a device that has a Universal Player:
2351. Native player is registered
2362. Controller finds matching Universal Player
2373. All protocol links transfer to the native player
2384. Universal Player is removed
2395. Native player becomes the visible entity
240
241## Development Guide
242
243### Adding Protocol Support
244
245When implementing a new protocol provider:
246
2471. Set `_attr_type = PlayerType.PROTOCOL` for generic devices (non-vendor devices)
2482. Set `_attr_type = PlayerType.PLAYER` for devices with native support (vendor's own devices)
2493. **Populate `device_info.identifiers`** with validated identifiers:
250   ```python
251   from music_assistant.helpers.util import is_valid_mac_address
252
253   # IMPORTANT: Validate MAC addresses before adding them
254   if is_valid_mac_address(mac_address):
255       self._attr_device_info.add_identifier(IdentifierType.MAC_ADDRESS, mac_address)
256   self._attr_device_info.add_identifier(IdentifierType.IP_ADDRESS, ip_address)
257   self._attr_device_info.add_identifier(IdentifierType.UUID, uuid)
258   ```
2594. Filter out devices that should only be handled by native providers (e.g., passive satellites)
2605. The Player Controller handles linking automatically
261
262### Adding Native Provider Support
263
264When implementing a native provider (e.g., Sonos, Bluesound) that should link to protocol players:
265
2661. Set `_attr_type = PlayerType.PLAYER` (or the property 'type') for all devices
2672. **Populate device identifiers** - This is critical for protocol linking:
268   ```python
269   from music_assistant.helpers.util import is_valid_mac_address
270
271   self._attr_device_info = DeviceInfo(
272       model="Device Model",
273       manufacturer="Manufacturer Name",
274   )
275   # Add identifiers in order of preference (MAC is most reliable)
276   # IMPORTANT: Validate MAC addresses before adding them
277   if is_valid_mac_address(mac_address):
278       self._attr_device_info.add_identifier(IdentifierType.MAC_ADDRESS, mac_address)
279   self._attr_device_info.add_identifier(IdentifierType.UUID, "device-uuid-here")
280   ```
2813. The controller will automatically:
282   - Find protocol players (AirPlay, Chromecast, DLNA) with matching identifiers
283   - Link them to your native player as `output_protocols`
284   - Replace any existing Universal Player for that device
285
286**Identifier Priority:**
287- `MAC_ADDRESS` - Most reliable, unique to network interface
288- `SERIAL_NUMBER` - Unique device serial number
289- `UUID` - Universally unique identifier
290- `player_id` - Fallback when no identifiers available
291
292**Important Notes:**
293- **Always validate MAC addresses** using `is_valid_mac_address()` before adding them
294  - Rejects invalid MACs like `00:00:00:00:00:00` or `ff:ff:ff:ff:ff:ff`
295  - Prevents false matches between unrelated devices
296  - The controller will attempt ARP lookup to resolve real MACs automatically
297- `IP_ADDRESS` is NOT used for matching as it can change with DHCP
298
299### Testing Protocol Linking
300
301Key scenarios to test:
302
3031. **Single protocol device** - Should create Universal Player
3042. **Multi-protocol device** - All protocols linked to one Universal Player
3053. **Late protocol discovery** - New protocol added to existing Universal Player
3064. **Native player appears** - Universal Player replaced by native
3075. **Protocol disappears** - Handle graceful degradation
308
309### Configuration Storage
310
311Protocol links are persisted in player configuration:
312- `linked_protocol_player_ids` - List of protocol player IDs
313- Restored on restart for fast reconnection
314
315### Key Methods (in protocol_linking.py)
316
317- `_evaluate_protocol_links()` - Entry point for link evaluation
318- `_try_link_protocol_to_native()` - Link protocol to existing native
319- `_schedule_protocol_evaluation()` - Delay evaluation for batching
320- `_create_or_update_universal_player()` - Create/update Universal Player
321- `_check_replace_universal_player()` - Replace Universal with native
322- `_select_best_output_protocol()` - Choose protocol for playback
323