java实现局域网内视频投屏播放(三)投屏原理

常见投屏方案

常见的投屏方案主要有以下几种:

DLNA

DLNA的全称是DIGITAL LIVING NETWORK ALLIANCE(数字生活网络联盟)。DLNA委员会已经于2017年1月5日正式解散,原因是旧的标准已经无法满足新设备的发展趋势,DLNA标准将来也不会再更新。但是DLNA协议的使用依然比较广泛,短时间内不会退出历史舞台,在某些情况下依然是最好的解决方案之一。

DLNA不是技术,而是一种方案,一种大家可以遵守的规范,其各种技术和协议都是目前所应用很广泛的技术和协议(SSDP、SOAP等)。

在我看来,DLNA协议栈为设备之间信息交流提供了一种彼此听得懂的语言工具。

AIRPLAY

AirPlay于DLNA类似,例如两种都是基于组播实现的设备发现,只不过DLNA基于SSDP(简单服务发现协议),而AirPlay基于mDNS(multicast DNS),甚至苹果曾经也是DLNA委员会的成员。相对DLNA,AirPlay提供了一套完善的官方标准实现,开发者只需要按照文档调用API即可,当然如果需要在第三方设备上实现AirPlay功能,需要自己实现一套与AirPlay兼容的功能,网上就有通过分析抓包实现的第三方AirPlay兼容库,包括发送端和接收端。

MIRACAST

以Wi-Fi Direct(和UPnP都是局域网P2P)为基础的无线显示标准,出现时间晚(2012),使用范围相对较小。支持此标准的设备可通过无线方式分享视频画面。与DLNA有较大差异的在于DLNA设备服务端(DMS,Digital Media Server)基于文件的方式提供服务,文件解码由接收端完成(DMR,Digital Media Render),因此DMR需要支持较多格式以保证兼容性;而Miracast则是由服务端完成解码并重新编码为H.264传输到接收端,接收端只需要对H.264解码即可。

基于以上对比来看,DLNA使用广泛,在主流的电视、智能机顶盒中都有支持,而且终端工作量小,是不错的方案。我们这个局域网投屏也是用的DLNA

DLNA投屏原理

UPnP

通用即插即用协议,使用了SSDP(简单设备发现协议)和SOAP(简单对象访问协议)等几个协议。可以说DLNA很大程度上是基于UPnP的。

UPnP协议中,定义了两个主要的组件,一个是设备(Device),一个是控制点(Control Point)。这就是为什么很多UPnP协议栈的SDK的接口代码一般都主要由Device和Control Point构成。设备是在网络中可见的对象,而控制点在网络中不可见。

一个UPnP的设备(Device)是不能直接访问和控制另一个UPnP的设备(Device)的,对设备的访问和控制都必须通过控制点(Control Point)来代为完成。而控制点对设备的控制则主要是由设备定义的"服务"(Service)来实现。

设备(Device)需要向网络中广播自己的信息,并提供设备描述和服务(Service)描述,并发送设备事件消息.

控制点(Control Point)则是搜索设备,并使用其提供的服务(Service)访问和控制设备,同时监听设备事件消息。

设备发现

设备发现也分为两步,第一步是获取设备基本描述,第二步是获取设备详细描述。

第一步有两种方式:主动发现和被动发现。

主动发现

主动发现是指设备主动通过UDP发出Search组播到指定地址和端口,ipv4为239.255.255.250:1900,ipv6为FF0x::C:1900,目标设备收到组播后会通过UDP单播发送设备基本信息(所以终端需要用Socket绑定search发送的那个随机端口,receive单播回包),然后根据基本信息中的设备描述地址获取设备的详细信息。

search包内容如下:

复制代码
M-SEARCH * HTTP/1.1
ST: upnp:rootdevice
HOST: 239.255.255.250:1900
MX: 3
MAN: "ssdp:discover"

其中ST是Search Type,常见的ST有ssdp:allupnp:rootdeviceuuid:device-某UUIDurn:schemas-upnp-org:device:device-Type:version等,投屏这里使用的是upnp:rootdevice,
HOST为组播地址,
MX为最大等待时间,
MAN为固定格式。

设备回包内容如:

复制代码
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
DATE: Fri, 23 Nov 2018 11:26:00 GMT
EXT: 
LOCATION: http://192.168.2.3:49153/description.xml
SERVER: SHP, UPnP/1.0, Samsung UPnP SDK/1.0
ST: upnp:rootdevice
USN: uuid:ecf9f8c1-e1a3-459e-a33e-1f6413af9aef::upnp:rootdevice
Content-Length: 0

其中最重要的是LOCATION,其中包含了目标设备的ip、upnp服务的端口、设备详细描述地址,有了这个地址就可以获取设备的详细信息,具体内容见下文;USN作为服务的唯一识别ID,在设备详细描述中还有,可以暂时忽略。

被动发现

被动发现是指目标设备通过UDP发送Notify组播到局域网(所以终端需要启动一个MulticastSocket joinGroup到上述组播地址监听组播),设备收到组播后可以得到设备描述地址获取设备的详细信息。

Notify报文的内容如下:

复制代码
NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=66
LOCATION: http://192.168.2.3:49153/description.xml
NT: upnp:rootdevice
NTS: ssdp:alive
SERVER: Linux/3.10.79, UPnP/1.0, Portable SDK for UPnP devices/1.6.13
USN: uuid:F7CA5454-3F48-4390-8009-2c3aed46c9a9::upnp:rootdevice

其中也包含了LOCATION,详细信息就不愁啦;还有两个需要关注的值:NTNTS,前者是Notify Type(与Search中的ST类似),后者表示NT的子类型,其值只可以是ssdp:alivessdp:byebye,目标设备会在生命周期中定期发送alive组播,在正常退出时发送byebye组播,也有实现会在目标设备上线时先发送byebye然后发送alive,便于控制端及时更新设备信息。NT有较多类型,我们只关注upnp:rootdevice类型的Notify即可。

设备描述

获取设备详细信息 至此,无论前面通过何种方式,我们都已经得到了一个重要的信息:LOCATION,向该地址发送一个简单的HTTP请求,即可得到详细的设备信息,无论是做投屏还是做基于DLNA的打印机,原理都是一样的,尤其是前面的部分,一模一样,而后面的部分也是换汤不换药,换成了打印相关的服务而已,设备描述示例如下。

head

复制代码
HTTP/1.1 200 OK
CONTENT-LENGTH: 2506
CONTENT-TYPE: text/xml
DATE: Mon, 07 Jan 2019 11:26:00 GMT
LAST-MODIFIED: Mon, 07 Jan 2019 11:25:17 GMT
SERVER: Linux/3.10.65, UPnP/1.0, Portable SDK for UPnP devices/1.6.13
X-User-Agent: redsonic
CONNECTION: close

body

复制代码
<?xml version="1.0" encoding="utf-8"?>

<root xmlns="urn:schemas-upnp-org:device-1-0">  
  <specVersion> 
    <major>1</major>  
    <minor>0</minor> 
  </specVersion>  
  <device> 
    <deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>  
    <presentationURL>/</presentationURL>  
    <friendlyName>客厅电视</friendlyName>  
    <manufacturer>XXXX</manufacturer>  
    <manufacturerURL>http://www.xxx.com</manufacturerURL>  
    <modelDescription>xxx Media Render</modelDescription>  
    <modelName>xxxxx</modelName>  
    <modelURL>http://www.xxx.com</modelURL>  
    <UDN>uuid:F7CA5454-3F48-4390-8009-dce3a07b5e48</UDN>  
    <UID>-1254112285</UID>  
    <serviceList> 
      <service> 
        <serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>  
        <SCPDURL>/dlna/Render/AVTransport_scpd.xml</SCPDURL>  
        <controlURL>_urn:schemas-upnp-org:service:AVTransport_control</controlURL>
        <eventSubURL>_urn:schemas-upnp-org:service:AVTransport_event</eventSubURL>
      </service>  
      <service>
        <serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>  
        <SCPDURL>/dlna/Render/ConnectionManager_scpd.xml</SCPDURL>
        <controlURL>_urn:schemas-upnp-org:service:ConnectionManager_control</controlURL>
        <eventSubURL>_urn:schemas-upnp-org:service:ConnectionManager_event</eventSubURL>
      </service>  
      <service> 
        <serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
        <serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>  
        <SCPDURL>/dlna/Render/RenderingControl_scpd.xml</SCPDURL>  
        <controlURL>_urn:schemas-upnp-org:service:RenderingControl_control</controlURL>
        <eventSubURL>_urn:schemas-upnp-org:service:RenderingControl_event</eventSubURL>
      </service> 
    </serviceList>  
    <av:X_RController_DeviceInfo xmlns:av="urn:mi-com:av">  
      <av:X_RController_Version>1.0</av:X_RController_Version>  
      <av:X_RController_ServiceList> 
        <av:X_RController_Service> 
          <av:X_RController_ServiceType>controller</av:X_RController_ServiceType>
          <av:X_RController_ActionList_URL>http://192.168.2.3:6095/</av:X_RController_ActionList_URL> 
        </av:X_RController_Service>  
        <av:X_RController_Service> 
          <av:X_RController_ServiceType>data</av:X_RController_ServiceType>
          <av:X_RController_ActionList_URL>http://api.tv.xx.com/bolt/3party/</av:X_RController_ActionList_URL> 
        </av:X_RController_Service> 
      </av:X_RController_ServiceList> 
    </av:X_RController_DeviceInfo> 
  </device>  
  <URLBase>http://192.168.2.3:49152/</URLBase> 
</root>

在长长的信息中,我们需要关注的有device标签下的deviceTypefriendlyNameUDN,其中friendlyName是设备的展示名,即给人看的名字,UDN是根据UUID生成的时间无关的设备失败码,其中包含了UUID,我们可以以此为设备id区分不同设备、处理设备的掉线和重连等。

紧接着,在serviceList中列出了设备提供的服务列表service,service标签下有serviceTypeserviceIdSCPDURLcontrolURLeventSubURL5个子标签,其中serviceType是判断设备提供的服务类型的依据,对于支持投屏的设备,一般有AVTransportRenderingControlConnectionManager三种服务,投屏过程中主要使用前两种,每个服务的支持的控制指令可以通过SCPDURL查看,或继续浏览下文用法;serviceId没啥好说的;SCPDURL为服务描述地址,请求会返回该服务的详细描述,包括服务支持的指令及其参数等,因为投屏使用的是比较标准的服务和指令,所以可以不需要请求服务的详细说明也能正常使用;controlURL是服务的控制地址,指令的发送就是往这个地址发的;eventSubURL是用来向目标设备订阅该服务相关的事件回调的,需要控制端运行一个ServerSocket监听tcp请求。

设备控制

前面已经提到,设备控制就是望设备的对应服务的controlURL发送HTTP请求,这里以POST方式为例,向TV的AVTransport服务发送SetAVTransportURI指令,作用是告诉TV需要播放的直播流的地址,内容如下:

head

复制代码
POST /_urn:schemas-upnp-org:service:AVTransport_control HTTP/1.1
Connection: close
SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"
Content-Type: text/xml;charset="utf-8"
Content-Length: 1464
Host: 192.168.2.3:49152
User-Agent: 

其中有两个参数用法说明一下(服务类型记即上面服务列表中服务的serviceType,控制地址为controlURL)

复制代码
POST 控制地址 HTTP/1.1  
SOAPACTION: "服务类型#Action"

body

复制代码
<?xml version="1.0" encoding="utf-8"?>

<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>http://xxx.xxx.com/xxx.m3u8?bizid=xxx&amp;txSecret=fcexxxxxab4cf1b8bbee6efbe6668bd4&amp;txTime=5c3c5de1&amp;uid=0</CurrentURI>  
      <CurrentURIMetaData>&lt;DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sec="http://www.sec.co.kr/"&gt;&lt;item id="123" parentID="-1" restricted="1"&gt;&lt;upnp:storageMedium&gt;UNKNOWN&lt;/upnp:storageMedium&gt;&lt;upnp:writeStatus&gt;UNKNOWN&lt;/upnp:writeStatus&gt;&lt;dc:title&gt;Video&lt;/dc:title&gt;&lt;dc:creator&gt;QGame&lt;/dc:creator&gt;&lt;upnp:class&gt;object.item.videoItem&lt;/upnp:class&gt;&lt;res protocolInfo="http-get:*:video/*:DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"&gt;http://xxx.xxx.com/xxx.m3u8?bizid=xxx&amp;amp;txSecret=fcexxxxxab4cf1b8bbee6efbe6668bd4&amp;amp;txTime=5c3c5de1&amp;amp;uid=0&lt;/res&gt;&lt;/item&gt;&lt;/DIDL-Lite&gt;</CurrentURIMetaData> 
    </u:SetAVTransportURI> 
  </s:Body>
</s:Envelope>

看起来一堆东西,其实大部分都是固定格式,只有其中的少部分参数需要说明一下(Action为服务提供的Action,可以从服务描述地址获取详细说明,下文也有常用Action的列表)

复制代码
<u:Action xmlns:u="服务类型">
  <参数名1>参数值1</参数名1>
  <参数名2>参数值2</参数名2>
  ...
</u:Action>

元数据中包含了协议的相关数据,没有特殊需求的话,套用常用的元数据内容即可,值得注意的是,元数据中res标签包含了转义过的视频地址(反转义一下就明显看出来了),而元数据也经过转义才放到body中的CurrentURIMetaData标签下,也就是说元数据中的视频URL经过了两次转义,&将转义为&amp;amp;,该转义为常用的xml转义,同样在处理upnp回包时,也要留意upnp需要转义的情况,具体规则为

原字符 转义字符
& &amp;
" &quot;
< &lt;
> &gt;
空格 &nbsp;
' '

转义这里不可以偷懒,以免造成兼容性问题,在已测的设备中,绝大多数电视取视频URL都是使用CurrentURI标签提供的URL,而三星电视则是从CurrentURIMetaData标签取得视频URL,如果元数据设置不对的话,很可能导致三星这种电视无法正常播放。

播放过程中不同的控制Action都是类似的,前后都是固定格式,稍作调整就成了另外一个控制Action,如PlayAction

head

复制代码
POST /_urn:schemas-upnp-org:service:AVTransport_control HTTP/1.1
Connection: close
SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#Play"
Content-Type: text/xml;charset="utf-8"
Content-Length: 327
Host: 192.168.2.3:49152
User-Agent: 

body

复制代码
<?xml version="1.0" encoding="utf-8"?>

<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>

具体每个Action的参数参考所属服务的详细描述,这里列出常用Action及其对应的服务和参数

Action Service 参数 常用值 说明
SetAVTransportURI AVTransport InstanceID、CurrentURI、CurrentURIMetaData 0、转义的视频地址、见上文 设置视频地址
Play AVTransport InstanceID、Speed 0、1 播放
Pause AVTransport InstanceID 0 暂停
Stop AVTransport InstanceID 0 停止
Seek AVTransport InstanceID、Unit、Target 0、见备注、见备注 跳转1
SetMute RenderingControl InstanceID、Channel、DesiredMute 0、Master、1/0 静音/取消2
SetVolume RenderingControl InstanceID、Channel、DesiredVolume 0、Master、0-100 设置音量
GetVolume RenderingControl InstanceID、Channel 0、Master 获取音量
GetCurrentTransportActions AVTransport InstanceID 0 获取Action列表3
GetMediaInfo AVTransport InstanceID 0 获取媒体信息
GetPositionInfo AVTransport instanceID 0 获取进度
GetTransportInfo AVTransport instanceID 0 获取传输状态4

1 UnitTRACK_NRABS_TIMEABS_COUNTREL_COUNTCHANNEL_FREQTAPE_INDEXFRAME 7种取值,参见微软定义,这里使用REL_TIMETarget格式因Unit而定,如果Unit=REL_TIME,则格式为 "00:11:26",表示跳转到某个进度,如果Unit=TRACK_NR,则格式为一个整数i,跳转到第i个视频(应该是的,未做验证)。

2 DesiredMute设置为1表示静音,0表示取消静音。

3 GetCurrentTransportActions返回结果不太准。

4 GetTransportInfo可以获取当前传输状态,如STOPPED、PLAYING等,但也不准确,有的设备已经用遥控器停止播放了,获取到的还是PLAYING。

Action的成功与否主要通过POST请求返回的状态码判断,如果是 200 OK,那应该就是成功的了,大多数Action回包内容都非常简单,没有需要处理的返回值,部分Action如GetVolume具有返回值,需要解析回包。如果失败,根据状态码(如500等)及body中说明的错误信息定位问题,对比可以正常投屏的其它应用的请求内容,分析问题原因。

事件处理

当控制端通过SetAVTransportURIPlay让目标设备开始播放视频时,设备会进行加载缓冲,并开始播放,或者用户通过遥控器暂停/继续播放,甚至其它控制端抢占了TV等,都是我们需要关心的事件,以便控制端进行状态处理。获取目标设备的状态变化有两种方式,

  1. 轮询GetTransportInfoGetMediaInfo等Action,
  2. 向目标设备注册订阅。
    两种方式各有优缺点,因为目标设备实现存在差异,每个设备的在状态的处理上不完全一致。前面已经提到过,GetTransportInfo返回结果不太靠谱;第二种方式,结果较为准确,但是对控制端抢占TV导致更换URL等情况大多不会告知订阅者;所以结合两种方式,可以达到较好的效果。

事件订阅

订阅

设备描述的服务列表中,每个服务都有一个eventSubURL,我们可以在控制端运行一个ServerSocket绑定一个端口(记为端口A),通过accept监听tcp请求,并将本机ip和端口A和自定义回调路径拼接为url通过SUBSCRIBE Action发送给目标设备,即可完成订阅,在必要的时候,通过SUBSCRIBE Action(与订阅使用同一个Action,但参数不同)续订,通过UNSUBSCRIBE Action取消订阅。

参数格式如下表:

| Action | 参数 | 常用值 | 说明 |

| ----------- | :-------------------: | :---------------------------------------: | :------: |

| SUBSCRIBE | Nt、Timeout、Callback | upnp:event、Second-时间、<自定义回调地址> | 订阅1 |

| SUBSCRIBE | SID、Timeout | 订阅ID、Second-时间 | 续订2 |

| UNSUBSCRIBE | SID | 订阅ID | 取消订阅 |

1 Callback的值URL用<>包裹。

2 SID为SUBSCRIBE ID,是订阅Action返回的值。

回调

ServerSocket接收Socket链接,并读取回调内容,后面过程与处理Action回包类似,解析upnp内容即可(upnp是XML子集),其中需要关注的只有lastchange标签里的内容,以播放事件为例,播放事件的LastChange标签内容为:

复制代码
<Event xmlns = "urn:schemas-upnp-org:metadata-1-0/AVT/">
<InstanceID val="0">
    <TransportState val="PLAYING"/>
    <TransportStatus val="OK"/>
</InstanceID>
</Event>
相关推荐
SelectDB13 小时前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
zzzzzz3102 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode2 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220703 天前
如何搭建本地yum源(上)
运维
大树886 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠6 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质6 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz6 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工6 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智6 天前
ARP代理--工作原理
运维·网络·arp·arp代理