# AirPlay
A pure-Elixir **AirPlay audio sender**. Discover receivers on your network and
stream lossless audio to them using Erlang/OTP primitives: `:crypto`, `:gen_udp`
and `:gen_tcp`.
It supports classic **AirPlay 1 / RAOP**: unencrypted ALAC over RTP with the
NTP-style timing/sync receivers require. Verified streaming real audio to
shairport-sync, AirPort Express, and Apple **HomePods**.
It also has working **AirPlay 2** support under `AirPlay.V2`: transient SRP
pairing, ChaCha20-Poly1305 encrypted RTSP, binary plist SETUP/RECORD/FLUSH,
PTP timing, encrypted ALAC RTP audio, volume control, periodic `/feedback`
keepalives, and multi-room AirPlay 2 group playback.
This is still WIP, created to scratch an itch.
## Installation
```elixir
def deps do
[{:airplay, "~> 0.3.0"}]
end
```
## AirPlay 1 / RAOP usage
```elixir
# Discover receivers (mDNS browse of _raop._tcp.local)
AirPlay.discover()
#=> [%{name: "Office", host: "172.16.42.35", port: 7000}, ...]
# Stream a file (decoded to PCM via ffmpeg) at 40% volume
{:ok, session} = AirPlay.play("172.16.42.35", "/music/track.flac", volume: 40)
AirPlay.set_volume(session, 25)
AirPlay.stop(session)
# Or stream raw PCM you already have (44.1 kHz, signed 16-bit LE, stereo, interleaved)
{:ok, session} = AirPlay.play_pcm("172.16.42.35", pcm, volume: 40)
```
A `session` is a lightweight GenServer that streams in the background and stops
itself when the track ends; pass it to `set_volume/2` and `stop/1`.
## AirPlay 2 usage
The AirPlay 2 API is lower-level than the RAOP `AirPlay.play/3` convenience API
and currently lives under `AirPlay.V2`.
```elixir
# Play one file to an AirPlay 2 receiver. This call runs until playback finishes.
{:ok, stats} =
AirPlay.V2.Player.play_file("172.16.42.35", "/music/track.flac",
volume: 0.4
)
# Play the same file to an AirPlay 2 receiver group.
{:ok, stats} =
AirPlay.V2.GroupPlayer.play_file(
[
%{host: "172.16.42.35", port: 7000},
%{host: "172.16.42.62", port: 7000}
],
"/music/track.flac",
volume: 0.4
)
```
For long-running apps, start AirPlay 2 playback in a supervised process or task.
`AirPlay.V2.Player.set_volume/2`, `AirPlay.V2.GroupPlayer.set_volume/2`,
`AirPlay.V2.Player.stop/1`, and `AirPlay.V2.GroupPlayer.stop/1` operate on that
running playback process.
## Requirements
- Elixir ~> 1.14
- `ffmpeg` on the `PATH` — **only** for `AirPlay.play/3` (file decoding).
`AirPlay.play_pcm/3` has no external dependency.
## How it works
`play/3` opens an RTSP control connection (`OPTIONS → ANNOUNCE → SETUP → RECORD`),
binds the UDP timing/control ports and answers the receiver's NTP timing probe
*before* `SETUP` (HomePods return `520 Origin Error` otherwise), then paces ALAC
RTP packets to the receiver against a wall-clock with periodic sync packets.
| Module | Role |
| --- | --- |
| `AirPlay.Discovery` | mDNS `_raop._tcp` browse |
| `AirPlay.Rtsp` / `AirPlay.Session` | RTSP control plane + handshake |
| `AirPlay.Player` | RTP audio streaming + timing/sync |
| `AirPlay.Rtp` / `AirPlay.Alac` / `AirPlay.Ntp` | packet builders + codecs |
| `AirPlay.Source` | file → PCM (ffmpeg) + framing |
| `AirPlay.Cast` | play/stop/volume session GenServer |
| `AirPlay.V2.Pairing` / `AirPlay.V2.Srp` / `AirPlay.V2.SecureChannel` | AirPlay 2 transient pairing + encrypted RTSP |
| `AirPlay.V2.Setup` / `AirPlay.V2.Rtsp2` / `AirPlay.V2.Plist` | AirPlay 2 control plane |
| `AirPlay.V2.PtpBmca` / `AirPlay.V2.Ptp` | AirPlay 2 PTP timing |
| `AirPlay.V2.Player` / `AirPlay.V2.GroupPlayer` | AirPlay 2 single-device and group playback |
## License
GNU General Public License v3.0 or later (`GPL-3.0-or-later`). See
[LICENSE](LICENSE).