openairplay2-receiver 0.2.0

Standalone AirPlay 2 receiver for Linux/ALSA
openairplay2-receiver-0.2.0 is not a library.

OpenAirPlay 2

crates.io docs.rs CI

An AirPlay 2 audio receiver for Linux, written in Rust — the AirPlay 2 counterpart of openairplay1 (a working AirPlay 1 / RAOP receiver).

AirPlay 2 is a substantially different protocol from AirPlay 1: HomeKit-style pairing (SRP + Curve25519 + Ed25519), a ChaCha20-Poly1305-encrypted control channel carrying binary plists, per-packet ChaCha20-Poly1305 audio, AAC as well as ALAC, and PTP timing.

See notes.md for the protocol research and the milestone plan, and notes/milestone-*.md for each milestone.

Status

openairplay2 is a working single-stream AirPlay 2 receiver. A real macOS or iOS device discovers it on the network, pairs with it, and streams to it, and audio comes out of an ALSA device with working transport controls — all verified against a real Mac.

The repository is a cargo workspace with two artifacts:

  • openairplay2 — an embeddable library: network in, decoded PCM + session events out. The host application provides the audio output (an AudioSink) and its own volume model; no ALSA dependency, builds and tests on macOS as well as Linux.
  • openairplay2-receiver — the standalone Linux/ALSA receiver binary, built on the library's public API.

It handles the full path end to end: mDNS/Bonjour discovery, HomeKit transient pairing, a ChaCha20-Poly1305-encrypted control channel, the FairPlay fp-setup handshake, two-phase SETUP, and buffered AAC playback — plus pause / resume, seek / skip, and volume control, and now-playing events (track title/artist/album and cover art) for embedding hosts to display.

By design it targets one Mac → one stream → one output. It deliberately does not implement PTP (it never binds UDP 319/320): PTP exists to align multiple outputs to a shared clock, and for a single output the sender's own buffering plus our backpressure are enough. Multi-room / grouped playback would require PTP and is out of scope.

The milestone-by-milestone development history is in notes/status.md.

Build & run

Building links against nothing exotic (libasound2-dev for the receiver binary); a running avahi-daemon is needed for discovery.

cargo build --release
./target/release/openairplay2-receiver --name "Living Room"

Or install the released binary straight from crates.io:

cargo install openairplay2-receiver

Options

Option Description Default
--name NAME Name advertised to senders (mDNS + GET /info) OpenAirPlay2
--port PORT TCP port for the HTTP/RTSP control server 7000
--mac AA:BB:CC:DD:EE:FF Device ID (deviceid) reported to senders discovered from a network interface, else a fixed fallback
--identity-file PATH Where the Ed25519 identity keypair is stored ~/.config/openairplay2/identity
--alsa-device NAME ALSA output device to play to default
--no-audio Decode but don't open ALSA (silent run) audio on
--no-avahi Don't advertise over Avahi / mDNS advertising on
-h, --help Print usage and exit

Set RUST_LOG=debug to log every request — useful for watching what a real sender sends.

Embedding

The library's public API is small: build a Receiver, hand it a sink factory and an event channel, run it on your tokio runtime.

[dependencies]
openairplay2 = "0.2"
use openairplay2::{AudioSink, Event, Receiver};

struct MySink; // your PCM → speaker path

impl AudioSink for MySink {
    fn write(&mut self, pcm: &[i16]) { /* blocking write paces playback */ }
    fn flush(&mut self) { /* seek: drop your device/prebuffer state */ }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let receiver = Receiver::builder()
        .name("Office")
        .identity_path("/var/lib/myapp/airplay-identity")
        .build()?;
    let (events, mut rx) = tokio::sync::mpsc::unbounded_channel();
    tokio::spawn(async move {
        while let Some(event) = rx.recv().await {
            if let Event::Volume { db } = event { /* your gain path */ }
        }
    });
    receiver.run(|_rate, _channels| Box::new(MySink), events).await
}

The library keeps the session semantics (pairing, decrypt, AAC decode, the pause gate, seek flushing, backpressure); the host sees only PCM and events — SessionStarted, Volume (in AirPlay dB), Paused, Flushed, SessionEnded. A host that owns its mDNS registration builds with .advertise(false) and publishes receiver.txt_records() itself.