56

视频秒开的秘密の为什么HLS能吊打MP4

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzU0OTExNzYwNg%3D%3D&%3Bmid=2247484968&%3Bidx=1&%3Bsn=0405f45e2ea0fcf70b5ff110c3aa616a
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

关注我们文末有福利

vUjaiqa.png!web

作者简介

UjyuqeU.png!web

刘继刚

一个前端&客户端“两栖”开发工程师

我司项目会在直播时进行视频录制(MP4格式),并在个人店铺页提供给录制视频回放功能。项目上线后发现,回放视频时能明显感觉到要loading很长时间才开始播放,依据视频大小不同,最长情况可能达到30秒,给用户的感觉就是“好卡”。

视频首播为什么那么卡

说卡顿之前,要先了解一些MP4文件的一些基本格式(本文以H.264-MP4为例)。

MP4 文件由许多个 原子 (也叫box)的数据块组成,大box中存放小box,一级嵌套一级来存放媒体信息。这些原子用以存储字幕和章节等内容, 当然也包括视频和音频等显而易见的数据。而视频和音频原子的元数据,以及有关如何播放视频的信息,如尺寸和每秒的帧数,则存储在叫做 moov 的特殊原子中。如果把MP4文件比喻成一本书的话,你可以认为 moov 是某种意义上的 MP4 文件目录。

基本结构

qmYbUrq.png!web

ftyp:File Type Box,处于文件的开始位置,描述的MP4文件的版本、兼容协议等

ZJBFBra.png!web

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区域

7FnYbiJ.png!web

图:MP4视频文件的结构概述

B3aiErz.png!web

图:通过moov-box中的trak去mdat区域视频

FvAFnuv.png!web

延迟问题定位

打开具体的某个回放页面,先会通过接口根据record_id去获取到页面的基本信息、视频的播放地址(videoUrl)、视频封面(poster);然后把videoUrl,poster赋值给video标签,后续浏览器根据标签参数就会开始获取视频信息,下载视频chunk,开始播放视频,由浏览器内部实现,js无法去过多干涉(暂停、播放等除外)。通过抓包能够大致了解浏览加载视频过程:

jm6biua.png!web

从network 面板中可以看到为了播放视频,浏览器先后发送了三次请求,来回耗时17s。来具体看下这三个请求(默认服务端支持range):

第一次:

vM7Rrii.png!web

浏览器第一次请求时尝试通过 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值了。

beuymuz.png!web

计算过程(单位字节):

视频文件大小 - ftyp大小 - free大小 - mdat大小 =》(Content-Length: 219157707)- (ftyp-size: 8+24)- (free-size: 8+0)- (mdat-size: 217910682)= 1246985

也就是,这一次请求的range的开始值最大值不能高于219157707-1246985=217910722。

ZF7Fzu3.png!web

可以看到发出去的请求 Range: bytes=217907200-∞ ,上面我们计算的217910722-∞ 包含在内 (为什么不从217910722 开始,而是多向前了3.45KB,猜测是为了更安全的box查找范围,具体的原因未在深究)。请求到数据后,接下来就是解析moov box了,然后根据视频”目录“然后第三次请求。

第三次:

根据第二次请求的moov解析后,开始下载”真正“的视频的内容准备播放。

通过上面的分析,不必要的两次请求或许是导致首播卡顿元凶之一,因此我们对视频文件进行了优化(三次请求变成一次),实际的效果是有提升但是依然很不理想。

UrAvuqb.png!web

那么还有没有其他的问题到导致播放卡顿呢?

分析&解决

在上面的第三次请求中,抓包中能够明显观察到视频要缓存到5M左右才能开始播放。那如何降低这个视频首播缓冲的chunk大小呢,首先想到的就是降低视频的分辨率。通过服务端后台录制的视频是高清的(720p),如果把分辨率降低到标清(360p)如何呢?实际把视频文件进行转码成标清后,下图是同一个视频 标清 首次播放请求结果对比:

byUV3mJ.png!web

数据上看,首播效果有略微提升,但差距并不大。目前看来,在格式已定前提下能优化的已经尽力了,只能另辟他经了。

接下来看下视频播放过程中浏览器是如何对视频数据处理的(图片来源:雷霄骅的博客

y67vmub.png!web

图中可以看到在播放前需要先解协议,解封装,解码音视频,同步,另外MP4的协议相对来说比较复杂,需要下载文件头比较大,而且要下载完才能解析(参考上面抓包的结果,头文件[ftyp+moov]大小将近1.19MB),解析后才去下载视频播放的内容。

有没有其他针对直播/点播的流协议呢?

答案是肯定(这里先不讨论FLV了),移动端苹果系统(iOS 3.0及更高版本的设备均支持)也给出了HTTP实时流的解决方案-HLS,安卓迫于iOS的淫威也开始系统层兼容HLS了,Can I use 上来看移动端兼容性相当不错。

ArMbmiM.png!web

关于HLS的介绍可以查看官方文档 developer.apple.com 。

首先服务端要根据源视频文件转换成HLS格式(m3u8和ts两种后缀)的文件,这个转换需要先把码源视频转码到目标编码格式(eg.H264),在进行 Stream segmenter(流分割)对视频切片后生成一个文本类型的xxx.m3u8索引文件和一组 xxxx.ts的分片文件,剩下的客户端获取.m3u8和.ts文件就交给HTTP服务器了(eg.nginx 、 apache)。整个从生成流到拉流的过程如下图:

jMB7jaE.png!web

客户端(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)

Vjammma.png!web

更多m3u8的格式信息可以点 ietf.org 查看,目前HLS还是草案,并不是国际标准。

其实关于自适性流技术的有个国际标准,是MPEG-DASH,不过这货在2011年11月才成为国际标准,相对较晚,也不好推,各浏览器基本上都未在系统层支持,总的来说位置比较尴尬。

yEbIF37.png!web

整体上来对比下这三种流格式的优缺点:

协议格式 播放体验 流量占用情况 DASH 对视频进行切片,按切片播放,缓存小播快; 拖动时间轴到任意时间播放时,可以快速定位到对应的切片进行播放,响应快。 HLS 对视频进行切片,按切片播放,缓存小播快;拖动时间轴到任意时间播放时,可以快速定位到对应的切片进行播放,响应快。 整体占用小,播放一个切片只下载一个切片内容;对于低码率的视频场景,因封装代价高导致流量占用相对较高 MP4 头文件较大,边下边缓存,起播相对HLS和DASH慢一些; 拖动时间轴播放时,需要一定的时间缓存; 市场上大多数的浏览器客户端均能够播放,播放成功率高。 拖动时间轴播放时,仍然需要下载整个头文件,耗费流量大; 因流量占用较大,建议用在短视频处理的场景HTTP-FLV 缓存小播快

从兼容性、性能、体验上来看,HLS是不错的选择(ps.最主要的是服务端转码不支持MPEG-DASH)。

同样的一段视频,转码成hls的后的实际播放效果非常不错, 首播延迟缩短的很明显(1-2s内播放)

ZbQrMry.png!web

整体概括下,录制的原始MP4视频,由于moov区域靠后,需要多请求两次才能获取到播放的“目录”,并且由于MP4格式播放的“目录”很大(上面例子中近1.19MB),导致首播卡顿很大。为了解决问题,使用了专门的实时流格式--HLS。

前端流水式接入

后端转码

由于我们直播使用的是腾讯云的服务,只需要在每次视频录制结束后,后端会自动创建一个转码任务,会生成一份hls的格式的录制视频。并在前端访问时,返回hls格式的播放地址给前端。

如果是自家平台的话,就需要后端借助FFmpeg这个很牛的库了,利用其提供的转码API进行操作。毕竟转码会很耗费性能的,任务一多起来就要排长队,这就需要考虑下是否需要动态扩容了。

7vmAVvN.png!web

前端接入(基于hls.js)

  • 安装

  • 绑定video标签&播放

A7Fji2m.png!web

更多API可以参考doc

ps.其他三方的一些播放器(eg. vidoe.js,TcPlayer.js 等等),其内部针对的hls的处理大部分页都引用的hls.js。

多码率适配

如果要实现这个功能,前提是服务端在转码时需要输出有多个分辨率的视频。然后只需要创建一个主的索引问题来定义不同的分辨率。

mastertplaylist.m3u8示例文件:

a2a2E3n.png!web

这里面索引了各个分类的m3u8的地址。

整体的结构如下如所示:

JRJNfmr.png!web

文中使用的测试视频信息

IvYjIrB.png!web

文末福利

转发本文并留下评论,我们将抽取第10名留言者(依据公众号后台顺序)送出转转纪念T恤一件:

3Ab2uuy.png!web

扫描二维码

关注我们

一个有意思的前端团队


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK