概述
1. DLNA简介
DLNA(Digital Living Network Alliance),即数字家庭网络联盟。 DLNA不是技术,而是一种解决方案,它是多种技术的整合,并致力于构建家庭媒体共享。 DLNA包含多种网络协议,如http、https、upnp等,其中upnp是其重要组成部分。 DLNA主要包含以下四种产品: DMS,即Digital Media Server(数字媒体服务器)的缩写,其主要作用是作为媒体内容的提供者,为DMP/DMR提供内容播放,DMS可控制提供哪些媒体内容。 DMP,即Digital Media Player(数字媒体播放器)的缩写,可搜索并播放DMS的内容,其作用相当于DMR+DMC。 DMC,即Digital Media Controller(数字媒体控制器)的缩写,可搜索并控制DMR播放DMS提供的内容,即控制DMR与DMS的交互。 DMR,即Digital Media Renderer(数字媒体渲染器)的缩写,可播放DMS提供的内容。
2.Upnp设备架构(Upnp Device Architecture)
Upnp DA将家庭网络中的角色分为三种:控制点、设备和服务。它们之间主要通过HTTP技术实现通信。设备和相关服务的规格信息使用XML方式向其他节点公布。 Upnp DA中各角色的关系如下图:
各个角色的基本概念如下:
- 根设备/设备,即硬件设备,如电脑、电视盒子等。一个设备可提供多个服务。但是家用电器内部一般都包含了多个功能不同的设备,这样由多个设备集合而成的设备集合体,被称为根设备。
- 设备所能提供的功能服务。分为控制服务、事件服务、展示服务。服务是Upnp系统中最小的可控制单元(动作与状态)。
- 控制点。即控制设备,可发现并控制其他设备。如控制其他设备的视频播放、暂停等。
在DLNA投屏的过程中,Android设备充当的角色是控制点,它初始化并配置好DMS和DMR设备之间的连接,并不直接参与真正的内容传输,内容传输由DMS和DMR完成。
3.DLNA网络传输流程
DLNA的网络传输由设备发现开始。首先,一个新的控制点(Control Point)加入局域网,那么这个Control Point就会开始搜索局域网内可用的设备了。当控制点开始搜索时,它会从UDP端口发送如下格式的搜索请求消息:
M-SEARCH * HTTP/1.1
MX: 1 //最大时间间隔数
ST: upnp:rootdevice //搜索的设备类型
MAN: "ssdp:discover"
User-Agent: iOS 10.2.1 product/version
Connection: close
Host: 239.255.255.250 //多播地址
复制代码
如果发现可用设备,则会从UDP端口收到如下响应消息:
HTTP/1.1 200 OK
Cache-control: max-age=1800
Date: Thu, 16 Feb 2017 09:09:45 GMT
EXT:
LOCATION: http://10.2.9.152:49152/TxMediaRenderer_desc.xml //URL for UPnP description for device
Server: search target
USN: uuid:3c970e3c0c0d0000_MR::upnp:rootdevice //composite identifier for the advertisment
BOOTID.UPNP.ORG: 1487062102 //number increased each time device sends an initial announce or an update message
CONFIGID.UPNP.ORG: 499354 //number used for caching description information
SEARCHPORT.UPNP.ORG: number identifies port on which device responds to unicast M-SEARCH
ST: upnp:rootdevice //device type
复制代码
可以看到,在响应消息中,LOCATION这个字段是一个url:
LOCATION: http://10.2.9.152:49152/TxMediaRenderer_desc.xml //URL for UPnP description for device
复制代码
这个url指向的是设备服务和信息描述文档,一般为xml格式。我们可以直接通过浏览器访问该链接查看该设备的相关信息:
<root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0" configId="499354">
<specVersion>
<major>1</major>
<minor>1</minor>
</specVersion>
<device>
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
<friendlyName>卧室的创维盒子Q+</friendlyName>
<manufacturer>Plutinosoft LLC</manufacturer>
<manufacturerURL>http://www.plutinosoft.com</manufacturerURL>
<modelDescription>Plutinosoft AV Media Renderer Device</modelDescription>
<modelName>AV Renderer Device</modelName>
<modelURL>http://www.plutinosoft.com/platinum</modelURL>
<UDN>uuid:9c443d47158b-dmr</UDN>
<dlna:X_DLNADOC xmlns:dlna="urn:schemas-dlna-org:device-1-0">DMR-1.50</dlna:X_DLNADOC>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
<SCPDURL>/AVTransport/9c443d47158b-dmr/scpd.xml</SCPDURL>
<controlURL>/AVTransport/9c443d47158b-dmr/control.xml</controlURL>
<eventSubURL>/AVTransport/9c443d47158b-dmr/event.xml</eventSubURL>
</service>
...
</serviceList>
</device>
</root>
复制代码
在设备信息描述文档中,有一个名为<serviceList>的节点,该节点是设备可提供服务的描述。有多个<service>节点,
在<serviceList>节点中,包含了多个在<service>节点,每一个<service>节点代表着一个服务。
<service>节点中主要包含着以下节点:
<serviceType> service的类型
<serviceId> service的ID值
<SCPDURL> 该节点的url指向服务的动作描述文档(SDD),可以直接通过浏览器访问
<controlURL>该节点为服务请求url,在发送动作请求消息时,需要将动作参数以规定格式发送给该url以获取响应消息
可以看出,<SCPDURL>节点的url并没有标出域名和端口号,因为其域名和端口号与响应消息中LOCATION字段的域名和端口号一致。如上述代码中的<SCPDURL>的完整url应为:
http://10.2.9.152:49152/AVTransport/9c443d47158b-dmr/scpd.xml
复制代码
通过浏览器访问url,可获取如下格式的内容:
<scpd xmlns="urn:schemas-upnp-org:service-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<actionList>
<action>
<name>SetAVTransportURI</name>
<argumentList>
<argument>
<name>InstanceID</name>
<direction>in</direction>
<relatedStateVariable>A_ARG_TYPE_InstanceID</relatedStateVariable>
</argument>
<argument>
<name>CurrentURI</name>
<direction>in</direction>
<relatedStateVariable>AVTransportURI</relatedStateVariable>
</argument>
<argument>
<name>CurrentURIMetaData</name>
<direction>in</direction>
<relatedStateVariable>AVTransportURIMetaData</relatedStateVariable>
</argument>
</argumentList>
</action>
...
<serviceStateTable>
<stateVariable sendEvents="no">
<name>AVTransportURI</name>
<dataType>string</dataType>
</stateVariable>
...
</serviceStateTable>
</scpd>
可以看到,文档中包含一个名为<actionList>的节点,该节点为服务的动作描述。
<actionList>中包含了多个<action>节点,每个节点对应着一个动作。
每个<action>节点中都包含着如下节点:
<name> 动作名
<argumentList> 参数列表。每个<argumentList>节点中包含了多个<argument>节点,每个<argument>节点代表着一个动作参数。一个<argument>节点包含着如下节点:
<name> 参数名
<direction> 该节点参数取值为in/out, 当<direction>的值为in时,表明这个参数的值是传入值,当为out时,表明这个参数的值是返回值,则该值会随着订阅事件一起返回给订阅者。
<relatedStateVariable> 该节点的值是一个<stateVariable>节点的映射,映射的内容可在下方<serviceStateTable>节点中找到,<stateVariable>节点包含值的类型及取值范围等信息。
复制代码
简单的来说,可以把一个action当成一个API接口,而action中的argument则可表示为API接口的参数,而service即是多个相关接口的集合。 通过上面的信息,就可以请求服务动作,实现设备控制了。开发者可根据文档中规定的请求参数,发送如下格式的请求消息:
POST /AVTransport/9c443d47158b-dmr/control.xml HTTP/1.1
HOST: 10.2.9.152
Content-Type: text/xml; charset="utf-8"
SOAPAction: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<CurrentURI>yourAVURI</CurrentURI>
</u:SetAVTransportURI>
</s:Body>
</s:Envelope>
POST 字段后面跟的是<controlURL>节点的url,HOST对应的LOCATION字段的IP。
SOAPAction的格式:"<serviceType>节点的值#请求的action的名称"
<s:Body>节点的内容是请求体,请求体的格式为:
<u:请求的action的名称 xmlns:u="<serviceType>节点的值">
<参数1>参数值</参数1>
<参数2>参数值</参数2>
...
</u:请求的action的名称>
复制代码
如果请求成功,会获取如下格式的响应消息:
HTTP/1.1 200 OK
Content-Type: text/xml; charset="utf-8"
Date: Thu, 16 Feb 2017 09:09:45 GMT
Server: OS/version UPnP/1.1 product/version
<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<u:SetAVTransportURIResponse>
<_xmlns:u>"urn:schemas-upnp-org:service:AVTransport:1"</_xmlns:u>
</u:SetAVTransportURIResponse>
</u:SetAVTransportURI>
</s:Body>
</s:Envelope>
复制代码
这样,整个传输流程就完成了。同一种类型的设备提供的服务大部分相同,但也可能有所差异,因此,在请求设备服务之前,最好先浏览其设备描述文档,查看设备支持哪些服务。
Android有如下几个用于DLNA开发的主流框架
- Cling. Cling是一个Java开源项目,开发者可直接编译源码生成jar包导入到Android项目中。目前Cling已停止维护,但这并不影响它的热度。
- Platinum. Platinum是一个C库,它支持编译成多个平台的库,如Windows、Mac、IOS和Android等。但其编译流程相对来说比较复杂,Android使用Platinum开发需要用到jni。
- CyberGarage. CyberGarage是一个Java Upnp开发包,开发者将其项目源码添加到Android工程当中,作为Android Library或者 Java Library直接使用。CyberGarage提供了jar包下载地址,但CyberGarage源码存在一些bug,需要对源码进行修改,因此不建议直接下载jar包。
CyberGarage
由于Upnp是基于xml格式通信的,因此需要先下载xml解析包以获取xml解析支持, CyberGarage支持以下几种xml解析包:
jaxp (java自带,不用下载)
XmlPullParser (Android自带,不用下载)
xerces2
kxml2
选择其中一种解析包添加到项目中,CyberGarage会在解析xml时使用该解析包,上述解析包在CyberGarage中的使用优先级是从 4 到 1。
ControlPoint
Android设备在投屏过程中主要扮演着控制点的角色。在CyberGarage项目中,与控制点相对应的类为ControlPoint类。只要创建并使用该类的实例,就能实现控制点的功能。
(1)初始化
实现初始化,只需要调用start方法即可,注意该方法要在子线程中调用:
ControlPoint controlPoint = new ControlPoint();
// 初始化
new Thread(new Runnable() {
public void run() {
controlPoint.start();
}
}).start();
复制代码
(2)搜索设备
搜索设备的方法为search方法,但与start方法一样,需要在子线程中调用:
new Thread(new Runnable() {
public void run() {
controlPoint.start();
controlPoint.search();
}
}).start();
复制代码
(3)设备通知监听
添加设备通知监听,只需实例化一个NotifyListener并实现其deviceNotifyReceived方法,然后与ControlPoint实例绑定:
controlPoint.addNotifyListener(new NotifyListener() {
@Override
public void deviceNotifyReceived(SSDPPacket packet) {
Log.i(TAG, "Got Notification from device, remoteAddress is" + packet.remoteAddress);
}
})
复制代码
(4)搜索结果监听
添加设备通知监听,则需要实例化一个SearchResponseListener并实现其deviceSearchResponseReceived方法,然后与ControlPoint实例绑定:
controlPoint.addSearchResponseListener(new SearchResponseListener() {
@Override
public void deviceSearchResponseReceived(SSDPPacket packet) {
Log.i(TAG, "A new device was searched, remoteAddress is" + packet.remoteAddress);
}
});
复制代码
(5)设备变化监听
如果需要在设备被移除/添加的时候,做一些操作,则需要实例化一个DeviceChangeListener并实现其deviceRemoved和deviceAdded方法,然后与ControlPoint实例绑定:
controlPoint.addDeviceChangeListener(new DeviceChangeListener() {
@Override
public void deviceRemoved(Device device) {
Log.i(TAG, "Device was removed, device name: " + device.friendlyName});
}
@Override
public void deviceAdded(Device device) {
Log.i(TAG, "Device was added, device name:" + device.friendlyName);
}
});
复制代码
(6)发送动作请求
要向设备发送动作请求,以实现对设备的控制,首先得获取已添加的设备(Device类)的实例。而支持投屏播放的设备的设备类型主要为DMR,deviceType的值为urn:schemas-upnp-org:device:MediaRenderer:x。因此,添加设备前要做一个对设备类型的判断:
controlPoint.addDeviceChangeListener(new DeviceChangeListener() {
@Override
public void deviceRemoved(Device device) {
if ("urn:schemas-upnp-org:device:MediaRenderer:1".equals(device.getDeviceType())) {
deviceList.remove(device);
}
}
@Override
public void deviceAdded(Device device) {
// 判断是否为DMR
if ("urn:schemas-upnp-org:device:MediaRenderer:1".equals(device.getDeviceType())) {
deviceList.add(device);
}
}
});
复制代码
这里用一个列表缓存已添加的设备,当要使用某个设备时,再从列表中获取对应实例。 获取设备实例后,需要从设备实例中根据serviceType获取Service类的实例,再从Service类实例中根据动作名获取Action类的实例,最后调用postControlAction方法发送请求。 DLNA投屏播放的服务的serviceType值为:urn:schemas-upnp-org:service:AVTransport:x; 实现播放需要发送两个动作请求:
-
SetAVTransportURI。设置播放URI。需要转入两个参数: 1. InstanceID 实例ID, 2. CurrentURI 要设置的URI。
-
Play。播放视频。需要传入一个参数: 1. InstanceID 实例ID.
因此,整个投屏播放的动作请求代码如下:
// 实例ID
String instanceID = "0";
// 播放视频地址
String currentURI = "http://hc.yinyuetai.com/uploads/videos/common/026E01578953FD0EF0E47204247B5D13.flv?sc=2d17ae37a9186da6&br=780&vid=2693509&aid=623&area=US&vst=2";
Device device = deviceList.get(0);
// 获取服务
Service service = device.getService("urn:schemas-upnp-org:service:AVTransport:1");
// 获取动作
Action transportAction = service.getAction("SetAVTransportURI");
// 设置参数
transportAction.setArgumentValue("InstanceID", instanceID);
transportAction.setArgumentValue("CurrentURI", transportURI);
// SetAVTransportURI
if(transportAction.postControlAction()) {
// 成功
Action playAction = service.getAction("Play");
playAction.setArgumentValue("InstanceID", instanceID);
// Play
if (!playAction.postControlAction()) {
Log.e("upnpErr", playAction.getStatus().getDescription());
}
} else {
// 失败
Log.e("upnpErr", transportAction.getStatus().getDescription());
}
复制代码
如果不清楚某个设备的服务和动作,则可以查看其设备描述文档和SDD,通过如下代码可以获取设备描述文档和SDD的链接地址:
// 设备描述文档
String locationUrl = device.getLocation();
// 获取服务
Service service = device.getService("urn:schemas-upnp-org:service:AVTransport:1");
URL url = new URL(locationUrl);
// SDD
String sddUrl = locationUrl的ip地址和端口号 + service.getSCPDURL();
复制代码
(7)事件订阅
如果设备在发生某些事件时,控制点需要跟着发生变化,如设备暂停播放,那么控制点的播放按钮理应变为暂停状态;则需要对设备进行事件订阅,订阅方法如下:
Device device = deviceList.get(0);
// 获取服务
Service service = device.getService("urn:schemas-upnp-org:service:AVTransport:1");
boolean ret = controlPoint.subscribe(service);
if (ret) {
// 订阅成功
} else {
// 订阅失败
}
复制代码
要监听事件回调,则需要创建一个EventListener与ControlPoint实例绑定,当设备发生事件时,会执行EventListener中的eventNotifyReceived方法:
controlPoint.addEventListener(new EventListener() {
@Override
public void eventNotifyReceived(String uuid, long seq, String name, String value) {
// 事件回调
...
}
});
复制代码
CyberGarage源码中的Bug
(1)getAction方法返回一直为空
在获取到Service类实例后,发现调用Service类实例的getAction方法获取Action类实例时,返回的结果一直为空。
// 获取服务
Service service = device.getService("urn:schemas-upnp-org:service:AVTransport:1");
// 返回一直为null
Action action = service.getAction("SetAVTransportURI");
复制代码
考虑到可能是设备服务中没有此动作,因此通过浏览器查看设备的sdd文档,发现文档中是有该SetAVTransportURI的动作描述,对此可以断定,设备是可以进行SetAVTransportURI的动作请求的。 为了找出问题,对getAction方法进行断点,并分析其源码执行情况,getAction的源码如下:
public Action getAction(String actionName)
{
ActionList actionList = getActionList();
int nActions = actionList.size();
for (int n=0; n<nActions; n++) {
Action action = actionList.getAction(n);
String name = action.getName();
if (name == null)
continue;
if (name.equals(actionName) == true)
return action;
}
return null;
}
复制代码
执行到getActionList方法时发现该方法直接返回一个空的列表。而根据文档描述,这里应该返回多个节点才对,因此我们看看getActionList这个方法的源码是否存在问题:
public ActionList getActionList()
{
ActionList actionList = new ActionList();
Node scdpNode = getSCPDNode();
if (scdpNode == null)
return actionList;
...
}
复制代码
执行到getSCPNode这个方法时,该方法返回为空了,导致getActionList这个方法返回了一个空的列表。这是什么原因呢? 我们再继续看看getSCPNode方法的源码:
private Node getSCPDNode()
{
...
try {
URL scpdUrl = new URL(rootDev.getAbsoluteURL(scpdURLStr));
System.out.println("SPCDURL: " + scpdURLStr);
scpdNode = getSCPDNode(scpdUrl);
if (scpdNode != null) {
data.setSCPDNode(scpdNode);
return scpdNode;
}
} catch (Exception e) {}
...
}
复制代码
当执行到URL scpdUrl = new URL(rootDev.getAbsoluteURL(scpdURLStr))这句代码的时候,出现很奇怪的现象,在调试工具中查看rootDev.getAbsoluteURL(scpdURLStr)的返回时,发现它的值时这样的:
http://192.168.42.37:2869/upnphost/udhisapi.dll?content=uuid:79884bb3-3148-433f-b140-e790b6ec22ed/upnphost/udhisapi.dll?content=uuid:fe18f6aa-02fc-4e53-891c-48ef5d5b6957
复制代码
终于找出原因了,这是因为rootDev.getAbsoluteURL(scpdURLStr)方法拼接SCDPURL出错了,导致无法获取并解析SDD文档中xml节点,从Android Profiler的记录中就可以看到SDD请求结果了:
返回的内容为空,自然无法获取对应的动作。那个rootDev.getAbsoluteURL(scpdURLStr)这个方法究竟错在哪里呢?我们继续看源码:
public String getAbsoluteURL(String urlString) {
String baseURLStr = null;
String locationURLStr = null;
Device rootDev = getRootDevice();
if (rootDev != null) {
baseURLStr = rootDev.getURLBase();
locationURLStr = rootDev.getLocation();
}
return getAbsoluteURL(urlString, baseURLStr, locationURLStr);
}
复制代码
这里依旧看不出什么问题,让我们看一下它的重载方法:
public String getAbsoluteURL(String urlString, String baseURLStr, String locationURLStr) {
if ((urlString == null) || (urlString.length() <= 0)) return "";
try {
URL url = new URL(urlString); return url.toString();
} catch (Exception e) {}
if (baseURLStr == null || baseURLStr.length() <= 0) {
if ((locationURLStr != null) && (0 < locationURLStr.length())) {
if (!locationURLStr.endsWith("/") || !urlString.startsWith("/")) {
String absUrl = locationURLStr + urlString;
try {
URL url = new URL(absUrl);
return url.toString();
} catch (Exception e) {}
}
}
...
}
...
}
复制代码
程序执行到了if (!locationURLStr.endsWith("/") || !urlString.startsWith("/"))这个判断中,问题就出现在下面这句代码中:
String absUrl = locationURLStr + urlString;
复制代码
这里直接拿locationURLStr和urlString拼接,这明显是不正确的,因为某些url可能在url后附带一些参数,如上例的locationURLStr是这样的:
http://192.168.42.37:2869/upnphost/udhisapi.dll?content=uuid:79884bb3-3148-433f-b140-e790b6ec22ed
复制代码
于是跟urlString拼接起来就出问题了,要解决这个问题,便是通过URL类实例,获取字符串的协议、ip地址和端口号,再与urlString拼接,如下:
public String getAbsoluteURL(String urlString, String baseURLStr, String locationURLStr) {
if ((urlString == null) || (urlString.length() <= 0)) return "";
try {
URL url = new URL(urlString); return url.toString();
} catch (Exception e) {}
if (baseURLStr == null || baseURLStr.length() <= 0) {
if ((locationURLStr != null) && (0 < locationURLStr.length())) {
if (!locationURLStr.endsWith("/") || !urlString.startsWith("/")) {
try {
URL locationURL = new URL(locationURL);
// 重新拼接url
String absUrl = locationURL.getProtocol() + "://" + locationURL.getHost() + ":" + locationURL.getPort() + urlString;
URL url = new URL(absUrl);
return url.toString();
} catch (Exception e) {}
}
}
...
}
...
}
复制代码
修改以后,getAction方法就能正确获取对应的动作了。
转载于:https://juejin.im/post/5d087c8b6fb9a07ec07fc184
最后
以上就是风中小鸽子为你收集整理的DLNA的全部内容,希望文章能够帮你解决DLNA所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复