All Posts
Charles4 min read

AirPlay 2 Is Not Always AWDL

I was troubleshooting AirPlay from my iPad to a Mac on my network. I'd set up an allow-all rule between the two devices at the highest priority. After an hour of checking VLAN configs, mDNS repeater settings, and multicast rules, I discovered I'd misconfigured something else entirely that had isolated the devices at the IP layer.

I was looking in the wrong place because I assumed AirPlay between iOS and macOS used AWDL — Apple Wireless Direct Link, the peer-to-peer protocol behind AirDrop. AWDL would bypass my firewall changes. I also expected that if the TCP path broke, AirPlay would fall back to AWDL. Instead, it fails.

I investigated because the mechanism wasn't obvious. I tried some dead ends first — reading encrypted RTSP fields, inspecting frameworks, parsing debug logs for codec names — then switched to having Claude Code parse the pcap and logs instead of doing it by hand.

The mechanism itself turned out to be straightforward: AirPlay 2 to a Mac uses TCP buffer push, not AWDL, not streaming.

What I Assumed

AWDL is Apple's peer-to-peer Wi-Fi protocol. AirDrop uses it. Handoff uses it. It operates on a separate virtual interface (awdl0), bypassing my local network — which means firewall rules don't apply to it. AirPlay doesn't always work this way.

What Actually Happens

AirPlay 2 audio from Apple Music is a TCP buffer-push model over standard routed Wi-Fi.

At track start, the iPad pre-encodes approximately 30 seconds of audio and pushes it over a single TCP connection to the Mac in about 2–3 seconds. The Mac plays entirely from its local buffer. The iPad then sits idle until the buffer needs topping up — which happens about every 10 seconds as a short burst. This obviously spikes if you skip, or change song.

The protocol stack:

Layer Detail
Discovery Bonjour / mDNS
Control RTSP over TCP, port 7000
Audio ALAC 24-bit/48kHz, TCP, port 55557
Transport Local Network

How I Investigated

I had three hypotheses: CDN pull (Mac fetches from Apple Music servers), live stream (iPad streams continuously), or buffer push (iPad pre-encodes a chunk and sends it). I tried some dead ends first, then switched to elimination.

What Didn't Work

Reading RTSP negotiation with tcpdump — AirPlay 2 encrypts everything. The entire stream is ciphertext with no visible field names.

Inspecting frameworks — I tried strings and nm on /System/Library/PrivateFrameworks/AirPlaySender.framework. Which, predictably didn't work

Debug logs for format negotiationlog stream --predicate 'process == "AirPlayXPCHelper"' --level debug gives connection events and BLE discovery. Codec negotiation happens inside the encrypted session before logging gets anywhere near that level.

What Worked

Rule out CDN pull — I monitored traffic to Apple and Akamai IP ranges (17.x.x.x) during active playback. Volume was negligible. The Mac wasn't fetching audio from the internet. I'd also assumed it could be a 3rd-party CDN provider, but there wasn't a huge amount of traffic to anywhere that would correlate to a lossless audio stream.

Rule out live streaming — During uninterrupted playback, the iPad sent near-zero packets. A live audio stream at any quality requires sustained throughput. None existed. iPad packet rate during steady playback: 1–5 packets per second.

Catch the burst at track change — At each track change, the iPad sent 3,000 packets over 2–3 seconds.

Capture and dissect the burst — I had Claude Code parse the pcap and break it down by port and timestamp:

Port Packets Bytes Role
55557 3,167 4,579,080 Audio data
7000 228 211,411 RTSP control (encrypted)
58089 470 0 TCP ACKs only

Average audio packet size: 1,446 bytes — near Ethernet MTU. Bulk TCP transfer, not a drip-feed stream.

Identify the audio format — Two signals confirmed the same thing:

  1. Hardware sample rate: system_profiler SPAudioDataType showed the Mac switched to 48,000 Hz during playback (not 192,000 Hz for Hi-Res Lossless).
  2. Back-calculate from bytes: ALAC 24-bit/48kHz stereo compresses to ~155,000 bytes/sec. 4,579,080 bytes ÷ ~155,000 bytes/sec ≈ 30 seconds of audio.

Both confirm Apple Music Lossless (24-bit/48kHz). Hi-Res Lossless apparently doesn't work over AirPlay, which would explain the 48kHz ceiling.

Confirm standard WiFi, not AWDL — iPad at 10.X.X.X, Mac at 10.X.Y.Z — different subnets but expected for my weird network setup, traffic routed through the L3 switch. AWDL would use the awdl0 interface with link-local addressing. AirPlayXPCHelper logs showed explicit TCP connections to routable IPs. Standard infrastructure WiFi throughout. AWDL capability gets advertised during discovery but doesn't get used for the session. The part I am still unclear on, is why didn't it fall back to AWDL.

What This Means for Unifi

If AirPlay isn't working across VLANs or subnets on a Unifi network, you need three things:

  1. mDNS repeater — AirPlay discovery uses Bonjour. Enable the mDNS repeater in Unifi settings across the relevant VLANs (or run Avahi).
  2. Firewall rules for TCP — RTSP control on port 7000 and audio on port 55557 from the source device to the receiver. The receiver accepts inbound TCP on both ports from the source subnet.
  3. No special AWDL configuration — no peer-to-peer setup, no link-local exceptions. Standard routed traffic handles everything after discovery.

Practical Use Case: Guest Network to Apple TV

Understanding the TCP requirement makes it straightforward to isolate a guest network while still letting it reach an Apple TV for AirPlay. If your Apple TV is on your main network (10.24.11.x) and guests are on a guest network (10.30.x.x), add firewall rules to allow TCP 7000 and 55557 from the guest VLAN to the TV's IP. Discovery (mDNS) still needs to cross the boundary via an mDNS repeater (which helpfully, is built in).

This works because you're not trying to peer-to-peer with AWDL — you're routing packets to a TCP port. As long as that path exists, AirPlay works.

Caveat — This is entirely speculation, based on what I've been able to observe.


Tools used: tcpdump (live capture and pcap analysis), system_profiler SPAudioDataType, log stream filtered to AirPlayXPCHelper, and Claude Code (though, you could comfortably do it without this) for parsing the pcap and cross-referencing logs. The AirPlay mechanism itself happened to be straightforward once I stopped looking in the wrong place.