简介:这是一份面向Windows平台开发者的AirPlay服务端程序源码包,围绕Air Media Server项目展开,适合具备网络编程与多媒体处理基础、希望自建AirPlay接收端的中高级开发者。资源核心为libairplaysdk与xindawn相关实现,可用于将iOS或macOS设备的音视频流及屏幕镜像实时接入Windows系统,解决跨平台无线投屏的服务端搭建问题。压缩包共841个文件,约91.29MB,其中617个dll提供音视频编解码与运行依赖,165个h与16个lib构成SDK头文件与静态库,另有c、cpp源码及sln、vcxproj工程文件,便于直接编译调试,doc与msi则补充说明文档和安装支持。目前已有361人学习下载。通过研读源码,读者可掌握AirPlay协议交互、设备认证、会话管理与媒体流转发等关键环节,并借助libairplaysdk的编解码与加解密接口,逐步搭建稳定可用的Windows AirPlay服务端。
1. 拆开 xindawn-windows-airplay-master:Windows 上跑 AirPlay 服务端到底靠什么
把 iPhone 画面投到 Windows 上,很多人第一反应是装个投屏软件,但如果你手里拿到的是xindawn-windows-airplay-master.zip这种源码包,思路就完全不一样了——它不是拿来双击安装的成品,而是一套能在 Windows 上编译出 AirPlay 接收端的工程骨架。包里能看到HomeXml.c、xdw_list.c、xdw_socket_ipc.c、xdw_lock.c、VideoSource.cpp,还有plugins.dat、avcodec-55.dll、libavcodec_plugin.dll这些运行时依赖,基本能判断出它走的是「C 写协议与 IPC 层 + C++ 写视频源 + FFmpeg 解码」的路子。适合谁?适合想自己搭一个 AirPlay 接收服务、或者要基于libairplaysdk做二次开发的人。如果你只是想找个开箱即用的投屏工具,这份资源会让你失望;但如果你想搞清楚 AirPlay 服务端在 Windows 上是怎么把音视频流接住、解码、再交给渲染的,它值得你花一个下午拆一遍。
2. 从源码结构看 AirPlay 服务端的运行链路:协议层、IPC 与解码器怎么串起来
2.1 先认清包里每个文件在链路里的位置
拿到一个源码包,最忌讳上来就找main函数然后一路 F10。AirPlay 服务端不是单线程顺序程序,它至少有三条并行的流:一条是网络协议流(接收 iOS 设备发来的 RTSP/HTTP 请求和加密媒体数据),一条是进程间通信流(把网络层收到的数据交给解码或渲染进程),一条是音视频处理流(解码、同步、输出)。xindawn-windows-airplay-master的文件命名其实已经把这三条流暴露出来了。
HomeXml.c大概率负责解析 AirPlay 客户端在配对和会话建立阶段发来的 XML 属性列表(plist),里面包含设备 ID、公钥、会话密钥这些信息。AirPlay 的配对流程依赖 plist 交换,解析错了后面握手直接失败。xdw_list.c是一个链表容器实现,用来管理会话、连接或数据包队列,C 项目里手写链表很常见,因为不想引入额外依赖。xdw_socket_ipc.c是进程间通信的 socket 封装,说明这个工程把网络接收和媒体处理拆到了不同进程或线程,靠本地 socket 传数据。xdw_lock.c是锁封装,多线程访问共享队列时必须用。VideoSource.cpp是 C++ 写的视频源抽象,负责把解码后的帧喂给渲染层。plugins.dat是插件配置或注册表,avcodec-55.dll和libavcodec_plugin.dll是 FFmpeg 的 avcodec 55 版本动态库,说明音视频解码走的是 FFmpeg。
提示:avcodec-55 对应的是 FFmpeg 2.x 时代的库版本,如果你打算替换成新版 FFmpeg,接口会有变化,不要直接把 dll 换掉了事。
2.2 编译前先把依赖和工具链对齐
这个工程是 C/C++ 混合,Windows 上编译最常见的选择是 Visual Studio。但别急着打开 sln,包里不一定有现成的解决方案文件。我一般会先确认三件事:编译器版本、FFmpeg 开发包、以及是否有平台相关的 socket 头文件依赖。
# 先看包里有没有现成的工程文件或 Makefile ls -la xindawn-windows-airplay-master/ # 重点找这几类文件 # *.sln / *.vcxproj -> Visual Studio 工程 # Makefile / CMakeLists.txt -> 跨平台构建 # *.def -> 动态库导出定义如果只有.c和.cpp散文件,没有工程文件,那就需要自己建一个 VS 工程,把源文件加进去,然后配置 FFmpeg 的头文件和库路径。FFmpeg 在 Windows 上一般用预编译的 dev 包,包含include/和lib/两个目录。
# FFmpeg dev 包解压后的典型结构 ffmpeg-dev/ include/ libavcodec/avcodec.h libavutil/avutil.h lib/ avcodec.lib avutil.lib在 VS 工程里,把ffmpeg-dev/include加到「附加包含目录」,把ffmpeg-dev/lib加到「附加库目录」,然后在链接器输入里加上avcodec.lib、avutil.lib。运行时需要的avcodec-55.dll和libavcodec_plugin.dll要放到 exe 同目录,或者放到系统 PATH 能找到的路径。这一步不做,编译能过,运行直接报找不到 dll。
2.3 把 IPC 通道和解码器初始化顺序理清楚
AirPlay 服务端启动后,第一件事不是等客户端连接,而是先把本地 IPC 通道建好、解码器上下文初始化好。xdw_socket_ipc.c里通常会有类似xdw_ipc_init或xdw_socket_create的函数,负责创建本地监听 socket 或命名管道。VideoSource.cpp里会有解码器初始化的逻辑,调用 FFmpeg 的avcodec_register_all、avcodec_find_decoder、avcodec_alloc_context3这一套。
// 典型的 FFmpeg 解码器初始化顺序(以 H.264 为例) avcodec_register_all(); AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264); AVCodecContext *ctx = avcodec_alloc_context3(codec); avcodec_open2(ctx, codec, NULL); // 之后每收到一个完整帧,调用 avcodec_send_packet / avcodec_receive_frame参数上要注意:AirPlay 传过来的视频流通常是 H.264,音频是 AAC 或 ALAC。ALAC 在 FFmpeg 里对应AV_CODEC_ID_ALAC,但老版本 avcodec-55 对 ALAC 的支持不一定完整,如果客户端是 Apple Music 这类走 ALAC 的场景,解码可能出问题。常见做法是先用 H.264 + AAC 的组合验证链路,确认 IPC 和解码都通了,再换 ALAC 测试。
注意:
avcodec_register_all在 FFmpeg 4.0 之后被废弃,但 avcodec-55 时代还必须调用。如果你换了新版 FFmpeg,这个调用要去掉,否则编译报错。
2.4 会话建立阶段最容易忽略的 plist 解析细节
AirPlay 客户端在正式推流之前,会先发一个/info请求,服务端要返回一个 plist,里面声明自己支持的特性,比如features、model、pk(公钥)等。HomeXml.c就是干这个的。很多人在这一步翻车,是因为返回的 plist 格式不对,或者features位掩码设错了,导致客户端认为服务端不支持视频,只推音频。
// 一个简化的 plist 响应片段(伪代码,实际用 XML 库生成) // 关键字段:features 决定客户端推什么流 // 常见值:0x5A7FFFF7 表示支持视频、音频、镜像等 const char *plist = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" ...>" "<plist version=\"1.0\"><dict>" "<key>features</key><integer>0x5A7FFFF7</integer>" "<key>model</key><string>AppleTV3,2</string>" "</dict></plist>";features的值不是随便写的,它决定了客户端愿意推哪些流。如果你只想要音频,可以把视频相关的位关掉;如果要镜像,必须把镜像对应的位打开。这个值在不同 AirPlay 版本里有差异,建议先用一个已知能工作的值跑通,再按需调整。model字段也重要,客户端会根据 model 判断服务端能力,填一个太老的 model 可能导致高清流被拒绝。
3. 把 AirPlay 流接住并解码:从 socket 收包到 VideoSource 出帧的实操
3.1 网络层收包与解密的基本流程
AirPlay 的媒体流走的是加密的 HTTP 连接,音频和视频分别在不同的端口或不同的 HTTP 会话里传输。服务端在完成配对后,会拿到会话密钥,之后收到的媒体数据包需要用 AES 解密。xdw_socket_ipc.c负责把网络层收到的原始数据通过 IPC 传给解码进程,但解密通常是在网络层做的,因为密钥在会话建立阶段就协商好了。
// 简化的收包与解密流程(伪代码) // 1. 从 socket 读取加密数据包 // 2. 用会话密钥做 AES-CBC 解密 // 3. 解密后的数据可能是 RTP 包,需要拆出音视频帧 // 4. 把帧通过 IPC 发给解码进程 int recv_encrypted_packet(int sock, uint8_t *buf, int len); int aes_decrypt(const uint8_t *key, const uint8_t *iv, const uint8_t *in, int in_len, uint8_t *out); int ipc_send_frame(int ipc_fd, const uint8_t *frame, int frame_len);参数上,AES 的 key 长度通常是 128 位,IV 可能来自握手阶段交换的随机数。如果解密后数据乱码,先检查 key 和 IV 是否用对了,再检查数据包是否完整——AirPlay 的媒体包有时会分片,需要先重组再解密。这一步没有捷径,只能抓包对比。
3.2 用 FFmpeg 解码 H.264 并处理时间戳
解码本身不复杂,难的是时间戳处理。AirPlay 传过来的视频帧带 PTS(显示时间戳),解码后要按 PTS 排序输出,否则画面会跳帧或倒放。VideoSource.cpp里通常有一个帧队列,按 PTS 排序,然后交给渲染线程。
// VideoSource 中解码与入队的简化逻辑 AVPacket pkt; av_init_packet(&pkt); pkt.data = frame_data; pkt.size = frame_len; pkt.pts = extract_pts(frame_data); // 从 RTP 头或自定义头里取 PTS int ret = avcodec_send_packet(ctx, &pkt); if (ret < 0) { /* 解码器拒绝,可能是帧不完整 */ } AVFrame *frame = av_frame_alloc(); ret = avcodec_receive_frame(ctx, frame); if (ret == 0) { frame_queue.push(frame); // 按 PTS 排序后入队 }PTS 的单位通常是 90kHz 时钟,渲染时要换算成毫秒。如果 PTS 不连续,可能是丢包了,这时候要么等重传,要么直接跳帧。AirPlay 对实时性要求高,等重传往往不如跳帧体验好。
3.3 音频同步与 ALAC 解码的坑
音频比视频更敏感,因为人对声音断续的容忍度远低于画面卡顿。AirPlay 音频流常见的是 AAC 和 ALAC 两种编码。AAC 在 FFmpeg 里支持很好,ALAC 在 avcodec-55 里可能有问题。如果你发现音频能解码但声音是噪音,大概率是 ALAC 的 magic cookie 没设置对。
// ALAC 解码前需要设置 extradata(magic cookie) // 这个 cookie 通常从 AirPlay 的 SETUP 请求里拿到 ctx->extradata = av_malloc(cookie_len + AV_INPUT_BUFFER_PADDING_SIZE); memcpy(ctx->extradata, cookie, cookie_len); ctx->extradata_size = cookie_len; // 然后再 avcodec_open2如果 cookie 缺失或错误,解码器可能不报错但输出静音或噪音。排查方法是把收到的音频包 dump 成文件,用 ffplay 直接播,看能不能正常出声。如果 ffplay 也播不了,说明包本身有问题;如果 ffplay 能播但你的程序播不了,说明解码器配置有问题。
3.4 渲染输出:从帧到窗口的最后一步
解码出来的帧是 YUV 格式,要显示到 Windows 窗口上,需要做颜色空间转换(YUV 转 RGB)和缩放。常见做法是用 FFmpeg 的sws_scale,或者用 Direct3D 的着色器做硬件转换。VideoSource.cpp里可能已经封装了这部分,但如果你要自己接渲染,注意帧的 stride 和窗口的宽高不一定一致。
// 用 sws_scale 做 YUV420P 到 RGB24 的转换 SwsContext *sws = sws_getContext( frame->width, frame->height, (AVPixelFormat)frame->format, dst_w, dst_h, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL); sws_scale(sws, frame->data, frame->linesize, 0, frame->height, dst_data, dst_linesize);SWS_BILINEAR是双线性插值,速度和质量折中。如果追求画质可以用SWS_BICUBIC,但 CPU 占用会高。Windows 上如果要用硬件加速,可以考虑 D3D11 的 video processor,但那就不是这份源码包直接能跑通的了,需要自己扩展。
4. 避坑与排查:AirPlay 服务端在 Windows 上最容易翻车的五个点
4.1 客户端一直转圈,服务端看不到任何连接
现象:iPhone 上点了投屏,列表里能看到设备名,但点进去一直转圈,服务端日志没有任何请求进来。
原因:AirPlay 的设备发现走的是 mDNS(Bonjour),服务端需要响应 mDNS 查询。如果 Windows 上没有装 Bonjour 服务,或者防火墙拦了 5353 端口,客户端根本发现不了服务端,或者发现了但连不上。
解决:确认 Windows 上装了 Bonjour(iTunes 会带),或者自己用 mDNS 库实现响应。防火墙放行 5353 UDP 和 AirPlay 用的 TCP 端口(通常是 7000、7100 等)。用netstat -ano | findstr 5353看有没有监听。
4.2 配对失败,日志显示 plist 解析错误
现象:客户端弹出配对码输入框,输入后提示失败,服务端日志里有 XML 解析错误。
原因:HomeXml.c里的 plist 解析对格式敏感,客户端发来的 plist 可能包含服务端没处理的字段,或者编码不是 UTF-8。
解决:在解析前先把原始 XML 打印出来,确认格式。用成熟的 XML 库(如 libxml2 或 Windows 自带的 MSXML)替代手写解析。注意 plist 里的<data>字段是 base64 编码的,要先解码再处理。
4.3 视频能播但花屏,音频正常
现象:画面能出来,但大面积绿色或马赛克,声音正常。
原因:H.264 解码时 SPS/PPS 没收到或没设置对。AirPlay 的 SPS/PPS 可能在带外传输,需要手动提取并设置到解码器上下文。
解决:检查avcodec_open2之前有没有设置extradata。如果 SPS/PPS 在流里,确认解码器能自动提取;如果不行,手动从第一个关键帧里解析出来,拼成 Annex B 格式的 extradata。
4.4 运行时报找不到 avcodec-55.dll
现象:编译通过,双击 exe 弹窗报缺少 dll。
原因:dll 不在 exe 同目录,也不在系统 PATH 里。
解决:把avcodec-55.dll、libavcodec_plugin.dll和 FFmpeg 的其他依赖 dll(如avutil-52.dll)一起复制到 exe 同目录。用 Dependency Walker 或dumpbin /dependents看 exe 依赖哪些 dll,缺哪个补哪个。
4.5 多客户端同时连接时崩溃
现象:一个客户端投屏正常,第二个连上来服务端直接挂掉。
原因:xdw_list.c和xdw_lock.c的线程安全没做好,多个会话并发访问共享链表时出现竞态。
解决:检查所有对共享链表的操作是否都在锁的保护下。xdw_lock.c如果只是简单封装了CRITICAL_SECTION,确认EnterCriticalSection和LeaveCriticalSection成对出现。用 Visual Studio 的并发分析器或 Application Verifier 跑一遍,能定位到大部分竞态。
5. 进阶:把 AirPlay 服务端做成能长期跑的后台服务
5.1 用 Windows 服务包装 exe,避免命令行窗口
源码包编译出来的是控制台程序,关掉窗口服务就停了。要长期跑,常见做法是用sc create注册成 Windows 服务,或者用 NSSM 这类工具把 exe 包成服务。
# 用 sc 注册服务(需要管理员权限) sc create AirPlayServer binPath= "C:\airplay\AirMediaServer.exe" start= auto sc start AirPlayServer # 查看状态 sc query AirPlayServerbinPath要写绝对路径,路径里有空格要用引号包住。start= auto表示开机自启。如果 exe 依赖同目录的 dll,服务工作目录默认是C:\Windows\System32,需要在服务配置里指定工作目录,或者用 NSSM 设置AppDirectory。
5.2 日志与崩溃转储:出问题时能拿到现场
长期跑的服务最怕的是偶发崩溃,没有日志根本没法查。在main入口加一个未处理异常过滤器,把崩溃时的调用栈写到文件。
// Windows 下设置崩溃转储 #include <dbghelp.h> #pragma comment(lib, "dbghelp.lib") LONG WINAPI UnhandledHandler(EXCEPTION_POINTERS *ep) { HANDLE hFile = CreateFileA("crash.dmp", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); MINIDUMP_EXCEPTION_INFORMATION info; info.ExceptionPointers = ep; info.ThreadId = GetCurrentThreadId(); info.ClientPointers = FALSE; MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hFile, MiniDumpNormal, &info, NULL, NULL); CloseHandle(hFile); return EXCEPTION_EXECUTE_HANDLER; } // 在 main 开头注册 SetUnhandledExceptionFilter(UnhandledHandler);拿到crash.dmp后用 WinDbg 打开,加载对应的 pdb 文件,就能看到崩溃在哪个函数、哪个线程。没有 pdb 的话至少能看到调用栈的模块和偏移,配合源码也能定位个大概。
5.3 验证服务端是否真的在工作的几个命令
服务跑起来后,怎么确认它真的在监听、真的能接收流?我一般用这几步:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 端口监听 | netstat -ano | findstr :7000 | 有 LISTENING 状态 |
| mDNS 响应 | dns-sd -B _airplay._tcp | 能看到服务实例 |
| 进程状态 | tasklist | findstr AirMedia | 进程存在且 CPU 占用正常 |
| 日志输出 | 看服务端日志文件 | 有客户端连接和会话建立记录 |
dns-sd是 Bonjour 自带的工具,Windows 上装了 iTunes 或 Bonjour Print Services 就有。如果dns-sd -B能看到你的服务,说明 mDNS 广播正常,客户端应该能发现。如果看不到,检查防火墙和 Bonjour 服务状态。
5.4 一个我踩过的坑:时间戳回绕导致画面卡死
AirPlay 的 RTP 时间戳是 32 位的,跑久了会回绕。如果代码里直接用uint32_t做差值比较,回绕时会出现巨大的正数或负数,导致帧队列排序错乱,画面卡死。血泪经验是:所有时间戳比较都要用带符号的差值,并且处理回绕。
// 正确的时间戳比较方式 int32_t ts_diff(uint32_t a, uint32_t b) { return (int32_t)(a - b); // 利用无符号减法回绕特性 } // 判断 a 是否在 b 之后 bool is_after(uint32_t a, uint32_t b) { return ts_diff(a, b) > 0; }这个坑在短时间测试时不会出现,只有连续跑几个小时才暴露。从那以后我每次写流媒体相关的时间戳比较,都强制走一遍ts_diff,不再直接写a > b。希望帮到你。
本文还有配套的精品资源,点击获取