A pure-Elixir AirPlay (RAOP) audio sender — discover receivers on the LAN and
stream lossless audio to them, with no native dependencies (just :crypto,
:gen_udp and :gen_tcp).
Targets classic AirPlay 1 / RAOP receivers (shairport-sync, AirPort Express, and — verified — Apple HomePods), streaming unencrypted ALAC over RTP with the NTP-style timing/sync the receiver requires.
Quick start
# Find receivers on the network (mDNS browse of _raop._tcp)
AirPlay.discover()
#=> [%{name: "Office", host: "172.16.42.35", port: 7000}, ...]
# Stream a file (decoded 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.1kHz, s16le, stereo interleaved)
{:ok, session} = AirPlay.play_pcm("172.16.42.35", pcm, volume: 40)play/3 requires ffmpeg on the PATH (used to decode the source file to PCM).
play_pcm/3 has no external dependency.
Experimental AirPlay 2 building blocks (transient pairing, ChaCha20-Poly1305
encrypted control channel, binary plist, SETUP) live under AirPlay.V2; the
control plane is verified against real HomePods but audio rendering is not yet
complete, so use the AirPlay 1 API above for playback.
Summary
Types
A running playback session (the AirPlay.Cast GenServer pid).
Functions
Browse the LAN for AirPlay/RAOP receivers for timeout_ms (default 2500).
Decode the audio file at path (via ffmpeg) and stream it to host.
Stream the audio file at path to host, decoding incrementally via
ffmpeg so memory stays bounded no matter how long the track is (a 4-hour
audiobook would be ~2.4 GB if fully decoded up front, as play/3 does).
Stream a raw PCM buffer (44.1 kHz, signed 16-bit little-endian, stereo,
interleaved) to host. Same options as play/3.
Set the playback volume (0–100) on a running session.
Stop a running session and tear down the RTSP connection.
Types
@type session() :: pid()
A running playback session (the AirPlay.Cast GenServer pid).
Functions
@spec discover(non_neg_integer()) :: [map()]
Browse the LAN for AirPlay/RAOP receivers for timeout_ms (default 2500).
Returns a list of %{name: String.t(), host: String.t(), port: pos_integer()}.
Decode the audio file at path (via ffmpeg) and stream it to host.
Options:
:volume— 0–100 (default 25):port— RTSP port (default 7000)
Returns {:ok, session} where session is a pid you pass to set_volume/2
and stop/1; it streams in the background and stops itself when the track ends.
Stream the audio file at path to host, decoding incrementally via
ffmpeg so memory stays bounded no matter how long the track is (a 4-hour
audiobook would be ~2.4 GB if fully decoded up front, as play/3 does).
Options:
:start_seconds— seek offset before decoding (default 0):volume— 0–100 (default 25):port— RTSP port (default 7000):ffmpeg— ffmpeg binary path (defaults to one onPATH)
Returns {:ok, session}. Requires ffmpeg (with the -re flag, used for
native-rate flow control).
Stream a raw PCM buffer (44.1 kHz, signed 16-bit little-endian, stereo,
interleaved) to host. Same options as play/3.
@spec set_volume(session(), 0..100) :: :ok
Set the playback volume (0–100) on a running session.
@spec stop(session()) :: :ok
Stop a running session and tear down the RTSP connection.