服务运行时,可能改变有些状态信息变量的值,这是需要及时地更新给控制点。因此控制点可以通过订阅操作,让服务通过发送事件消息来发布更新。
事件消息包括一个或多个状态变量以及他们的当前数值。这些消息也是采用 XML 格式,遵循通用事件通知体系 GENA 规定。
服务运行过程中,该服务的 服务描述文件SDD
中 状态变量 <stateVariable>
发生了变化并且该变量的 <sendEvents>
属性为 yes
时,将会产生一个事件(Event)消息。如该状态变量的 <multicast>
属性为 yes
,则该服务把这个事件消息向整个网进行多播(Multicast)。如果为 no
或者不存在这个属性,则通过单播(Unicast)给订阅者发送消息。
单播事件消息的订阅及推送是遵循通用事件通知结构(General Event Notification Architecture,GENA)协议。协议中,控制点通常是个订阅者(Subscriber),它向服务提供者(通常是某个设备上的服务)发送订阅消息(SUBSCRIBE),建立订阅关系,然后可以继续更新订阅消息(Renewal),或者最后退订消息(Cancel)。另外,UPnP对GENA进行了一些扩展,如在事件消息中增加了一个key,来表示事件的顺序。
事件订阅和通知过程如下。
订阅 事件订阅说白了就是给某个服务的 订阅 URL<eventSubURL>
发送一条包含 回调 URL<Callback URL>
和 订阅期限 <duration>
的订阅请求。
以 设备描述文档 DDD
中描述 AVTransport
服务的片段例,默认其 HOST: 192.168.1.243:46201
1 2 3 4 5 6 7 <service > <serviceType > urn:schemas-upnp-org:service:AVTransport:1</serviceType > <serviceId > urn:upnp-org:serviceId:AVTransport</serviceId > <controlURL > /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/action</controlURL > <eventSubURL > /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event</eventSubURL > <SCPDURL > /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/desc.xml</SCPDURL > </service >
订阅请求 上述服务的订阅请求如下,其中注意点就是 回调URL CALLBACK
必须带有 <>
否则回调不成功。为了接受回调还需要手机上运行一个 HTTP Server
,具体实现请看下一部分。
1 2 3 4 5 6 SUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1 HOST : 192.168.1.243:46201USER-AGENT : iOS/9.2.1 UPnP/1.1 SCDLNA/1.0CALLBACK : <http://192.168.1.100:5000/dlna/callback>NT : upnp:eventTIMEOUT : Second-3600 // 订阅期限
订阅响应 成功响应 如果订阅成功,则服务 30s 内返回如下的响应。其中 SID
为订阅标识符,必须以uuid开头。订阅成功后需要保存,后续续订和取消订阅均需要提供该标识符。此外还需要保存订阅期限 TIMEOUT: Second-3600
1 2 3 4 5 6 HTTP/1.1 200 OKServer : Linux/3.10.33 UPnP/1.0 IQIYIDLNA/iqiyidlna/NewDLNA/1.0SID : uuid:f392-a153-571c-e10bContent-Type : text/html; charset="utf-8"TIMEOUT : Second-3600Date : Thu, 03 Mar 2016 19:01:42 GMT
订阅失败 若订阅失败,发布者必须返回一个订阅失败响应。格式如下:
1 2 3 4 5 HTTP/1.1 error code errordescrioption Server : OS/Version UPnP/1.1 product/versionSID : uuid:subscibe-UUIDContent-Length : 0Date : Thu, 03 Mar 2016 19:01:42 GMT
iOS实现 用Swift实现的订阅请求如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func subscribe () { let url = "192.168.1.243:46201" + "/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event" let request = NSMutableURLRequest (URL: NSURL (string: url)! ) request.HTTPMethod = "SUBSCRIBE" request.addValue("iOS/9.2.1 UPnP/1.1 SCDLNA/1.0" , forHTTPHeaderField: "User-Agent" ) request.addValue("<http://192.168.1.100:5000/dlna/callback>" , forHTTPHeaderField: "CALLBACK" ) request.addValue("upnp:event" , forHTTPHeaderField: "NT" ) request.addValue("Second-3600" , forHTTPHeaderField: "TIMEOUT" ) let task = NSURLSession .sharedSession().dataTaskWithRequest(request) { data, response, error in guard error == nil && data != nil else { print ("error=\(error) " ) return } if let httpStatus = response as? NSHTTPURLResponse where httpStatus.statusCode != 200 { print ("Subscribe Filed With Error Code:\(httpStatus.statusCode) " ) print ("response = \(response) " ) return } if let response = response as? NSHTTPURLResponse { self .lastSubscribeSID = response.allHeaderFields["SID" ] as? String ?? "" } } task.resume() }
续订 如果需要续订某个服务,则必须在订阅期限过期前,将续订消息发往服务器进行续订。
续订请求 1 2 3 4 SUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1 HOST : 192.168.1.243:46201SID : uuid:subscibe-UUIDTIMEOUT : Second-3600 // 订阅期限
取消订阅 不需要在关注特定服务的事件时,需要向服务器发送取消订阅消息。
取消订阅请求 1 2 3 UNSUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1 HOST : 192.168.1.243:46201SID : uuid:subscibe-UUID
单播事件消息 当服务器上的状态变量发生变数时,通过单播给订阅者发送通知。单播通过 HTTP 协议发送。需要在本地运行一个 HTTP Server
来接受请求。接收事件消息成功后,只需要简单返回一个 HTTP/1.1 200 OK
作为回应即刻。
坑:有些设备返回的xml中 <
>
被转义,导致解析时候出错。所以需要先反转义,然后再解析。 单播消息格式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 NOTIFY /dlna/callback HTTP/1.0 Host: 192.168.1.100:5000 Content-Length: 325 Content-Type: text/xml; charset="utf-8" User-Agent: Neptune/1.1.3, 6 SID: uuid:ac6dce5a-6047-7862-fd41-e5596960f57a // 订阅标识符 NTS: upnp:propchange // GENA规定,必须是 upnp:propchange NT: upnp:event // GENA规定,必须是 upnp:event SEQ: 4 // 事件编号,初始值为0。 <?xml version="1.0" encoding="UTF-8" ?> <e:propertyset xmlns:e ="urn:schemas-upnp-org:event-1-0" > <e:property > <variableName > new values</variableName > </e:property > </e:propertyset >
播放消息 忽略头部的停止播放消息
1 2 3 4 5 6 7 8 9 10 11 12 <?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 >
停止播放消息 忽略头部的停止播放消息
1 2 3 4 5 6 7 8 9 10 11 12 <?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 >
iOS实现 iOS实现我用到了一下开源库
GCDWebServer - 轻量 iOS/OSX GCD的服务器框架
AEXML - 轻量 XML 解析库
创建 HTTP Server 首先需要利用 GCDWebServer 创建一个 HTTP server 接受事件消息回调。具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private func startWebServer () { let webServer = GCDWebServer () webServer.addHandlerForMethod("NOTIFY" , pathRegex: "/dlna/callback" , requestClass: GCDWebServerDataRequest .self ) { (request) -> GCDWebServerResponse ! in if let re = request as? GCDWebServerDataRequest { if re.hasBody() { self .parseNotifMassage(re.data) } } return GCDWebServerDataResponse (HTML:"<html><body><p>Hello World</p></body></html>" ) } webServer.startWithPort(8899 , bonjourName: nil ) }
创建 webServer 后,可以通过 webServer.serverURL
获取 serverURL
。 这时把 "<\(webServer.serverURL)dlna/callback>"
作为回调 URL 。按照前文给出代码进行订阅就可以收到事件消息了。
解析消息 接收到通知消息后,利用 GCDWebServer 解析 XML,获取具体的动作。目前只对播放状态做了处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private func parseNotifMassage (data :NSData ) { do { let string = (NSString (data: data, encoding: NSUTF8StringEncoding ) as! String ).reTransfer() let xmlData = string.dataUsingEncoding(NSUTF8StringEncoding )! let xml = try AEXMLDocument (xmlData: xmlData) let status = xml.root["e:property" ]["LastChange" ]["Event" ]["InstanceID" ]["TransportState" ].attributes if ! status.isEmpty { switch status.first! .1 .uppercaseString { case "TRANSITIONING" : print ("正在传输" ) case "PLAYING" : print ("播放" ) case "PAUSED_PLAYBACK" : print ("暂停播放" ) case "STOPPED" : print ("停止播放" ) default : print ("未定义动作 - \(status.first! .1 ) " ) } } else { print ("未定义XML - \(xml.xmlString) " ) } } catch { print (error) return } } extension String { func reTransfer () -> String { let re1 = self .stringByReplacingOccurrencesOfString(">" , withString: ">" ) let re2 = re1.stringByReplacingOccurrencesOfString("<" , withString: "<" ) return re2 } }