乐趣区

短视频宝贝=慢?阿里巴巴工程师这样秒开短视频

前言
随着短视频兴起,各大 APP 中短视频随处可见,feeds 流、详情页等等。怎样让用户有一个好的视频观看体验显得越来越重要了。大部分 feeds 里面滑动观看视频的时候,有明显的等待感,体验不是很好。针对这个问题我们展开了一波优化,目标是:视频播放秒开,视频播放体验良好。无图无真相,上个对比图,左边是优化之前的,右边是优化之后的:

问题分析
视频格式的选择
在正式分析问题之前有必要说明下:我们现在首页的视频,都是 320p H.264 编码的 mp4 视频。

H.264 & H.265
H.264 也称作 MPEG-4AVC(Advanced Video Codec,高级视频编码),是一种视频压缩标准,同时也是一种被广泛使用的高精度视频的录制、压缩和发布格式。H.264 因其是蓝光光盘的一种编解码标准而著名,所有蓝光播放器都必须能解码 H.264。H.264 相较于以前的编码标准有着一些新特性,如多参考帧的运动补偿、变块尺寸运动补偿、帧内预测编码等,通过利用这些新特性,H.264 比其他编码标准有着更高的视频质量和更低的码率.

H.265/HEVC 的编码架构大致上和 H.264/AVC 的架构相似,也主要包含:帧内预测 (intra prediction)、帧间预测(inter prediction)、转换 (transform)、量化 (quantization)、去区块滤波器(deblocking filter)、熵编码(entropy coding) 等模块。但在 HEVC 编码架构中,整体被分为了三个基本单位,分别是:编码单位(coding unit,CU)、预测单位(predict unit,PU) 和转换单位(transform unit,TU)。

总的来说 H.265 压缩效率更高,传输码率更低,视频画质更优。看起来使用 H.265 似乎是很明智的选择,
但我们这里选择的是 H.264。原因是:H.264 支持的机型范围更为广泛。

PS:闲鱼 H.265 视频在宝贝详情页会在近期上线,敬请关注体验!

TS & FLV & MP4

TS 是日本高清摄像机拍摄下进行的封装格式,全称为 MPEG2-TS。TS 即 ”Transport Stream” 的缩写。MPEG2-TS 格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。下述命令可以把 mp4 转换成 ts 格式,从结果来看 ts 文件 (4.3MB) 比 mp4 文件 (3.9MB) 大 10% 左右。
ffmpeg -i input.mp4 -c copy output.ts
FLV 是 FLASH VIDEO 的简称,FLV 流媒体格式是随着 Flash MX 的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入 Flash 后,使导出的 SWF 文件体积庞大,不能在网络上很好的使用等问题。FLV 只支持一个音频流、一个视频流,不能在一个文件里包含多路音频流。音频采样率不支持 48k,视频编码不支持 H.265。相同编码格式下,文件大小和 mp4 几乎没有区别。
ffmpeg -i input.mp4 -c copy output.flv
MP4 是为大家所熟知的一种视频封装格式,MP4 或称 MPEG- 4 第 14 部分是一种标准的数字多媒体容器格式。MPEG- 4 第 14 部分的扩充名为.mp4,以存储数字音频及数字视频为主,但也可以存储字幕和静止图像。因其可容纳支持比特流的视频流,MP4 可以在网络传输时使用流式传输。其兼容性很好,几乎所有的移动设备都支持,而且还能在浏览器、桌面系统进行播放。综合上面几个封装格式的特点,我们的最终选择是 MP4.
播放流程
一个视频在客户端的播放流程是怎么样的?播放首开慢耗时在什么地方?耗时点是否能够快速低成本的解决?了解视频的播放流程有助于找到问题的突破口。视频从加载到播放可以分为三个阶段:

读取(IO):“获取”内容 -> 从“本地”or“服务器”上获取
解析(Parser):“理解”内容 -> 参考“格式 & 协议”来“理解”内容
渲染(Render):“展示”内容 -> 通过扬声器 / 屏幕来“展示”内容

可以看出内容获取从“服务器”改为“本地”,这样会节省很大一部分时间,而且成本很低,是一个很好的切入点。事实也是如此,我们的优化正是围绕此点展开。
PS: 我们使用的网络库,播放器都是集团内部的,本身做了很多优化。本文不涉及网络协议,播放器方面的优化讨论。
技术方案
鉴于上面的分析,我们要做的工作是:把 mp4 文件提前缓存一部分,到 feeds 滑动要播放的时候,播放本地的 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 左右。
回过头来,或许我们还可以更进一步,可以对预加载收到的数据进行验证,确保缓存了准确的信息,而不是固定的数值。还可以进行更加深度的优化,让用户观看视频的体验更加顺滑。
参考文献
* [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache)
* [视频的封装格式和编码格式](https://www.jianshu.com/p/8034fa1ed682)
* [播放器技术分享(1):架构设计](http://blog.51cto.com/ticktick/2324928?source=dra)
* [MP4 文件格式的解析,以及 MP4 文件的分割算法](https://cloud.tencent.com/developer/article/1120604)
* [从天猫某活动视频 3 次请求说起](https://juejin.im/post/5c0e0f75e51d45410c5e1aea)
* [视音频编解码学习工程:FLV 封装格式分析器]https://blog.csdn.net/leixiaohua1020/article/details/17934487
* https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10_1.pdf
* https://baike.baidu.com/item/flv
* https://standards.iso.org/ittf/PubliclyAvailableStandards/index.html

本文作者:闲鱼技术 - 邻云阅读原文
本文为云栖社区原创内容,未经允许不得转载。

退出移动版