AirPlay (AirPlay v0.4.6)

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,
#=>     model_identifier: "AudioAccessory5,1",
#=>     txt: %{"am" => "AudioAccessory5,1"}
#=>   }
#=> ]

# 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, gPTP timing packet helpers) live under AirPlay.V2; the control plane is verified against real HomePods but a complete AP2 sender loop is not yet the default playback path, so use the AirPlay 1 API above for production 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).

Experimental AirPlay 2 transient pairing.

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 receiver maps with :name, :host, and :port, plus any advertised mDNS TXT metadata such as :txt, :model_identifier, :manufacturer, :source_version, :os_version, and :airplay_protocol.

pair_v2(host, opts \\ [])

@spec pair_v2(
  String.t(),
  keyword()
) :: {:ok, AirPlay.V2.Pairing.t()} | {:error, term()}

Experimental AirPlay 2 transient pairing.

This establishes the AP2 /pair-setup SRP exchange and upgrades the returned RTSP state to the encrypted control channel. It does not yet start audio; AP2 playback still needs the PTP timing subsystem.

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.