HLS 及多媒体基础知识介绍

仅以本文向永远的雷神致敬:有的人死了,他还活着。

这是拖了很久的一篇 Blog。
最近在做一些多媒体相关的项目,因此打算陆陆续续整理成 Blog 发表出来。从 HLS 开始吧,中间可能会穿插一些多媒体的基础知识介绍。

HLS 简介

HLS 的全称是 Http Live Streaming,是一种流媒体分发协议。由苹果公司最先提出并提交到IETF,目前协议已经更新到第 7 版。

关于流媒体

那么什么是流媒体(Streaming Media)?
形象的来说,就是媒体数据像水流一样从网络经过用户的设备。用户接收到的只是当前正在看的媒体片段的数据,无需把整个媒体文件都下载到本地——当然,如果把媒体完整观看一遍的话,整个媒体文件都会“流经”用户设备。

流媒体最早可以追溯到上世纪 90 年代,互联网刚刚兴起。一家叫做 RealNetworks 的公司发明了名为 RealMedia 的流媒体格式,并为之开发了专门的播放器 RealPlayer。但随着时代进步,ReadNetworks 不思进取,RealMedia 也逐渐被抛弃。现在已经很少能看到后缀名为 .rm、.rmvb 的文件了。

HLS 的简单原理

HLS 实际上就是把一段媒体文件拆分成多个小的分片,组织成一个 Playlist。客户端首先下载 Playlist,然后根据时间推移逐个下载、播放分片,达到“流”的效果。
目前根据 HLS 协议,支持的封装格式包括 TS(MPEG transport stream) 和 MP4。

关于封装格式

上面提到了“封装格式”,这就顺便说一下多媒体数据的编码(Encoding)和封装(Muxing)。

编码

我们知道,一张静态的图片是由成千上万个像素点构成的,每个像素点又是由颜色通道的数据构成。一张完全不压缩的图片(BMP格式),根据其分辨率,常常有数 MB 甚至数十 MB 大小。
视频就是由这样一帧一帧的画面构成的,一秒钟连续播放 24 帧画面,在人眼中就形成了“动画”。一秒钟 24 张图片,一分钟就是 1440 张,两小时的电影就是 172,800 张图片。如果每张图片都按原始大小存储,一部电影可能就要占用几个 TB 的空间了。

现代的硬盘虽然越来越便宜,还是架不住一部电影就要几个 TB,而且网络传输也会成为瓶颈,因此需要对这些数据先进行压缩。
压缩包括帧内压缩,也就是对图片本身进行压缩;还有帧间压缩,就是以一帧画面为基准(称为 I 帧,或关键帧),其后的画面根据 I 帧仅保留差异部分(称为 P 帧)。这个过程就叫做视频的“编码”。
编码的算法不同,即媒体数据的编码格式不同。现在流行的有 H264/265、VP8/VP9 等。

经过压缩,媒体数据可能就从几个 TB 变成几个 GB 甚至几百 MB 了。

封装

经过编码的多媒体数据,仍然是一堆二进制,此时就需要选择一种合适的容器来承载这些数据。为了便于播放,这个容器需要包含多媒体数据之外的一些信息,例如元数据、帧的索引、音频数据和视频数据的交织方式等等。
把数据放进这个容器的过程,就叫“封装”。而容器的类型,就是多媒体文件的“封装格式”。例如 MP4、MKV、AVI、MOV 等等。

HLS 的优势

HLS 能流行起来,个人觉得有如下几个原因(也就是 HLS 的优势所在)。

  • HLS 基于 HTTP 协议,更符合 Web 为主的现代互联网的使用环境
  • 相对于 RTP,HLS 协议更加轻量,对于开发者来说门槛更低
  • HLS 有苹果爸爸背书

基于这几个优势,HLS 不但逐渐在很多场合取代了传统的 RTP,而且干翻了同样基于 HTTP 的流媒体协议(Mpeg Dash、微软的 Smooth Streaming、Adobe HDS 等),成为当下互联网应用中,流媒体的最主流格式。

HLS 协议详解

介绍完基本概念,就可以深入到 HLS 协议内部看看了。
前面也提到了,HLS 的原理是把媒体文件拆分成小的分片,HLS 在网络上传输的形式,就是这些分片组成的一个 Playlist,即后缀为 .m3u8 的文件。
苹果的开发者网站上提供了一组样例。以其中一个为例,使用 Safari 打开页面之后,可以看到网页上已经内嵌了一个播放器,这是由于 Safari 支持直接播放 HTML5 <video> 标签中写入的 m3u8 文件 URL,毕竟是苹果自家的产品。
此时可以通过右键菜单中的“Copy Video Address”功能,找到对应的 URL https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_ts/master.m3u8。或是通过“Download Video”,下载该 m3u8 文件。

初识 m3u8

m3u8 实际上就是一个纯文本文件,可以用任意的文本编辑器打开。
打开之后可能会有些眼花缭乱,但仔细看就可以发现,每一行都是一个以“#”开头的 TAG 及其参数组成。最基本的是开头的三行。

1
2
3
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS
  • #EXTM3U 代表这是一个 m3u8 文件
  • #EXT-X-VERSION:6 表示该文件遵循 HLS 协议第 6 版
  • #EXT-X-INDEPENDENT-SEGMENTS 代表 m3u8 中所有的小分片均可独立解码

Variant Stream

接下来可以看到一组子 m3u8 文件,每个文件上都指定了一个 #EXT-X-STREAM-INF TAG,TAG 上附加了 BANDWIDTHCODECSRESOLUTION 等参数。例如:

1
2
#EXT-X-STREAM-INF:AVERAGE-BANDWIDTH=2218327,BANDWIDTH=2227464,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,CLOSED-CAPTIONS="cc1",AUDIO="aud1",SUBTITLES="sub1"
v5/prog_index.m3u8

这条内容表示一条独立的码流。不同码流之间,编码、帧率、分辨率、码率可能各不相同,由#EXT-X-STREAM-INF 标示。客户端可根据网络状况选择最合适的那条码流进行播放。
这样的多条码流的形式,称为“Variant Stream”。
由此也可以看出,m3u8 是支持嵌套的。即,m3u8 文件中可以指定具体的媒体文件(分片),也可以指定另一个 m3u8 文件。

Media Segments

继续打开 v5/prog_index.m3u8,就可以看到真正的媒体文件了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:6.00000,
#EXT-X-BITRATE:377
fileSequence0.ts
#EXTINF:6.00000,
#EXT-X-BITRATE:385
fileSequence1.ts
...
#EXT-X-ENDLIST

跳过开头的 #EXTM3U#EXT-X-TARGETDURATION:6 等常规性声明及元数据(想了解这些 TAG 的含义可以进一步查阅 HLS 协议),真正的媒体文件就是 fileSequence0.ts 开始的一系列 TS 文件。客户端在播放这个 m3u8 时,实际就是按顺序逐个下载、播放所有的 TS 文件。

TS 文件上附加的 TAG 主要是如下两个。

  • #EXTINF 后面的数字指示了该分片的时长
  • #EXT-X-BITRATE 指示了该分片的码率(实际经过查阅 HLS 协议,这个 TAG 在协议中并未规定,可能是苹果的 Bug 或是已废弃的 TAG)

最后,该 m3u8 以 #EXT-X-ENDLIST TAG 标志结束。

Segments based on single media file

从 HLS 协议第 4 版开始,分片可以指定为同一个媒体文件的不同范围。例如,

1
2
3
4
5
6
7
8
9
10
11
12
#EXTINF:4.96907,
#EXT-X-BYTERANGE:25312@560
main.ts
#EXTINF:4.96907,
#EXT-X-BYTERANGE:25440@25872
main.ts
#EXTINF:4.96907,
#EXT-X-BYTERANGE:25440@51312
main.ts
#EXTINF:4.96907,
#EXT-X-BYTERANGE:25440@76752
main.ts

可以看到,这一组分片全部是基于同一个 main.ts,通过#EXT-X-BYTERANGE TAG 指定分片在文件中的起始范围。
#EXT-X-BYTERANGE 后跟随的值,包括 @ 及左右侧各一数字。左边的数字表示分片长度(length),右边的数字表示分片相对于文件开头的偏移量(offset)。如25312@560,代表该分片从距文件头 560 Bytes 的位置开始,长度为 25312 Bytes。

I-Frame Playlist

回到主 Playlist,跳过所有 #EXT-X-STREAM-INF TAG 标示的码流数据,可以看到一个新的标签 #EXT-X-I-FRAME-STREAM-INF

1
#EXT-X-I-FRAME-STREAM-INF:AVERAGE-BANDWIDTH=182077,BANDWIDTH=186522,CODECS="avc1.64002a",RESOLUTION=1920x1080,URI="v7/iframe_index.m3u8"

#EXT-X-I-FRAME-STREAM-INF 标示了一个特殊的 Playlist(由参数 URI 指定其文件)。
打开 v7/iframe_index.m3u8 可以看到,这是一个类似于媒体分片的 Playlist,但它包含一个特殊的 TAG #EXT-X-I-FRAMES-ONLY。该 TAG 表示这个 m3u8 中的每个分片都仅仅是一个 I 帧。因此直接播放这个 m3u8 只会看到一跳一跳的画面。

那么,这个 Playlist 是做什么用的呢?
实际上,是为了 Trick Mode 播放——也就是快进快退模式——准备的。
播放器进入快进快退模式时,就会根据当前码流的 BANDWIDTH,找到匹配的 I-Frame Playlist 进行播放。所以 HLS 的快进快退,本质上就是只播放 I 帧。

为什么要设计成这样?
个人认为主要还是出于对网络带宽的考量。
试想,快进快退时如果还播放完整的码流,单位时间里需要下载的数据就变多,势必会对带宽提出更高的要求。而且播放速度加快之后,人的眼睛和大脑也来不及处理所有的画面,中间很多帧都是浪费。
同时,在一些性能不高的移动设备上,解码性能未必能跟得上。此时少解码一部分帧数据,也能降低对设备性能的要求。

当然,在网络带宽和设备性能足够的情况下,播放器也可以选择不使用 I-Frame Playlist,在快进快退模式下解码所有数据,这样播放效果更好,画面更加流畅和平滑。

Media Track

在这个 Sample 的最后,还有一组 TAG #EXT-X-MEDIA
该 TAG 指示了一组相关的 Playlist,即同一内容的不同轨道。
例如以下的片段,

1
2
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES,CHANNELS="2",URI="a1/en/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="ch",NAME="Chinese",AUTOSELECT=NO,DEFAULT=NO,CHANNELS="6",URI="a1/chi/prog_index.m3u8"

表示了对同一个视频的两条音轨。通过 LANGUAGE 参数可以看出,一条是英语,一条是中文。
组与组之间通过 GROUP-ID 区分。在 #EXT-X-STREAM-INF TAG 的 AUDIO 参数中指定该 GROUP-ID,即可在播放该条码流的时候,切换同一组中不同的 AUDIO。

#EXT-X-MEDIATYPE 可以指定为 4 种:

  • VIDEO 标示多个视角
  • AUDIO 标示多条音轨
  • CLOSED-CAPTIONS 标示多条隐藏字幕(解释型字幕)
  • SUBTITLES 标示多条一般字幕

小结

以上,以一个实际的 m3u8 为例,介绍几个常见的 TAG 及其含义。根据 HLS 协议,还有更多的功能及 TAG,感兴趣的童鞋可以自行查阅苹果开发者网站中提供的相关内容及 IETF 的相关协议。

参考