服務執行時,可能改變有些狀態資訊變數的值,這是需要及時地更新給控制點。因此控制點可以通過訂閱操作,讓服務通過傳送事件訊息來發布更新。
事件訊息包括一個或多個狀態變數以及他們的當前數值。這些訊息也是採用 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:46201 USER-AGENT: iOS/9.2.1 UPnP/1.1 SCDLNA/1.0 CALLBACK: <http://192.168.1.100:5000/dlna/callback> NT: upnp:event TIMEOUT: Second-3600 // 訂閱期限 |
訂閱響應
成功響應
如果訂閱成功,則服務 30s 內返回如下的響應。其中 SID
為訂閱識別符號,必須以uuid開頭。訂閱成功後需要儲存,後續續訂和取消訂閱均需要提供該識別符號。此外還需要儲存訂閱期限 TIMEOUT: Second-3600
1 2 3 4 5 6 |
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 Date: 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/version SID: uuid:subscibe-UUID Content-Length: 0 Date: 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 } // 若訂閱成功,則儲存SID 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:46201 SID: uuid:subscibe-UUID TIMEOUT: 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:46201 SID: 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 |
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 // 轉換 request 型別為 GCDWebServerDataRequest,然後讀取請求 body if let re = request as? GCDWebServerDataRequest { if re.hasBody() { // 如果請求有 body 部分,則開始解析。 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 { // 這裡有個坑,有些裝置返回的xml中<>被轉義,導致解析時候出錯。所以需要先反轉義,然後再解析。 // reTransfer()是我寫的簡單的 String 擴充套件,具體看最後 let string = (NSString(data: data, encoding: NSUTF8StringEncoding) as! String).reTransfer() let xmlData = string.dataUsingEncoding(NSUTF8StringEncoding)! // 把 XML 轉換成 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 } } |