ios开发——AirPlay的琢磨经历

来源:互联网 发布:linux ssh登录限制ip 编辑:程序博客网 时间:2024/04/28 20:28

iOS 4.3出来的时候,苹果有了个神奇的功能airplay;它可以神奇的将iphone,ipad的音频传输到appletv, airport-express基座上;也可以将照片传输给apple tv通过HDMI投射到电视机上;
这个不亚于当年Mac os支持双屏拖拽般神奇.
那么这个技术是如何实现的呢?

历史上没被苹果收购前,有个airtunes的开源协议库,它可以实现随时随地的家庭音乐无线流媒体传输;
后来苹果收购了它,更名为airplay,将airtunes升级到airtune 2版本(但是大体基于原有版本扩充),增加了视频,照片的传输,完整的变为airplay非开源功能.
作为一个轻度影音发烧友,音效的追求是永无止境的,吃得消的支出才是合理的;而airplay作为家庭音乐的一个神器,必然想要收入囊中的.
那是不是必须要买apple TV呢?或者买airport express ?
两者价格都接近700,800;一个支持音视频;一个支持仅有音频,但是多出来一个双频路由器功能;
老实说,apple tv在美国很超值,但是无法越狱,谁会看和谐的youtube ?而且apple tv的越狱频率,远不如iphone ipad集万千宠爱于一身,砖家肯定不热衷越狱apple tv.

所以apple tv很不超值.但是,如何拥有airplay呢?
目前的现实解决方案,就是买支持airplay的音响设备;那我们目前可选的有Denon,Marantz,B&W,JBL,iHome;这些设备,我都听过;不得不说,真是蛋疼,一个个功能很挫,音质压根没达到起步水准.而面对家里的Bose135影院,我真是想瞬间给它插个airplay的翅膀.我想,很多家里已经有音响设备的,肯定希望就花一点点钱,就能实现airplay吧.

于是我就一头冲进去这airplay的世界.
好吧,科普一下,我们研究的是airplay的receiver,不是sender;所谓sender就是iphone 4s/ipad 2/3;

首先,ipad能在家庭无线网络里神奇般的搜寻到receiver;
搜到后,还能继续勾搭互联互通上;
基本推算出来的原理大概如此;

那么,搜到recevier并且确定receiver的IP;那么肯定是用了bonjour服务;其实就是mDNS这个技术;mDNS就是mini DNS的意思;公网用网址域名对应IP,那么小网络怎么办?只能寄托mDNS,即很小的微型DNS,这个功能是由路由器代替实现的;

原理如下,每当有局域网的client申请路由器代为广播,谁需要airplay服务就找他;那这个client就向路由器申请mDNS的service,提供名字,功能类型,端口,等等;
然后路由器会有一个hash表,存着各个client申请的service表;
每当有client希望找到airplay的服务的时候,它就会去问路由器,谁提供airplay服务?路由器会表里查询,并且告诉你IP;
同理,airprint也是这个类似原理;

话说前段时间做一个huawe的外包;丫的弄了个android机顶盒,要我实现airplay;结果仔细一问,就是mDNS发现机顶盒服务;然后用TCP控制机顶盒;更搞笑的是,当我正儿八经拿着mDNS的代码跟机顶盒联调的时候,居然说机顶盒没实现mDNS,真是无语国内外包的发自内心的偷懒省事,不专业.

看文档得知,Apple TV一共publish两个服务;一个是airtunes的ROAP协议;一个是airplay的service,包含照片,视频,镜像;
好吧,下面代码演练一下;

NSNetService*publish=[[NSNetService alloc]initWithDomain:@"local."type:@"_airplay._tcp."name:@"Jacob"port:7000];
[publish publish];

以上在Mac OS下运行,可以在iphone, ipad的照片上看到airplay的选项;

可惜,当我尝试如法炮制roap的airtunes的时候,无法在ipad的ipod上看到神奇的airplay按钮了.

到底出错在哪儿?端口和服务字段不对?
噢,我有个办法,有个免费的airplay的server java版程序,能在ipod看到airplay;要不来个NSnetserviceBrowser,扫描一下人家神马端口不就结了;
折腾了半天,我发现每次端口都不一样;

于是我继续网络搜罗,功夫不有心人,有个AirView,有开源,是一个ipad版的airplay receiver;这个虽然有点儿绕远了,但是好歹的确能在ipod/镜像的地方看到airplay按钮.虽然功能不行,但是好歹,证明了这个代码能实现mDNS的正确引导;

于是顺藤摸瓜:

if(airplay== nil)
airplay= [[AirPlayControlleralloc] initWithWindow:window];
[airplaystartServer];

岂不是很明显的跟进去!

- (void)publishBonjour
{
if(type)
{
netService= [[NSNetServicealloc] initWithDomain:domaintype:typename:nameport:[asyncSocketlocalPort]];
[netServicesetDelegate:self];
}
其中domain= @"local.";
[httpServersetType:@"_airplay._tcp."];
这么推测下来,我的前期准备airplay都是对的,但是仅在photo里发现,肯定有问题;最终的问题,就是看端口了;
蛋疼的是,我看到这段代码
dispatch_sync(socketQueue, ^{
// No need for autorelease pool
if(socket4FD!= SOCKET_NULL)
result= [selflocalPortFromSocket4:socket4FD];
elseif(socket6FD!= SOCKET_NULL)
result= [selflocalPortFromSocket6:socket6FD];
});

又是G_C_D,又是block,去年CC DEV大会,还记得zenny兄讲G_C_D的好处,讲openCL,当时的感觉是,这些东西,都是务虚,不落地;
每次都是拿着个例子计算1累加到多少,我当时就想,老师连举例都很难编造个实在的案例,那我们学了有嘛用?
好吧,既然现在人家高手写个代码用这两货,我就补一下课,到底实在的靠谱用一下.

Block再补课

NSString* (^calculate)(NSString*,NSString*);
int(^Multiply)(int, int);

以上是两个Block的申明定义
可以放在头文件之上,即不要放在interface definition里
也可以放在implement里,注意,不要放在函数里,否则不具备函数块内可见
上面是两个申明的block类型的变量;可以理解为一个函数指针,比如calculate,Multiply两个函数指针;

calculate=^(NSString*part1,NSString*part2)
{
return[part1 stringByAppendingFormat:part2];
};
NSString*test=calculate(@"fuck",@"U");
//test is "fuckU"
Multiply= ^(intnum1, intnum2) {
returnnum1 * num2;
};
intresult = Multiply(7, 4); // result is 28

上面是两个函数指针,最终给予赋值;
calculate等于的东西,必须要跟类型匹配上;
下面的test即可执行了这个函数;结果也验证了;

好吧,这么理解:
如果^在括号里面,那么与^同在括号内的英文字符,代表这是一个”函数指针”的概念,类似快速引用; 例如: char (^square) (int); 前面是返回值类型char,后面是参数int类型;
如果^在括号外,那么就是一个具体的block的实现函数的抬头符.之后的内容,无非是参数,大括号,实现内容;可参考square = ^(int a ) {return a*a ;};
square(5)即是25;

总之,block基本就是这样;省去了你定义一个不必要的函数,然后再调用,烦死了;还要考虑备份现场;因为block是实时运算,运算的数据全部重新拷贝一份;你可以理解为new了一个程序在沙盒里计算,怎么着都不会影响;当然了,更复杂的有__block;唉.复杂的结果,就是代码可读性差;
注意,block是一个称呼;不是关键字; ^才是关键符号;
我个人觉得,在代码里用block单词作为block的”函数指针”的,都TMD脑子有病;这不故意混淆视听么?比如,你一个用来解析json的block,你丫干脆用JsonBlock,何必故意弄个这么绕口的?
那么block适合干啥?
我现在思维禁锢,因为之前没有^,我们也活得好好的,现在唯一想出来的好处,就是偷懒,随取随用,这要这个函数没有复用的必要,那就放心大胆的用吧.省了头文件定义,冗余的格式.

再补课GrandCentralDispatch
好吧,我看完51CTO的一篇文章,我彻底懂了.这货,就是为了解决NSThread解决不了的问题的;
是啊,多线程,就是多倍任务;经典的案例就是,UI不卡死,后台处理网络数据;
而用Queue来任务排队处理,经典的应用就是,请求有各种各样的类别,A类请求在A queue里排队; B类请求在B queue排队;这里A, B不一定是网络与本地的区分,更重要的是事务的区分,有人为的概念在其中;
dispatch_queue_t 真没啥可怕的,就当做ASIHttpRequestQueue一样,创建一个队列,然后你所有的操作,都是围绕这个队列里;
你可以添加任务,终止任务,执行任务;
连执行任务函数都非常类似
startSynchronous
dispatch_async
dispatch_sync

下面我抄袭一下
声明一个队列
如下会返回一个用户创建的队列:
dispatch_queue_t myQueue =dispatch_queue_create(“com.iphonedevblog.post”, NULL);
其中,第一个参数是标识队列的,第二个参数是用来定义队列的参数(目前不支持,因此传入NULL)。
执行一个队列
如下会异步执行传入的代码:
dispatch_async(myQueue, ^{ [selfdoSomething]; });
其中,首先传入之前创建的队列,然后提供由队列运行的代码块。
声明并执行一个队列
如果不需要保留要运行的队列的引用,可以通过如下代码实现之前的功能:
dispatch_async(dispatch_queue_create(“com.iphonedevblog.post”, NULL), ^{ [self doSomething]; });
暂停一个队列
如果需要暂停一个队列,可以调用如下代码。暂停一个队列会阻止和该队列相关的所有代码运行。
dispatch_suspend(myQueue);
恢复一个队列
如果暂停一个队列不要忘记恢复。暂停和恢复的操作和内存管理中的retain和release类似。调用dispatch_suspend会增加暂停计数,而dispatch_resume则会减少。队列只有在暂停计数变成零的情况下才开始运行。dispatch_resume(myQueue);
从队列中在主线程运行代码
有些操作无法在异步队列运行,因此必须在主线程(每个应用都有一个)上运行。UI绘图以及任何对NSNotificationCenter的调用必须在主线程长进行。在另一个队列中访问主线程并运行代码的示例如下:
dispatch_sync(dispatch_get_main_queue(), ^{[self dismissLoginWindow]; });

dispatch_get_global_queue
dispatch_get_main_queue
dispatch_get_current_queue
谁能告诉我以上3个queue的区别?
言归正传,回到这段代码

dispatch_sync(socketQueue, ^{
// No need for autorelease pool
if(socket4FD!= SOCKET_NULL)
result= [selflocalPortFromSocket4:socket4FD];
elseif(socket6FD!= SOCKET_NULL)
result= [selflocalPortFromSocket6:socket6FD];
});

原来,丫的就是在socketQueue的任务队列里,寻找端口号
实际在如下函数里,传入port为0,系统会自动给你分配一个随机端口;难怪丫的每次测试端口都不一样;我去!
success = [asyncSocketacceptOnInterface:interfaceport:porterror:&err];

网上的文章,说神马airplay,airtunes,aircontroll端口分别是7000,6000,6001的,你们对得起自己良心么?NND,端口就是无关因素.

好吧,既然代码一样,参数一致,端口又是无关因素,那么到底是什么导致它的app在系统各个airplay窗口都能识别到呢?

后来想了一个很简单的,我光看了startServer,还没看初始化server的构造函数呢.
于是看到了很重要的一段,原来NSNetservice的setTXTRecordData居然非常重要;
于是一番周折,终于我自己的testApp也能让ipod,photo,mirror的地方看到神奇的自己的选项了;
代码如下

NSNetService*publish=[[NSNetService alloc]initWithDomain:@"local."type:@"_airplay._tcp."name:@"Jacob"port:56486];
NSDictionary*txtRecordDic=[NSDictionary dictionaryWithObjectsAndKeys:
@"0x7", @"features",
[DeviceInfoplatform], @"model",
[DeviceInfodeviceId], @"deviceid",
nil];
NSData*txtRecordData =nil;
if(txtRecordDic)
txtRecordData = [NSNetService dataFromTXTRecordDictionary:txtRecordDic];
[publish setTXTRecordData:txtRecordData];//very Important
[publish publish];

尽管如此,我依然很不解,为什么要是ox7 ?为什么要有features?为什么有model?deviceID?
很简单,暴力测试一下,把所有的字符串改掉;
果然结果证明,只有deviceid是必备的,不能随意修改的;而且数值也必须要符合device ID的规则,例如: 10:9A:DD:65:19:3D

于是代码压缩成如下:

NSNetService*publish=[[NSNetServicealloc]initWithDomain:@"local."type:@"_airplay._tcp."name:@"Jacob"port:56486];
NSDictionary*txtRecordDic=[NSDictionarydictionaryWithObjectsAndKeys:
[DeviceInfodeviceId], @"deviceid",
nil];
NSData*txtRecordData =nil;
if(txtRecordDic)
txtRecordData = [NSNetServicedataFromTXTRecordDictionary:txtRecordDic];
[publish setTXTRecordData:txtRecordData];//very Important
[publish publish];

好吧,研究到底告一段落,这段研究,实现了mDNS最神奇的一部分,苹果的规则,要求airplay字段,tcp类型,local局域网内,并且附带一个text,内容必须是device id,而且数值要符合规范.
接下来的几天,我将继续研究airplay的实现原理;
比如iphone/ipad搜到airplay的receiver后,必然要有业务交集;比如send一个照片,send一段音频,放一段视频,等等,我作为receiver该如何交接请求等等.

Ok,还记得new了一个G_C_DAsyncSocket,并且让他监听这个端口;
现在我用photo里选择airplay,同步一个照片显示过去;

果然,TCP的如下函数,收到了动作

- (void)socket:(G_C_DAsyncSocket*)sock didAcceptNewSocket:(G_C_DAsyncSocket*)newSocket
它把接收到的新socket的信息筛选,重新打包,Http标准信息包发了一个报文过去
if([method isEqualToString:@"GET"]&& [path isEqualToString:@"/server-info"])
{
NSString*str = @"<?xml version=\"1.0\"encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTDPLIST 1.0//EN\"\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plistversion=\"1.0\"><dict><key>deviceid</key><string>58:55:CA:06:BD:9E</string><key>features</key><integer>119</integer><key>model</key><string>AppleTV2,1</string><key>protovers</key><string>1.0</string><key>srcvers</key><string>101.10</string></dict></plist>";
NSData*response = [strdataUsingEncoding:NSUTF8StringEncoding];
HTTPDataResponse*res = [[HTTPDataResponseallocinitWithData:response];
[res setHttpHeaderValue:@"text/x-apple-plist+xml"forKey:@"Content-Type"];
return[res autorelease];
}

这段代码,显示了苹果的设计airplay的思路;
即用tcp长连接来简单/核心的控制,来回事件相应;http消息来处理内容,消息;
所以代码常常从TCP socketHttp message互相倒;

不过显然,这个代码谨慎的作者的example,也是直接发了一段截取的报文;可能临时存在成功,无法长期使用的;再深入研究下去意义不大;大体逻辑可以猜出来,我发一个照片共享请求,tcp收到请求后,http返回给我一个应答,请求服务器信息;我的手机肯定在序列号里给予了文件地址,receiver去下载,最终呈现出来;

但是这也导致airplayairtunes更隐蔽的原因,receiver的逻辑本该苹果公布的,却仅为自己所使用.
那么,还有两个,就是airtune 2airprint目前是公开给厂商的,我们是否能够拿到呢?我所知道已经实现完美的,就是XMBC, OMG,难道我又要扑入xmbc的海洋中去??

好吧,几经联系,找到了变态的德国原籍作者,此人还很狂热的在iTunesU发布了德语音频得瑟讲解,但是太悲剧,完全听不懂;

好在,我看懂了代码..哦也

注意,今儿只讲第一代开源的airtunes在实际中的原理,以及展望可以在其他平台实现的方式
那些私信问我如何实现airplay到另外一台iphone查看的人,你们消停下;个人建议你们使用gamekit来小范围内数据传输共享;
airplay是私有协议,而且仅在apple tv里有实现代码;apple tv不同于airportexpress; TV可以随时升级,而那基座很难升级;
破解airport expresskey才有意义;破解TV的结局就是随时升级,换个key,你又没有普遍实现价值了;


言归正传,airtunes如何实现;

self.netService= [[NSNetServiceallocinitWithDomain:@""type:@"_raop._tcp"name:[NSStringstringWithFormat:@"%@@%@", [selfMACAddressToRawString], self.nameport:MSShairportServerPort];
 
概括下,
_raop._tcp airtunes的类型;仅在mirroripod里能看到;
namemac地址;个人觉得这个是可选项,可自定义
port 5000 

以上是publishservice的实现;
这里我插播一下,winandroid平台上的实现方法;那就是你悲剧了;你首先要找Zeroconfig的开源库,其次你要了解神马是NetService;然后写一个基础库类似…NetWork.cpp里面支持upnp等等乱七八糟的东西;
有些平台似乎还支持度不高;Bonjour的世界里,互相识别的代码实现都很简单;但是跨到其他平台,简直是大悲剧;
NSNetservice是这么自我介绍的,Http ftp是有名的service;其实你也可以自定义service;一句话就简洁明了了;
可是win的开发库要遇到这么简介透彻的事情,简直是无法想象;
作为开发者,咱有必要去深究底层原理么?既然流行社会化大分工,那软件代码都要让别人用得畅快,而不是让开发者深入到spec,protocol的海洋里拼命苦逼;国内很多小有成就的牛人,就擅长吹牛各种苦逼学习经历,原理等等作为谈资,软件开发又不是什么都要学会,把手头里干得好,不就行了么?
这一点,苹果的理念真是让人觉得好爽
只要大家都能轻松学会的,那就是成功的设计;

好吧,下面就是进入正题了;
你的servicepublish之后,同时起来一个监听5000端口的tcpsocket连接;

当你在ipod里选择那个设备作为接收端后,点击音乐播放,你就会收到一个tcp in coming connection;
这里要吐槽一下os x世界里的tcp udp通信,咋就用的这么不习惯呢?
好好的accept,bind, listen , send ,receive不用;非丫的要玩CFNetwork,搞神马CFReadStream CFWriteStream;虽然原理大致相同,但是好不习惯;

好吧,你收到了connection,你于是接受内容呗;


OPTIO_NS * RTSP/1.0
CSeq: 0
Apple-Challenge: Sx5Dd77BJM5q1v37RhtrQg==
DACP-ID: CB33FA1059D0B517
Active-Remote: 96355174
这是一段原始数据,Optio_ns代表类型;这是第一次握手的必经过程;
经过解析后变成如下数据:
request: {
"Active-Remote" = 96355174;
"Apple-Challenge" = "Sx5Dd77BJM5q1v37RhtrQg==";
Body = "";
CSeq = 0;
"DACP-ID" = CB33FA1059D0B517;
Method = "OPTIO_NS * RTSP/1.0";
}
有没有感觉很像JSON?其实就是NSDictionary,方便键值编程判断梳理而用;后面文章只发梳理后的数据;
这是一段测试数据,我收到了,必然要回复,对不对;
经过梳理后,我返回了如下数据
RTSP/1.0 200 OK
CSeq: 0
Audio-Jack-Status: connected; type=analog
Public: ANNOUNCE, SETUP, RECORD, PAUSE, FLUSH, TEARDOWN,OPTIO_NS, GET_PARAMETER, SET_PARAMETER
Apple-Respo_nse:vWJZGiyR/a0f5QOF23ThtJZeKcBklL4BCBh9hOMLDKaF9n9C1hbV8vI5EPXLis1B7fZlYZWUzGh8ElLFA79PX28TSOyrahc6E50HdTQzUeX8Gn5riakuN7wLrPAtErytZPPOUSus0lzaB2VMMgMsHCR5XbiTlbJgNCw9UVFuGVaVyKjZMk1sCrGihYQlPihW296qNvkDLLM+WHbcCgRnuUbZ+TUQqXHu5qLupjWTUlzifCX4E+VnpudXsVYxbUz5I93+72xBzbKYBi8xKyk2kCDH2ZGxUTht2MeAV2KdjSx8ehCoqJdgkxJsDD2g/+NqsWhIdkgp9yTsixlppwG/Zg

注意,以上数据使用OpenSSL加密;key在哪儿?显然,肯定是从airportrom里挖出来的,感谢德国那变态小伙儿
描述一下以上的返回的数据键值:
CSeq:必须是接收到的CSeq的数值,保持同样;(是不是类似邮件中的那种批处理单号呢?)
Audio-Jack-Status:这键值必须是等于: connected; type=analog 
Apple-Challenge:
这个要细说,根据发过来的Apple-Challenge的数值,进行一定的处理:
1. Apple-Challenge的数值要转64
2. ipodIP地址获得,转为16进制编码,IP地址分为左块,右块,中间的IPV6自适应,依次以拉丁的编码字符串追加到第一段的后面
3. 继续将自身的IPutf8的编码追加后面
4. 再用key加密
5. base64编码形式
Public: 如果你是收到optionmethod,那你必须要回答: @"ANNOUNCE, SETUP, RECORD, PAUSE,FLUSH, TEARDOWN, OPTIO_NS, GET_PARAMETER, SET_PARAMETER"
当然了,你的头消息必须是:
@"RTSP/1.0 200 OK";

这段算是Option第一次握手的消息反馈
当你send回去后,你会立即收到第二条消息:

{
"Active-Remote" = 2751919765;
Body = "v=0
\no=AirTunes 15733635783794265891 0 IN IP4 192.168.1.102
\ns=AirTunes
\nc=IN IP4 192.168.1.102
\nt=0 0
\nm=audio 0 RTP/AVP 96
\na=rtpmap:96 AppleLossless
\na=fmtp:96 352 0 16 40 10 14 2 255 0 0 44100
\na=rsaaeskey:JudLbvhjioWkbjIAdb9+sM5MACk8gr7tnftoxC709ClcWR7P+qxrPh3roLc2CHniZvSub+9Tq5IJL4I49vZhHADiKeyxFYGipG/R/kRRzl4sGzKXpDO8infacAMWDb1Ls/7XgAa/8wRG0bSZyiFlZT4CrMLZeXmH/cZhdjWgJ7i1Ae/9QB2OGvRVytySjRpMNcxkvOLhiaK6pecL3fN7fDTD3O984ki1x93C6efr+5pIARjao5TU4yn5TR5o4/cfHEU8/XA43oMk8utZ5dzzO+/CpZ1Ur6oBeLPFt12gamUye4lvY8wvvM20M1CNORVjIJ0ffXoYCzCrqQQM4UOGNg==
\na=aesiv:1xiXS3aeXnfS2fKQ5iTR1w==
\na=min-latency:11025
\n";
CSeq = 1;
"Content-Length" = 611;
"Content-Type" = "application/sdp";
"DACP-ID" = CB33FA1059D0B517;
Method ="ANNOUNCE rtsp://192.168.1.105/15733635783794265891 RTSP/1.0";
"X-Apple-Device-ID" = 0x4cb1991cef64;
}

解释一下:
注意看到CSeq变大了;我们有理由认为, CSeq是一个序列号,用来保证在网络频繁交互的时候,所问得所答;
这一点设计不错;回想过去,我们之前的一些网络交互的应用,咋就没想到弄个序列号来控制呢?
Body可以看到很大;有很大的信息量;里面包含了aesiv的值;rsaaeskey的键值;ftmp的数值,大概意思是44100kHz的意思?
于是回复一个消息过去:
RTSP/1.0 200 OK
Audio-Jack-Status: connected; type=analog
CSeq: 1
以上基本代表,音乐服务已经搭设好了;

于是收到第三条消息
request: {
"Active-Remote" = 2751919765;
Body = "";
CSeq = 2;
"DACP-ID" = CB33FA1059D0B517;
Method ="SETUP rtsp://192.168.1.105/15733635783794265891/audio RTSP/1.0";
Transport ="RTP/AVP/UDP;unicast;mode=record;timing_port=58372;events;control_port=49885";
}

转自:http://www.cocoachina.com/bbs/read.php?tid=103810&page=e&#a


0 0
原创粉丝点击