UPdP网络中,控制点和服务之间使用简单对象访问协议(Simple Object Access Protocol,SOAP)
根据基于DLNA实现iOS,Android投屏:SSDP发现设备收到设备描述文档(DDD)和服务描述文档(SDD),通过解析DDD获取 <controlURL>
控制点可以知道该设备上某个服务的控制点地址。再通过解析 DDD 中 <action>
中的 <name>
和 <argumentList>
获取该服务动作的动作名称,参数要求。控制点向 controlURL
发出服务调用信息,表明动作名称和相应参数来调用相应的服务。
控制点和服务之间使用简单对象访问协议(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 ,回答能不能执行调用,如果出错则返回一个错误代码。
使用POST方法发送控制消息的格式如下
1234567891011121314151617181920 | POST <control URL> HTTP/1.0HOST: hostname:portNumberCONTENT-LENGTH: byte in bodyCONTENT-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> |
设备描述文件
中 urn:upnp-org:serviceId:AVTransport
服务的 <controlURL>
服务描述文件<SCPDURL>
中的<action>
的 <name>
字段。服务描述文件<SCPDURL>
中的<action>
<argument>
<name>
字段。服务描述文件<SCPDURL>
<action>
<relatedStateVariable>
提到的状态变量来得知值得类型。设备描述文件
相应服务的<serviceType
字段。收到控制点发来的动作调用请求后,设备上的服务必须执行动作调用。,并在 30s 内响应。如果需要超过 30s 才能完成执行的动作,则可以先返回一个应答消息,等动作执行完成再利用事件机制返回动作响应。
1234567891011121314151617 | HTTP/1.0 200 OK // 响应成功响应头CONTENT-TYPE: text/xml; charset="utf-8" Date: Tue, 01 Mar 2016 10:00:36 GMT+00:00CONTENT-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> |
如果处理动作过程中出现错误,则返回一个一下格式的错误响应。
123456789101112131415161718192021 | HTTP/1.0 500 Internal Server Error // 响应成功响应头CONTENT-TYPE: text/xml; charset="utf-8" Date: Tue, 01 Mar 2016 10:00:36 GMT+00:00CONTENT-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> |
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均如下所示,其中:
设备描述文件
中 urn:upnp-org:serviceId:AVTransport
服务的 <controlURL>
。设备描述文件
相应服务的 <serviceType
字段。服务描述文件<SCPDURL>
中的<action>
的 <name>
字段。12345 | POST /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值
设置当前播放视频动作统一名称为 SetAVTransportURI
。 需要传递参数有
有些设备传递播放URI后就能直接播放,有些设备设置URI后需要发送播放命令,可以在接收到 SetAVTransportURIResponse
响应后调用播放动作来解决。
12345678910 | <?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> |
123456 | <?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
。 需要传递参数有
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> |
123456 | <?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
。 需要传递参数有
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> |
123456 | <?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
。 需要传递参数有
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> |
获取播放进度响应中包含了比较多的信息,其中我们主要关心的有一下三个:
注:目前为止还没发现 RelTime
AbsTime
和不一样的情况,选用 RelTime
就ok。
123456789101112131415 | <?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
动作,传递参数有:
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> |
123456 | <?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> |
需要用到库
首先利用 AEXML 构造动作 XML 部分。由于所有动作结构相似,写了个构造方法
12345678910111213 | 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
和 播放动作
1234567891011121314151617181920212223242526 | /**投屏- 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")} |
123456789101112131415161718192021222324252627 | 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()} |
解析请求响应
1234567891011121314151617181920212223242526272829 | 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 }} |
联系客服