DLNA开源库——Cling
目前主流的智能网络设备解决方案有DLNA
,AirPlay
,Miracast
DLNA的全称是Digital Living Network Alliance
(数字生活网络联盟),旨在解决个人PC,消费电器,移动设备在内的无线网络和有线网络的互联互通
DLNA将其整个应用规定成5个功能组件,从下到上依次为:
- 网络互连:802.3以太网,802.11WiFi,802.15蓝牙
- 网络协议:IPV4
- 设备的发现控制和管理:UPnP
- 媒体传输:Http
- 媒体格式
DLNA不是一种协议,但包括了实现相关标准所需要的一系列协议栈,UPnP
是其中的关键协议
DLNA原理
要实现从移动端将网络视频投放至智能电视或机顶盒,首先要保证这些设备在同一个局域网的相同网段下,即共享同一个网关,这样所有设备都能够拥有独立的IP,从而具备相互通信的基础了
设备发现
当一个新的Control Point)
加入一个局域网时,为了获取当前网段里都有哪些智能设备,需要遵循SSDP
向默认多播IP和端口发送获取信息的请求,这是一个UDP消息,所以建议在设备搜索过程多做几次发现请求,以免丢包带来的遗漏
控制点可以获得的信息是:有一台设备,它的IP和端口号,设备描述文档(DDD
)在什么位置、UUID是什么
请求设备描述文档
DDD
以及后面要说到的服务描述文档 (SDD, Service Description Document
)都是以XML格式返回给请求端的,这一步的通信则是基于TCP /HTTP进行可靠传输的。我们请求Location
字段的内容即
1 | http://10.2.9.152:49152/TxMediaRenderer_desc.xml |
响应
1 | <root xmlns="urn:schemas-upnp-org:device-1-0" xmlns:dlna="urn:schemas-dlna-org:device-1-0" configId="499354"> |
返回设备的详细信息:设备名称、设备类型、UUID、服务列表,每个服务都有serviceType
,SCPDURL
,serviceId
, controlURL
和eventSubURL
请求服务描述文档
如何使用这个服务需要参考该服务的SDD
。SCPDURL
这个字段的内容就是请求SDD
的路径地址,我们将其与之前在发现设备阶段获取到的响应消息中的Location字段内容中设备的IP和端口号拿过来,拼接成完整URL字符串
1 | http://10.2.9.152:49152/AVTransport/9c443d47158b-dmr/scpd.xml |
返回一个动作列表,一个服务会包含一个或多个功能请求动作
1 | <scpd xmlns="urn:schemas-upnp-org:service-1-0"> |
如SetAVTransportURI
这个请求的功能是将一个音视频资源的URI发送给渲染端。一个Action
就好比一个API请求,你还需要传递一些要求的参数,这时就会用到该Action后面参数列表里规定的参数
服务动作请求
有了动作所需要的全部信息,就可以按照DLNA规定的方式发给设备请求服务
1 | POST /AVTransport/9c443d47158b-dmr/control.xml HTTP/1.1 |
响应
1 | HTTP/1.1 200 OK |
UPnP工作原理
UPnP的工作分为6个步骤……
寻址
当设备首次与网络建立连接后,利用DHCP服务,使设备得到一个IP地址。这个IP地址可以是DHCP系统指定的,也可以是由设备选择的。设备还可以使用Friendly Name,这就需要DNS来转换得到IP
发现
当一个设备被添加到网络后,UPnP的发现协议允许该设备向网络上的控制点(Control Point, CP)通知自己拥有的服务。同样,当一个CP被添加到网络后,UPnP发现协议允许该CP搜索网络上可用的设备。这两种情况下的组播消息一般是设备和服务的基本信息,如设备类型,唯一标识符,状态参数等等。要注意设备信息和服务信息都是要组播出去的
发现过程使用的协议是SSDP(Simple Service Discovery Protocol,简单服务发现协议),采用UDP传输
描述
描述分为两部分:一个是设备描述,另一个是服务描述
控制
在设备描述部分,还有关于如何控制设备的描述,会给出一个Control URL,CP可以向这个URL发送不同的控制信息就可以控制了,然后设备也可以返回一个信息反馈。
这种CP和设备之间沟通信息按照SOAP(Simple Object Access Protocol)的格式来写。消息体里面就可以写想调用的动作了,叫做Action Invocation,可能还要传参数,比如想播放一个视频,要把视频的URL传过去,设备收到后会响应,表示能不能执行调用,出错的话会返回一个错误代码
事件
在服务进行的整个时间内,只要变量值发生了变化或者模式的状态发生了改变,就产生了一个事件,该事件服务提供者(某设备的某个服务)会把该事件向整个网络进行多播。而且,CP也可以事先向事件服务器订阅事件信息,保证将该CP感兴趣的事件及时准确地单播传送过来
事件的订阅和推送这块用的通信协议是General Event Notification Architecture(GENA),通过HTTP/TCP/IP传送。订阅过程如下:
- 订阅。Subscriber发送订阅消息主要包含事件URL,服务ID号,这两个可以在设备服务描述信息中找到,以及寄送URL,还会包含一个订阅期限Duration
- 成功订阅。Publisher收到订阅信息,如果同意订阅的话就会为每个新Subscriber生成一个唯一的ID并记录Subscriber的Duration和Delivery URL。还会记录一个顺序增长EventKey用来保证事件确实推送到Subscriber那里
- 首次推送。订阅同意订阅之后还会向Subscriber发送一组初始变量或状态值,进行首次同步
- 续订。Subscriber必须在订阅到期前发送Renewal续订
- 订阅到期。订阅到期后Publisher会把Subscriber的信息删除,Subscriber又回到订阅前的状态。
- 退订。Subscriber发送Cancel信息将会取消订阅。Subscriber因非正常退出网络的话,则不会退订直到订阅到期
- 订阅操作失败信息。当订阅、续订和退订不能被Publisher接收或者出现错误时,Publisher会发送一个错误代码
表达
只要得到了设备的URL,就可以取得该设备表达的URL,取得该设备表达的HTML,然后可以将此HTML纳入CP的本地浏览器上。这部分还包括与用户对话的界面,以及与用户进行会话的处理。因此设备表达可以理解成“遥控器”。这部分定义描述界面,规范界面以及传输界面内容。远程界面是供CP用户使用的,CP用户通过远程界面完成设备描述的获取,控制设备,订阅收取设备事件等等
Cling
Cling类库是由JAVA实现的DLNA/UPnP协议栈
重要组件
UpnpService
通过UpnpService
接口获得ControlPoint
,基于ControlPoint
进行操作
UpnpService.java
1 | public interface UpnpService { |
主要提供控制点、协议工厂、注册表、路由和配置信息,UpnpServiceImpl
是UpnpService
的实现类
ControlPoint
ControlPoint.java
1 | public interface ControlPoint { |
主要包含搜索和执行命令两个功能
UpnpServiceConfiguration
UPnP协议栈的配置信息,AndroidUpnpServiceConfiguration
是Android平台上的实现类,继承了DefaultUpnpServiceConfiguration
ProtocoFactory
UPnP协议的工厂类,用于根据收到的协议或是本地设备的信息,创建一个可执行的协议
Registry
设备资源管理器,用于设备、资源、订阅消息的管理,包括添加、更新、移除、查询
情景分析
搜索设备
ControlPoint()
进行search()
,需要两个东西:
- 通过
UpnpServiceConfiguration.getAsyncProtocolExecutor()
获得的UPnP协议栈的异步线程池 - 通过
ProtocoFactory.createSendingSearch()
获得的执行内容即搜索动作
得到的搜索命令就是SendingSearch
SendingSearch.java
1 |
|
可见搜索就是是构造了一个OutgoingSearchRequest
,它是一个OutgoingDatagramMessage
,可见是一个UDP数据报消息,然后循环发送了若干次
Router.send()
开始了网络传输过程
RouterImpl.java
1 |
|
在所有的UDP端口上发送消息
DatagramIOImpl.java
1 | synchronized public void send(OutgoingDatagramMessage message){ |
先由DatagramProcessor
把传入的OutgoingDatagramMessage
转化成了一个DatagramPacket
,然后发送出去
DatagramIOImpl.java
1 | synchronized public void send(DatagramPacket datagram) { |
调用了DatagramSocket.send()
发送消息
DatagramIOImpl
是一个Runnable
,在运行时一直接收数据报
DatagramIOImpl.java
1 | public void run() { |
DatagramSocket.receive()
接收数据报,Router.received()
处理接收的数据报
RouterImpl.java
1 |
|
通过ProtocolFactory.createReceivingAsync()
创建一个异步消息,并放入异步线程池中
ProtocolFactory.java
1 |
|
根据消息是UPnP请求还是UPnP响应,创建不同的接受消息ReceivingNotification
/ReceivingSearch
/ReceivingSearchResponse
,这里是接收的ReceivingSearchResponse
(搜索响应,不是搜索命令)
ReceivingSearchResponse
1 | protected void execute() { |
先处理EasyLink消息,然后更新设备列表,最后发送消息请求设备描述文件
RegistryImpl.java
1 |
|
发送命令
ControlPoint()
通过execute()
发送命令,同样的,也需要两个东西:
- 通过
UpnpServiceConfiguration.getSyncProtocolExecutor()
获得命令的同步线程池 - 获得命令,通常传入的
ActionCallback
就是,它是一个Runnable
ActionCallback.java
1 |
|
首先获得服务,如果是本地服务,就用本地的线程池执行,等待响应;如果是远程服务,拿到控制URL,构造命令发送并等待响应
ActionInvocation.getAction().getService()
如何获得服务?
是执行命令的时候由外部传入的,以播放命令为例:
1 | getControlPoint().execute(new PlayActionCallback(device.getAVservice()) { |
可见是从设备获得了所需的服务,通过Device.findDevice()
找到对应描述的服务
然后通过ProtocolFactory.createSendingAction()
创建发送命令,并执行
SendingAction.java
1 |
|
通过sendRemoteRequest()
发送请求,处理结果
用SOAP处理器转换得到SOAP消息,然后发送
RouteImpl.java
1 |
|
StreamClientImpl.java
1 |
|
创建请求,添加参数并调用HttpClient.execute()
处理,createResponseHandler()
处理响应并返回
然后回到SendingAction
处理返回值
SendingAction.java
1 | protected void handleResponse(IncomingActionResponseMessage responseMsg) throws ActionException { |
用SOAP处理器读取响应体,最终通过ActionInvocation.setOutput()
将解析得到的响应字段设置到ActionInvocation
里
总结:
- 获得信息,构造命令
- 通过SOAP处理器转换为SOAP消息
- 通过Http发送消息
- 接受Http响应消息
- 通过SOAP处理器解析SOAP消息
- 将解析的字段回传
订阅事件
ControlPoint()
通过execute()
订阅事件
- 通过
UpnpServiceConfiguration.getSyncProtocolExecutor()
获得同步线程池 - 传入
SubscriptionCallback
SubscriptionCallback.java
1 |
|
构造一个GENA订阅回调,通过ProtocolFactory.createSendingSubscribe()
创建订阅消息并发送
SendingSubscribe.java
1 |
|
调用Router.send()
发送订阅消息,并接收响应,通过Registry.addRemoteSubscription()
添加订阅
接收消息
UDP数据报的接收上面说过了,这里看Http数据流
SteamServer
接口描述了接收Http数据流的能力,SteamServerImpl
是其实现类
创建监听Socket,通过accept()
接收连接,并创建新的通信Socket
StreamServerImpl.java
1 | UpnpStream connectionStream = new HttpServerConnectionUpnpStream(router.getProtocolFactory(), httpServerConnection, globalParams); |
从连接中得到一个UpnpStream,交给Router,放到同步线程池中执行
Router.java
1 |
|
HttpServerConnectionUpnpStream.java
1 |
|
UpnpHttpService.java
1 | protected void doService(HttpRequest httpRequest, HttpResponse httpResponse, HttpContext ctx) throws HttpException, IOException { |
UpnpStream.java
1 | public StreamResponseMessage process(StreamRequestMessage requestMsg) { |
ProtocolFactoryImpl.java
1 |
|
根据UPNP消息头构造不同的接收消息
以收到事件为例
ReceivingEvent.java
1 |
|
最终通知到注册的监听器上
重要的类
UpnpOperation
UPnP操作,有UpnpRequest
和UpnpResponse
两个子类
UpnpMessage
UPnP消息,类结构如下:
StreamRequestMessage
请求数据流StreamResponseMessage
响应数据流IncomingDatagramMessage
接收数据报OutgoingDatagramMessage
发送数据报
有很多具体的消息类如OutgoingSubscribeRequestMessage
/OutgoingSearchRequest
/OutgoingActionRequestMessage
等
Device
设备类,有LocalDevice
和RemoteDevice
两个子类,表示本地和网络设备
SendingAsync
异步命令,类结构如下:
SendingSync
同步命令SendingSubscribe
SendingRenewal
SendingEvent
SendingAction
SendingUnsubscribe
SendingNotification
通知SendingNotificationByebye
SendingNotificationAlive
SendingSearch
搜索
ReceivingAsync
接收异步消息,类结构如下
ReceivingSearch
接收搜索请求ReceivingNotification
接收通知ReceivingSearchResponse
接收搜索响应ReceivingSync
接收同步消息ReceivingEvent
ReceivingAction
ReceivingSubscribe
ReceivingRetrieval
ReceivingUnsubscribe
协议相关
org.teleal.cling.transport
包里包含大量涉及到的协议的接口和实现类,如SOAP/GENA/TCP/UDP等
应用相关
org.teleal.cling.support
包含应用层方面的代码,可以看到一些基本的服务AVTransport
/MediaManager
/PlayQueue
/RenderingControl
callback
子目录里面都是相关命令,是ActionCallback
的子类
还有个lastchange
子目录,里面是XML的解析器,是LastChangeParser
的子类,收到订阅事件时,GENASubscription
的currentValues
保存了一张表,里面的LastChange
则描述了事件变化,需要用LastChangeParser
解析,得到Event