Skip to content
Projects

metaneutrons/shairplay-rust has 27 GitHub stars and 11 forks. GitHot measured 0 stars in 24 hours. Last checked 2026-11-14 09:15 UTC. Last push 2026-11-04.

How these counts are measured

What you can do

  • ๐ŸŽต - AirPlay 1 (Classic) - ALAC/L16 decode, optional AES encryption, DACP remote control โ€” rock solid
  • ๐ŸŽต - AirPlay 2 - Buffered AAC, SRP-6a pairing, ChaCha20-Poly1305 encrypted RTSP
  • ๐Ÿ”Š - Multichannel - 5.1 and 7.1 AAC decode with ITU-R BS.775 mixdown to stereo
  • ๐Ÿ”„ - Resampling - Automatic sample rate conversion via rubato

Measured star growth

in 24h
0
in 7 days
Collecting history
in 15 days
Collecting history
in 1 month
Collecting history

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

About

Pure Rust AirPlay server library (AP1 + AP2 audio, video)

Creative & Media

README.md

Original

shairplay-rust

Pure Rust AirPlay server library

CI crates.io docs.rs License: LGPL-3.0 Rust

A complete AirPlay audio and video receiver as a Rust library. Supports both classic AirPlay (AP1) and AirPlay 2 (AP2) with buffered audio, encrypted transport, HomeKit pairing, and screen mirroring. #![forbid(unsafe_code)].

This is a pure Rust implementation โ€” not a wrapper or FFI binding. Every protocol handler, codec, and cryptographic primitive is implemented in safe Rust. No C code from shairplay, shairport-sync, or any other project is linked or called.


โš ๏ธ Pre-1.0 notice: This crate is under active development. Minor version bumps (e.g. 0.1 โ†’ 0.2) may include breaking API changes. Pin your dependency to a specific minor version (shairplay = "0.2") and review the CHANGELOG before upgrading.

โš–๏ธ FairPlay notice: The FairPlay code (feature fairplay, on by default) has a provenance uncertainty: earlier releases were a port of a GPL-3.0 library, and the current rewrite is not a clean-room implementation. Details and how to opt out: docs/fairplay-provenance.md.

Installation

Add the current release from crates.io:

cargo add shairplay

Enable only the protocol capabilities required by the application:

cargo add shairplay --features ap2
cargo add shairplay --features video

The fairplay feature is on by default. If you need to leave the FairPlay code out because of its provenance uncertainty, see docs/fairplay-provenance.md:

cargo add shairplay --no-default-features

Features

Feature Details
๐ŸŽต AirPlay 1 (Classic) ALAC/L16 decode, optional AES encryption, DACP remote control โ€” rock solid
๐ŸŽต AirPlay 2 Buffered AAC, SRP-6a pairing, ChaCha20-Poly1305 encrypted RTSP
๐Ÿ”Š Multichannel 5.1 and 7.1 AAC decode with ITU-R BS.775 mixdown to stereo
๐Ÿ”„ Resampling Automatic sample rate conversion via rubato
๐Ÿ” HomeKit pairing Transient (PIN 3939) and normal (persistent key storage)
๐Ÿ“บ Video Screen mirroring โ€” work in progress, behind video feature gate
๐ŸŽฌ HLS Video YouTube and other HLS streams โ€” receiver relays URL to app
๐ŸŒ Cross-platform macOS (native Bonjour) + Linux (pure Rust mDNS)
๐Ÿ”’ Pure safe Rust #![forbid(unsafe_code)], no C code in this crateยน
โšก Async Built on tokio
๐Ÿ›Ž๏ธ Error notifications Pairing, SETUP, and codec failures surfaced to your app via AudioHandler::on_error

ยน The crate itself contains no unsafe code. On macOS, mDNS service registration uses astro-dnssd, which internally calls Apple's Bonjour C API via FFI. On Linux, mdns-sd provides a pure Rust mDNS implementation with no native dependencies.

Quick Start

AirPlay 1 (Classic)

use std::sync::Arc;
use shairplay::{RaopServer, AudioHandler, AudioSession, AudioFormat};

struct MyHandler;
impl AudioHandler for MyHandler {
    fn audio_init(&self, format: AudioFormat) -> Box<dyn AudioSession> {
        println!("Stream: {}ch {}bit {}Hz", format.channels, format.bits, format.sample_rate);
        Box::new(MySession)
    }
}

struct MySession;
impl AudioSession for MySession {
    fn audio_process(&mut self, samples: &[f32]) {
        // F32 interleaved PCM โ€” same format for AP1 and AP2
    }
}

#[tokio::main]
async fn main() -> Result<(), shairplay::ShairplayError> {
    let mut server = RaopServer::builder()
        .name("My Speaker")
        .build(Arc::new(MyHandler))?;
    server.start().await?;
    tokio::signal::ctrl_c().await.unwrap();
    server.stop().await;
    Ok(())
}

AirPlay 2

// AP2 adds resampling and multichannel mixdown.
// Output is always f32 interleaved PCM โ€” same as AP1.
let mut server = RaopServer::builder()
    .name("My Speaker")
    .output_sample_rate(48000)
    .output_max_channels(2)
    .build(Arc::new(MyHandler))?;

// Or force AP1 mode at runtime (even with ap2 feature compiled in):
use shairplay::AirPlayMode;
let mut server = RaopServer::builder()
    .name("My Speaker")
    .mode(AirPlayMode::AirPlay1)
    .build(Arc::new(MyHandler))?;

Builder Options

Method Default Feature Description
.name() "Shairplay" AirPlay display name
.hwaddr() random locally administered address 6-byte MAC address for mDNS
.port() 5000 RTSP listening port
.password() none HTTP Digest auth password
.pipewire_auth_setup_compat() false pipewire-auth-setup-compat Experimental fixed-probe acknowledgement in classic RAOP; not MFi authentication
.max_clients() 10 Maximum concurrent connections
.bind() all interfaces Bind to specific IPs (multi-interface)
.mdns_scope() MdnsScope::All Limit the mDNS announcement to the interfaces that carry given addresses (Linux, Windows and macOS)
.header_diagnostics() Disabled diagnostic-headers Structured, redacted RTSP header diagnostics
.advertise_codecs() PCM + ALAC AP1 Set the ordered AP1 cn capability list
.advertise_encryption() none AP1 Set the ordered AP1 et capability list
.output_sample_rate() source rate resample Resample all audio to this rate
.output_max_channels() source channels resample Mix down to this channel count
.pin() "3939" ap2 PIN for HomeKit pairing
.mode() AirPlayMode::AirPlay2 ap2 Runtime protocol mode (AP1 or AP2)
.pairing_store() MemoryPairingStore ap2 Persistent key storage
.video_handler() none video Video session factory
.hls_handler() none hls HLS video playback handler

Limiting the mDNS announcement

.bind() limits the listeners only. Without .mdns_scope() the records still leave on every interface of the host, so a receiver bound to eth0 is also announced on nebula1.

use shairplay::{BindConfig, MdnsScope, RaopServer};

let bind = BindConfig::new().addrs(["192.168.1.10".parse().unwrap()]);
let server = RaopServer::builder()
    .mdns_scope(MdnsScope::from_bind(&bind))
    .bind(bind)
    .build(handler)?;

start() fails if an address is a wildcard or belongs to no interface of the host. It never falls back to announcing everywhere. RaopServer::mdns_scope_applied() reports what applies:

Platform Result
Linux and others (mdns-sd) Interfaces(..). The address selects the interface, so other addresses on it are announced too.
macOS (Bonjour) Interfaces(..). Each service is registered once per interface index, which limits its service records (PTR, SRV, TXT). The address records of the host belong to the system and stay on every interface.

AirPlay 1 Discovery Capabilities

AP1 advertises PCM and ALAC by default (cn=0,1) and uses unencrypted audio transport by default (et=0) for broad sender compatibility. HTTP Digest authentication configured with .password() controls access but does not encrypt the audio payload.

Deployments that require encrypted AP1 transport can advertise only the modes their senders support:

use shairplay::{Ap1Codec, Ap1Encryption, RaopServer};

let builder = RaopServer::builder()
    .advertise_codecs([Ap1Codec::Alac, Ap1Codec::Pcm])
    .advertise_encryption([Ap1Encryption::Rsa, Ap1Encryption::FairPlay]); // FairPlay needs the `fairplay` feature (on by default)

The order supplied by the application is preserved. Empty and duplicate lists are rejected during build(). With the ap2 feature enabled, also select AirPlayMode::AirPlay1; AP2 uses a separate fixed capability profile.

Conformant MFi-SAP (et=4) is neither advertised nor implemented. The experimental pipewire-auth-setup-compat feature can acknowledge one fixed POST /auth-setup probe; it does not authenticate the receiver or encrypt audio. See the /auth-setup protocol status for the evidence, limits and requirements for conformant receiver authentication.

Feature Flags

Flag Dependencies Description
(default) fairplay AirPlay 1 with FairPlay
pipewire-auth-setup-compat none Experimental PipeWire probe acknowledgement; explicit runtime opt-in required
diagnostic-headers none Opt-in, redacted RTSP request and response header diagnostics
dangerous-raw-headers (implies diagnostic-headers) Raw header values for explicit use in debug-assertion builds
resample rubato Sample rate conversion + channel mixdown
fairplay none (on by default) FairPlay handshake and key decryption (/fp-setup, a=fpaeskey, Ap1Encryption::FairPlay). Provenance uncertainty, see docs/fairplay-provenance.md
ap2 chacha20poly1305, hkdf, symphonia, โ€ฆ (implies resample) Full AirPlay 2 audio
video (implies ap2 and fairplay) Legacy feature set for screen mirroring (0x527FFEE6)
hls (implies video) HLS video playback (YouTube, etc.) โ€” receiver relays URL to app

Experimental PipeWire Probe Compatibility

Compile with --features pipewire-auth-setup-compat, then explicitly opt in:

let builder = shairplay::RaopServer::builder()
    .pipewire_auth_setup_compat(true);

When ap2 is also compiled in, select classic mode explicitly:

let builder = shairplay::RaopServer::builder()
    .mode(shairplay::AirPlayMode::AirPlay1)
    .pipewire_auth_setup_compat(true);

The default stays disabled, including in debug and all-feature builds. Enabling it in AirPlayMode::AirPlay2 is rejected; already AP2-paired connections cannot use this compatibility response. Discovery is unchanged. Only an exact, bounded public probe receives an empty acknowledgement, after the normal Digest check. This is not MFi authentication or a confidentiality guarantee. Do not enable it as a substitute for encryption.

Local simulated-sender tests cover PCM delivery over UDP/TCP, teardown and reconnect. Real PipeWire playback remains unqualified, and the current sender's Digest flow does not establish password-protected interoperability. See the contract and known limitations before deployment; the feature does not automatically configure a PipeWire sender.

RTSP Header Diagnostics

Header diagnostics are compiled out by default. With the diagnostic-headers feature enabled, applications must still select a policy and enable the dedicated shairplay::protocol_headers tracing target at TRACE:

use shairplay::{HeaderDiagnostics, RaopServer};

let builder = RaopServer::builder()
    .header_diagnostics(HeaderDiagnostics::Redacted);

Redacted exposes values only for a conservative metadata allowlist. Sensitive and unknown header values are replaced with <redacted>. Bodies and media payloads are never logged.

The separate dangerous-raw-headers feature adds HeaderDiagnostics::Raw. Raw mode can expose credentials, pairing material, and stable device identifiers, requires explicit runtime selection, and is rejected by build() when debug assertions are disabled. Control characters remain escaped in every mode.

Implementation Status

โœ… AirPlay 1 โ€” Production Ready

Rock solid. ALAC and raw L16 PCM decoding, optional AES encryption, DACP remote control, metadata (artwork, progress, track info). Works with iPhone, iPad, Mac, iTunes, and PipeWire senders.

โœ… AirPlay 2 Audio โ€” Production Ready

Full pipeline: SRP-6a pairing โ†’ encrypted RTSP โ†’ FairPlay โ†’ PTP timing โ†’ buffered AAC decode โ†’ f32 PCM output. Multichannel 5.1/7.1 with ITU-R BS.775 mixdown. Automatic resampling. Both stream types implemented:

  • Type 103 (buffered) โ€” AAC over TCP with timed playout buffer. Used for music.
  • Type 96 (realtime) โ€” ALAC over UDP with immediate delivery. Used for Siri, phone calls, system sounds.

Scope: the AP2 profile implements HomeKit pairing and, with the fairplay feature, FairPlay, but not licensed MFi-SAP receiver authentication through /auth-setup. The profile does not advertise et=4 or the MFi feature bits; see the /auth-setup protocol status.

Connections are instant (starts streaming in under 50ms) using deterministic key persistence and correct eventPort handling on isRemoteControlOnly channels, matching commercial receiver capabilities.

๐Ÿงช Video (Screen Mirroring) โ€” Work in Progress

Behind the video feature gate. Screen mirroring uses the legacy/UxPlay-compatible feature set; AP2 buffered audio is intentionally disabled while video is enabled.

The video feature switches to a UxPlay-compatible legacy feature set (0x527FFEE6) to receive screen mirroring data from iOS 18+. AP2 buffered audio is not available โ€” the iPhone falls back to legacy ALAC (type 96) which is fully supported with FairPlay key decryption and NTP timing.

Video decryption is implemented for the legacy feature-set path; AP2+video hybrid operation remains research work. See AP2-STATUS.md for details.

๐Ÿ”ฌ Remote Control

AP1 DACP remote control is fully implemented and works.

Third-party AP2 receivers cannot send playback commands (play/pause/skip) to the iPhone. All paths require Apple ecosystem trust:

  • Type 130 MRP data channel โ€” requires HomeKit seed (Home app pairing)
  • Companion-link protocol โ€” requires same Apple ID
  • DACP โ€” iPhone doesn't send Active-Remote header in AP2

See AP2-STATUS.md for the full research. If you have further insights or want to help investigate, please reach out!

Example Player

The included example plays AirPlay audio through the system's default output device:

# AirPlay 1
cargo run --example player

# AirPlay 2
cargo run --example player --features ap2

# With resampling to match output device rate (e.g. 44100โ†’96000 Hz)
cargo run --example player --features ap2 -- --resample

# With persistent device identity (stable MAC + paired keys across restarts)
cargo run --example player --features ap2 -- --persist state.json

# Custom name and interface binding
cargo run --example player --features ap2 -- --name "Kitchen" --bind 192.168.1.100 --persist state.json --resample

# HLS video (YouTube) โ€” requires mpv installed
cargo run --example player --features hls -- --resample

Without --persist, the device gets a random MAC each run and the iPhone treats it as a new device. With --persist, the MAC and paired keys are saved to a JSON file.

Without --resample, audio is delivered at the source's native rate (44100 Hz). If your output device runs at a different rate (e.g. 96000 Hz), use --resample to convert in the example app.

Architecture

src/
โ”œโ”€โ”€ raop/                RAOP server, RTSP handlers, RTP streaming
โ”‚   โ”œโ”€โ”€ buffered_audio   AP2 timed playout buffer with decrypt/decode/resample
โ”‚   โ”œโ”€โ”€ event_channel    AP2 encrypted event channel
โ”‚   โ”œโ”€โ”€ video            Video handler traits (experimental)
โ”‚   โ””โ”€โ”€ video_stream     Video stream receiver (experimental)
โ”œโ”€โ”€ crypto/              RSA, Ed25519+Curve25519, AES, FairPlay
โ”‚   โ”œโ”€โ”€ pairing_homekit  AP2 SRP-6a + HomeKit pairing + pair-verify
โ”‚   โ”œโ”€โ”€ chacha_transport AP2 ChaCha20-Poly1305 encrypted RTSP
โ”‚   โ”œโ”€โ”€ video_cipher     AES-128-CTR streaming cipher for video
โ”‚   โ””โ”€โ”€ tlv              AP2 TLV codec for pairing messages
โ”œโ”€โ”€ codec/               Audio decoders
โ”‚   โ”œโ”€โ”€ alac             ALAC decoder (AP1)
โ”‚   โ”œโ”€โ”€ aac              AAC decoder via symphonia (AP2)
โ”‚   โ””โ”€โ”€ resample         Sample rate conversion + channel mixdown
โ”œโ”€โ”€ proto/               SDP, HTTP/RTSP, binary plist, HTTP Digest auth
โ”œโ”€โ”€ net/                 Async TCP server, mDNS, PTP timing, feature flags
โ”œโ”€โ”€ dacp/                DACP remote control client (AP1)
โ””โ”€โ”€ error/               Error types

Test Coverage

The test suite includes 17 C-verified pairing vectors from pair_ap and 256 golden FairPlay vectors produced by running the C reference of shairplay (tests/data/fairplay_golden.txt), plus vectors for the individual steps:

cargo test                    # AP1 tests
cargo test --features ap2     # AP1 + AP2 tests
cargo test --features video   # All tests

Development

brew install lefthook gitleaks
lefthook install
lefthook run pre-push

The hooks enforce commit format, staged-file hygiene, secret scanning, formatting, and the bounded pre-push checks. CI runs the complete feature, platform, dependency-policy, and coverage matrix.

Acknowledgments

This project builds on the work of many contributors to the AirPlay open-source ecosystem:

  • shairplay โ€” Original C library this project is a complete rewrite of
  • playfair โ€” FairPlay library by EstebanKubata, shipped by shairplay under the GPL-3.0. The FairPlay code here was written with knowledge of it and verified against its output, see docs/fairplay-provenance.md
  • shairport-sync โ€” AirPlay 2 C reference implementation by Mike Brady
  • pair_ap โ€” HomeKit pairing reference (SRP-6a, TLV, HKDF) by Scott Ickle / ejurgensen. 17 C-verified test vectors generated from this codebase
  • AirPlay 2 Internals โ€” Protocol documentation by Emanuele Cozzi
  • Unofficial AirPlay Specification โ€” Legacy protocol documentation
  • rairplay โ€” Rust AirPlay 2 receiver with video support
  • pyatv โ€” Python Apple TV library (companion-link protocol research)
  • Dissecting the Media Remote Protocol โ€” Evan Coleman's Apple TV reverse engineering

License

LGPL-3.0-or-later.

FairPlay. The FairPlay key exchange (feature fairplay, enabled by default) has a provenance uncertainty that I describe openly in docs/fairplay-provenance.md. In short: earlier releases (up to 0.10.0) were a port of the GPL-3.0 playfair library. The current code is a rewrite with a different structure, tested against golden vectors from the C reference. It still contains the constant tables and reproduces the forced algorithm, because interoperability leaves no alternative. I take the position that constants have no copyright and that the algorithm is a forced sequence, and I publish it under the LGPL. This is not a clean-room implementation, and no legal question is ever fully certain. If you cannot accept the residual uncertainty, build with default-features = false.

Disclaimer

All resources in this repository are written using only freely available information from the internet. The code and related resources are meant for educational purposes only. It is the responsibility of the user to make sure all local laws are adhered to. Parts of this project are generated with the help of AI and the expert-in-the-loop approach.

Explore related projects