琢磨AirPlay的经历

转自:http://www.cocoachina.com/bbs/read.php?tid=103810&page=e&#a
话说应该好些人想捉摸这个吧?我也有想,只是没空啊。看着不错,转一下

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该如何交接请求等等.

转载请注明: 转自Rainbird的个人博客
   本文链接: 琢磨AirPlay的经历


相关博文

    分享到:

About rainbird

IOS攻城狮
This entry was posted in IOS开发 and tagged , , , , , , , . Bookmark the permalink.

2 Responses to 琢磨AirPlay的经历

  1. jinlong says:

    你好!小弟拜读了您对AirPlay的阐述,对AirPlay产生了浓厚的兴趣,但是一直无从下手,对AirPlay也是不是很清楚,因此想请您分享一份AirPlay协议文档,或者相关的资料给我呢?如果有开源代码可以发一份给我,那我将不胜感激
    如果您愿意可以发到我的邮箱上一份资料jinlong0906@163.com
    小弟一定会重谢您的

发表评论