我是靠谱客的博主 风中小鸽子,最近开发中收集的这篇文章主要介绍DLNA,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

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开发的主流框架

  1. Cling. Cling是一个Java开源项目,开发者可直接编译源码生成jar包导入到Android项目中。目前Cling已停止维护,但这并不影响它的热度。
  2. Platinum. Platinum是一个C库,它支持编译成多个平台的库,如Windows、Mac、IOS和Android等。但其编译流程相对来说比较复杂,Android使用Platinum开发需要用到jni。
  3. 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; 实现播放需要发送两个动作请求:

  1. SetAVTransportURI。设置播放URI。需要转入两个参数: 1. InstanceID 实例ID, 2. CurrentURI 要设置的URI。

  2. 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所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(34)

评论列表共有 0 条评论

立即
投稿
返回
顶部