iOS-DLNA(UPnP-GENA)

简介

服务运转时,可能改动有些状况信息变量的值,这是需求及时地更新给控制点。因而控制点可以经过订阅操作,让服务经过发送事情音讯来发布更新。

事情音讯包含一个或多个状况变量以及他们的当时数值。这些音讯也是采用XML格式,遵从通用事情告诉体系GENA规定。

服务运转过程中,该服务的服务描述文件SDD状况变量 <stateVariable>产生了改变并且该变量的<sendEvents>特点为yes时,将会产生一个事情(Event)音讯。如该状况变量的<multicast>特点为yes,则该服务把这个事情音讯向整个网进行多播(Multicast)。假如为no或许不存在这个特点,则经过单播(Unicast)给订阅者发送音讯。

单播事情音讯的订阅及推送是遵从通用事情告诉结构(General Event Notification Architecture)协议。协议中控制点通常是个订阅者(Subscriber),它向服务供给者(通常是某个设备上的服务)发送订阅音讯(SUBSCRIBE),树立订阅关系,然后可以持续更新订阅音讯(Renewal),或许最终退订音讯(Cancel)。另外UPnPGENA进行了一些扩展,如在事情音讯中增加了一个key,来表明事情的次序。

过程如下

iOS-DLNA(UPnP-GENA)

由于涉及到了需求服务接受事情音讯回调,因而我们需求使用框架[GCDWebServer]进行创立本地的HTTP server

订阅

事情订阅说白了就是给某个服务的订阅 URL<eventSubURL>发送一条包含回调 URL<Callback URL>订阅期限 <duration>的订阅恳求。

恳求信息如下

SUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1
HOST: xxx.xxx.xx.xx:xxxxx
USER-AGENT: iOS/15.0 UPnP/1.1 SCDLNA/1.0
CALLBACK: <http://xxxx.xxxx.x.xxx:xxxx/dlna/callback>
NT: upnp:event
TIMEOUT: Second-3600    // 订阅期限

恳求路径为设备描述文档中<service></service>标签对中的<eventSubURL>
恳求域名为SSDP协议发现的设备信息中的LOCATION

  • SUBSCRIBEHTTPMethod
  • CALLBACK:告诉回调地址
  • NT:固定为upnp:event

响应

假如订阅成功,则服务30s内回来如下的响应。其中SID为订阅标识符,有必要以uuid开头。订阅成功后需求保存,后续续订和撤销订阅均需求供给该标识符。

/// 成功
HTTP/1.1 200 OK
Server: Linux/3.10.33 UPnP/1.0 IQIYIDLNA/iqiyidlna/NewDLNA/1.0
SID: uuid:f392-a153-571c-e10b
Content-Type: text/html; charset="utf-8"
TIMEOUT: Second-3600
/// 失败
HTTP/1.1 error code errordescrioption
Server: OS/Version UPnP/1.1 product/version
SID: uuid:subscibe-UUID
Content-Length: 0

中心代码如下

/// 注意前提是webServer现已创立成功,serverURL地址现已存在
/// 订阅指定服务的状况响应告诉
- (void)subscribeEventNotificationForService:(CLUPnPDevice * _Nonnull)service response:(void (^ _Nullable)(NSString * _Nullable subscribeID, NSURLResponse * _Nullable response, NSError * _Nullable error))responseBlock {
    NSString *url = nil;
    NSString *eventSubURL = service.AVTransport.eventSubURL;
    if ([eventSubURL hasPrefix:@"/"]) {
        url = [NSString stringWithFormat:@"%@%@", service.URLHeader, eventSubURL];
    } else {
        url = [NSString stringWithFormat:@"%@/%@", service.URLHeader, eventSubURL];
    }
    NSString *str = self.webServer.serverURL.absoluteString;
    if ([str hasSuffix:@"/"]) {
        str = [str substringToIndex:str.length-1];
    }
    NSString *webServerURL = [NSString stringWithFormat:@"<%@%@>", str, SERVER_PATH];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url.stringByRemovingPercentEncoding]];
    request.HTTPMethod = @"SUBSCRIBE";
    [request addValue:webServerURL forHTTPHeaderField:@"CALLBACK"];
    [request addValue:@"upnp:event" forHTTPHeaderField:@"NT"];
    [request addValue:@"Infinite" forHTTPHeaderField:@"TIMEOUT"];
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSString *sid = nil;
    if (error == nil) {
        NSHTTPURLResponse *resp = (NSHTTPURLResponse *)response;
        if (resp.statusCode == 200) {
            sid = resp.allHeaderFields[@"SID"] ? resp.allHeaderFields[@"SID"] : nil;
        }
    } 
    if (responseBlock) {
        responseBlock(nil, response, error);
    }
    }] resume];
}

续订、撤销订阅

假如需求续订某个服务,则有必要在订阅期限过期前,将续订音讯发往服务器进行续订。

续订

SUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1
HOST: xxx.xxx.xx.xx:xxxxx
SID: uuid:subscibe-UUID
TIMEOUT: Second-3600  

撤销订阅

UNSUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1
HOST: xxx.xxx.xx.xx:xxxxx
SID: uuid:subscibe-UUID

单播事情音讯

当服务器上的状况变量产生变数时,经过单播给订阅者发送告诉。单播经过HTTP协议发送。需求在本地运转一个HTTP Server来接受恳求。

单播音讯格式如下

/// 播映
<?xml version="1.0" encoding="UTF-8"?>
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
    <e:property>
        <LastChange>
            <Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">
                <InstanceID val="0">
                    <TransportState val="PLAYING"/>
                </InstanceID>
            </Event>
        </LastChange>
    </e:property>
</e:propertyset>
/// 中止
<?xml version="1.0" encoding="UTF-8"?>
<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">
    <e:property>
        <LastChange>
            <Event xmlns="urn:schemas-upnp-org:metadata-1-0/AVT/">
                <InstanceID val="0">
                    <TransportState val="STOPPED"/>
                </InstanceID>
            </Event>
        </LastChange>
    </e:property>
</e:propertyset>

有些设备回来的xml<>被转义,导致解析时候犯错。所以需求先反转义,然后再解析。

中心代码如下

/// 启动Server
- (void)start {
    if (self.webServer == nil) {
        self.webServer = [[GCDWebServer alloc] init];
        __weak typeof(self) weakSelf = self;
        //(Asynchronous version) The handler returns immediately and calls back GCDWebServer later with the generated HTTP response
        [weakSelf.webServer addHandlerForMethod:@"NOTIFY" path:SERVER_PATH requestClass:[GCDWebServerDataRequest class] asyncProcessBlock:^(__kindof GCDWebServerRequest *request, GCDWebServerCompletionBlock completionBlock) {
            // Do some async operation like network access or file I/O (simulated here using dispatch_after())
            GCDWebServerDataRequest *req = (GCDWebServerDataRequest *)request;
            __strong typeof(self) strongSelf = weakSelf;
            if (req.hasBody && strongSelf) {
                [strongSelf parseEventNotificationMessage:req.data];
            }
            GCDWebServerDataResponse* response = [GCDWebServerDataResponse responseWithHTML:@"<html><body><p>Hello</p></body></html>"];
            if (completionBlock) {
                completionBlock(response);
            }
        }];
        [self.webServer startWithPort:LOCAL_SERVER_PORT bonjourName:nil];
    }
}
/// 告诉接受解析
- (void)parseEventNotificationMessage:(NSData *)data {
    if (data == nil) {
        return;
    }
    NSDictionary *dictData = [NSDictionary dictionaryWithXMLData:data];
    NSString *lastChange = [dictData stringValueForKeyPath:@"e:property.LastChange"];
    if (lastChange == nil || [lastChange isKindOfClass:[NSNull class]] || lastChange.length <= 0) {
        return;
    }
    NSDictionary *eproperty = [NSDictionary dictionaryWithXMLString:lastChange];
    NSString *transportstate = [eproperty stringValueForKeyPath:@"InstanceID.TransportState._val"] ? [eproperty stringValueForKeyPath:@"InstanceID.TransportState._val"] : [eproperty stringValueForKeyPath:@"InstanceID.TransportState.val"];
    if (transportstate == nil || [transportstate isKindOfClass:[NSNull class]] || transportstate.length <= 0) {
        return;
    }
    /// 处理transportstate,这里的transportstate为PAUSED_PLAYBACK、PLAYING、STOPPED、TRANSITIONING等
}