最近一直在搞iOS端的投屏,说实话,挺费劲的。如果可以直接用乐播的sdk,就最好不过了,毕竟人家专业。
DLNA有份基本材料,可以参考DLNA 投屏相关简析 和 基于DLNA实现iOS,Android投屏:订阅事件通知,他们的文章写的比较详细。
按道理讲,投屏的话,直接在电视上播放,电视端的操作,手机不用接收,但是呢,现在的需求是,要同步。所以 我们需要 知道 电视的状态。
按照 给出的2篇文章,知道 有两种方法: 轮询 和 订阅。
轮询的话,需要我们实现一个定时任务,不断的去查询设备的播放状态和播放进度。这个方法我操作失败了,很容易造成循环,我没有想好一套机制去处理。如果有高人已经实现了,可以共享一下方案嘛。
我采用的是订阅的方式,本地起服务,监听端口。
当然,需要 GCDWebServer。对了,我们使用的库,是MRDLNA。(我直接使用的是CLUPnp的内容,库封装的一般般)。
导入GCDWebServer,pod立马实现。
启动本地server
_listener = [GCDWebServer new];
_listener.delegate = self;
WS(weakSelf);
[_listener addHandlerForMethod:@"NOTIFY" path:@"/dlna/callback" requestClass:[GCDWebServerDataRequest class] processBlock:^GCDWebServerResponse * _Nullable(__kindof GCDWebServerRequest * _Nonnull request) {
if (request && [request hasBody]) {
[weakSelf parseWebServerData:((GCDWebServerDataRequest *)request).data];
}
return [[GCDWebServerDataResponse alloc] initWithHTML:@"<html><body><p>Hello World</p></body></html>"];
}];
[_listener startWithPort:8899 bonjourName:nil];
这里,我们监听的端口是8899。
发送订阅请求给设备。
这一步,我们要向设备发送订阅信息,内容包括接收回调的地址和端口,这个地址和端口,自然而然就是我们手机端的设备。在我们启动webServer后,就已经知道了手机的地址以及监听的端口。
在订阅之前,我们首先要投屏成功,成功后,我们再订阅。关于设备的信息,都在CLUPnPDevice这个类中。
// URLHeader--电视端设备的地址,eventSubURL--事件的url
NSString *url = [NSString stringWithFormat:@"%@%@", device.URLHeader, device.AVTransport.eventSubURL];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
req.HTTPMethod = @"SUBSCRIBE";
[req addValue:@"iOS/9.2.1 UPnP/1.1 SCDLNA/1.0" forHTTPHeaderField:@"User-Agent"];
// 通过webServer,取到http://*.*.*.*:*/
[req addValue:[NSString stringWithFormat:@"<%@dlna/callback>", [_listener serverURL].absoluteString] forHTTPHeaderField:@"CALLBACK"];
[req addValue:@"upnp:event" forHTTPHeaderField:@"NT"];
[req addValue:@"Second-3600" forHTTPHeaderField:@"TIMEOUT"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error || !data || !response) {
//订阅失败
device.sid = @"";
} else {
NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
if (res.statusCode == 200) {
//订阅成功
NSString *sid = res.allHeaderFields[@"SID"] ? res.allHeaderFields[@"SID"] : @"";
self.lastSubscribedID = sid;
device.sid = sid;
} else {
device.sid = @"";
}
}
}];
[task resume];
});
这里获取到的sid的格式为uuid:**-**-**-**-**。
接收订阅信息。
主要就是处理parseWebServerData这个方法,在这个方法里,去解析xml(一般使用三方,如GDtataXML),取得值。
首先处理<和>,主要是为了兼容场景。
NSString *originStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *tranferLeft = [originStr stringByReplacingOccurrencesOfString:@"<" withString:@"<"];
NSString *result = [tranferLeft stringByReplacingOccurrencesOfString:@">" withString:@">"];
处理后,我们得到的xml的字符串:
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
<e:property>
<LastChange>
<Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:schemas-upnp-org:metadata-1-0/AVT/http://www.upnp.org/schemas/av/avt-event-v1-20060531.xsd">
<InstanceID val="0">
<TransportState val="PLAYING"/>
<CurrentTransportActions val="Play,Stop,Pause,Seek,X_DLNA_SeekTime,Next,Previous"/>
<CurrentTrackDuration val="00:23:20"/>
</InstanceID>
</Event>
</LastChange>
</e:property>
</e:propertyset>
通过👆的字符串,我们可以看到,我们只能监听到这些数据,主要还是设备的播放状态。解析出播放状态TransportState。
GDataXMLDocument *document = [[GDataXMLDocument alloc] initWithXMLString:result options:0 error:&error];
if (!error) {
GDataXMLElement *root = document.rootElement;
GDataXMLElement *property = [root elementsForName:@"e:property"].firstObject;
if (!property) {
return;
}
GDataXMLElement *lastChange = [property elementsForName:@"LastChange"].firstObject;
if (!lastChange) {
return;
}
GDataXMLElement *event = [lastChange elementsForName:@"Event"].firstObject;
if (!event) {
return;
}
GDataXMLElement *instance = [event elementsForName:@"InstanceID"].firstObject;
if (!instance) {
return;
}
GDataXMLElement *states = [instance elementsForName:@"TransportState"].firstObject;
if (!states) {
return;
}
GDataXMLNode *node = [states attributeForName:@"val"];
if (node) {
//真正做字符串比较时,最好uppercaseString一下。
[self playingStatus:[node stringValue]];
}
}
有时候控制台会打印出解析失败,应该是xml的问题,我是直接给忽略了,因为我们真正需要的信息,还是可以解析成功的。
问题来了,设备状态有了,那设备的播放进度呢?这是一个巨坑。
目前的解决方案是,获取到设备状态后,主要发送action,请求设备的播放进度。
[_render getPositionInfo];
CLUPnPAction *action = [[CLUPnPAction alloc] initWithAction:@"GetPositionInfo"];
[action setArgumentValue:@"0" forName:@"InstanceID"];
[self postRequestWith:action];
这些在三方库里,应该都有,这里就不再罗列了。
取消订阅
结束投屏或断开连接后,别忘记取消订阅。在订阅的时候,我们不是保存了一个sid吗,这时候派上用场了。
NSString *url = [NSString stringWithFormat:@"%@%@", device.URLHeader, device.AVTransport.eventSubURL];
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
req.HTTPMethod = @"UNSUBSCRIBE";
[req addValue:_lastSubscribedID forHTTPHeaderField:@"SID"];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error || !data || !response) {
//取消订阅失败
} else {
NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
if (res.statusCode == 200) {
//取消订阅成功
self.lastSubscribedID = @"";
device.sid = @"";
}
}
}];
[task resume];
});
目前,我这边就进行到这里了,同步的没有那么精确,还需要继续调整。不知道乐播sdk是如何实现的,好想知道。
上面的文章,有问题或者有💡的,欢迎积极回复留言啦。有自己实现轮询的,欢迎分享一下方案。