视频边下边播
背景
目前视频相关的需求越来越多,众所周知,视频文件一般都比较大,在移动端播放会耗费很大的流量,如何让用户以最少的流量播放网络视频,且以最快的速度满足视频的播放及用户拖动响应,这篇文章将分享一下已经实现的一些策略及方案。
mp4文件基本知识:
对于播放器而言,只要视频文件的头信息(时长,帧率,码率,视频数据偏移量等)解析到了,然后根据视频播放的当前时间对应的内容数据就可以播放视频,mp4的基本格式可参考http://www.jianshu.com/p/3ab4bd0d4219。基于以上,只要解析到视频的头信息,然后缓存视频数据内容就可以实现缓存播放及seek播放。
两种方案
视频的缓存播放目前有两种方案,
1、通过解析mp4的格式,将mp4的数据直接下载并写入文件,然后让播放器直接播放的是本地的视频文件;
2、使用本地代理服务器进行文件缓存,并将视频url地址转换成本地代理服务器地址来实现视频的缓存播放。
第一种方案
图1如图1所示,第一种方案是先下载视频到本地文件,然后把本地视频文件地址传给播放器,播放器实际播放的是本地文件。当播放器的播放进度大于当前的可播放的下载缓存进度,则暂停播放,等缓存到足够播放时间之后,再让播放器开始播放。这种方案的下载方式是与播放器完全没有关系的,只是顺序的将服务器下发的视频数据写入本地文件,然后让播放器来读取数据。
但是在调研的过程中发现,对于mp4文件其实有两种格式的数据,一种是头信息(即moov)在视频头部,一种是在视频尾部,之前已经提到过,视频播放器只有解析到了头部才可以播放视频,所以应先获得mp4的moov才能播放。因此对于moov信息在后面的mp4文件,必须在视频缓存的时候把它写到文件前面才可以正常播放。对调换的过程以及mp4格式感兴趣的同学同样可以参考http://www.jianshu.com/p/3ab4bd0d4219 这篇文章。
这种方式虽然能够满足缓存播放这个需求,但是会产生很多问题,例如视频下载到本地,下载多少才可以把本地文件作为视频源传给播放器即视频开启播放速度;播放的速度大于下载速度的话,该怎么办?如果播放器seek到文件没有缓存的位置,应该怎么处理?对于视频关闭之后,第二次进入如何知道已经下载了多少?等等问题。
目前的解决方案是,当缓存到500kb才把缓存的地址传给播放器,视频文件小于500kb则下载完之后再播放,起播慢(需要改进)。当下载进度比播放进度多5秒的数据量才让播放器播放,不然的话就暂停。如果seek到没有缓存的地方就切换到网络上停止当前的下载,浪费一些流量。每次下载都会保存一份配置文件,来保存是否下载完成,没下载完成则第二次根据当前缓存文件大小,重新开始顺序下载。这个时候有些同学会想,这些数据怎么来的,---只是我们测试出来的经验值(亟待改进)。
总的来说第一种方案有如下缺点:
1、用户播放视频的时候可能等待的时间较长(起播慢)
2、流量浪费(seek之后会播网络流,停止下载)
3、需要太多控制视频播放的逻辑来进行辅助,与播放器代码耦合严重。
4、seek之后切源会耗时,每次seek比较慢
因此经过一段时间的研究,新的缓存方案应运而生。
第二种方案
图 2核心技术要点:
1、 通过代理服务器,从socket截取播放器请求数据;
2、 根据截取的range信息,从网络服务器请求视频数据;
3、 视频数据写入本地文件,seek后可以从seek位置继续写入并播放;
4、 边下边播,加快播放速度;
5、 与播放器逻辑完全解耦,对于播放器只是一个地址
如图2所示,新的方案是在播放器与视频源服务器之间加一层代理服务器,截取视频播放器发送的请求,根据截取的请求,向网络服务器请求数据,然后写到本地。本地代理服务器从文件中读取数据并发送给播放器进行播放。过程如图3所示:
图 3具体流程如下:
1、启动本地代理服务器。
2、视频源地址传给本地代理服务器。
3、将视频源地址转换成本地代理服务器的地址作为播放器的视频源地址。
4、播放器向本地代理服务器发送请求。
5、本地代理服务器截取这个请求,再根据解析出来请求的信息向真正的服务器发起请求。
6、本地代理服务器开始接受数据,写入文件并将文件数据再返回到播放器。
7、播放器接收到这些数据之后播放。
8、seek之后重新进行以上步骤。
代理服务器视频文件下载方案
考虑到播放视频的时候,用户会拖动进度条进行seek,而此时需要从用户拖动的位置进行下载,这样会让视频文件产生许多的空洞,如图4所示:
图 4为了节省流量,只会下载文件中没有数据的部分,也就是图 4蓝色的部分。因此需要存储下载的片段信息。目前采用的数据结构如下所示:
fragment = [start,end];
array = [fragment 0,fragment 1,fragment 2,fragment 3];
其中fragment指的是下载的片段,start指的是片段开始的位置,end为片段的结束位置。
array指的是存储fragment的数组,数组中的fragment是依靠start从小到大来来插入到数组中的,保证了数组的有序性。
下载的片段是记录在一个数组中:array = [fragment0 ,fragment 1,fragment 2,fragment 3];
下载共分为两个阶段:seek阶段和补洞阶段。
seek阶段:即为在播放的时候,根据用户seek的位置来进行下载。
根据seek到的位置分为两种情况:
情况一:如果seek到的位置是在已有的片段中(例如图中的seek1的位置,该处有数据),就从该片段(fragment1)的末尾请求数据(end1),直到下个片段的开始位置处(fragment2的start),也就是向服务器请求的range为:
rang1 = (end1 ) —— start2;
这个片段下载完成后,假如把下载的片段记为fragment1.1,则会把fragment1、fragment1.1、fragment2合为一个片段为fragment1-2,则array = [fragment 0,fragement1-2,frament3];这次下载后的状态图5所示:
图 5接下来一直下载直到array = [fragment 0,fragement1-3];之后会判断fragement1-3有没有到文件末尾,如果到了就下载结束,如果没到就从从fragement3的(end3)开始下载直到文件末尾。
情况二:如果seek到的位置没有在已有的片段中,(例如说是在图4中的seek2的位置),就从seek到的位置开始下载数据直到下一个片段的start(fragment2的start2),假如这个片段记为fragment1.1,则会把fragment1.1和fragment2合并即数组为:array= [fragment 0,fragment1,fagment1.1-2,fragment3];合并后的情况如图6所示:接下来的操作就是继续下载,直到下载到文件末尾;
图 6如果片段太小保存起来就会让播放器下次播放的时候多发送一次请求,这样是很耗费资源。例如:如图6所示,如果fragment1的大小只有1kb,想要补充fragment0与fragment1.1-2之间的数据,就需要发送两次请求,这样频繁的发送请求,比较浪费资源。因此当fragment太小,就不存在配置数组中。这样会少发一次请求,也不会浪费很大的流量。
当下载片段太小(例如说下载的长度<20KB),就不保存在片段数组中(为了控制片段的粒度)。这样会产生一个问题,当视频文件中间有一个空洞小于20KB,这个片段永远补不上。这个时候就需要用到第二阶段。
第二阶段补洞阶段,就是第二次播放的时候,如果文件中有空洞,这个时候不论片段再小,也会存到片段中。
最后当配置数组中存的数据只剩下最后的{0,length},length为视频总长度的时候,表示文件已全部下载完成。
性能比较
下表的数据都是在第一次播放60秒,7.8M的视频得出来的性能数据。从表1中可以看出方案二性能比方案一的性能高出很多。
表 1