简介
服务运转时,可能改动有些状况信息变量的值,这是需求及时地更新给控制点。因而控制点可以经过订阅操作,让服务经过发送事情音讯来发布更新。
事情音讯包含一个或多个状况变量以及他们的当时数值。这些音讯也是采用XML
格式,遵从通用事情告诉体系GENA
规定。
服务运转过程中,该服务的服务描述文件SDD
中状况变量 <stateVariable>
产生了改变并且该变量的<sendEvents>
特点为yes
时,将会产生一个事情(Event
)音讯。如该状况变量的<multicast>
特点为yes
,则该服务把这个事情音讯向整个网进行多播(Multicast
)。假如为no
或许不存在这个特点,则经过单播(Unicast
)给订阅者发送音讯。
单播事情音讯的订阅及推送是遵从通用事情告诉结构(General Event Notification Architecture
)协议。协议中控制点通常是个订阅者(Subscriber
),它向服务供给者(通常是某个设备上的服务)发送订阅音讯(SUBSCRIBE
),树立订阅关系,然后可以持续更新订阅音讯(Renewal
),或许最终退订音讯(Cancel
)。另外UPnP
对GENA
进行了一些扩展,如在事情音讯中增加了一个key
,来表明事情的次序。
过程如下
由于涉及到了需求服务接受事情音讯回调,因而我们需求使用框架[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
-
SUBSCRIBE
:HTTPMethod
-
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等
}