最近公司想要给会议室加 AirPlay 无限投影功能,使用的设备是一个叫 EZCast Pro 的无线同屏器(以下简称“棒子”),这个棒子破解了 AirPlay 协议,像小米盒子一样可以当做 Apple TV 来用。
但是问题来了,如果把所有的棒子都接入网络并且进行 AirPlay Bonjour 广播,那么每个人的手机上会搜索到上百个 AirPlay 设备,无疑大大降低了用户体验。所以负责人找到了一个国外大学的例子,在关闭 Bonjour 广播的情况下使用 AirPlay 设备,并且找到了 Demo app。这个 app 果真有用,在选好教室之后,系统的 AirPlay 菜单中真的出现了该设备,点击后还会尝试连接。于是,我们也想实现这样的效果。这样一来相当于客户端和服务端都对 AirPlay 协议进行了破解,想想都激动啊◑ω◑
大概看了下他们的代码,选好学校、地点、教室后,app 会向服务器发出一个请求,返回的是这个 AirPlay 设备的相关信息。然后 app 调用了 libappletv.a 这个库:
- (void)connectToAppleTVWithSettings:(NSDictionary *)settingsJSON andDevices:(NSArray *)devicesJSONArray;
传入的参数是上一步返回的信息,这个设备就添加成功了。敢情最核心的部分并没有开源,害得我白激动一番→_→。
所以最后还是得靠自己来实现,看了下 Demo app 的日志,传入的两个参数大概长这样:
defaultJSON
{
defaults = {
"audio_port" = 5000;
"audio_txtRecord" = {
am = "AppleTV3,2";
cn = "0,1,2,3";
da = true;
et = "0,3,5";
ft = "0x4A7FFFF7,0xE";
md = "0,1,2";
pk = ad6e2f1b089096a9f7fc665dc4fa95f36d64c78bd767ec32a1981546c5022da5;
sf = 0x4;
tp = UDP;
vn = 65537;
vs = "220.68";
vv = 2;
};
"audio_type" = "_raop._tcp.";
"video_port" = 7000;
"video_txtRecord" = {
features = "0x4A7FFFF7,0xE";
flags = 0x4;
pin = 1;
pk = ad6e2f1b089096a9f7fc665dc4fa95f36d64c78bd767ec32a1981546c5022da5;
srcvers = "220.68";
vv = 2;
};
"video_type" = "_airplay._tcp.";
}
devicesJsonArray
{
host = "appletv301.mascsa.psu.edu";
id = 41;
model = "AppleTV3,2";
name = "301 Bank of America";
};
再参考下网上的这篇博文,以及这个非官方协议文档,可以大致整理出整个过程的思路:
正常情况下是棒子进行 Bonjour 广播,在 DNS 中注册两个服务,分布对应 AirPlay 的视频和音频,现在棒子的 Bonjour 广播被关闭,就要靠自己的 iOS / MacOS 设备手动注册该设备,以达到被系统识别的目的。于是问题又来了:注册什么?怎么注册?
注册什么?根据上述分析,我们可以得知,需要注册两项 DNS 服务,类型分别为 _airplay._tcp.
和 _raop._tcp.
,分别对应 AirPlay 服务的视频和音频,名称分别为 name
和 deviceid@name
,其中 name
可以任意取,deviceid
在此处需去除冒号分隔符。每个服务又需要更新对应的 TXT 记录,这个 TXT 记录可以通过抓棒子开启广播时发的包来获取,格式大致如上面的 video_txtRecord
和 audio_txtRecord
,也可参考那篇 CSDN 博文,不过其中 pk
字段实测必不可少,虽然这个字段的内容可以随意更改。pk
字段在那份非官方协议文档中也没有找到解释,并不知道真正的含义。
怎么注册?苹果提供了两种与 DNS 有关的方案,封装程度由低到高分别是 DNSService
(一套 C 语言库)和 NSNetService
(一套 Objective-C 库),NSNetService
用起来很方便,但是有一个致命的缺点,就是它不支持指定 host,也就是说发布的只能是自己的服务,而我们的需求是要发布一个模拟棒子发出的服务,host 应该为这个棒子,因此,最后只能选择 DNSService
库。对于 DNSService
库,苹果官方有一个对其进行了简单封装的 Objective-C 类 DNSSDObjects,对这个类再进行修改,使其符合我们的需求。其中更新 TXT 记录的部分拿出来进行额外说明:
char rdata[1024];
int index = 0;
for (NSString *key in txtRecord.allKeys) {
NSString *keyValuePair = [NSString stringWithFormat:@"%@=%@", key, txtRecord[key]];
index += sprintf(rdata + index, "%c%s", (int)[keyValuePair length], [keyValuePair UTF8String]);
}
DNSServiceUpdateRecord(sdRef, NULL, flags, index, rdata, 0);
合法的 TXT 记录使用的是连续的字符串,每条记录的格式是 %c%s=%s
,其中,%s=%s
是 key=value
的格式,前面的 %c
是用一个 char 字符表示出后面键值对字符串的总长度。
注册流程大致如此,这里只贴出来关键代码:
#define VideoDomain @"local."
#define VideoPort 7000
#define VideoType @"_airplay._tcp."
#define AudioDomain @"local."
#define AudioPort 47000
#define AudioType @"_raop._tcp."
NSDictionary *videoTXT = @{
@"deviceid": _deviceID,
@"features": @"0x0A7FEFF3",
@"flags": @"0x4",
@"model": @"AppleTV3,2",
@"srcvers": @"220.68",
@"vv": @"2",
@"pk": @"60ff9700b9e44cfb85b5577b5b34b431ca5a638142cae7f44ad36a4bb939133c",
};
NSDictionary *audioTXT = @{
@"cn": @"0,1,3",
@"da": @"true",
@"et": @"0,3,5",
@"ft": @"0x0A7FEFF3",
@"md": @"0,1,2",
@"tp": @"UDP",
@"vn": @"65537",
@"vs": @"220.68",
@"am": @"AppleTV3,2",
@"vv": @"2",
@"sf": @"0x4",
@"pk": @"60ff9700b9e44cfb85b5577b5b34b431ca5a638142cae7f44ad36a4bb939133c",
};
videoRegistration = [[DNSSDRegistration alloc] initWithDomain:VideoDomain type:VideoType name:_name host:_address port:VideoPort txtRecord:videoTXT];
[videoRegistration start];
audioRegistration = [[DNSSDRegistration alloc] initWithDomain:AudioDomain type:AudioType name:[NSString stringWithFormat:@"%@@%@", [_deviceID stringByReplacingOccurrencesOfString:@":" withString:@""], _name] host:_address port:AudioPort txtRecord:audioTXT];
[audioRegistration start];
至此 AirPlay 服务的注册流程就差不多实现了,写了一个 iOS / MacOS软件来验证正确性,点击“连接”按钮后,在系统的 AirPlay 菜单里应该就可以看到所注册的 AirPlay 设备,还需注意一点,address
字段(也就是注册时的 host
)不能写 ip 地址,因为系统连接 AirPlay 设备时会先去请求 DNS 解析这个 address
,因此还需要向路由器的 DNS 中写入 address
和真正 ip 地址的对应关系,这样就可以实现 AirPlay 无线同屏到棒子上了。
对于同一厂商的同型号棒子,变化的参数应该只有 deviceid
和 address
了,通过调用 BDAirPlayManager
单例的
- (void)connectToAppleTVWithDevideID:(NSString *)deviceID name:(NSString *)name address:(NSString *)address andBlock:(BDAirPlayManagerResultBlock)block;
方法,可以将 AirPlay 设备手动写入系统。