akustikrausch/airplay2-sender-cpp

GitHub: akustikrausch/airplay2-sender-cpp

一个经过实际验证的 C++ AirPlay 2 实时音频发送端,实现了现代苹果设备所需的完整加密 RAOP/RTSP 配对与流传输流程。

Stars: 1 | Forks: 0

# AirPlay 2 发送端 (C++) **作者:Akustikrausch (Andreas Wendorf)**

ci license Apache-2.0 C++20 AirPlay 2 realtime ALAC lossless crypto proven in FXChainPlayer

一个可运行且经过验证的 c++ **AirPlay 2 实时音频发送端**。它可以与现代的 **Apple TV 4K**、**HomePod** 或 **macOS** 接收端配对,并通过苹果目前实际使用的加密 RAOP/RTSP 路径向其传输纯净、无损的 **ALAC** 音频。支持双向音量调节、无缝切换曲目等全部功能。 这是“苹果税”中从未被公开的那一部分。你可以找到上百个*接收端*。你也可以找到用 python 写的。但你绝对找不到一个仅仅用于向当前的苹果设备**发送** AirPlay 2 实时音频并保持会话存活的小型 c++ 程序。所以它来了,并且附带了写下的完整配方。 ## 为什么会有这个项目 开源的 AirPlay 生态中全都是接收端,使用的语言不对,或者还停留在 2014 年: - **shairport-sync** 是一个*接收端*。非常出色,但方向反了。 - **owntone** (forked-daapd) 是一个完整的媒体服务器,而不是一个发送端库。它*可以*发送,但你不能直接把它塞进你的应用里。 - **pyatv** 是基于 python 的客户端/控制库,而不是 c++ 的实时音频管道。 - **AirConnect / raop_play / 旧版 shairport “客户端”分支** 实现的都是 AirPlay **1** / 传统 RTSP。它们无法完成 2024 年的 Apple TV 所要求的 AP2 握手(加密控制通道、事件通道 + RECORD 的顺序、硬编码 ALAC 的实时流、30 秒的 keep-alive)。 苹果从未公开过其中的任何内容。这里的每一个字节都是通过将上述项目作为*规范*阅读(绝不抄袭一行代码)、抓包分析,以及无数次思考“为什么 socket 在恰好一毫秒后就关闭了”而恢复出来的。这件事居然花了这么长时间,这正是这个仓库存在的全部理由。 ## 配方(这是最有价值的部分) 如果你只看一个章节,那就看这个。向现代 Apple TV 进行 AP2 实时流传输是按**正确顺序**进行的 **七** 件事,任何一件弄错都会导致你的会话*看起来*已连接,但播放**没有声音**,或者在大约 30 秒后断开,或者在 SETUP 阶段直接被拒绝。顺序如下: 1. **配对,紧接着是加密的 RTSP 控制通道。** 在 pair-verify 之后,每一个 RTSP 请求/响应都通过 ChaCha20-Poly1305 加密。帧格式 = `[2-byte LE len][cipher][16-byte tag]`,数据块 ≤ 1024 B,AAD = 长度前缀,nonce = `[4 zero bytes][8-byte LE counter]`,独立的发送/接收计数器(Control-Write / Control-Read 密钥)。如果跳过这一步,Apple TV 会在 pair-verify 后约 1 毫秒断开 socket。 2. **会话/流设置是 RTSP 的 `SETUP rtsp://host/sessionId` 方法**,而不是 `POST /setup`(那会返回 404),并且在此之前必须先发起一次 `GET /info`。 3. **打开事件通道,按 owntone 的顺序发送 RECORD。** 通过 TCP 连接到会话 SETUP 返回的 `eventPort`,并且**在会话 SETUP 之后 / 流 SETUP 之前**发送 RECORD。如果不打开事件通道,你会收到 `RECORD=500` / `FLUSH=455` 错误。 4. **音频密钥 (`shk`) = 配对共享密钥的前 32 个字节,原始格式。** 不经过 HKDF 处理。直接用作音频 payload 的 ChaCha20-Poly1305 密钥,**并且**原封不动地发送到 stream-SETUP 的 plist 中。(参见下方的 **transient** 注意事项;这就是 macOS 让我们踩坑的地方。) 5. **实时流强制要求使用 ALAC。** 接收端硬编码了 ALAC 并忽略 `ct` / `audioFormat`。发送*未压缩的* ALAC 帧(`audioFormat 0x40000`,`ct 2`,type `0x60`):MSB 优先的 `3b stereo-CPE(=1) · 4b 0 · 12b 0 · 1b hasSize=0 · 2b 0 · 1b isNotCompressed=1 · 352×{L16,R16} · 3b END(=7) · byte-align`。 6. **keep-alive(保活)就是加密的事件通道。** 发送 RECORD 后,除非你解密接收端推送的 `POST /command` (updateInfo) 事件并回复 `200 OK`,否则 Apple TV 会在大约 25-30 秒后断开整个会话。事件的密钥是基于 pair-verify 密钥通过 `HKDF "Events-Salt" / "Events-Write|Read-Encryption-Key"` 生成的,但是**交换过的**(反向连接 → eventIn 使用 WRITE 密钥解密,eventOut 使用 READ 密钥加密)。`POST /feedback` 必须是 `RTSP/1.0`(而不是 HTTP/1.1),但单单发送 feedback 并*不是* keep-alive,事件通道才是。 7. **200-OK 响应必须极其精简。** 只能是 `RTSP/1.0 200 OK\r\nServer: …\r\n[CSeq]\r\n\r\n`,不能有任何多余内容。添加 `Audio-Latency: 0` 或 `Content-Length: 0` 会破坏接收端的实时时间线 → 会导致会话保持**已连接状态**但播放出**静音**。这就是最终那个“稳定但没声音”的 bug:时间、同步和 ALAC 全都是正确的,多出的两个响应头就是罪魁祸首。 ### 两种配对路径(以及 macOS 的陷阱) 第 4 步中的音频密钥是*配对共享密钥的前 32 个字节*。但这个密钥的**长度各不相同**,具体取决于你的配对方式: - **pair-verify (Apple TV, 屏幕 PIN 码, `sf=0x644`):** X25519 ECDH = **32 字节**。直接全部使用。 - **HAP transient (MacBook / HomePod, `sf=0x4`):** 配对会停留在 pair-setup 的 **M4** 阶段(没有 pair-verify),此时的密钥是 SRP 会话密钥 `K = SHA-512(S) = 64 字节`。 如果你把完整的 64 字节 `K` 喂给 ChaCha 密钥,它会在**每个音频数据包**上抛出 `chacha key size` 错误 → 导致无法发送任何音频 → 接收端在约 30 秒的无音频超时后会断开原本健康的会话(封面图 + 控制通道仍然有效,因为*这些*密钥是基于完整 K 进行 HKDF 生成的,与长度无关)。**必须将音频密钥截断为前 32 个字节。** owntone 的 `airplay.c` (`AIRPLAY_AUDIO_KEY_LEN = 32`) 直白地指出:*“对于 transient 配对,key_len 将会是 64 字节,但只有前 32 字节被用于音频 payload 的加密。”* 而 pair-verify 的密钥本身已经是 32 字节,所以这种截断操作在那里等于无操作。 这一行代码就是“MacBook 显示着封面图但没声音”与“MacBook 成功播放”之间的区别所在。 ## 包含哪些内容 ``` src/ airplay_crypto.h / .cpp the qt-free crypto + wire-format core raop_sender.h / .cpp the AP2 sender state machine (the recipe, in code) ring_buffer.h the lock-free spsc tap the audio thread feeds third_party/ed25519/ the one primitive mbed tls lacks (zlib, vendored) ``` **`airplay_crypto`** 是真正可复用的、**不依赖 Qt** 的核心(仅使用 `std::vector` + `std::string`):包含 SRP-6a-3072 / SHA-512、X25519 ECDH、Ed25519 签名/验证、ChaCha20-Poly1305 AEAD、HKDF-SHA512、HomeKit TLV8,以及一个极简的 `bplist00` 编码/解码器,恰好提供了 AP2 配对和加密通道所需的组件,不多不少。由 **Mbed TLS 3.6** (Apache-2.0) + orlp 的 **ed25519** (zlib) 提供底层支持。直接引入即可使用。 **`raop_sender`** 是完全实现了上述“配方”的状态机:配对、加密控制通道、事件通道、ALAC 实时编码器和 keep-alive。 ## 状态说明(请阅读) 这部分代码是从 **FXChainPlayer** 中提取出来的,经过了验证,并且**运行良好**,它每天都会向真实的 Apple TV 4K (`AppleTV14,1`) 和 MacBook 投送音频。它**目前还不是可以直接拿来即用的独立库**:`raop_sender` 目前使用 **Qt** (`QTcpSocket` / `QUdpSocket` / `QTimer`) 处理网络连接,并包含了几个宿主项目的头文件。在 **路线图** (`ROADMAP.md`) 中,计划将 socket 抽象封装在一个小型的(约 5 个方法)传输接口背后,以便整个项目能够完全脱离 Qt 编译,并提供一个 `airplay-send ` 的 CLI 演示。**加密核心模块目前已经可以独立编译**,这是你今天就能直接使用的部分;而发送端是你用来参考实现的标杆。 如果你想要体验它所依托的打磨完善的播放器,在这里: → **https://github.com/akustikrausch/FXChainPlayer-Releases** ## 安全性(范围说明,请务必阅读) 这是一项互操作性研究,而不是经过审计的生产级安全栈。有一件事必须提前说明:**发送端目前尚未对接收端的身份进行加密验证**。pair-verify 签名和 SRP 校验目前采用的是*记录日志并继续*的策略,而不是严格拦截,因此局域网内的中间人攻击原则上可以接受你的会话,你也就把音频流发给了它(你会泄露音频和 transient 会话密钥,而不会将攻击者的数据引入信任边界,因为它只是个*发送端*)。不过,针对不可信输入的解析器(bplist / TLV8 / RTSP / 加密的事件帧)确实进行了边界检查以防范越界 (OOB) 和内存分配型 DoS 攻击,并且 AEAD 的使用方式是先验证后使用,并采用了独立的单通道密钥和计数器。 结论是:**请在受信任的网络上使用它。** 实现严格拦截的接收端身份验证是一个已知且明确范围的 TODO 事项(参见 `ROADMAP.md` / `SECURITY.md`),非常适合作为首个 PR。如有任何问题,请通过 `SECURITY.md` 报告。 ## 许可证 `src/` 目录下的所有内容均采用 **Apache-2.0** 许可证。© 2026 Andreas Wendorf (Akustikrausch)。 特意选择 Apache-2.0:因为这是*逆向工程苹果协议*的代码,所以该许可证包含**明确的专利授权**,你可以将其嵌入到产品中,无需担心“发布是否安全”的专利纠纷,这正是 MIT 许可的协议代码通常无法进入企业级代码库的原因。完全宽松的许可,没有传染性限制;请保留 `NOTICE` 文件。 代码来源,如实说明: - **加密与线路传输格式的核心代码**(`airplay_crypto.*`)是**净室**成果,是通过仅将 owntone / pyatv / shairport-sync / pair_ap / emanuelecozzi 的 AP2 笔记 / 非官方规范**作为文档阅读**而重新构建的。没有将任何上游代码直接复制进来;只采用了线路上的字节格式定义。 - `raop_sender.cpp` 中的 **RAOP / AirPlay 传输层** 在某种程度上是 **pyatv**(MIT 许可证,© 2020 Pierre Ståhl)的 **C++ 移植版**,其 RTSP/RTP/同步/时间模型以及 HAP 配对流程遵循了 pyatv 的模块结构。这里没有捆绑任何 python 代码;pyatv 的 MIT 许可声明附在 [`licenses/THIRD-PARTY-NOTICES.txt`](licenses/THIRD-PARTY-NOTICES.txt) 中。 第三方引入/构建依赖项保留了它们各自的许可证:**Mbed TLS** 为 Apache-2.0,**ed25519** 为 zlib 许可证,具体细节在同一文件中。 ## 免责声明 本项目与 **Apple Inc.** 没有任何隶属关系,未获其授权,也未被其认可。*AirPlay*、*Apple TV*、*HomePod*、*HomeKit* 和 *macOS* 是 Apple Inc. 的商标,此处使用仅用于描述本代码通信的对象。本项目不提供任何苹果密钥、证书或提取的固件;它仅仅是一个网络协议的净室客户端,旨在实现与**你自己的**设备的互操作。请在归你所有且允许使用的上使用它。 *致敬每一位曾看着 socket 在一毫秒后关闭,却拒绝让它得逞的人。*
标签:AirPlay, C++, RAOP协议, 实时音频, 数据擦除, 无损音频, 音频流媒体