前言
随着短视频的兴起,短视频在各大app中随处可见,比如feeds流、详情页等等。如何让用户拥有良好的视频观看体验变得越来越重要。大部分提要滑动观看视频时,有明显的等待感,体验不是很好。针对这个问题,我们发起了一波优化,目标是:视频播放秒,视频播放体验好。图中无真。上一张对比图,左图为优化前,右图为优化后:
问题分析
视频格式的选择
在正式分析问题之前,有必要说明一下,我们首页的视频是320p H.264编码的mp4视频
H.264 H.265H.264也称为MPEG-4AVC(高级视频编解码器),是一种视频压缩标准,也是一种广泛用于录制、压缩和发布高精度视频的格式。H.264作为蓝光光盘的编码和解码标准而闻名。所有蓝光播放器都必须能够解码H.264,与之前的编码标准相比,H.264有一些新的特性,例如多参考帧运动补偿、可变块大小运动补偿、帧内预测编码等。通过使用这些新功能,H.264比其他编码标准具有更高的视频质量和更低的比特率。H.265/HEVC的编码架构与H.264/AVC大致相似,同样包括帧内预测、帧间预测、变换、量化、去块滤波、熵编码等模块。然而,在HEVC编码体系结构中,整个系统分为三个基本单元:编码单元(CU)、预测单元(PU)和变换单元(TU)。一般来说,H.265的压缩效率更高,传输速率更低,视频质量更好。看起来使用H.265是一个明智的选择,但是我们这里选择H.264。原因是:H.264支持的机型范围更广。PS:闲鱼H.265视频将于近期在宝贝详情页上线,敬请关注体验!TS FLV MP4TS是日本高清相机拍摄的一种打包格式,称为MPEG2-TS。TS“传输流”的缩写。MPEG-2 TS格式的特点是要求视频流的任何一段都可以独立解码。以下命令可以将mp4转换为ts格式。从结果来看,ts文件(4.3MB)比mp4文件(3.9MB)大10%左右。
mpeg-I输入. mp4-c复制输出。ts FLV是FLASH VIDEO的缩写,FLV流媒体格式是随着Flash MX的引入而开发的视频格式。由于其极小的文件和极快的加载速度,使得在互联网上观看视频文件成为可能。它的出现有效地解决了视频文件导入Flash后,导出的SWF文件体积庞大,不能很好地在互联网上使用的问题。FLV只支持一个音频流和一个视频流,不能在一个文件中包含多个音频流。音频采样率不支持48k,视频编码同样编码格式下不支持H.265,文件大小和mp4几乎没有区别。
Mpeg-I输入. mp4-c复制输出。flvmp4是一种众所周知的视频打包格式,mp4或MPEG-4 Part 14是一种标准的数字多媒体容器格式。MPEG-4的第14部分的扩展称为. mp4,主要存储数字音频和视频,但也可以存储字幕和静止图像。因为可以容纳支持比特流的视频流,所以MP4在网络传输时可以使用流传输。它的兼容性非常好,几乎所有的移动设备都支持,也可以在浏览器和桌面系统中播放。综合以上包装格式的特点,我们最终选择的是MP4。
播放流程
在客户端播放一个视频的流程是怎样的?开播慢在哪里花时间?耗时点能否快速低成本解决?了解视频的播放过程,有助于找到问题的突破口。视频从加载到播放可以分为三个阶段:
读取(IO):获取内容-从本地或服务器获取解析器:理解内容-参考格式协议理解内容渲染:显示内容-通过扬声器/屏幕显示内容010-6999
可以看出,内容获取由“服务器端”改为“本地端”,会节省大量时间,成本较低。这是一个很好的起点。事实也是如此,我们的优化就是围绕这一点展开的。
PS:我们使用的网络库和播放器都是集团内部的,他们做了很多优化。本文不涉及网络协议和播放器的优化讨论。
技术方案
鉴于以上分析,我们要做的就是:提前缓存一些mp4文件,在提要滑动播放的时候播放本地的mp4文件。由于用户可能会继续观看视频,因此在本地数据播放完毕后,需要从网络上下载数据进行播放。这
里需要解决两个问题:应该提前下载多少数据缓存数据播放完成后该怎么切换到网络数据MOOV BOX的位置
对于第一个问题,我们不得不分析一下mp4的文件结构,看看我们应该下载多少数据量合适。MP4是由很多Box 组成的,Box里面可以嵌套Box:
这里不详细介绍MP4的格式信息。但是可以看出moov box对播放很关键,它提供的信息如:宽高、时长、码率、编码格式、帧列表、关键帧列表等等。播放器没有获取到moov box是没办法进行播放的。所以下载的数据应该要包含moov box再加上几十帧的数据。
做了一个简单的计算:闲鱼短视频一般最长是30s,feeds里面的分辨率是320p,码率是1141kb/s,ftyp+moov这个视频的数据量在31kb左右(打开文件可以看出mdat是从31754byte的位置开始的),所以,头部信息+10帧的数据大约是:(31kb + 1141kb/3)/8 = 51KB
Proxy
第二个问题:缓存数据播放完成后该怎么切换到网络数据呢?在本地数据播放完成之后,设置一个网络地址给播放器,告诉播放器下载的offset是多少,然后继续从网络下载数据播放。这样看起来可行,但是需要播放器提供支持:本地数据播放完成的回调;设置网络url并支持offset。另外,服务端需要支持range参数,而且切换到网络播放的时候需要新建立网络连接,很可能会造成卡顿。
最终,我们选择了proxy的方式,把proxy作为中间人,负责预加载数据、给播放器提供数据,切换逻辑在proxy里面来完成。未加入proxy之前流程是这样的:
加入了proxy之后流程是这样的:
这样做的好处很明显,我们可以在proxy里面做很多事情:例如本地文件缓存数据和网络数据的切换工作。甚至是和CDN使用其它的协议进行通信。我们这里假定预加载工作已经完成,看看播放器是怎么和proxy进行交互的。播放的时候会用Proxy提供的一个localhost的url进行播放,这样代理服务器会收到网络请求,把本地预加载的数据返回给播放器。播放器完全感知不到proxy模块、预加载模块的存在。播放器、预加载模块都是Proxy的client,调用逻辑都是一样。图示说明如下:
下面逐步解释一下,数据的加载过程:
Client发起http请求获取数据,箭头1所示文件缓存如果存在所请求的数据则直接返回数据,箭头2所示若本地文件缓存数据不够,则发起网络请求,向CDN请求数据,箭头3所示获取网络数据,写入文件缓存,箭头4所示返回请求的数据给Client,箭头2所示
实现模块
预加载模块确定了技术方案后,预加载模块还是有很多工作要做的。在列表网络数据解析完成后会触发视频预加载,首先会根据url生成md5值,然后去查看这个md5值对应的任务是否存在,如果存在则不会重复提交。生成任务后会提交到线程池,在后台线程进行处理。网络从Wifi切换到3G的时候,会把任务取消,防止消耗用户的数据流量。
预加载任务在线程池执行的时候,其流程是这样的:首先会获取一个本地代理的url。然后发起http请求。Proxy会收到http请求进行处理,开始做真正的数据预加载工作。预加载模块读取到指定的数据量后终止。到此,预加载的任务就已完成。流程图如下所示:
在用户快速滑动的时候,怎么能保证视频还能继续秒开呢?预加载模块对于每一个任务都会维护一个状态机,在Fling的时候会把划过的任务暂停下,把最新要显示的任务优先级提高,让其优先执行。
Proxy模块
Proxy内部有个local的httpServer负责拦截播放器和预加载模块的http请求。client在请求时会带入CDN的url,在本地缓存数据没有的时候会去CDN获取新鲜数据。因为有多个地方向Proxy请求数据,所以用线程池来处理多个client的连接很有必要,这样多个client可以并行,不会因为前面有client在请求而阻塞。文件缓存使用LruDiskCache,在超过指定文件大小后,老的缓存文件会删除,这是一个在使用文件缓存时很容易忽视的问题。由于我们的场景视频是连续播放的,不存在seek的情况,所以文件缓存相对比较简单,不用考虑文件分段的情况。Proxy内部对于同一个url会映射到一个client,如果预加载和播放同时进行,数据只会有一份,不会去重复下载数据。再来一个Proxy内部构造示意图:
遇到的问题
在测试中发现,有的视频还是会播放很慢,仔细查看本地的确缓存了期望的数据大小,但是播放的时候还是有较长的等待时间,这种视频有个特点:moov box在尾部。对于moov在尾部的视频,是整个文件都下载完成后才进行播放的,原因是moov box里面存了很多关键信息,前面分析mp4格式的时候有提到。对于这个问题有两个解法:
解法一:服务端在进行转码的时候保证moov的头部在前面,发现moov位置不正确的视频服务端进行订正。
PS:查看moov在文件中的位置可以用hex文本编辑器打开,按字符搜索moov所在的位置即可,MAC上面还可以使用MediaParser , 另外还可以用ffmpeg命令生成moov在头部或者尾部的mp4文件。
例如: 从1.mp4 copy一个文件,使其moov头在尾部 ffmpeg -i 1.mp4 -c copy -f mp4 output.mp4 从1.mp4 copy一个文件,使其moov头在头部:ffmpeg -i 1.mp4 -c copy -f mp4 -movflags faststart output2.mp4解法二不用修改moov box的位置,而是在播放端进行处理,播放端需要检测流信息,如果moov前面没有,就去请求文件的尾部信息。具体就是:发起 HTTP MP4 请求,读取响应 body 的开头,如果发现 moov 在开头,就接着往下读mdat。如果发现开头没有,先读到 mdat,马上 RESET 这个连接,然后通过 Range 头读取文件末尾数据,因为前面一个 HTTP 请求已经获取到了 Content-Length ,知道了 MP4 文件的整个大小,通过 Range 头读取部分文件尾部数据也是可以的。示意图如下
这个方案的缺点是:对于moov box在尾部的视频会多两次http connection。
结语
本文介绍了常见的视频编码格式,视频封装格式,介绍了moov头信息对于视频播放的影响。随着对于播放流程的分析,我们找到了问题的切入点。简单说就是围绕着数据预加载展开,把网络请求数据的工作提前完成,播放的时候直接从缓存读取,而且后续的视频回看都是从缓存读取,不仅解决了视频初始化播放慢的问题,还解决了播放缓存问题,可以说是一箭双雕。Proxy是这个方案的核心思想,本地localhost的url是一个关键纽带,视频预加载模块和播放器模块解耦彻底,换了播放器照样可以使用。到此为止,视频feeds秒开优化就已完成。上线后的数据来看视频打开速度在800ms左右。
回过头来,或许我们还可以更进一步,可以对预加载收到的数据进行验证,确保缓存了准确的信息,而不是固定的数值。还可以进行更加深度的优化,让用户观看视频的体验更加顺滑。
参考文献
*