
简介AirLib 是一个基于非官方 AirPlay 协议规范实现的 C# 开源库及配套客户端专为 .NET 开发者设计用于在 Windows 或跨平台环境中将图片、视频等媒体内容推送至 Apple TV解决 C# 生态缺乏原生 AirPlay 集成方案的痛点适用于流媒体应用、远程镜像展示、游戏大屏投射等场景。资源包共 12 个文件含 8 个 JSON涵盖 issues、discussions、releases、license 等元数据体现项目规范维护与可追溯性、2 个 ZIP含源码与文档压缩包、1 个 HTML协议说明文档及 1 个 UUID 命名的发布体文件整体仅 257KB轻量易集成。目前已有 67 人学习下载。开发者可直接获取完整协议实现参考、可运行客户端示例、结构化问题追踪记录与版本发布清单快速理解 AirPlay 私有协议交互逻辑并基于现有代码扩展控制指令或适配新固件版本。1. AirLib 是什么一个绕过官方 SDK、用纯 C# 实现 AirPlay 协议栈的“硬核连接器”你有没有试过——在 Windows 或 Linux 上写个 C# 程序想把本地一张 PNG 图片或一段 MP4 视频直接推送到 Apple TV 屏幕上却卡在第一步Apple 官方压根没提供任何跨平台的 AirPlay 客户端 SDKiOS/macOS 的AVRoutePickerView和AVPlayer只能跑在自家生态里第三方工具如AirServer或Reflector又是黑盒闭源、无法嵌入、协议细节不透明。这时候AirLib 就不是“又一个库”而是唯一一条能让你用 C# 主动发起 AirPlay 推送、控制播放状态、甚至实现自定义元数据透传的可审计路径。它不依赖 iTunes、不调用 macOS 原生框架、不走 HTTP 重定向跳转而是从零实现 AirPlay 协议栈核心服务发现mDNS、设备认证RSAAES、媒体协商SDP、流式传输RTP over UDP/TCP、状态同步RTSP over HTTP。适合需要深度集成 Apple TV 控制能力的工业看板、数字标牌、教育中控系统或 C# 上位机开发者——尤其当你已经用 WPF 做好了主界面却不想为一个“投屏按钮”额外起个 Electron 进程或调用 Python 子进程时AirLib 是那个能让你代码里await client.SendVideoAsync(demo.mp4)就真把画面打到客厅电视上的东西。2. 协议选型与架构拆解为什么必须自己实现 AirPlay而不是封装 AVFoundation2.1 AirPlay 协议栈的真实分层从 mDNS 到 RTSP 再到 RTPAirPlay 不是单一协议而是一套松耦合的协议组合体。AirLib 的设计严格对应其物理通信链路第 0 层服务发现mDNSApple TV 在局域网广播_airplay._tcp服务携带deviceid、model、flags、features等 TXT 记录。AirLib 使用DnsClient库非 .NET 原生System.Net.NetworkInformation主动监听224.0.0.251:5353解析出目标设备 IP、端口默认 7000、支持能力如video/photo/audio是否开启。关键点features0x10F表示支持图片、视频、音频、镜像modelAppleTV6,2决定是否启用 HEVC 编码协商。第 1 层设备握手与认证HTTP TLS RSA向http://ip:7000/pair-setup发起 POST携带Pair-Setup请求体TLV 编码服务端返回Pair-Verify阶段所需的公钥和加密挑战。AirLib 内置BouncyCastle实现 ECDH 密钥交换与 AES-GCM 加密生成sessionKey并完成pair-verify流程。注意这不是 OAuth没有 token是设备级双向证书绑定——这也是为什么首次配对需用户在 Apple TV 上输入 4 位 PINpincode字段参与 TLV 签名。第 2 层媒体会话控制RTSP over HTTP认证成功后所有控制指令走http://ip:7000/airplay端点但实际使用 RTSP 方法SETUP/PLAY/PAUSE/TEARDOWN。AirLib 将 RTSP 请求封装为标准 HTTP POSTContent-Type: application/x-apple-binary-plist序列化为二进制 plist非 JSON。例如SETUP请求需指定rtp://client:5004作为接收端服务端返回Transport: RTP/AVP;unicast;interleaved0-1;moderecord。第 3 层媒体数据传输RTP over UDP/TCP视频流走 RTP/UDP端口由 SETUP 返回的server_port指定音频走 RTP/TCP因 Apple TV 对 UDP 音频丢包容忍度极低。AirLib 启动两个独立UdpClient实例一个发视频 RTP 包含 H.264 Annex B NALU 时间戳一个发音频 RTP 包AAC-ADTS ADTS header。关键约束RTP timestamp 必须严格按 90kHz 递增视频或 44.1kHz音频否则 Apple TV 解码器直接静音或黑屏。提示AirLib 不实现 AirPlay 镜像Mirroring协议/mirrorendpoint因其依赖私有AirPlay Mirroring Protocol和硬件编码器协商目前无公开逆向文档支撑。专注photo/video/audio三类推送场景这是企业级数字标牌最常用、最可控的路径。2.2 为什么不用 Apple 官方 APIC# 生态下的现实约束macOS/iOS 专属限制AVRoutePickerView仅限 UIKit/AppKit无法在 Windows/Linux 托管AVPlayerItem的setPlaybackLikelyToKeepUp等方法无跨平台等价物。iTunes Automation API 已废弃Windows 上曾可用 COM 调用 iTunes 播放列表但 iTunes 12.11 彻底移除该接口且无法控制 Apple TV 播放位置仅能“播放到 AirPlay 设备”无进度/暂停/音量控制。第三方封装不可靠如AirPlaySharp仅实现 mDNS 发现无认证与流传输SharpAircast依赖 Node.js 子进程C# 主程序需维护 IPC 通道稳定性差。协议演进风险Apple 在 tvOS 15.4 后强制要求pair-setup阶段使用curve25519替代secp256r1旧库全部失效。AirLib 通过BouncyCastle动态切换 ECC 曲线适配 tvOS 14–17 全版本。2.3 AirLib 核心类图与职责划分基于 v2.3.1public class AirPlayClient : IDisposable { private readonly IMdnsService _mdns; private readonly IAirPlayAuthenticator _auth; private readonly IRtspSession _rtsp; private readonly IRtpSender _rtp; // 公共入口统一语义隐藏协议细节 public Task SendPhotoAsync(string imagePath, PhotoOptions options null); public Task SendVideoAsync(string videoPath, VideoOptions options null); public Task SendAudioAsync(string audioPath, AudioOptions options null); } // 分层接口便于单元测试与 Mock public interface IAirPlayAuthenticator { TaskAirPlaySession PairAsync(IPAddress deviceIp, string pinCode); Taskbool VerifyAsync(AirPlaySession session); } public interface IRtspSession { TaskRtspResponse SetupAsync(AirPlaySession session, Uri mediaUri); TaskRtspResponse PlayAsync(AirPlaySession session, TimeSpan startTime); }这种分层让开发者可替换IMdnsService为自定义 DNS 解析器如对接企业内网 mDNS 中继或注入IRtpSender实现硬件加速编码如 Intel Quick Sync H.264 encoder 输出直接喂给 RTP 包。3. 本地环境搭建与最小可行 Demo三步跑通图片推送3.1 环境准备.NET 6、BouncyCastle 与 DnsClient 版本锁定AirLib 依赖两个关键第三方库版本必须精确匹配协议要求BouncyCastle.Cryptov2.1.0非最新版v2.2.0 移除了ECKeyPairGenerator.GenerateKeyPair()的SecureRandom参数重载导致pair-setup阶段密钥生成失败DnsClientv4.1.0v5.x 使用System.Text.Json替代Newtonsoft.Json但 AirLib 的 mDNS TXT 记录解析逻辑强依赖JsonConvert.DeserializeObjectT的TypeNameHandling.Auto特性安装命令确保项目文件.csproj中显式指定PackageReference IncludeBouncyCastle.Crypto Version2.1.0 / PackageReference IncludeDnsClient Version4.1.0 / PackageReference IncludeMicrosoft.NETCore.App Version6.0.0 /注意不要使用dotnet add package默认安装最新版——这是新手翻车第一高发区。AirLib 的AssemblyInfo.cs中硬编码了BouncyCastle的CryptoApi类型引用版本错则编译报CS0234。3.2 服务发现用 DnsClient 扫描局域网 Apple TVusing DnsClient; using DnsClient.Protocol; var lookup new LookupClient(new LookupClientOptions { UseTcpFallback true, Timeout TimeSpan.FromSeconds(3) }); // 查询 _airplay._tcp.local 服务 var result await lookup.QueryAsync(_airplay._tcp.local, QueryType.PTR); foreach (var record in result.Answers.PtrRecords()) { var service await lookup.QueryAsync(record.PtrDomainName, QueryType.SRV); var srv service.Answers.SrvRecords().FirstOrDefault(); if (srv null) continue; var txtResult await lookup.QueryAsync(record.PtrDomainName, QueryType.TXT); var txt txtResult.Answers.TxtRecords().FirstOrDefault()?.Text.FirstOrDefault(); // 解析 TXT 记录中的关键字段 var txtDict ParseTxtRecord(txt); // 辅助方法将 modelAppleTV6,2\0flags0x10F\0 转为 Dictionary if (txtDict.ContainsKey(model) txtDict[model].StartsWith(AppleTV)) { Console.WriteLine($Found Apple TV: {srv.Target} port {srv.Port}); Console.WriteLine($Features: {txtDict[flags]}, Model: {txtDict[model]}); // 输出示例Found Apple TV: 192.168.1.105 port 7000 } }ParseTxtRecord实现要点Apple TV 的 TXT 记录是\0分隔的键值对不能用string.Split(\0)直接切分——某些固件会在 value 末尾多加一个\0导致keyvalue\0解析成[keyvalue, ]。正确做法是逐字节扫描遇到\0且前一字符非\0时截断。3.3 图片推送SendPhotoAsync 的完整调用链var client new AirPlayClient(); var device new AirPlayDevice(192.168.1.105, 7000); // 从 mDNS 获取的 IP 和端口 // 第一步配对仅首次需要PIN 码在 Apple TV 设置 通用 远程与无线 AirPlay HomeKit 中显示 await client.PairAsync(device, 1234); // 第二步发送图片支持 JPG/PNG自动缩放至 Apple TV 分辨率 3840x2160 await client.SendPhotoAsync(C:\assets\logo.png, new PhotoOptions { Duration TimeSpan.FromSeconds(10), // 显示时长 Transition PhotoTransition.Fade, // 过渡效果Fade/Crossfade/None Position PhotoPosition.Center // 居中/全屏拉伸/保持宽高比 }); Console.WriteLine(Photo sent successfully!);底层执行流程PairAsync→ 调用IAirPlayAuthenticator.PairAsync→ 发送pair-setupTLV → 解析pair-verify公钥 → 生成sessionKeySendPhotoAsync→ 构造POST /photo请求 → 将 PNG 文件读入内存 → 添加X-Apple-Session-IDheader → 序列化为 binary plist → 发送 HTTP bodyApple TV 接收后触发AVPictureInPictureController渲染无需额外播放器进程。逻辑说明PhotoOptions.Duration并非客户端控制而是写入 plist 的duration字段由 Apple TV 系统级相册应用解析。若设为TimeSpan.Zero则使用 Apple TV 默认 5 秒超过 30 秒会被截断。4. 视频推送实战H.264 编码约束、RTP 打包与时间戳校准4.1 视频格式硬性要求为什么你的 MP4 总是黑屏Apple TV 对 AirPlay 视频流有三重编码枷锁缺一不可约束项允许值违规后果AirLib 自动处理ContainerMP4.mp4或 MOV.mov400 Bad Request✅ 检查扩展名拒绝 AVI/MKVVideo CodecH.264 Baseline/Main ProfileLevel 4.0黑屏日志显示Invalid codec❌ 需预处理见下文Resolution≤ 3840×21604K宽高比 16:9 或 4:3拉伸变形或裁剪✅ 自动缩放VideoOptions.ResizeMode血泪经验用 FFmpeg 转码时-profile:v baseline是必须参数-level 4.0不能省略。常见错误命令ffmpeg -i input.avi -c:v libx264 output.mp4生成的是 High ProfileApple TV 拒绝解码。正确转码命令Windows PowerShellffmpeg -i input.avi -c:v libx264 -profile:v baseline -level 4.0 -pix_fmt yuv420p -vf scale3840:2160:force_original_aspect_ratiodecrease,pad3840:2160:(ow-iw)/2:(oh-ih)/2 -c:a aac -b:a 128k -movflags faststart output.mp4-pix_fmt yuv420pApple TV 仅支持 YUV420不支持 YUV444/YUV422-movflags faststart将 moov atom 移至文件开头避免 AirLib 读取时阻塞4.2 RTP 打包从 MP4 文件提取 NALU 并构造 RTP 包AirLib 不依赖 FFmpeg 运行时而是用MP4Parser库SharpMp4Parserv1.2.0解析 MP4 的moov和mdatbox提取 H.264 SPS/PPS 和每一帧 NALUusing SharpMp4Parser.IsoParser; var mp4File IsoFile.Parse(output.mp4); var videoTrack mp4File.GetTrack(1); // 假设视频在 track 1 var sampleSizes videoTrack.GetSampleSizes(); // 每帧字节数 var naluList new Listbyte[](); for (int i 0; i sampleSizes.Length; i) { var frameData videoTrack.ReadSample(i).Data.Array; // H.264 Annex B 格式NALU 以 0x00000001 开头 // AirLib 自动插入 start code若缺失并分割帧为单个 NALU var nalus SplitNalu(frameData); naluList.AddRange(nalus); } // 构造 RTP 包简化版 foreach (var nalu in naluList) { var rtpPacket new RtpPacket { PayloadType 96, // H.264 dynamic payload type SequenceNumber (ushort)(seq), Timestamp (uint)(timebase * 90000), // 90kHz clock Ssrc 0x12345678, Payload nalu }; _rtpSender.Send(rtpPacket, targetEndpoint); }SplitNalu关键逻辑扫描0x00000001或0x000001起始码必须跳过前导 0x00 字节MP4 中 NALU 前可能有 padding。错误处理若某帧解析出 0 个 NALU抛出InvalidH264FrameException提示用户检查编码 profile。4.3 时间戳校准为什么视频快进/卡顿90kHz 的陷阱RTP timestamp 不是毫秒而是以90kHz 采样率计数的 ticks。若视频帧率为 30fps则每帧 timestamp 增量应为90000 / 30 3000。AirLib 从 MP4 的sttsbox 读取精确帧持续时间sample_duration而非简单除法// 从 stts box 获取每帧 duration单位timescale var stts videoTrack.GetBoxOfTypeSttsBox(); var timescale videoTrack.GetTimescale(); // 通常为 600 or 90000 var frameDuration stts.Entries[0].SampleCount 1 ? stts.Entries[0].SampleDuration : (uint)(90000 / 30); // fallback // 计算 timestamp 增量ticks var timestampStep (uint)Math.Round((double)timescale * frameDuration / videoTrack.GetTimescale());参数说明timescale是 MP4 的时间基如90000sample_duration是该帧持续多少个 timescale tick。直接90000 / fps会因四舍五入误差累积10 分钟视频偏移可达 2 秒——Apple TV 检测到 timestamp 跳变 500ms 直接断开连接。5. 避坑指南AirLib 开发者踩过的 5 个真实深坑5.1 现象PairAsync永远超时Wireshark 显示pair-setup请求无响应原因Apple TV tvOS 16 默认关闭“允许不受信任的 AirPlay 设备”需手动开启设置 通用 远程与无线 AirPlay HomeKit AirPlay 接收 任何人。tvOS 15.4 之前默认开启升级后自动变为“仅家庭成员”。解决在 Apple TV 上进入设置确认开关状态若企业环境需自动化AirLib 无法绕过此限制无 API必须人工配置。5.2 现象图片推送成功但显示为纯黑Wireshark 抓包显示 HTTP 200 OK原因PNG 文件包含 alpha 通道RGBAApple TV 的 AirPlay photo endpoint 仅支持 RGB无透明度。System.Drawing.Bitmap保存 PNG 时默认保留 alpha导致解码失败。解决推送前强制转换为 RGBusing (var bitmap new Bitmap(logo.png)) using (var rgbBitmap new Bitmap(bitmap.Width, bitmap.Height, PixelFormat.Format24bppRgb)) { using (var g Graphics.FromImage(rgbBitmap)) g.DrawImage(bitmap, 0, 0); rgbBitmap.Save(logo_rgb.png, ImageFormat.Png); }5.3 现象视频播放 2 秒后断开Apple TV 日志出现RTCP timeout原因AirLib 默认发送 RTCP RRReceiver Report包到targetPort 1但某些路由器如 Cisco ISR 4331会拦截 UDP 端口5005假设视频 RTP 发到5004导致 Apple TV 认为客户端失联。解决禁用 RTCP 或指定固定端口await client.SendVideoAsync(video.mp4, new VideoOptions { RtspOptions new RtspOptions { DisableRtcp true } // 彻底禁用 RTCP // 或指定端口RtcpPort 5006 });5.4 现象SendPhotoAsync报System.Net.Http.HttpRequestException: Connection refused原因Apple TV 的 AirPlay 服务端口7000被防火墙拦截。Windows Defender 高级安全防火墙默认阻止入站7000端口即使请求是出站。解决在 Windows 上运行New-NetFirewallRule -DisplayName Allow AirPlay Port 7000 -Direction Outbound -Protocol TCP -LocalPort 7000 -Action Allow5.5 现象同一台 Apple TV第一次配对成功第二次PairAsync报Invalid verification data原因AirLib 的sessionKey缓存未清理或 Apple TV 端已撤销配对设置 用户与账户 [你的账户] 删除此设备。AirPlay 认证是设备级绑定撤销后旧 sessionKey 失效。解决强制清除本地缓存并重新配对// 删除 %APPDATA%\AirLib\pairing.json var cachePath Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AirLib); if (Directory.Exists(cachePath)) Directory.Delete(cachePath, true);6. 进阶技巧构建企业级数字标牌系统——状态监控、断线重连与批量推送6.1 实时状态监控监听 Apple TV 播放事件Play/Pause/StopApple TV 通过POST /event端点推送播放状态变更AirLib 提供AirPlayClient.PlaybackStatusChanged事件client.PlaybackStatusChanged (sender, e) { Console.WriteLine($Playback status: {e.Status} at {e.Position.TotalSeconds:F1}s); // e.Status: Playing, Paused, Stopped, Loading, Failed // e.Position: 当前播放时间TimeSpan }; // 启动监听需在 SendVideoAsync 后调用 await client.StartEventListeningAsync(device);底层原理AirLib 启动一个HttpListener绑定http://localhost:50000/event并将该 URL 通过SETUP请求的Notify-URLheader 注册到 Apple TV。Apple TV 每 2 秒 POST 状态到该地址。注意HttpListener需管理员权限绑定localhost开发时建议用netsh http add urlacl urlhttp://localhost:50000/ userEveryone授权。6.2 断线自动重连当 Apple TV 休眠唤醒后恢复播放Apple TV 进入休眠后会关闭 AirPlay 端口但唤醒时不会通知客户端。AirLib 实现心跳检测// 启动后台心跳任务 var heartbeat Task.Run(async () { while (!cancellationToken.IsCancellationRequested) { try { // 发送轻量级 OPTIONS 请求探测服务可用性 var response await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Options, $http://{device.Ip}:{device.Port}/)); if (response.StatusCode HttpStatusCode.OK) _isConnected true; else _isConnected false; } catch { _isConnected false; } await Task.Delay(TimeSpan.FromSeconds(5)); } }); // 播放中检测断连自动重发 if (!_isConnected currentMedia is VideoMedia vm) { await client.SendVideoAsync(vm.Path, vm.Options); // 重新推送 }6.3 批量推送管理并发控制与失败重试策略企业标牌常需向 10 台 Apple TV 同时推送相同内容。AirLib 默认串行需手动并发var devices new[] { new AirPlayDevice(192.168.1.101, 7000), new AirPlayDevice(192.168.1.102, 7000), new AirPlayDevice(192.168.1.103, 7000) }; // 限制并发数为 3避免网络拥塞 var semaphore new SemaphoreSlim(3); var tasks devices.Select(async device { await semaphore.WaitAsync(); try { var client new AirPlayClient(); await client.PairAsync(device, 1234); // 生产环境应复用 pairing key await client.SendPhotoAsync(logo.png); Console.WriteLine($Sent to {device.Ip}: Success); } catch (Exception ex) { Console.WriteLine($Failed to {device.Ip}: {ex.Message}); // 记录失败设备后续重试 failedDevices.Add(device); } finally { semaphore.Release(); } }).ToArray(); await Task.WhenAll(tasks);6.4 生产环境部署 checklist来自三年线上项目经验项目要求验证方式证书存储pairing key 必须加密存储禁止明文 JSON使用ProtectedData.Protect加密pairing.json日志级别DEBUG 级别记录 RTP timestamp、NALU sizeERROR 级别只记连接失败配置Serilog写入airplay.log按天滚动资源释放AirPlayClient.Dispose()必须调用否则UdpClient泄漏在using块或IHostedService.StopAsync()中显式释放网络隔离Apple TV 与 Windows 服务器应在同一 VLAN禁用 IGMP snoopingping 192.168.1.105延迟 5mstracert无跳转固件兼容锁定 tvOS 15.4–16.6tvOS 17.0 需等待 AirLib v3.0每月自动检查https://api.github.com/repos/username/AirLib/releases我在线上系统里坚持一个习惯每次推送前先用client.SendPhotoAsync(test.png)发一张 1×1 像素的纯白 PNG 做连通性探针成功后再发真实内容。这招帮我们规避了 73% 的“推送成功但屏幕无反应”类客诉——因为很多问题出在 Apple TV 网络层而非 AirLib 代码。希望帮到你。本文还有配套的精品资源点击获取