iOS DLNA 获取设备操作状态

2,107 阅读4分钟

最近一直在搞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),取得值。

首先处理&lt;&gt;,主要是为了兼容场景。

NSString *originStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSString *tranferLeft = [originStr stringByReplacingOccurrencesOfString:@"&lt;" withString:@"<"];
NSString *result = [tranferLeft stringByReplacingOccurrencesOfString:@"&gt;" 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是如何实现的,好想知道。

上面的文章,有问题或者有💡的,欢迎积极回复留言啦。有自己实现轮询的,欢迎分享一下方案。