AirPlay (AirPlay v0.2.0)

Copy Markdown View Source

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

session()

@type session() :: pid()

A running playback session (the AirPlay.Cast GenServer pid).

Functions

discover(timeout_ms \\ 2500)

@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()}.

play(host, path, opts \\ [])

@spec play(String.t(), Path.t(), keyword()) :: {:ok, session()} | {:error, term()}

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.

play_file(host, path, opts \\ [])

@spec play_file(String.t(), Path.t(), keyword()) ::
  {:ok, session()} | {:error, term()}

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 on PATH)

Returns {:ok, session}. Requires ffmpeg (with the -re flag, used for native-rate flow control).

play_pcm(host, pcm, opts \\ [])

@spec play_pcm(String.t(), binary(), keyword()) :: {:ok, session()} | {:error, term()}

Stream a raw PCM buffer (44.1 kHz, signed 16-bit little-endian, stereo, interleaved) to host. Same options as play/3.

set_volume(session, volume)

@spec set_volume(session(), 0..100) :: :ok

Set the playback volume (0–100) on a running session.

stop(session)

@spec stop(session()) :: :ok

Stop a running session and tear down the RTSP connection.