视频秒开的秘密の为什么HLS能吊打MP4
关注我们 文末有福利
作者简介
刘继刚
一个前端&客户端“两栖”开发工程师
背景
我司项目会在直播时进行视频录制(MP4格式),并在个人店铺页提供给录制视频回放功能。项目上线后发现,回放视频时能明显感觉到要loading很长时间才开始播放,依据视频大小不同,最长情况可能达到30秒,给用户的感觉就是“好卡”。
视频首播为什么那么卡
说卡顿之前,要先了解一些MP4文件的一些基本格式(本文以H.264-MP4为例)。
MP4 文件由许多个 原子 (也叫box)的数据块组成,大box中存放小box,一级嵌套一级来存放媒体信息。这些原子用以存储字幕和章节等内容, 当然也包括视频和音频等显而易见的数据。而视频和音频原子的元数据,以及有关如何播放视频的信息,如尺寸和每秒的帧数,则存储在叫做 moov 的特殊原子中。如果把MP4文件比喻成一本书的话,你可以认为 moov 是某种意义上的 MP4 文件目录。
基本结构
ftyp:File Type Box,处于文件的开始位置,描述的MP4文件的版本、兼容协议等
free:Free Space Box,预留字段、可省略,常被忽略。
mdat:Media Data Box,媒体数据内容,是实际的视频的内容存储区域。该区域通常占整个文件99%+大小的(音视频)。
moov:Movie Box,视频文件的必要区域,该区域包含了视频文件的metedata信息的mvhd(eg.存储视频的时长,创建、修改信息,时间刻度,速率)区域和 媒体数据引用、描述的track区(对于媒体数据来说,track表示一个视频或音频序列,hint-track 除外)。trak区域至少存在一个,大部分情况是两个(音频和视频),因trak内容知识点较多,这里不在过多概述,可以参考下图。
形象点来说,moov可以比如成是整个视频的目录,想要播放视频的话,必须要先加载moov区域拿到视频文件目录才能播放视频内容。
图:moov-box的第一个子box: mvhd区域
图:MP4视频文件的结构概述
图:通过moov-box中的trak去mdat区域视频
延迟问题定位
打开具体的某个回放页面,先会通过接口根据record_id去获取到页面的基本信息、视频的播放地址(videoUrl)、视频封面(poster);然后把videoUrl,poster赋值给video标签,后续浏览器根据标签参数就会开始获取视频信息,下载视频chunk,开始播放视频,由浏览器内部实现,js无法去过多干涉(暂停、播放等除外)。通过抓包能够大致了解浏览加载视频过程:
从network 面板中可以看到为了播放视频,浏览器先后发送了三次请求,来回耗时17s。来具体看下这三个请求(默认服务端支持range):
第一次:
浏览器第一次请求时尝试通过 HTTP range request(0-∞)尝试下载整个视频。但是实际只下载了47.2KB(ps.31.4KB为传输中经压缩后的传输流量)整个请求就完成了。来分析一下具体流程:
浏览器通过Range: bytes=0- 首先获取到了ftyp信息,box大小 [ftyp] size=8+24 字节(这个大小组成会在下面提到);
接下来继续尝试查找free区域(如果没有就跳过),box大小 [free] size=8+0 字节;
接着尝试查找下一个区域(moov或mdat),结果不幸匹配到的区域是mdat区域;这时浏览器就会主动终止请求,尝试从尾部查找视频的moov区域(ps.在上面MP4基本结构中,已经说明moov的重要性了:不解析moov就无法播放视频),就紧接着开始了第二次请求的发起了。
第二次:
在第一次请求中已经知道了整个视频文件的大小了,如何去确定请求的尾部的大小呢?就是 HTTP range request中的range值要从何值开始。由于MP4是个由box组成,标准的box开头的4个字节(32位)为box size,该大小包括box header和box body表面的整个box的大小,这样浏览器在第一次请求后就可以确定文件中剩下未解析到的box的开始的range值了。
计算过程(单位字节):
视频文件大小 - ftyp大小 - free大小 - mdat大小 =》(Content-Length: 219157707)- (ftyp-size: 8+24)- (free-size: 8+0)- (mdat-size: 217910682)= 1246985
也就是,这一次请求的range的开始值最大值不能高于219157707-1246985=217910722。
可以看到发出去的请求 Range: bytes=217907200-∞ ,上面我们计算的217910722-∞ 包含在内 (为什么不从217910722 开始,而是多向前了3.45KB,猜测是为了更安全的box查找范围,具体的原因未在深究)。请求到数据后,接下来就是解析moov box了,然后根据视频”目录“然后第三次请求。
第三次:
根据第二次请求的moov解析后,开始下载”真正“的视频的内容准备播放。
通过上面的分析,不必要的两次请求或许是导致首播卡顿元凶之一,因此我们对视频文件进行了优化(三次请求变成一次),实际的效果是有提升但是依然很不理想。
那么还有没有其他的问题到导致播放卡顿呢?
分析&解决
在上面的第三次请求中,抓包中能够明显观察到视频要缓存到5M左右才能开始播放。那如何降低这个视频首播缓冲的chunk大小呢,首先想到的就是降低视频的分辨率。通过服务端后台录制的视频是高清的(720p),如果把分辨率降低到标清(360p)如何呢?实际把视频文件进行转码成标清后,下图是同一个视频 标清 首次播放请求结果对比:
数据上看,首播效果有略微提升,但差距并不大。目前看来,在格式已定前提下能优化的已经尽力了,只能另辟他经了。
接下来看下视频播放过程中浏览器是如何对视频数据处理的(图片来源:雷霄骅的博客):
图中可以看到在播放前需要先解协议,解封装,解码音视频,同步,另外MP4的协议相对来说比较复杂,需要下载文件头比较大,而且要下载完才能解析(参考上面抓包的结果,头文件[ftyp+moov]大小将近1.19MB),解析后才去下载视频播放的内容。
有没有其他针对直播/点播的流协议呢?
答案是肯定(这里先不讨论FLV了),移动端苹果系统(iOS 3.0及更高版本的设备均支持)也给出了HTTP实时流的解决方案-HLS,安卓迫于iOS的淫威也开始系统层兼容HLS了,Can I use 上来看移动端兼容性相当不错。
关于HLS的介绍可以查看官方文档 developer.apple.com 。
首先服务端要根据源视频文件转换成HLS格式(m3u8和ts两种后缀)的文件,这个转换需要先把码源视频转码到目标编码格式(eg.H264),在进行 Stream segmenter(流分割)对视频切片后生成一个文本类型的xxx.m3u8索引文件和一组 xxxx.ts的分片文件,剩下的客户端获取.m3u8和.ts文件就交给HTTP服务器了(eg.nginx 、 apache)。整个从生成流到拉流的过程如下图:
客户端(H5端)首先会拉取到索引文件xxx.m3u8,解析后根据索引的中播放列表的顺序依序拉取ts文件,如果当前客户端不支持HLS,就需要通过js去手动下载解析这些ts文件,然后解析重组成视频流保存到SourceBuffer 中,在媒体源扩展 API(MSE) 进行播放(现有方案hls.js)。这里的注意的H.264视频解码能力是系统本身提供的,如果系统本身不支持那就gg了。
m3u8 文件实质是一个播放列表(playlist),其可能是一个媒体播放列表(Media Playlist),或者是一个主列表(Master Playlist)。但无论是哪种播放列表,其内部文字使用的都是 utf-8 编码。格式示例:
具体的的某个码流(eg.voide.m3u8)
更多m3u8的格式信息可以点 ietf.org 查看,目前HLS还是草案,并不是国际标准。
其实关于自适性流技术的有个国际标准,是MPEG-DASH,不过这货在2011年11月才成为国际标准,相对较晚,也不好推,各浏览器基本上都未在系统层支持,总的来说位置比较尴尬。
整体上来对比下这三种流格式的优缺点:
协议格式 | 播放体验 | 流量占用情况 |
DASH | 对视频进行切片,按切片播放,缓存小起播快;拖动时间轴到任意时间播放时,可以快速定位到对应的切片进行播放,响应快。 | 小 |
HLS | 对视频进行切片,按切片播放,缓存小起播快;拖动时间轴到任意时间播放时,可以快速定位到对应的切片进行播放,响应快。 | 整体占用小,播放一个切片只下载一个切片内容;对于低码率的视频场景,因封装代价高导致流量占用相对较高 |
MP4 | 头文件较大,边下边缓存,起播相对HLS和DASH慢一些;拖动时间轴播放时,需要一定的时间缓存;市场上大多数的浏览器客户端均能够播放,播放成功率高。 | 拖动时间轴播放时,仍然需要下载整个头文件,耗费流量大;因流量占用较大,建议用在短视频处理的场景。 |
HTTP-FLV | 缓存小起播快 | 小 |
从兼容性、性能、体验上来看,HLS是不错的选择(ps.最主要的是服务端转码不支持MPEG-DASH)。
同样的一段视频,转码成hls的后的实际播放效果非常不错,首播延迟缩短的很明显(1-2s内播放)。
整体概括下,录制的原始MP4视频,由于moov区域靠后,需要多请求两次才能获取到播放的“目录”,并且由于MP4格式播放的“目录”很大(上面例子中近1.19MB),导致首播卡顿很大。为了解决问题,使用了专门的实时流格式--HLS。
前端流水式接入
后端转码
由于我们直播使用的是腾讯云的服务,只需要在每次视频录制结束后,后端会自动创建一个转码任务,会生成一份hls的格式的录制视频。并在前端访问时,返回hls格式的播放地址给前端。
如果是自家平台的话,就需要后端借助FFmpeg这个很牛的库了,利用其提供的转码API进行操作。毕竟转码会很耗费性能的,任务一多起来就要排长队,这就需要考虑下是否需要动态扩容了。
前端接入(基于hls.js)
安装
npm install hls.js
绑定video标签&播放
更多API可以参考doc
ps.其他三方的一些播放器(eg. vidoe.js,TcPlayer.js 等等),其内部针对的hls的处理大部分页都引用的hls.js。
多码率适配
如果要实现这个功能,前提是服务端在转码时需要输出有多个分辨率的视频。然后只需要创建一个主的索引问题来定义不同的分辨率。
mastertplaylist.m3u8示例文件:
这里面索引了各个分类的m3u8的地址。
整体的结构如下如所示:
文中使用的测试视频信息
文末福利
转发本文并留下评论,我们将抽取第10名留言者(依据公众号后台顺序)送出转转纪念T恤一件:
扫描二维码
关注我们
一个有意思的前端团队
本文地址:https://blog.csdn.net/P6P7qsW6ua47A2Sb/article/details/107273414
上一篇: 一款Android漏洞测试套件 – AndroidVTS
下一篇: QQ推广产品被火绒当“病毒”拦截