UPdP網路中,控制點和服務之間使用簡單物件訪問協議(Simple Object Access Protocol,SOAP)
根據基於DLNA實現iOS,Android投屏:SSDP發現裝置收到裝置描述文件(DDD)和服務描述文件(SDD),通過解析DDD獲取 <controlURL>
控制點可以知道該裝置上某個服務的控制點地址。再通過解析 DDD 中 <action>
中的 <name>
和 <argumentList>
獲取該服務動作的動作名稱,引數要求。控制點向 controlURL
發出服務呼叫資訊,表明動作名稱和相應引數來呼叫相應的服務。
SOAP簡單物件訪問協議
控制點和服務之間使用簡單物件訪問協議(Simple Object Access Protocol,SOAP)的格式。SOAP 的底層協議一般也是HTTP。在 UPnP 中,把 SOAP 控制/響應資訊分成 3 種: UPnP Action Request、UPnP Action Response-Success 和 UPnP Action Response-Error。SOAP 和 SSDP 不一樣,所使用的 HTTP 訊息是有 Body 內容,Body 部分可以寫想要呼叫的動作,叫做 Action invocation,可能還要傳遞引數,如想播放一個網路上的視訊,就要把視訊的URL傳過去;服務收到後要 response ,回答能不能執行呼叫,如果出錯則返回一個錯誤程式碼。
動作呼叫(UPnP Action Request)
使用POST方法傳送控制訊息的格式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
POST <control URL> HTTP/1.0 Host: hostname:portNumber Content-Lenght: byte in body Content-Type: text/xml; charset="utf-8" SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName" <!--必有欄位--> <?xml version="1.0" encoding="utf-8"?> <!--SOAP必有欄位--> <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <!--Body內部分根據不同動作不同--> <!--動作名稱--> <u:actionName xmlns:u="urn:schemas-upnp-org:service:serviceType:v"> <!--輸入引數名稱和值--> <argumentName>in arg values</argumentName> <!--若有多個引數則需要提供--> </u:actionName> </s:Body> </s:Envelope> |
- control URL: 基於DLNA實現iOS,Android投屏:SSDP發現裝置 中提到的
裝置描述檔案
中urn:upnp-org:serviceId:AVTransport
服務的<controlURL>
- HOST: 上述伺服器的根地址和埠號。
- actionName: 需要呼叫動作的名稱,對應相應服務的
服務描述檔案<SCPDURL>
中的<action>
的<name>
欄位。 - argumentName: 輸入引數名稱,對應相應服務的
服務描述檔案<SCPDURL>
中的<action>
<argument>
<name>
欄位。 - in arg values: 輸入引數值,具體的可以通過 ,可以通過
服務描述檔案<SCPDURL>
<action>
<relatedStateVariable>
提到的狀態變數來得知值得型別。 - urn:schemas-upnp-org:service:serviceType:v:對應該
裝置描述檔案
相應服務的<serviceType
欄位。
動作響應(UPnP Action Response-Succes)
收到控制點發來的動作呼叫請求後,裝置上的服務必須執行動作呼叫。,並在 30s 內響應。如果需要超過 30s 才能完成執行的動作,則可以先返回一個應答訊息,等動作執行完成再利用事件機制返回動作響應。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
HTTP/1.0 200 OK // 響應成功響應頭 Content-Type: text/xml; charset="utf-8" Date: Tue, 01 Mar 2016 10:00:36 GMT+00:00 Content-Length: byte in body <?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <!--之前部分為固定欄位--> <!--之前部分為固定欄位--> <u:actionNameResponse xmlns:u="urn:schemas-upnp-org:service:serviceType:v"> <!--輸出變數名稱和值--> <arugumentName>out arg value</arugumentName> <!--若有多個輸出變數則繼續寫,沒有可以不存在輸出變數--> </u:actionNameResponse> </s:Body> </s:Envelope> |
- actionNameResponse: 響應的動作名稱
- arugumentName: 當動作帶有輸出變數時必選,輸出變數名稱
- out arg values: 輸出變數名稱值
動作錯誤響應(UPnP Action Response-Succes)
如果處理動作過程中出現錯誤,則返回一個一下格式的錯誤響應。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
HTTP/1.0 500 Internal Server Error // 響應成功響應頭 Content-Type: text/xml; charset="utf-8" Date: Tue, 01 Mar 2016 10:00:36 GMT+00:00 Content-Length: byte in body <?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:Fault> <!--之前部分為固定欄位--> <faultcode>s:Client</faultcode> <faultstring>UPnPError</faultstring> <detail> <UPnPError xmlns="urn:schemas-upnp-org:control-1-0"> <errorCode>402</errorCode> <errorDescription>Invalid or Missing Args</errorDescription> </UPnPError> </detail> </u:actionNameResponse> </s:Body> </s:Envelope> |
- faultcode: SOAP規定使用元素,呼叫動作遇到的錯誤型別,一般為s:Client。
- faultstring: SOAP規定使用元素,值必須為 UPnPError。
- detail: SOAP規定使用元素,錯誤的詳細描述資訊。
- UPnPError: UPnP規定元素。
- errorCode: UPnP規定元素,整數。詳見下表。
- errorDescription: UPnP規定元素,簡短錯誤描述。
errorCode | errorDescription | 描述 |
---|---|---|
401 | Invalid Action | 這個服務中沒有該名稱的動作 |
402 | Invalid Args | 引數資料錯誤 not enough in args, too many in arg, no in arg by that name, one or more in args 之一 |
403 | Out of Sycs | 不同步 |
501 | Action Failed | 可能在當前服務狀態下返回,以避免呼叫此動作 |
600 ~ 699 | TBD | 一般動作錯誤,由 UPnP 論壇技術委員會定義 |
700 ~ 799 | TBD | 面向標準動作的特定錯誤,由 UPnP 論壇工作委員會定義 |
800 ~ 899 | TBD | 面向非標準動作的特定錯誤,由 UPnP 廠商會定義 |
投屏基本命令及其響應
所有命令以發向 基於DLNA實現iOS,Android投屏:SSDP發現裝置 發現的裝置。除了網址以外,其餘部分均不需要修改。
所有動作請求使用 POST
請求傳送,並且請求Header均如下所示,其中:
- control URL: 基於DLNA實現iOS,Android投屏:SSDP發現裝置 中提到的
裝置描述檔案
中urn:upnp-org:serviceId:AVTransport
服務的<controlURL>
。 - HOST: 上述伺服器的根地址和埠號。
- urn:schemas-upnp-org:service:serviceType:v:對應相應裝置的
裝置描述檔案
相應服務的<serviceType
欄位。 - actionName: 需要呼叫動作的名稱,對應相應服務的
服務描述檔案<SCPDURL>
中的<action>
的<name>
欄位。
12345POST /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/action HTTP/1.0Host: 192.168.1.243:46201Content-Length: byte in bodyContent-Type: text/xml; charset="utf-8"SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName"
下面請求和響應均忽略Header,引數列表中列出Header的SOAPACTION值
設定播放資源URI
動作請求
設定當前播放視訊動作統一名稱為 SetAVTransportURI
。 需要傳遞引數有
- InstanceID:設定當前播放時期時為 0 即可。
- CurrentURI: 播放資源URI
- CurrentURIMetaData: 媒體meta資料,可以為空
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#SetAVTransportURI”
有些裝置傳遞播放URI後就能直接播放,有些裝置設定URI後需要傳送播放命令,可以在接收到 SetAVTransportURIResponse
響應後呼叫播放動作來解決。
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"> <InstanceID>0</InstanceID> <CurrentURI>http://125.39.35.130/mp4files/4100000003406F25/clips.vorwaerts-gmbh.de/big_buck_bunny.mp4</CurrentURI> <CurrentURIMetaData /> </u:SetAVTransportURI> </s:Body> </s:Envelope> |
響應
1 2 3 4 5 6 |
<?xml version="1.0" encoding="UTF-8"?> <s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"/> </s:Body> </s:Envelope> |
播放
動作請求
播放視訊動作統一名稱為 Play
。 需要傳遞引數有
- InstanceID:設定當前播放時期時為 0 即可。
- Speed:播放速度,預設傳 1 。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#Pause”
123456789<?xml version="1.0" encoding="utf-8" standalone="no"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Speed>1</Speed></u:Play></s:Body></s:Envelope>
響應
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1" /> </s:Body> </s:Envelope> |
暫停
動作請求
暫停視訊動作統一名稱為 Pause
。 需要傳遞引數有
- InstanceID:設定當前播放時期時為 0 即可。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#Pause”
12345678<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Speed>1</Speed></u:Play></s:Body></s:Envelope>
響應
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1" /> </s:Body> </s:Envelope> |
獲取播放進度
動作請求
獲取播放進度動作統一名稱為 GetPositionInfo
。 需要傳遞引數有
- InstanceID:設定當前播放時期時為 0 即可。
- MediaDuration: 可以為空。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#MediaDuration”
123456789<?xml version="1.0" encoding="utf-8" standalone="no"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetPositionInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><MediaDuration /></u:GetPositionInfo></s:Body></s:Envelope>
響應
獲取播放進度響應中包含了比較多的資訊,其中我們主要關心的有一下三個:
- TrackDuration: 目前播放視訊時長
- RelTime: 真實播放時長
- AbsTime: 相對播放時長
注:目前為止還沒發現 RelTime
AbsTime
和不一樣的情況,選用 RelTime
就ok。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:GetPositionInfoResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"> <Track>0</Track> <TrackDuration>00:04:32</TrackDuration> <TrackMetaData /> <TrackURI /> <RelTime>00:00:07</RelTime> <AbsTime>00:00:07</AbsTime> <RelCount>2147483647</RelCount> <AbsCount>2147483647</AbsCount> </u:GetPositionInfoResponse> </s:Body> </s:Envelope> |
跳轉至特定進度或視訊
動作請求
跳轉到特定的進度或者特定的視訊(多個視訊播放情況),需要呼叫 Seek
動作,傳遞引數有:
- InstanceID: 一般為 0 。
- Unit:REL_TIME(跳轉到某個進度)或 TRACK_NR(跳轉到某個視訊)。
- Target: 目標值,可以是 00:02:21 格式的進度或者整數的 TRACK_NR。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#Seek”
12345678910<?xml version="1.0" encoding="utf-8" standalone="no"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:Seek xmlns:u="urn:schemas-upnp-org:service:AVTransport:1"><InstanceID>0</InstanceID><Unit>REL_TIME</Unit><Target>00:02:21</Target></u:Seek></s:Body></s:Envelope>
響應
1 2 3 4 5 6 |
<?xml version="1.0" encoding="utf-8" standalone="no"?> <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"> <s:Body> <u:SeekResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1" /> </s:Body> </s:Envelope> |
iOS實現
需要用到庫
- AEXML – 輕量 XML 庫,用於構造和解析XML
構造動作XML
首先利用 AEXML 構造動作 XML 部分。由於所有動作結構相似,寫了個構造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private func prepareXMLFileWithCommand(command:AEXMLElement) -> String { // 建立 AEXMLDocument 例項 let soapRequest = AEXMLDocument() // 設定XML外層 let attributes = [ "xmlns:s" : "http://schemas.xmlsoap.org/soap/envelope/","s:encodingStyle" : "http://schemas.xmlsoap.org/soap/encoding/"] let envelope = soapRequest.addChild(name: "s:Envelope", attributes: attributes) let body = envelope.addChild(name: "s:Body") // 把 command 新增到 XML 中間 body.addChild(command) return soapRequest.xmlString } |
根據不同動作構造 XML ,比如 傳遞URI
和 播放動作
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 |
/** 投屏 - parameter URI: 視訊URL */ func SetAVTransportURI(URI:String) { let command = AEXMLElement("u:SetAVTransportURI",attributes: ["xmlns:u" : "urn:schemas-upnp-org:service:AVTransport:1"]) command.addChild(name: "InstanceID", value: "0") command.addChild(name: "CurrentURI", value: URI) command.addChild(name: "CurrentURIMetaData") let xml = self.prepareXMLFileWithCommand(command) self.sendRequestWithData(xml,action: "SetAVTransportURI") } /** 播放視訊 */ func Play() { let command = AEXMLElement("u:Play",attributes: ["xmlns:u" : "urn:schemas-upnp-org:service:AVTransport:1"]) command.addChild(name: "InstanceID", value: "0") command.addChild(name: "Speed", value: "1") let xml = self.prepareXMLFileWithCommand(command) self.sendRequestWithData(xml,action: "Play") } |
傳送動作請求
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 |
private func sendRequestWithData(xml:String, action:String) { let request = NSMutableURLRequest(URL: NSURL(string: controlURL)!) // 使用 POST 請求傳送動作 request.HTTPMethod = "POST" request.addValue("text/xml", forHTTPHeaderField: "Content-Type") // 新增SOAPAction動作名稱 request.addValue("\(service.serviceId)#\(action)", forHTTPHeaderField: "SOAPAction") request.HTTPBody = xml.dataUsingEncoding(NSUTF8StringEncoding) 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("statusCode should be 200, but is \(httpStatus.statusCode)") print("response = \(NSString(data: data!, encoding: NSUTF8StringEncoding)))") } // 解析響應 self.parseRequestResponseData(data!) } task.resume() } |
解析響應
解析請求響應
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 |
private func parseRequestResponseData(data:NSData) { do { let xmlDoc = try AEXMLDocument(xmlData: data) if let response = xmlDoc.root["s:Body"].first?.children.first { switch response.name { case "u:SetAVTransportURIResponse": print("設定URI成功") //獲取播放長度 case "u:GetPositionInfoResponse": // 進度需要進一步解析。如realTime = response["RelTime"].value print("已獲取播放進度") case "u:PlayResponse": print("已播放") case "u:PauseResponse": print("已暫停") case "u:StopResponse": print("已停止") default : print("未定義響應 - \(xmlDoc.xmlString)") } } else { print("返回不符合規範 - XML:\(xmlDoc.xmlString)") } } catch { return } } |