HLS 协议详解

HLS 概述

HLS 全称是 HTTP Live Streaming, 是一个由 Apple 公司实现的基于 HTTP 的媒体流传输协议. 他跟 DASH 协议的原理非常类似. 通过将整条流切割成一个小的可以通过 HTTP 下载的媒体文件, 然后提供一个配套的媒体列表文件, 提供给客户端, 让客户端顺序地拉取这些媒体文件播放, 来实现看上去是在播放一条流的效果.

由于传输层协议只需要标准的 HTTP 协议, HLS 可以方便的透过防火墙或者代理服务器, 而且可以很方便的利用 CDN 进行分发加速, 并且客户端实现起来也很方便.

HLS 目前广泛地应用于点播和直播领域.

在 HTML5 页面上使用 HLS 非常简单:

直接:

<video src="example.m3u8" controls></video>

或者:

<video controls>
    <source src="example.m3u8"></source>
</video>

下面, 我将会概括性地介绍 HLS 协议的方方面面(暂时不包括 AES 加密部分的内容), 配合 HLS 的 RFC 食用效果更佳.

HLS 协议详解

hls_arch

上面是 HLS 整体架构图, 可以看出, 总共有三个部分: Server, CDN, Client.

其实, HLS 协议的主要内容是关于 M3U8 这个文本协议的, 其实生成与解析都非常简单. 为了更加直接地说明这一点, 我下面举两个简单的例子:

简单的 Media Playlist:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:2680

#EXTINF:7.975,
https://priv.example.com/fileSequence2680.ts
#EXTINF:7.941,
https://priv.example.com/fileSequence2681.ts
#EXTINF:7.975,
https://priv.example.com/fileSequence2682.ts

包含多种比特率的 Master Playlist:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1280000
http://example.com/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2560000
http://example.com/mid.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=7680000
http://example.com/hi.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=65000,CODECS="mp4a.40.5"
http://example.com/audio-only.m3u8

HLS Media Segments

支持的 Media Segment 格式

MPEG-2 Transport Streams
Fragmented MPEG-4
Packed Audio
WebVTT

HLS Playlists

Attribute Lists

Basic Tags

Basic Tags 可以用在 Media Playlist 和 Master Playlist 里面.

Media Segment Tags

每一个 Media Segment 通过一系列的 Media Segment tags 跟一个 URI 来指定. 有的 Media Segment tags 只应用与下一个 segment, 有的则是应用所有下面的 segments. 一个 Media Segment tag 只能出现在 Media Playlist 里面.

Media Playlist Tags

Media Playlist tags 描述 Media Playlist 的全局参数. 同样地, Media Playlist tags 只能出现在 Media Playlist 里面.

Master Playlist Tags

Master Playlist tags 定义 Variant Streams, Renditions 和 其他显示的全局参数. Master Playlist tags 只能出现在 Master Playlist 中.

Media or Master Playlist Tags

这里的 tags 可以出现在 Media Playlist 或者 Master Playlist 中. 但是如果同时出现在同一个 Master Playlist 和 Media Playlist 中时, 必须为相同值.

服务器端与客户端逻辑

以下流程仅供参考, 其实不同的播放器客户端以及服务器端的拉取规则都有很多细节差异.

服务器端逻辑

  1. 将媒体源切片成 Media Segment, 应该优先从可以高效解码的时间点来进行切片(如: I-frame).
  2. 为每一个 Media Segment 生成 URI.
  3. Server 需要支持 “gzip” 方式压缩文本内容.
  4. 创建一个 Media Playlist 索引文件, EXT-X-VERSION 不要高于他需要的版本, 来提供更好的兼容性.
  5. Server 不能随便修改 Media Playlist, 除了 Append 文本到文件末尾, 按顺序移除 Media Segment URIs, 增长 EXT-X-MEDIA-SEQUENCEEXT-X-DISCONTINUITY-SEQUENCE, 添加 EXT-X-ENDLIST 到文件尾.
  6. 在最后添加 EXT-X-ENDLIST tag, 来减少 Client reload Playlist 的次数.
  7. 注意点播与直播服务器不同的地方是, 直播的 m3u8 文件会不断更新, 而点播的 m3u8 文件是不会变的, 只需要客户端在开始时请求一次即可.

客户端逻辑

  1. 客户端通过 URI 获取 Playlist. 如果是 Master Playlist, 客户端可以选择一个 Variant Stream 来播放.
  2. 客户端检查 EXT-X-VERSION 版本是否满足.
  3. 客户端应该忽略不可识别的 tags, 忽略不可识别的属性键值对.
  4. 加载 Media Playlist file.
  5. 播放 Media Playlist file.
  6. 重加载 Media Playlist file.
  7. 决定下一次要加载的 Media Segment.

HLS 的优势

HLS 的劣势

改进的 HLS 技术

由于客户端每次请求 TS 或 M3U8 有可能都是一个新的连接请求, 所以, 我们无法有效的标识客户端, 一旦出现问题, 基本无法有效的定位问题, 所以, 一般工业级的服务器都会对传统的 HLS 做一些改进.

这里主要介绍网宿的 Variant HLS 与又拍云的 HLS+.

网宿的 Variant HLS

首先, 我们可以下载一条网宿的 M3U8 文件:

wget http://bililive.kksmg.com/hls/stvd6edb9a6_45b34047833af658bf4945a8/playlist.m3u8

然后, 打开下载得到的 playlist 文件:

#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=781000
http://bililive.kksmg.com/hls/stvd6edb9a6_45b34047833af658bf4945a8/playlist.m3u8?wsSession=0105cb4e8fe63bccab511a4a-149017212774715&wsIPSercert=b80d38c068c9e3634a7ebb2f2bbf9b89&wsMonitor=-1

可以看出这是一个 Master Playlist, 里面嵌套了一层 M3U8, 同时可以看出网宿采用 wsSession 来标识一条播放连接.

又拍云的 HLS+

Variant HLS

首先, 我们可以下载一条又拍云的 M3U8 文件:

wget http://uplive.b0.upaiyun.com/live/loading.m3u8

然后, 打开下载得到的 playlist 文件:

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:1
#EXTINF:0.998, no desc
http://183.158.35.12:8080/uplive.b0.upaiyun.com/live/loading-0.ts?shp_uuid=e4989f34fcab282e21ef1fd2980284cb&shp_ts=1490172420851&shp_cid=17906&shp_pid=3370578&shp_sip0=127.0.0.1&shp_sip1=183.158.35.12&domain=uplive.b0.upaiyun.com&shp_seqno=0

可以看出又拍云的 HLS+ 也支持这种 Variant HLS 方式来标识一条 HLS 连接, 可以看出, 又拍云使用 uuid 来表示一条 HLS 连接.

HTTP 302

首先, 以 HTTP 302 方式来请求播放地址.

❯ curl -v http://uplive.b0.upaiyun.com/live/loading.m3u8\?shp_identify\=302 -o playlist
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 183.158.35.59...
* TCP_NODELAY set
* Connected to uplive.b0.upaiyun.com (183.158.35.59) port 80 (#0)
> GET /live/loading.m3u8?shp_identify=302 HTTP/1.1
> Host: uplive.b0.upaiyun.com
> User-Agent: curl/7.51.0
> Accept: */*
>
< HTTP/1.1 302 Found
< Server: marco/0.26
< Date: Wed, 22 Mar 2017 08:54:11 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 259
< Connection: keep-alive
< Access-Control-Allow-Methods: GET
< Access-Control-Allow-Origin: *
< Location: http://183.158.35.19:8080/uplive.b0.upaiyun.com/live/loading.m3u8?shp_uuid=2862b1b817a74cf719b1cd8f554616cd&shp_ts=1490172851450&shp_cid=59553&shp_pid=1730488&shp_sip0=127.0.0.1&shp_sip1=183.158.35.19&domain=uplive.b0.upaiyun.com&shp_identify=302
<
{ [259 bytes data]
* Curl_http_done: called premature == 0
100   259  100   259    0     0   4813      0 --:--:-- --:--:-- --:--:--  4886
* Connection #0 to host uplive.b0.upaiyun.com left intact

打开 playlist 内容:

Redirect to http://183.158.35.19:8080/uplive.b0.upaiyun.com/live/loading.m3u8?shp_uuid=2862b1b817a74cf719b1cd8f554616cd&shp_ts=1490172851450&shp_cid=59553&shp_pid=1730488&shp_sip0=127.0.0.1&shp_sip1=183.158.35.19&domain=uplive.b0.upaiyun.com&shp_identify=302

在跳转之后的地址存放真正的 playlist, 同时, 也能够将 uuid 加入到了连接上.

总地来说, 不管通过哪种方式, 最终我们都能通过一个唯一的 id 来标识一条流, 这样在排查问题时就可以根据这个 id 来定位播放过程中的问题.

HLS 延时分析

HLS 理论延时 = 1 个切片的时长 + 0-1个 td (td 是 EXT-X-TARGETDURATION, 可简单理解为播放器取片的间隔时间) + 0-n 个启动切片(苹果官方建议是请求到 3 个片之后才开始播放) + 播放器最开始请求的片的网络延时(网络连接耗时)

为了追求低延时效果, 可以将切片切的更小, 取片间隔做的更小, 播放器未取到 3 个片就启动播放. 但是, 这些优化方式都会增加 HLS 不稳定和出现错误的风险.

Demo

Refs