由于我司需求,需要在iOS和安卓客户端实现DLNA投屏和控制。经过一番折腾,决定由我来研究DLNA。说起来又兴奋又紧张,兴奋希望自己能够弄出来然后跟安卓组讲解原理,紧张是因为怕自己能力不足做不出来。
DLNA网上的资料比较笼统不好入门,官方资料直接是每个1000多页的10几个PDF文档,根本无从下手。相关开源项目有名的有Platinum UPnP,但是由于它是基于C++实现的,相关文档并不全面。iOS相关开源项目都三四年没更新的,找来找去只好自己去啃自己去实现了。还好买到一本不错的书《智能家庭网络:技术、标准与应用实践》。通过近俩星期的研究,搞懂了DLNA核心协议UPnP基本逻辑,实现了投屏和控制功能的Demo。
下面就整理一下实现基本概念,实现过程和一些坑。
如果要直接看实现过程,请看以下三篇文章:
- 基于DLNA实现iOS,Android投屏:SSDP发现设备
- 基于DLNA实现iOS,Android投屏:SOAP控制设备
- 基于DLNA实现iOS,Android投屏:订阅事件通知
基础概念
DLNA
DLNA的全称是DIGITAL LIVING NETWORK ALLIANCE(数字生活网络联盟), 其宗旨是Enjoy your music, photos and videos, anywhere anytime, DLNA(Digital Living Network Alliance) 由索尼、英特尔、微软等发起成立、旨在解决个人PC,消费电器,移动设备在内的无线网络和有线网络的互联互通,使得数字媒体和内容服务的无限制的共享和增长成为可能,目前成员公司已达280多家。
DLNA标准包括多项协议及标准,其中最重要的部分是UPnP。对于我们目前的需求UPnP就能满足全部要求。
UPnP
通用即插即用(英语:Universal Plug and Play,简称UPnP)是由“通用即插即用论坛”(UPnP™ Forum)推广的一套网络协议。该协议的目标是使家庭网络(数据共享、通信和娱乐)和公司网络中的各种设备能够相互无缝连接,并简化相关网络的实现。UPnP通过定义和发布基于开放、因特网通讯网协议标准的UPnP设备控制协议来实现这一目标。
UPnP这个概念是从即插即用(Plug-and-play)派生而来的,即插即用是一种热拔插技术。
协议栈
UPnP设备体系结构包含了设备之间、控制点之间、设备和控制点之间的通信。完整的UPnP由设备寻址、设备发现、设备描述、设备控制、事件通知和基于Html的描述界面几部分构成。
- UPnP是一个多层协议构成的框架体系,每一层都以相邻的下层为基础,同时又是相邻上层的基础。直至达到应用层为止。该图中的最下面是就是IP和TCP,共两层,负责设备的IP地址。
- 三层是HTTP、HTTPU、HTTPMU,这一层,属于传送协议层。传送的是内容都经过“封装”后,存放在特定的XML文件中的。对应的SSDP、GENA、SOAP指的是保存在XML文件中的数据格式。到这一层,已经解决了UPnP设备的IP地址和传送信息问题。
- 第四层是UPnP设备体系定义,仅仅是一个抽象的、公用的设备模型。任何UPnP设备都必须使用这一层。
- 第五层是UPnP论坛的各个专业委员会的设备定义层,在这个论坛中,不同电器设备由不同的专业委员会定义,例如:电视委员会只负责定义网络电视设备部分,空调器委员会只负责定义网络空调设备部分,依此类推。所有的不同类型的设备都被定义成一个专门的架构或者模板,供建立设备的时候使用。可以推知,进入这一层,设备已经被指定了明确用途。当然,这些都必须遵守标准化的规范。从目前看,UPnP已经可以支持大部分的设备:从电脑、电脑外设,移动设备和家用消费类电子设备等等,无所不包,随着这个体系的普及,将可能有更多的厂家承认这一标准,最终,可能演化为公认的行业标准。
- 最上层,也就是应用层,由UPnP设备制造厂商定义的部分。这一层的信息是由设备制造厂商来“填充” 的,这部分一般有设备厂商提供的、对设备控制和操作的底层代码,然后,就是名称序列号呀,厂商信息之类的东西。
设备
设备是提供服务的网路实体,是一个逻辑概念,一个屋里设备可以包含一个或者多个逻辑设备。例如一台PC可以有两个逻辑设备———视频播放器和图片浏览器。
服务
服务是UPnP中最小的可控单元,它包括一系列可控制而动作和一组记录该服务目前情况的状态。服务是依赖于设备存在的。
控制点
控制UPnP设备工作的网络终端,主要功能包括获取设备描述和相关服务列表;获取感兴趣的服务描述;发出控制消息控制设备动作;向感兴趣的服务发出订阅消息,以便当服务状态改变时,自动获得时间通知。
一些术语
UUID含义是通用唯一识别码(Universally Unique Identifier),其目的是让分布式系统中的所有元素,都有唯一的辨识资讯,而不需要透过中央控制端来做辨识资讯的指定。其格式为xxxxxxxx-xxxx-xxxx-xxxxxxxxxxxxxxxx(8-4-4-16),分别为当前日期和时间,时钟序列,全局唯一的IEEE机器识别号,如果有网卡,从网卡mac地址获得,没有网卡以其他方式获得。
单一设备名(Unique Device Name),基于UUID,表示一个设备。在不同的时间,对于同一个设备此值应该是唯一的。
Web上可用的每种资源 - HTML文档、图像、视频片段、程序等 - 由一个通用资源标志符(Universal Resource Identifier,简称”URI”)进行定位。 URI一般由三部分组成:访问资源的命名机制;存放资源的主机名;资源自身的名称,由路径表示。考虑下面的URI,它表示了当前的HTML 4.0规范:http://www.webmonkey.com.cn/html/html40/它表示一个可通过HTTP协议访问的资源,位于主机www.webmonkey.com.cn上,通过路径/html/html40访问。
URL是URI命名机制的一个子集,URL是Uniform Resource Location的缩写,译为“统一资源定位符”。通俗地说,URL是Internet上用来描述信息资源的字符串,主要用在各种www客户程序和服务器程序上。采用URL可以用一种统一的格式来描述各种信息资源,包括文件、服务器的地址和目录等。
URN:URL的一种更新形式,统一资源名称(URN,Uniform Resource Name)。唯一标识一个实体的标识符,但是不能给出实体的位置。标识持久性Internet资源。URN可以提供一种机制,用于查找和检索定义特定命名空间的架构文件。尽管普通的URL可以提供类似的功能,但是在这方面,URN 更加强大并且更容易管理,因为 URN 可以引用多个 URL。
实现
工作机制
UPnP设备的发现和控制分为6个步骤:寻址、发现、描述、控制、事件及展现。
这三点分别在后面的三篇文章中进行介绍。
整体流程
整体工作流程如下:
参考
基于DLNA实现iOS,Android投屏:SSDP发现设备
SSDP能够在局域网能简单地发现设备提供的服务。SSDP有两种发现方式:主动通知和搜索响应方式。
寻址
UPnP 技术是架构在 IP 网络之上。因此拥有一个网络中唯一的 IP 地址是 UPnP 设备正常工作的基础。UPnP 设备首先查看网络中是否有 DHCP 服务器,如果有,那么使用 DHCP 分配的 IP 即可;如果没有,则需要使用LLA技术来为自己找适合的IP地址。
另外,在 UPnP 运行过程中,UPnP 设备都需要周期性检测网络中是否有 DHCP 服务器存在,一旦发现有 DHCP 服务器,就必须终止使用 LLA 技术获取的 IP 地址,改用 DHCP 分配的 IP 地址。
发现
SSDP
SSDP:Simple Sever Discovery Protocol,简单服务发现协议,此协议为网络客户提供一种无需任何配置、管理和维护网络设备服务的机制。此协议采用基于通知和发现路由的多播发现方式实现。协议客户端在保留的多播地址:239.255.255.250:1900(IPV4)发现服务,(IPv6 是:FF0x::C)同时每个设备服务也在此地址上上监听服务发现请求。如果服务监听到的发现请求与此服务相匹配,此服务会使用单播方式响应。
常见的协议请求消息有两种类型,第一种是服务通知,设备和服务使用此类通知消息声明自己存在;第二种是查询请求,协议客户端用此请求查询某种类型的设备和服务。
iOS中使用GCDAsyncUdpSocket发送和接受SSDP请求、响应及通知,安卓也需要用类此框架来完成
所以我们发现设备也有两种方法
- 主动通知方式:当设备加入到网络中,向网络上所有控制点通知它所提供的服务,通知消息采用多播方式。
- 搜索——响应方式:当一个控制点加入到网络中,在网络搜索它感兴趣的所有设备和服务,搜索消息采用多播方式发送,而设备针对搜索的响应则是使用单播方式发送。
SSDP 设备类型及服务类型
设备类型 |
表示文字 |
UPnP_RootDevice |
upnp:rootdevice |
UPnP_InternetGatewayDevice1 |
urn:schemas-upnp-org:device:InternetGatewayDevice:1 |
UPnP_WANConnectionDevice1 |
urn:schemas-upnp-org:device:WANConnectionDevice:1 |
UPnP_WANDevice1 |
urn:schemas-upnp-org:device:WANConnectionDevice:1 |
UPnP_WANCommonInterfaceConfig1 |
urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 |
UPnP_WANIPConnection1 |
urn:schemas-upnp-org:device:WANConnectionDevice:1 |
UPnP_Layer3Forwarding1 |
urn:schemas-upnp-org:service:WANIPConnection:1 |
UPnP_WANConnectionDevice1 |
urn:schemas-upnp-org:service:Layer3Forwarding:1 |
服务类型 |
表示文字 |
UPnP_MediaServer1 |
urn:schemas-upnp-org:device:MediaServer:1 |
UPnP_MediaRenderer1 |
urn:schemas-upnp-org:device:MediaRenderer:1 |
UPnP_ContentDirectory1 |
urn:schemas-upnp-org:service:ContentDirectory:1 |
UPnP_RenderingControl1 |
urn:schemas-upnp-org:service:RenderingControl:1 |
UPnP_ConnectionManager1 |
urn:schemas-upnp-org:service:ConnectionManager:1 |
UPnP_AVTransport1 |
urn:schemas-upnp-org:service:AVTransport:1 |
主动通知方式
当设备添加到网络后,定期向(239.255.255.250:1900)发送SSDP通知消息宣告自己的设备和服务。
宣告消息分为 ssdp:alive(设备可用)
和 ssdp:byebye(设备不可用)
ssdp:alive 消息
1
2
3
4
5
6
7
8
9
10
11 |
NOTIFY * HTTP/1.1 // 消息头
NT: // 在此消息中,NT头必须为服务的服务类型。(如:upnp:rootdevice)
HOST: // 设置为协议保留多播地址和端口,必须是:239.255.255.250:1900(IPv4)或FF0x::C(IPv6
NTS: // 表示通知消息的子类型,必须为ssdp:alive
LOCATION: // 包含根设备描述得URL地址 device 的webservice路径(如:http://127.0.0.1:2351/1.xml)
CACHE-CONTROL: // max-age指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在 (如:max-age=1800)
SERVER: // 包含操作系统名,版本,产品名和产品版本信息( 如:Windows NT/5.0, UPnP/1.0)
USN: // 表示不同服务的统一服务名,它提供了一种标识出相同类型服务的能力。如:
// 根/启动设备 uuid:f7001351-cf4f-4edd-b3df-4b04792d0e8a::upnp:rootdevice
// 连接管理器 uuid:f7001351-cf4f-4edd-b3df-4b04792d0e8a::urn:schemas-upnp-org:service:ConnectionManager:1
// 内容管理器 uuid:f7001351-cf4f-4edd-b3df-4b04792d0e8a::urn:schemas-upnp-org:service:ContentDirectory:1 |
ssdp:byebye 消息
当设备即将从网络中退出时,设备需要对每一个未超期的 ssdp:alive
消息多播形式发送 ssdp:byebye
消息,其格式如下:
1
2
3
4 |
NOTIFY * HTTP/1.1 // 消息头
HOST: // 设置为协议保留多播地址和端口,必须是:239.255.255.250:1900(IPv4)或FF0x::C(IPv6
NTS: // 表示通知消息的子类型,必须为ssdp:byebye
USN: // 同上 |
搜索——响应方式
当控制点,如手机客户端,加入到网络中,可以通过多播搜索消息来寻找网络上感兴趣的设备。我写DLNA模块时候也用主动搜索方式来发现设备。主动搜索可以使用多播方式在整个网络上搜索设备和服务,也可以使用单播方式搜索特定主机上的设备和服务。
多播搜索消息
一般情况我们使用多播搜索消息来搜索所有设备即可。多播搜索消息如下:
1
2
3
4
5
6
7
8
9
10 |
M-SEARCH * HTTP/1.1 // 请求头 不可改变
MAN: "ssdp:discover" // 设置协议查询的类型,必须是:ssdp:discover
MX: 5 // 设置设备响应最长等待时间,设备响应在0和这个值之间随机选择响应延迟的值。这样可以为控制点响应平衡网络负载。
HOST: 239.255.255.250:1900 // 设置为协议保留多播地址和端口,必须是:239.255.255.250:1900(IPv4)或FF0x::C(IPv6
ST: upnp:rootdevice // 设置服务查询的目标,它必须是下面的类型:
// ssdp:all 搜索所有设备和服务
// upnp:rootdevice 仅搜索网络中的根设备
// uuid:device-UUID 查询UUID标识的设备
// urn:schemas-upnp-org:device:device-Type:version 查询device-Type字段指定的设备类型,设备类型和版本由UPNP组织定义。
// urn:schemas-upnp-org:service:service-Type:version 查询service-Type字段指定的服务类型,服务类型和版本由UPNP组织定义。 |
如果需要实现投屏,则设备类型 ST
为 urn:schemas-upnp-org:service:AVTransport:1
多播搜索响应
多播搜索 M-SEARCH
响应与通知消息很类此,只是将NT字段作为ST字段。响应必须以一下格式发送:
1
2
3
4
5
6
7
8
9
10 |
HTTP/1.1 200 OK // * 消息头
LOCATION: // * 包含根设备描述得URL地址 device 的webservice路径(如:http://127.0.0.1:2351/1.xml)
CACHE-CONTROL: // * max-age指定通知消息存活时间,如果超过此时间间隔,控制点可以认为设备不存在 (如:max-age=1800)
SERVER: // 包含操作系统名,版本,产品名和产品版本信息( 如:Windows NT/5.0, UPnP/1.0)
EXT: // 为了符合HTTP协议要求,并未使用。
BOOTID.UPNP.ORG: // 可以不存在,初始值为时间戳,每当设备重启并加入到网络时+1,用于判断设备是否重启。也可以用于区分多宿主设备。
CONFIGID.UPNP.ORG: // 可以不存在,由两部分组成的非负十六进制整数,由两部分组成,第一部分代表跟设备和其上的嵌入式设备,第二部分代表这些设备上的服务。
USN: // * 表示不同服务的统一服务名
ST: // * 服务的服务类型
DATE: // 响应生成时间 |
其中主要关注带有 *
的部分即可。这里还有一个大坑,有些设备返回来的字段名称可能包含有小写,如LOCATION和Location,需要做处理。
此外还需根据LOCATION保存设备的IP和端口地址。
响应例子如下:
1
2
3
4
5
6
7
8 |
HTTP/1.1 200 OK
Cache-control: max-age=1800
Usn: uuid:88024158-a0e8-2dd5-ffff-ffffc7831a22::urn:schemas-upnp-org:service:AVTransport:1
Location: http://192.168.1.243:46201/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/desc.xml
Server: Linux/3.10.33 UPnP/1.0 Teleal-Cling/1.0
Date: Tue, 01 Mar 2016 08:47:42 GMT+00:00
Ext:
St: urn:schemas-upnp-org:service:AVTransport:1 |
描述
控制点发现设备之后仍然对设备知之甚少,仅能知道UPnP类型,UUID和设备描述URL。为了进一步了解设备和服务,需要获取并解析XML描述文件。
描述文件有两种类型:设备描述文档(DDD)
和服务描述文档(SDD)
设备描述文档
设备描述文档是对设备的基本信息描述,包括厂商制造商信息、设备信息、设备所包含服务基本信息等。
设备描述采用XML格式,可以通过HTTP GET请求获取。其链接为设备发现消息中的Location。如上述设备的描述文件获取请求为
1
2 |
GET http://192.168.1.243:46201/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/desc.xml HTTP/1.1
HOST: 192.168.1.243:46201 |
设备响应如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 |
HTTP/1.1 200 OK
Content-Length : 3612
Content-type : text/xml
Date : Tue, 01 Mar 2016 10:00:36 GMT+00:00
1
0
urn:schemas-upnp-org:device:MediaRenderer:1
uuid:88024158-a0e8-2dd5-ffff-ffffc7831a22
客厅的小米盒子
QPlay:1
Xiaomi
http://www.xiaomi.com/
Xiaomi MediaRenderer
Xiaomi MediaRenderer
1
http://www.xiaomi.com/hezi
11262/180303452
device_presentation_page.html
123456789012
DMR-1.50
,
image/png
128
128
8
icon/icon128x128.png
urn:schemas-upnp-org:service:AVTransport:1
urn:upnp-org:serviceId:AVTransport
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/action
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/desc.xml
urn:schemas-upnp-org:service:RenderingControl:1
urn:upnp-org:serviceId:RenderingControl
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/RenderingControl/action
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/RenderingControl/event
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/RenderingControl/desc.xml
urn:schemas-upnp-org:service:ConnectionManager:1
urn:upnp-org:serviceId:ConnectionManager
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/ConnectionManager/action
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/ConnectionManager/event
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/ConnectionManager/desc.xml
urn:mi-com:service:RController:1
urn:upnp-org:serviceId:RController
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/RController/action
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/RController/event
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/RController/desc.xml
1.0
controller
http://192.168.1.243:6095/
data
http://api.tv.duokanbox.com/bolt/3party/
|
其中响应消息体为XML格式的设备描述内容。信息结构比较明确,就不一一介绍了。解析该XML,保存设备的一些基本信息如 deviceType
、 friendlyName
、 iconList
等。之后我们关注该设备提供的服务列表,投屏最关注的服务为 urn:schemas-upnp-org:service:AVTransport:1
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
urn:schemas-upnp-org:service:AVTransport:1
urn:upnp-org:serviceId:AVTransport
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/action
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/desc.xml
|
- serviceId : 必有字段。服务表示符,是服务实例的唯一标识。
- serviceType : 必有字段。UPnP服务类型。格式定义与deviceType类此。详看文章开头表格。
- SCPDURL : 必有字段。Service Control Protocol Description URL,获取设备描述文档URL。
- controlURL : 必有字段。向服务发出控制消息的URL,详见 基于DLNA实现iOS,Android投屏:SOAP控制设备
- eventSubURL : 必有字段。订阅该服务时间的URL,详见 基于DLNA实现iOS,Android投屏:SOAP控制设备
如只需要实现简单的投屏,则保存urn:schemas-upnp-org:service:AVTransport:1
服务的上述信息即可。如需要进一步了解该服务,则需要获取并解析服务描述文档。
坑点1:有些设备 SCPDURL
、 controlURL
、 eventSubURL
开头包含 /
,有些设备不包含,拼接URL时需要注意。
服务描述文档
为了实现简单的投屏和控制(播放、暂停、停止、快进)操作并不需要解析服务描述文件。所有动作均为UPnP规范动作,具体动作请求参见基于DLNA实现iOS,Android投屏:SOAP控制设备
服务描述文档是对服务功能的基本说明,包括服务上的动作及参数,还有状态变量和其数据类型、取值范围等。
和设备描述文档一致,服务描述文档也是采用XML语法,并遵守标准UPnP服务schema文件格式要求。获取上述服务SDD语法如下:
1
2 |
GET http://192.168.1.243:46201/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/desc.xml
HOST: 192.168.1.243:46201 |
设备响应如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93 |
HTTP/1.1 200 OK
Content-Length : 3612
Content-type : text/xml
Date : Tue, 01 Mar 2016 10:00:36 GMT+00:00
1
0
Pause
InstanceID
in
A_ARG_TYPE_InstanceID
Play
InstanceID
in
A_ARG_TYPE_InstanceID
Speed
in
TransportPlaySpeed
Previous
InstanceID
in
A_ARG_TYPE_InstanceID
SetAVTransportURI
InstanceID
in
A_ARG_TYPE_InstanceID
CurrentURI
in
AVTransportURI
CurrentURIMetaData
in
AVTransportURIMetaData
...
CurrentTrackURI
string
CurrentMediaDuration
string
AbsoluteCounterPosition
i4
RelativeCounterPosition
i4
A_ARG_TYPE_InstanceID
ui4
...
|
- actionList 目前服务上所包含的动作列表。
- actionList 目前服务上所包含的状态变量。
以Pause动作为例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 |
Pause
InstanceID
in
A_ARG_TYPE_InstanceID
...
A_ARG_TYPE_InstanceID
ui4
...
|
为了实现简单的投屏和控制(播放、暂停、停止、快进)操作并不需要解析服务描述文件。所有动作均为UPnP规范动作,具体动作请求参见基于DLNA实现iOS,Android投屏:SOAP控制设备
基于DLNA实现iOS,Android投屏:SOAP控制设备
UPdP网络中,控制点和服务之间使用简单对象访问协议(Simple Object Access Protocol,SOAP)
根据基于DLNA实现iOS,Android投屏:SSDP发现设备收到设备描述文档(DDD)和服务描述文档(SDD),通过解析DDD获取
控制点可以知道该设备上某个服务的控制点地址。再通过解析 DDD 中
中的
和
获取该服务动作的动作名称,参数要求。控制点向 controlURL
发出服务调用信息,表明动作名称和相应参数来调用相应的服务。
SOAP简单对象访问协议
控制点和服务之间使用简单对象访问协议(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 ,回答能不能执行调用,如果出错则返回一个错误代码。
动作调用(UPnP Action Request)
使用POST方法发送控制消息的格式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 |
POST HTTP/1.0
Host: hostname:portNumber
Content-Lenght: byte in body
Content-Type: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName"
in arg values
|
- control URL: 基于DLNA实现iOS,Android投屏:SSDP发现设备 中提到的
设备描述文件
中 urn:upnp-org:serviceId:AVTransport
服务的
- HOST: 上述服务器的根地址和端口号。
- actionName: 需要调用动作的名称,对应相应服务的
服务描述文件
中的
的
字段。
- argumentName: 输入参数名称,对应相应服务的
服务描述文件
中的
字段。
- in arg values: 输入参数值,具体的可以通过 ,可以通过
服务描述文件
提到的状态变量来得知值得类型。
- urn:schemas-upnp-org:service:serviceType:v:对应该
设备描述文件
相应服务的 字段。
动作响应(UPnP Action Response-Succes)
收到控制点发来的动作调用请求后,设备上的服务必须执行动作调用。,并在 30s 内响应。如果需要超过 30s 才能完成执行的动作,则可以先返回一个应答消息,等动作执行完成再利用事件机制返回动作响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 |
HTTP/1.0 200 OK // 响应成功响应头
Content-Type: text/xml; charset="utf-8"
Date: Tue, 01 Mar 2016 10:00:36 GMT+00:00
Content-Length: byte in body
out arg value
|
- actionNameResponse: 响应的动作名称
- arugumentName: 当动作带有输出变量时必选,输出变量名称
- out arg values: 输出变量名称值
动作错误响应(UPnP Action Response-Succes)
如果处理动作过程中出现错误,则返回一个一下格式的错误响应。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 |
HTTP/1.0 500 Internal Server Error // 响应成功响应头
Content-Type: text/xml; charset="utf-8"
Date: Tue, 01 Mar 2016 10:00:36 GMT+00:00
Content-Length: byte in body
s:Client
UPnPError
402
Invalid or Missing Args
|
- faultcode: SOAP规定使用元素,调用动作遇到的错误类型,一般为s:Client。
- faultstring: SOAP规定使用元素,值必须为 UPnPError。
- detail: SOAP规定使用元素,错误的详细描述信息。
- UPnPError: UPnP规定元素。
- errorCode: UPnP规定元素,整数。详见下表。
- errorDescription: UPnP规定元素,简短错误描述。
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均如下所示,其中:
- control URL: 基于DLNA实现iOS,Android投屏:SSDP发现设备 中提到的
设备描述文件
中 urn:upnp-org:serviceId:AVTransport
服务的
。
- HOST: 上述服务器的根地址和端口号。
- urn:schemas-upnp-org:service:serviceType:v:对应相应设备的
设备描述文件
相应服务的 字段。
- actionName: 需要调用动作的名称,对应相应服务的
服务描述文件
中的
的
字段。
1
2
3
4
5 |
POST /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/action HTTP/1.0
Host: 192.168.1.243:46201
Content-Length: byte in body
Content-Type: text/xml; charset="utf-8"
SOAPACTION: "urn:schemas-upnp-org:service:serviceType:v#actionName" |
下面请求和响应均忽略Header,参数列表中列出Header的SOAPACTION值
设置播放资源URI
动作请求
设置当前播放视频动作统一名称为 SetAVTransportURI
。 需要传递参数有
- InstanceID:设置当前播放时期时为 0 即可。
- CurrentURI: 播放资源URI
- CurrentURIMetaData: 媒体meta数据,可以为空
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#SetAVTransportURI”
有些设备传递播放URI后就能直接播放,有些设备设置URI后需要发送播放命令,可以在接收到 SetAVTransportURIResponse
响应后调用播放动作来解决。
1
2
3
4
5
6
7
8
9
10 |
0
http://125.39.35.130/mp4files/4100000003406F25/clips.vorwaerts-gmbh.de/big_buck_bunny.mp4
|
响应
播放
动作请求
播放视频动作统一名称为 Play
。 需要传递参数有
- InstanceID:设置当前播放时期时为 0 即可。
- Speed:播放速度,默认传 1 。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#Pause”
响应
暂停
动作请求
暂停视频动作统一名称为 Pause
。 需要传递参数有
- InstanceID:设置当前播放时期时为 0 即可。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#Pause”
响应
获取播放进度
动作请求
获取播放进度动作统一名称为 GetPositionInfo
。 需要传递参数有
- InstanceID:设置当前播放时期时为 0 即可。
- MediaDuration: 可以为空。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#MediaDuration”
响应
获取播放进度响应中包含了比较多的信息,其中我们主要关心的有一下三个:
- TrackDuration: 目前播放视频时长
- RelTime: 真实播放时长
- AbsTime: 相对播放时长
注:目前为止还没发现 RelTime
AbsTime
和不一样的情况,选用 RelTime
就ok。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
00:04:32
00:00:07
00:00:07
2147483647
2147483647
|
跳转至特定进度或视频
动作请求
跳转到特定的进度或者特定的视频(多个视频播放情况),需要调用 Seek
动作,传递参数有:
- InstanceID: 一般为 0 。
- Unit:REL_TIME(跳转到某个进度)或 TRACK_NR(跳转到某个视频)。
- Target: 目标值,可以是 00:02:21 格式的进度或者整数的 TRACK_NR。
- Header_SOAPACTION: “urn:upnp-org:serviceId:AVTransport#Seek”
1
2
3
4
5
6
7
8
9
10 |
0
REL_TIME
00:02:21
|
响应
iOS实现
需要用到库
- AEXML - 轻量 XML 库,用于构造和解析XML
构造动作XML
首先利用 AEXML 构造动作 XML 部分。由于所有动作结构相似,写了个构造方法
1
2
3
4
5
6
7
8
9
10
11
12
13 |
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
和 播放动作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 |
/**
投屏
- 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")
} |
发送动作请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 |
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()
} |
解析响应
解析请求响应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 |
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
}
} |
基于DLNA实现iOS,Android投屏:订阅事件通知
服务运行时,可能改变有些状态信息变量的值,这是需要及时地更新给控制点。因此控制点可以通过订阅操作,让服务通过发送事件消息来发布更新。
事件消息包括一个或多个状态变量以及他们的当前数值。这些消息也是采用 XML 格式,遵循通用事件通知体系 GENA 规定。
服务运行过程中,该服务的 服务描述文件SDD
中 状态变量
发生了变化并且该变量的
属性为 yes
时,将会产生一个事件(Event)消息。如该状态变量的
属性为 yes
,则该服务把这个事件消息向整个网进行多播(Multicast)。如果为 no
或者不存在这个属性,则通过单播(Unicast)给订阅者发送消息。
单播事件消息的订阅及推送是遵循通用事件通知结构(General Event Notification Architecture,GENA)协议。协议中,控制点通常是个订阅者(Subscriber),它向服务提供者(通常是某个设备上的服务)发送订阅消息(SUBSCRIBE),建立订阅关系,然后可以继续更新订阅消息(Renewal),或者最后退订消息(Cancel)。另外,UPnP对GENA进行了一些扩展,如在事件消息中增加了一个key,来表示事件的顺序。
事件订阅和通知过程如下。
订阅
事件订阅说白了就是给某个服务的 订阅 URL
发送一条包含 回调 URL
和 订阅期限
的订阅请求。
以 设备描述文档 DDD
中描述 AVTransport
服务的片段例,默认其 HOST: 192.168.1.243:46201
1
2
3
4
5
6
7 |
urn:schemas-upnp-org:service:AVTransport:1
urn:upnp-org:serviceId:AVTransport
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/action
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event
/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/desc.xml
|
订阅请求
上述服务的订阅请求如下,其中注意点就是 回调URL CALLBACK
必须带有 <>
否则回调不成功。为了接受回调还需要手机上运行一个 HTTP Server
,具体实现请看下一部分。
1
2
3
4
5
6 |
SUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1
HOST: 192.168.1.243:46201
USER-AGENT: iOS/9.2.1 UPnP/1.1 SCDLNA/1.0
CALLBACK:
NT: upnp:event
TIMEOUT: Second-3600 // 订阅期限 |
订阅响应
成功响应
如果订阅成功,则服务 30s 内返回如下的响应。其中 SID
为订阅标识符,必须以uuid开头。订阅成功后需要保存,后续续订和取消订阅均需要提供该标识符。此外还需要保存订阅期限 TIMEOUT: Second-3600
1
2
3
4
5
6 |
HTTP/1.1 200 OK
Server: Linux/3.10.33 UPnP/1.0 IQIYIDLNA/iqiyidlna/NewDLNA/1.0
SID: uuid:f392-a153-571c-e10b
Content-Type: text/html; charset="utf-8"
TIMEOUT: Second-3600
Date: Thu, 03 Mar 2016 19:01:42 GMT |
订阅失败
若订阅失败,发布者必须返回一个订阅失败响应。格式如下:
1
2
3
4
5 |
HTTP/1.1 error code errordescrioption
Server: OS/Version UPnP/1.1 product/version
SID: uuid:subscibe-UUID
Content-Length: 0
Date: Thu, 03 Mar 2016 19:01:42 GMT |
iOS实现
用Swift实现的订阅请求如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 |
func subscribe() {
let url = "192.168.1.243:46201" + "/dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event"
let request = NSMutableURLRequest(URL: NSURL(string: url)!)
request.HTTPMethod = "SUBSCRIBE"
request.addValue("iOS/9.2.1 UPnP/1.1 SCDLNA/1.0", forHTTPHeaderField: "User-Agent")
// 必须加上<>,不要问我为什么,不然没法订阅成功
request.addValue("", forHTTPHeaderField: "CALLBACK")
request.addValue("upnp:event", forHTTPHeaderField: "NT")
request.addValue("Second-3600", forHTTPHeaderField: "TIMEOUT")
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("Subscribe Filed With Error Code:\(httpStatus.statusCode)")
print("response = \(response)")
return
}
// 若订阅成功,则保存SID
if let response = response as? NSHTTPURLResponse {
self.lastSubscribeSID = response.allHeaderFields["SID"] as? String ?? ""
}
}
task.resume()
} |
续订
如果需要续订某个服务,则必须在订阅期限过期前,将续订消息发往服务器进行续订。
续订请求
1
2
3
4 |
SUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1
HOST: 192.168.1.243:46201
SID: uuid:subscibe-UUID
TIMEOUT: Second-3600 // 订阅期限 |
取消订阅
不需要在关注特定服务的事件时,需要向服务器发送取消订阅消息。
取消订阅请求
1
2
3 |
UNSUBSCRIBE /dev/88024158-a0e8-2dd5-ffff-ffffc7831a22/svc/upnp-org/AVTransport/event HTTP/1.1
HOST: 192.168.1.243:46201
SID: uuid:subscibe-UUID |
单播事件消息
当服务器上的状态变量发生变数时,通过单播给订阅者发送通知。单播通过 HTTP 协议发送。需要在本地运行一个 HTTP Server
来接受请求。接收事件消息成功后,只需要简单返回一个 HTTP/1.1 200 OK
作为回应即刻。
坑:有些设备返回的xml中 <
>
被转义,导致解析时候出错。所以需要先反转义,然后再解析。
单播消息格式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 |
NOTIFY /dlna/callback HTTP/1.0
Host: 192.168.1.100:5000
Content-Length: 325
Content-Type: text/xml; charset="utf-8"
User-Agent: Neptune/1.1.3, 6
SID: uuid:ac6dce5a-6047-7862-fd41-e5596960f57a // 订阅标识符
NTS: upnp:propchange // GENA规定,必须是 upnp:propchange
NT: upnp:event // GENA规定,必须是 upnp:event
SEQ: 4 // 事件编号,初始值为0。
new values
|
播放消息
忽略头部的停止播放消息
1
2
3
4
5
6
7
8
9
10
11
12 |
|
停止播放消息
忽略头部的停止播放消息
1
2
3
4
5
6
7
8
9
10
11
12 |
|
iOS实现
iOS实现我用到了一下开源库
- GCDWebServer - 轻量 iOS/OSX GCD的服务器框架
- AEXML - 轻量 XML 解析库
创建 HTTP Server
首先需要利用 GCDWebServer 创建一个 HTTP server 接受事件消息回调。具体代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 |
private func startWebServer() {
let webServer = GCDWebServer()
// 为回调消息添加处理回调事件
webServer.addHandlerForMethod("NOTIFY", pathRegex: "/dlna/callback", requestClass: GCDWebServerDataRequest.self) {
(request) -> GCDWebServerResponse! in
// 转换 request 类型为 GCDWebServerDataRequest,然后读取请求 body
if let re = request as? GCDWebServerDataRequest {
if re.hasBody() {
// 如果请求有 body 部分,则开始解析。
self.parseNotifMassage(re.data)
}
}
return GCDWebServerDataResponse(HTML:"Hello World ")
}
webServer.startWithPort(8899, bonjourName: nil)
} |
创建 webServer 后,可以通过 webServer.serverURL
获取 serverURL
。 这时把 "<\(webServer.serverURL)dlna/callback>"
作为回调 URL 。按照前文给出代码进行订阅就可以收到事件消息了。
解析消息
接收到通知消息后,利用 GCDWebServer 解析 XML,获取具体的动作。目前只对播放状态做了处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 |
private func parseNotifMassage(data:NSData) {
do {
// 这里有个坑,有些设备返回的xml中<>被转义,导致解析时候出错。所以需要先反转义,然后再解析。
// reTransfer()是我写的简单的 String 扩展,具体看最后
let string = (NSString(data: data, encoding: NSUTF8StringEncoding) as! String).reTransfer()
let xmlData = string.dataUsingEncoding(NSUTF8StringEncoding)!
// 把 XML 转换成
let xml = try AEXMLDocument(xmlData: xmlData)
let status = xml.root["e:property"]["LastChange"]["Event"]["InstanceID"]["TransportState"].attributes
if !status.isEmpty {
switch status.first!.1.uppercaseString {
case "TRANSITIONING":
print("正在传输")
case "PLAYING":
print("播放")
case "PAUSED_PLAYBACK":
print("暂停播放")
case "STOPPED":
print("停止播放")
default :
print("未定义动作 - \(status.first!.1)")
}
} else {
print("未定义XML - \(xml.xmlString)")
}
}
catch {
print(error)
return
}
}
extension String {
func reTransfer() -> String {
let re1 = self.stringByReplacingOccurrencesOfString(">", withString: ">")
let re2 = re1.stringByReplacingOccurrencesOfString("<", withString: "<")
return re2
}
} |
iOS 实现基于 DLNA 的本机图片,视频投屏
DLNA 投网络上的媒体文件已经在前几篇实现过了。现在记录一下把本地图片和视频文件投到 DLNA 设备。
基础知识
DLNA
关于 DLNA 的基础知识请看一下四篇文章:
- 基于 DLNA 实现 iOS,Android 投屏:基本概念
- 基于 DLNA 实现 iOS,Android 投屏:SSDP发现设备
- 基于 DLNA 实现 iOS,Android 投屏:SOAP控制设备
- 基于 DLNA 实现 iOS,Android 投屏:订阅事件通知
GCDWebServer
GCDWebServer 是一个现代化的轻量级的基 于HTTP 1.1 的 GCD server,它主要用于嵌入 OS X & iOS apps。GCDWebServer 在我们的实现中扮演 HTTP Server 的作用。使用前确保你已阅读一下两篇:
- (译)GCDWebServer概述
- GCDWebServer Readme
实现思路
目前我有两种思路
- 实现完整的 DLNA Media Server,提供媒体目录和存储。
- 跑一个 HTTP Server,产生文件 URL,然后把 URL 投到 DLNA 设备,相当于投网络视频。
其中方案1是 DLNA 标准的做法,方案2相当于是简化版的 DMS( DLNA Media Server)。考虑到我的 DLNA 实现全都是自己写的,我选择了方案2。
注:
- 相册 - iOS 系统相册
- DLNAMediaHelper - 我写的一个 Helper 单例,用于保存随机产生的 url 和对应的 PHAsset 资源、获取某个具体 URL 对应的媒体文件数据或者路径。
- DLNAManager - DLNA 投屏部分是实现类,用于处理 SSDP发现设备,发送 SOAP 命令以及接受订阅。
- DLNA 设备 - 目标 DLNA 设备
- Webserver - 本机(iphone)HTTP Server
具体实现过程
相册获取 PHAsset
从相册获取媒体需要熟悉 PhotoKit,具体不阐述了。此处我选择了能够获取图片和视频的第三方开源框架 CTAssetsPickerController
产生 URL,保存字典
其实这一步没什么难度,首先根据时间戳产生一个 URL,并把这个 URL 作为 key, asset 作为 value 存到一个字典内,用于后期处理请求即可。具体实现看下面。
投 URL 到 DLNA 设备
详见 基于 DLNA 实现 iOS,Android 投屏:SOAP 控制设备
获取 PHAsset 对应的资源文件
PHAsset 代表照片库中的一个资源,不是真正的原始数据。所以我们还需要使用 PHAsset 获取对应资源。具体的获取方法我均放在 DLNAMediaManager
类中。
DLNAMediaManager 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
class DLNAMediaManager {
static let shared = DLNAMediaManager()
/// url:asset关系字典
var mediaList: [String:PHAsset] = [:]
/// 处理媒体资源请求的 webserver 的 URL,我这是直接用的是我的 DLNAManager 里面的 Server
var serverURL: String {
get {
return DLNAManager.sharedManager.webServer.serverURL.URLString
}
}
/**
产生 url 方法
- parameter forAsset: 目标 asset
- returns: 产生的响应 url
*/
func generateURL(forAsset :PHAsset) -> String {
var url = ""
if forAsset.mediaType == .Video {
url = serverURL + "videos/" + UIUTil.gettimestampForNow().md5() + ".mov"
} else {
url = serverURL + "images/" + UIUTil.gettimestampForNow().md5() + ".jpg"
}
mediaList[url] = forAsset
return url
}
/**
获取 Image 文件的 NSData,只需要在响应时候去获取再返回即可
- parameter url: 资源对应的请求url
- parameter callBack: 资源获取完成回调
*/
func fetchImageData(url:String, callBack:((data:NSData?)->Void)) {
if let asset = imageList[url] {
let options = PHImageRequestOptions()
options.synchronous = false
options.deliveryMode = .HighQualityFormat
options.networkAccessAllowed = true
PHImageManager.defaultManager().requestImageForAsset(asset,
targetSize: PHImageManagerMaximumSize,
contentMode: .Default,
options: options)
{ (result, info) -> Void in
if let image = result, data = UIImageJPEGRepresentation(image, 1.0) {
callBack(data:data)
} else {
callBack(data: nil)
}
}
} else {
callBack(data: nil)
}
}
/**
获取视频文件文件路径
- parameter url: 资源对应请求url
- parameter callBack: 资源获取完成回调
*/
func fetchVideoFile(url:String, callBack:((fileURL:String?)->Void)) {
if let asset = imageList[url] {
let imageManager = PHImageManager.defaultManager()
let videoRequestOptions = PHVideoRequestOptions()
videoRequestOptions.deliveryMode = .Automatic
videoRequestOptions.version = .Current
videoRequestOptions.networkAccessAllowed = true
imageManager.requestAVAssetForVideo(asset,
options: videoRequestOptions,
resultHandler:
{ (avAsset, avAudioMix, info) -> Void in
if let nextURLAsset = avAsset as? AVURLAsset,
filepath = nextURLAsset.URL.path {
callBack(fileURL: filepath)
} else {
callBack(fileURL: nil)
}
})
} else {
return callBack(fileURL: nil)
}
}
}
|
Webserver 接受请求
使用 GCD,具体处理方法如下,请注意其中图片和视频文件处理方法略有不同。关于 GCDWebServer 具体细节请见 GCDWebServer Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
func startWebServer() {
/**
* 若不符合其中两个请求方式,则返回404
*/
let MediaNotFoundResponse = GCDWebServerResponse(statusCode:404)
/**
* 图片文件响应,图片文件返回 Data Response
*/
webServer.addHandlerForMethod("GET", pathRegex: "/images/", requestClass: GCDWebServerRequest.self) { (request, completionBlock) in
let url = request.URL.URLString
DLNAImageHelper.fetchImageData(url, callBack: { (data) in
if let data = data {
let response = GCDWebServerDataResponse(data: data, contentType: "image/jpeg")
completionBlock(response)
} else {
completionBlock(MediaNotFoundResponse)
}
})
}
/**
* Video 文件响应,Video 文件数据很大,必须使用FileResonse
*
* 注意 `byteRange: request.byteRange` 这里,如果不这么处理 DLNA 设备无法播放媒体文件。
*
*/
webServer.addHandlerForMethod("GET", pathRegex: "/videos/", requestClass: GCDWebServerRequest.self) { (request, completionBlock) in
let url = request.URL.URLString
DLNAImageHelper.fetchVideoFile(url, callBack: { (fileURL) in
if let file = fileURL {
let response = GCDWebServerFileResponse(file: file, byteRange: request.byteRange)
completionBlock(response)
} else {
completionBlock(MediaNotFoundResponse)
}
})
}
webServer.startWithPort(8899, bonjourName: nil)
}
|
参考
- iOS 开发之照片框架详解
- How to get URL for a PHAsset?
- How to implement Video Seek support with an embedded HTTP server on iOS?
- 照片框架