music-assistant-server
23.3 KB•MD
README.md
23.3 KB • 486 lines • markdown
1# AirPlay Provider
2
3## Overview
4
5The AirPlay provider enables Music Assistant to stream audio to AirPlay-enabled devices on your local network. It supports both **RAOP (AirPlay 1)** and **AirPlay 2** protocols, providing compatibility with a wide range of devices including Apple HomePods, Apple TVs, Macs, and third-party AirPlay-compatible speakers.
6
7### Key Features
8
9- **Dual Protocol Support**: Automatically selects between RAOP and AirPlay 2 based on device capabilities
10- **Native Pairing**: Supports pairing with Apple devices (Apple TV, HomePod, Mac) using HAP (HomeKit Accessory Protocol) or RAOP pairing
11- **Multi-Room Audio**: Synchronizes playback across multiple AirPlay devices with NTP timestamp precision
12- **DACP Remote Control**: Receives remote control commands (play/pause/volume/next/previous) from devices while streaming
13- **Late Join Support**: Allows adding players to an existing playback session without interrupting other players
14- **Flow Mode Streaming**: Provides gapless playback and crossfade support by streaming the queue as one continuous audio stream
15
16## Architecture
17
18### Component Overview
19
20```
21âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
22â AirPlay Provider â
23â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
24â â MDNS Discovery (_airplay._tcp, _raop._tcp) â â
25â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
26â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
27â â DACP Server (_dacp._tcp) - Remote Control Callbacks â â
28â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
29âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
30 â
31 âââââââââââââââââââââââ¼ââââââââââââââââââââââ
32 â â â
33âââââââââ¼âââââââ ââââââââââ¼âââââââââ ââââââââ¼âââââââ
34â AirPlayPlayerâ â AirPlayPlayer â âAirPlayPlayerâ
35â (Leader) â â (Sync Child) â â(Sync Child) â
36âââââââââ¬âââââââ ââââââââââ¬âââââââââ ââââââââ¬âââââââ
37 â â â
38 âââââââââââââââââââââââ¼ââââââââââââââââââââââ
39 â
40 âââââââââââ¼âââââââââââ
41 â AirPlayStreamSessionâ
42 â (manages session) â
43 âââââââââââ¬âââââââââââ
44 â
45 âââââââââââââââââââââââ¼ââââââââââââââââââââââ
46 â â â
47âââââââââ¼âââââââ ââââââââââ¼âââââââââ ââââââââ¼âââââââ
48â RaopStream â â AirPlay2Stream â â RaopStream â
49â ââââââââââââ â â ââââââââââââââ â âââââââââââââ â
50â â cliraop â â â â cliap2 â â ââ cliraop â â
51â ââââââ²ââââââ â â âââââââ²âââââââ â âââââââ²ââââââ â
52â â â â â â â â â
53â ââââââ´ââââââ â â âââââââ´âââââââ â âââââââ´ââââââ â
54â â FFmpeg â â â â FFmpeg â â ââ FFmpeg â â
55â ââââââââââââ â â ââââââââââââââ â âââââââââââââ â
56ââââââââââââââââ âââââââââââââââââââ âââââââââââââââ
57```
58
59### File Structure
60
61```
62airplay/
63âââ provider.py # Main provider class, MDNS discovery, DACP server
64âââ player.py # AirPlayPlayer implementation
65âââ stream_session.py # Manages streaming sessions for synchronized playback
66âââ pairing.py # HAP and RAOP pairing implementations
67âââ helpers.py # Utility functions (NTP conversion, model detection, etc.)
68âââ constants.py # Constants and enums
69âââ protocols/
70â âââ _protocol.py # Base protocol class with shared logic
71â âââ raop.py # RAOP (AirPlay 1) streaming implementation
72â âââ airplay2.py # AirPlay 2 streaming implementation
73âââ bin/ # Platform-specific CLI binaries
74 âââ cliraop-* # RAOP streaming binaries
75 âââ cliap2-* # AirPlay 2 streaming binaries
76```
77
78## Protocol Selection: RAOP vs AirPlay 2
79
80### RAOP (AirPlay 1)
81
82- **Used for**: Older AirPlay devices, some third-party implementations
83- **Features**:
84 - Encryption support (can be disabled for problematic devices)
85 - ALAC compression option to save network bandwidth
86 - Password protection support
87 - Device-reported volume feedback via DACP
88- **Binary**: `cliraop` (based on [libraop](https://github.com/music-assistant/libraop))
89
90### AirPlay 2
91
92- **Used for**: Modern Apple devices, some third-party devices
93- **Features**:
94 - Better compatibility with newer devices
95 - More robust protocol
96 - Required for some devices that don't support RAOP
97- **Binary**: `cliap2` (based on [OwnTone](https://github.com/music-assistant/cliairplay))
98
99### Automatic Selection
100
101When protocol is set to "Automatically select" (default):
102- **Prefers AirPlay 2** for known models (e.g., Ubiquiti devices) that work better with it
103- **Falls back to RAOP** for all other devices
104- Users can manually override via player configuration if needed
105
106## Discovery and Player Setup
107
108### MDNS Service Discovery
109
110The provider discovers AirPlay devices via two MDNS service types:
111
1121. **`_airplay._tcp.local.`** - Primary AirPlay service (preferred)
113 - Contains detailed device information
114 - Announced by most modern devices
115
1162. **`_raop._tcp.local.`** - Legacy RAOP service
117 - Fallback for older devices
118 - If only RAOP service is found, provider attempts to query for AirPlay service
119
120### Player Setup Flow
121
1221. **MDNS service discovered** â `on_mdns_service_state_change()` in [provider.py](provider.py)
1232. **Extract device info** from MDNS properties:
124 - Device ID (from `deviceid` property or service name)
125 - Display name
126 - Manufacturer and model (via `get_model_info()` in [helpers.py](helpers.py))
1273. **Filter checks**:
128 - Skip if player is disabled in config
129 - Skip ShairportSync instances running on the same Music Assistant server (to avoid conflicts with AirPlay Receiver provider)
1304. **Create player** â `AirPlayPlayer` instance
1315. **Register with player controller** â `mass.players.register()`
132
133### Player ID Format
134
135Player IDs follow the format: `ap{mac_address}` (e.g., `ap1a2b3c4d5e6f`)
136
137## Pairing for Apple Devices
138
139Apple TV and Mac devices require pairing before they can be used for streaming.
140
141### Pairing Protocols
142
1431. **HAP (HomeKit Accessory Protocol)** - For AirPlay 2
144 - 6-step SRP authentication with TLV encoding
145 - Ed25519 key exchange
146 - ChaCha20-Poly1305 encryption
147 - Produces 192-character hex credentials
148
1492. **RAOP Pairing** - For AirPlay 1
150 - 3-step SRP authentication with plist encoding
151 - Ed25519 key derivation from auth secret
152 - AES-GCM encryption
153 - Produces `client_id:auth_secret` format credentials
154
155### Pairing Flow
156
1571. **Start pairing** â POST to `/pair-pin-start` (or protocol-specific endpoint)
1582. **Device displays 4-digit PIN** on screen
1593. **User enters PIN** in Music Assistant configuration
1604. **Complete pairing** â SRP authentication and key exchange
1615. **Store credentials** in player config (protocol-specific key: `raop_credentials` or `airplay_credentials`)
162
163**Important**: The DACP ID used during pairing must match the ID used during streaming. The provider uses the first 16 hex characters of `server_id` as a persistent DACP ID to ensure compatibility across restarts.
164
165## Streaming Architecture
166
167### Audio Pipeline
168
169```
170âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
171â Music Assistant Core â
172â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â
173â â Queue Manager (assembles tracks into continuous stream) â â
174â âââââââââââââââââââââââââââ¬âââââââââââââââââââââââââââââââââ â
175ââââââââââââââââââââââââââââââ¼ââââââââââââââââââââââââââââââââââââââ
176 â PCM Audio (44.1kHz, 32-bit float)
177 ââââââââââ¼ââââââââââ
178 â StreamSession â
179 â _audio_streamer()â
180 ââââââââââ¬ââââââââââ
181 â Chunks of PCM audio
182 ââââââââââââââââââââââ¼âââââââââââââââââââââ
183 â â â
184âââââââââ¼âââââââ ââââââââââ¼âââââââââ ââââââââ¼âââââââ
185â FFmpeg â â FFmpeg â â FFmpeg â
186â (resample, â â (resample, â â (resample, â
187â filter, â â filter, â â filter, â
188â convert) â â convert) â â convert) â
189âââââââââ¬âââââââ ââââââââââ¬âââââââââ ââââââââ¬âââââââ
190 â PCM 44.1kHz 16-bit â â
191âââââââââ¼âââââââ ââââââââââ¼âââââââââ ââââââââ¼âââââââ
192â cliraop â â cliap2 â â cliraop â
193â (RAOP â â (AirPlay 2 â â (RAOP â
194â protocol) â â protocol) â â protocol) â
195âââââââââ¬âââââââ ââââââââââ¬âââââââââ ââââââââ¬âââââââ
196 â â â
197 â Network (RTP) â Network (RTP) â Network (RTP)
198 â â â
199âââââââââ¼âââââââ ââââââââââ¼âââââââââ ââââââââ¼âââââââ
200â AirPlay â â AirPlay â â AirPlay â
201â Device 1 â â Device 2 â â Device 3 â
202ââââââââââââââââ âââââââââââââââââââ âââââââââââââââ
203```
204
205### Stream Session Management
206
207The `AirPlayStreamSession` class in [stream_session.py](stream_session.py) manages streaming to one or more synchronized players:
208
2091. **Initialization** (`start()` method)
210 - Calculates start time with connection delay buffer
211 - Converts start time to NTP timestamp for precise synchronization
212
2132. **Client Setup** (per player, `_start_client()` method)
214 - Creates protocol instance (`RaopStream` or `AirPlay2Stream`)
215 - Starts CLI process with NTP start timestamp
216 - Configures FFmpeg for audio format conversion and optional DSP filters
217 - Pipes FFmpeg output to CLI process stdin
218
2193. **Audio Streaming** (`_audio_streamer()` method)
220 - Receives PCM audio chunks from Music Assistant core
221 - Distributes chunks to all players via FFmpeg
222 - Tracks elapsed time based on bytes sent
223 - Handles silence padding if audio source is slow (watchdog mechanism)
224
2254. **Connection Monitoring**
226 - Waits for all devices to connect before starting playback
227 - Monitors CLI stderr for connection status and errors
228 - Removes players that fail to keep up (write timeouts)
229
230### Flow Mode Streaming
231
232AirPlay uses **flow mode** streaming, which means:
233- The entire queue is streamed as one continuous audio stream
234- Enables true gapless playback between tracks
235- Supports crossfade between tracks
236- Once started, the stream continues until explicitly stopped
237
238
239## Multi-Room Synchronization
240
241### Synchronized Playback
242
243The provider supports synchronized multi-room audio by:
244
2451. **Using a single `AirPlayStreamSession`** for the group leader and all sync children
2462. **Coordinating start times** via NTP timestamps
2473. **Distributing identical audio** to all players simultaneously
2484. **Per-player sync adjustment** via `sync_adjust` config option (in milliseconds)
249
250### Group Management
251
252- **Leader**: The primary player that manages the stream session
253- **Members**: Child players synchronized to the leader
254- **Adding members**: Use `set_members()` method in [player.py](player.py)
255- **Removing members**: Stream continues for remaining players
256
257### Late Join Support
258
259When adding a player to an already-playing session (`add_client()` in [stream_session.py](stream_session.py)):
260
2611. **Calculate offset**: Determine how much audio has already been sent
2622. **Adjusted start time**: Start new player at `original_start_time + offset`
2633. **Receive same stream**: New player receives the same audio chunks as existing players
2644. **Automatic synchronization**: NTP timestamps keep all players in sync
265
266**Config option**: `enable_late_join` (default: `True`)
267- If disabled: Session restarts with all players when members change
268- If enabled: New players join seamlessly without interrupting others
269
270## DACP (Digital Audio Control Protocol)
271
272### Purpose
273
274DACP allows AirPlay devices to send remote control commands back to Music Assistant while streaming is active. This enables:
275- Using physical buttons on devices (e.g., Apple TV remote)
276- Volume control from the device
277- Play/pause/next/previous commands
278- Shuffle toggle
279- Source switching detection
280
281### DACP Server
282
283The provider registers a MDNS service `_dacp._tcp.local.` (in `handle_async_init()` method in [provider.py](provider.py)) and runs a TCP server to receive HTTP requests from devices.
284
285### Active-Remote ID
286
287Each streaming session generates an `active_remote_id` (via `generate_active_remote_id()` in [helpers.py](helpers.py)) from the player's MAC address. This ID is:
288- Passed to the CLI binary
289- Sent to the device during streaming
290- Used to match incoming DACP requests to the correct player
291
292### Supported DACP Commands
293
294Handled in `_handle_dacp_request()` in [provider.py](provider.py):
295
296| DACP Path | Action |
297|-----------|--------|
298| `/ctrl-int/1/nextitem` | Skip to next track |
299| `/ctrl-int/1/previtem` | Go to previous track |
300| `/ctrl-int/1/play` | Resume playback |
301| `/ctrl-int/1/pause` | Pause playback |
302| `/ctrl-int/1/playpause` | Toggle play/pause |
303| `/ctrl-int/1/stop` | Stop playback |
304| `/ctrl-int/1/volumeup` | Increase volume |
305| `/ctrl-int/1/volumedown` | Decrease volume |
306| `/ctrl-int/1/shuffle_songs` | Toggle shuffle |
307| `dmcp.device-volume=X` | Volume changed by device (RAOP only) |
308| `device-prevent-playback=1` | Device switched to another source or powered off |
309| `device-prevent-playback=0` | Device ready for playback again |
310
311### Volume Feedback
312
313Both **RAOP** and **AirPlay 2** protocols support devices reporting their volume level via DACP.
314
315**Config option**: `ignore_volume` (default: `False`, auto-enabled for Apple devices)
316- Useful when device volume reports are unreliable
317- Apple devices always ignore volume feedback (handled internally)
318
319### Device Source Switching
320
321When `device-prevent-playback=1` is received:
322- User switched the device to another input source
323- Device is powered off
324- Streaming session removes the player from the active session
325
326## External CLI Binaries
327
328### Why External Binaries?
329
330Python is not suitable for real-time audio streaming with precise timing requirements. The AirPlay protocols (especially AirPlay 2) require:
331- Accurate NTP timestamp handling
332- Real-time RTP packet transmission
333- Low-latency audio buffering
334- Precise synchronization across multiple devices
335
336Therefore, the provider uses C-based CLI binaries for the actual streaming.
337
338### Binary Selection
339
340The provider automatically selects the correct binary based on:
341- **Platform**: Linux, macOS
342- **Architecture**: x86_64, arm64, aarch64
343- **Protocol**: RAOP (`cliraop-*`) or AirPlay 2 (`cliap2-*`)
344
345Binaries are located in [bin/](bin/) directory and validated on first use.
346
347### Binary Communication
348
349**Input** (stdin):
350- PCM audio data piped from FFmpeg
351
352**Commands** (named pipe):
353- Interactive commands sent via `AsyncNamedPipeWriter`
354- Examples: `ACTION=PLAY`, `ACTION=PAUSE`, `VOLUME=50`, `TITLE=Song Name`
355
356**Output** (stderr):
357- Status messages and logs
358- Connection state
359- Playback state changes
360- Elapsed time updates
361- Error messages
362
363The provider monitors stderr in a separate task (`_stderr_reader()` in [raop.py](protocols/raop.py) and [airplay2.py](protocols/airplay2.py)) to:
364- Update player state
365- Detect connection completion
366- Handle errors and packet loss
367- Track elapsed time
368
369## NTP Timestamp Synchronization
370
371AirPlay uses **NTP (Network Time Protocol)** timestamps for synchronized playback.
372
373### NTP Format
374
375- **64-bit integer**: Upper 32 bits = seconds, lower 32 bits = fractional seconds
376- **NTP epoch**: January 1, 1900 (not Unix epoch 1970)
377- **Precision**: Nanosecond-level timing
378
379### Key Functions
380
381Available in [helpers.py](helpers.py):
382- `get_ntp_timestamp()`: Get current NTP time
383- `ntp_to_unix_time()`: Convert NTP to Unix timestamp
384- `unix_time_to_ntp()`: Convert Unix to NTP timestamp
385- `add_seconds_to_ntp()`: Add offset to NTP timestamp
386
387### Usage in Streaming
388
3891. Calculate desired start time: `current_time + connection_buffer`
3902. Convert to NTP timestamp
3913. Pass to CLI binary via `-ntpstart` argument
3924. All players start at the exact same NTP time
3935. Per-player `sync_adjust` config allows fine-tuning (+/- milliseconds)
394
395## Player Types
396
397The provider creates players with different types based on whether the device is a native Apple player or a third-party AirPlay receiver.
398
399### PlayerType.PLAYER
400- **Devices**: Apple HomePod, Apple TV, Mac
401- **Reason**: These are standalone music players with native AirPlay support
402- **Behavior**: Exposed as top-level players in Music Assistant UI
403- **Not merged**: These players are NOT combined with other protocols
404
405### PlayerType.PROTOCOL
406- **Devices**: Third-party AirPlay receivers (Sonos, receivers, smart speakers, soundbars)
407- **Reason**: AirPlay is just one output protocol among many for these devices (often supporting Chromecast, DLNA, etc.)
408- **Behavior**: Automatically merged into a **Universal Player** if other protocols are detected for the same device
409- **Example**: A Sonos speaker supporting both AirPlay and Chromecast will appear as a single "Sonos" player with selectable output protocols
410
411**Detection**: Player type is determined in [player.py](player.py) `__init__()` method based on `manufacturer == "Apple"`
412
413**For more details on output protocols and protocol linking**, see the [Player Controller README](../../controllers/players/README.md), which explains:
414- How multiple protocol players for the same physical device are automatically linked
415- The Universal Player concept for devices without native vendor support
416- Protocol selection and device identifier matching
417- Native player linking vs. Universal Player creation
418
419## Configuration Options
420
421### Protocol Selection
422- **`airplay_protocol`**: Choose RAOP, AirPlay 2, or automatic (default: automatic)
423
424### RAOP-Specific
425- **`encryption`**: Enable/disable encryption (default: enabled)
426- **`alac_encode`**: Enable ALAC compression to save bandwidth (default: enabled)
427- **`ignore_volume`**: Ignore device volume reports (default: false)
428
429### General
430- **`password`**: Device password if required
431- **`sync_adjust`**: Per-player timing adjustment in milliseconds (default: 0)
432
433### Pairing (Apple devices only)
434- **`raop_credentials`**: Stored RAOP pairing credentials (hidden)
435- **`airplay_credentials`**: Stored AirPlay 2 pairing credentials (hidden)
436
437## Known Issues
438
439### Broken AirPlay Models
440
441Some devices have known broken AirPlay implementations (see `BROKEN_AIRPLAY_MODELS` in [constants.py](constants.py)):
442- **Samsung devices**: Known issues with both RAOP and AirPlay 2
443- These players are disabled by default
444
445### Limitations
446
4471. **DACP remote control**: Only active while streaming (not when idle)
4482. **Pause while synced**: Not supported; uses stop instead
4493. **Companion protocol**: Not yet implemented for idle state monitoring
450
451## Development Notes
452
453### Testing CLI Binaries
454
455Each binary can be validated with a test command:
456- **cliraop**: `cliraop -check` (should output "cliraop check")
457- **cliap2**: `cliap2 --testrun` (should output "cliap2 check")
458
459### Adding New CLI Commands
460
461To add a new command to the CLI binaries:
4621. Update the CLI binary source code (external repositories)
4632. Update `send_cli_command()` method in [_protocol.py](protocols/_protocol.py)
4643. Send command via named pipe: `await stream.send_cli_command("YOUR_COMMAND=value")`
465
466### Debugging Streaming Issues
467
468Enable verbose logging in Music Assistant to see:
469- CLI binary arguments
470- stderr output from binaries
471- DACP requests
472- Connection state changes
473- Packet loss warnings
474
475## Credits
476
477- **libraop**: RAOP streaming implementation - https://github.com/music-assistant/libraop
478- **OwnTone**: AirPlay 2 implementation - https://github.com/OwnTone
479- **pyatv**: Reference for HAP pairing protocol - https://github.com/postlund/pyatv
480
481## Future Enhancements
482
483- **Companion protocol**: Implement idle state monitoring for Apple devices
484- **AirPlay 2 volume feedback**: Add DACP volume support for AirPlay 2
485- **Better late-join handling**: Reduce time to start a late joiner
486