HLS协议详解

HLS协议详解

HLS (HTTP Live Streaming), 是由 Apple 公司实现的基于 HTTP 的媒体流传输协议。Apple 的全系列产品支持,由于 HLS 是苹果提出的, 所以在 Apple 的全系列产品包括 iphone、 ipad、safari 都不需要安装任何插件就可以原生支持播放 HLS, 现在Android 也加入了对 HLS 的支持。但PC端目前除了Microsoft Edge外,Chrome、Firefox等浏览器均不支持该协议的播放。

HLS跟 DASH 协议的原理非常类似,通过将整条流切割成一个小的可以通过 HTTP 下载的媒体文件,然后提供一个配套的媒体列表文件给客户端,让客户端顺序地拉取这些媒体文件播放, 来实现看上去是在播放一条流的效果。 HLS 目前广泛地应用于点播和直播领域

HLS 的优势

  • 客户端支持简单,只需要支持 HTTP 请求即可,HTTP 协议无状态,只需要按顺序下载媒体片段即可。

  • 使用 HTTP 协议网络兼容性好, HTTP 数据包也可以方便地通过防火墙或者代理服务器,CDN 支持良好。

  • 自带多码率自适应,Apple 在提出 HLS 时,就已经考虑了码率自适应的问题。

HLS 的劣势

  • 相比 RTMP 这类长连接协议,延时较高,难以用到互动直播场景。

  • 对于点播服务来说,由于 TS 切片通常较小,海量碎片在文件分发、一致性缓存、存储等方面都有较大挑战。

协议详解

上图可以看出,HLS总共有三个部分: Serve、CDN、Client。HLS 协议的主要内容是关于 M3U8 这个文本协议,其实生成与解析都非常简单,示例如下:

简单的Media Playlist:

包含多种比特率的 Master Playlist:

  • HLS 通过 URI(RFC3986) 指向的一个 Playlist 来表示一个媒体流.
  • 一个 Playlist 可以是一个 Media Playlist 或者 Master Playlist, 使用 UTF-8 编码的文本文件, 包含一些 URI 跟描述性的 tags.
  • 一个 Media Playlist 包含一个 Media Segments 列表,当顺序播放时, 能播放整个完整的流.
  • 要想播放这个 Playlist, 客户端需要首先下载他, 然后播放里面的每一个 Media Segment.
  • 更加复杂的情况是, Playlist 是一个 Master Playlist, 包含一个 Variant Stream 集合, 通常每个 Variant Stream 里面是同一个流的多个不同版本(如: 分辨率, 码率不同).

HLS Media Segments

  • 每一个 Media Segment 通过一个 URI 指定, 可能包含一个 byte range.
  • 每一个 Media Segment 的 duration 通过 EXTINF tag 指定.
  • 每一个 Media Segment 有一个唯一的整数 Media Segment Number.
  • 有些媒体格式需要一个 format-specific sequence 来初始化一个 parser, 在 Media Segment 被 parse 之前. 这个字段叫做 Media Initialization Section, 通过 EXT-X-MAP tag 来指定.

支持的Media Segment格式

MPEG-2 Transport Streams
  • 即最常见的 TS 文件.
  • RFC: ISO_13818.
  • Media Initialization Section: PAT(Program Association Table) 跟 PMT(Program Map Table).
  • 每个 TS segment 必须值含一个 MPEG-2 Program.
  • 每个 TS segment 包含一个 PAT 和 PMT, 最好在 segment 的开始处, 或者通过一个 EXT-X-MAP tag 来指定.
Fragmented MPEG-4
  • 即常提到的 fMP4.
  • RFC: ISOBMFF.
  • Media Initialization Section: ftyp box(包含一个高于 ios6 的 brand), ftyp box 必须紧跟在 moov box 之后. moov box 必须包含一个 trak box(对于每个 fMP4 segment 里面的 traf box, 包含匹配的 track_ID). 每个 trak box 应该包含一个 sample table, 但是他的 sample count 必须为 0. mvhd box 跟 tkhd 的 duration 必须为 0. mvex box 必须跟在上一个 trak box 后面.
  • 不像普通的 MP4 文件包含一个 moov box(包含 sample tables) 和一个 mdat box(包含对应的 samples), 一个 fMP4 包含一个 moof box (包含 sample table 的子集), 和一个 mdat box(包含对应的 samples).
  • 在每一个 fMP4 segment 里面, 每一个 traf box 必须包含一个 tfdt box, fMP4 segment 必须使用 movie-fragment relative addressing. fMP4 segments 绝对不能使用外部的 data references.
  • 每一个 fMP4 segment 必须有一个 EXT-X-MAP tag.
Packed Audio
  • 一个 Packed Audio Segment 包含编码的 audio samples 和 ID3 tags. 简单的打包到一起, 包含最小的 framing, 并且没有 per-sample timestamp.
  • 支持的 Packed Audio: AAC with ADTS framing [ISO_13818_7], MP3 [ISO_13818_3], AC-3 [AC_3], Enhanced AC-3 [AC_3].
  • 一个 Packed Audio Segment 没有 Media Initialization Section.
  • 每一个 Packed Audio Segment 必须在他的第一个 sample 指定 timestamp 通过一个 ID3 PRIV tag.
  • ID3 PRIV owner identifier 必须是 com.apple.streaming.transportStreamTimestamp.
  • ID3 payload 必须是一个 33-bit MPEG-2 Program Elementary Stream timestamp 的大端 eight-octet number, 高 31 为设置为 0.
WebVTT
  • 一个 WebVTT Segment 是一个 WebVTT 文件的一个 section, WebVTT Segment 包含 subtitles.
  • Media Initialization Section: WebVTT header.
  • 每一个 WebVTT Segment 必须有以一个 WebVTT header 开始, 或者有一个 EXT-X-MAP tag 来指定.
  • 每一个 WebVTT header 应该有一个 X-TIMESTAMP-MAP 来保证音视频同步.

HLS Playlists

  • Playlist 文件的格式是起源于 M3U, 并且继承两个 tag: EXTM3U 和 EXTINF
  • 下面的 tags 通过 BNF-style 语法来指定.
  • 一个 Playlist 文件必须通过 URI(.m3u8 或 m3u) 或者 HTTP Content-Type 来识别(application/vnd.apple.mpegurl 或 audio/mpegurl).
  • 换行符可以用 \n 或者 \r\n.
  • 以 # 开头的是 tag 或者注释, 以 #EXT 开头的是 tag, 其余的为注释, 在解析时应该忽略.
  • Playlist 里面的 URI 可以用绝对地址或者相对地址, 如果使用相对地址, 那么是相对于 Playlist 文件的地址.
Attribute Lists
  • 有的 tags 的值是 Attribute Lists.
  • 一个 Attribute List 是一个用逗号分隔的 attribute/value 对列表.
  • 格式为: AttributeName=AttributeValue.
Basic Tags

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

  • EXTM3U: 必须在文件的第一行, 标识是一个 Extended M3U Playlist 文件.
  • EXT-X-VERSION: 表示 Playlist 兼容的版本.
Media Segment Tags

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

  • EXTINF: 用于指定 Media Segment 的 duration
  • EXT-X-BYTERANGE: 用于指定 URI 的 sub-range
  • EXT-X-DISCONTINUITY: 表示不连续.
  • EXT-X-KEY: 表示 Media Segment 已加密, 该值用于解密.
  • EXT-X-MAP: 用于指定 Media Initialization Section.
  • EXT-X-PROGRAM-DATE-TIME: 和 Media Segment 的第一个 sample 一起来确定时间戳.
  • EXT-X-DATERANGE: 将一个时间范围和一组属性键值对结合到一起.
Media Playlist Tags

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

  • EXT-X-TARGETDURATION: 用于指定最大的 Media Segment duration.
  • EXT-X-MEDIA-SEQUENCE: 用于指定第一个 Media Segment 的 Media Sequence Number.
  • EXT-X-DISCONTINUITY-SEQUENCE: 用于不同 Variant Stream 之间同步.
  • EXT-X-ENDLIST: 表示结束.
  • EXT-X-PLAYLIST-TYPE: 可选, 指定整个 Playlist 的类型.
  • EXT-X-I-FRAMES-ONLY: 表示每个 Media Segment 描述一个单一的 I-frame.
Master Playlist Tags

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

  • EXT-X-MEDIA: 用于关联同一个内容的多个 Media Playlist 的多种 renditions.
  • EXT-X-STREAM-INF: 用于指定一个 Variant Stream.
  • EXT-X-I-FRAME-STREAM-INF: 用于指定一个 Media Playlist 包含媒体的 I-frames.
  • EXT-X-SESSION-DATA: 存放一些 session 数据.
  • EXT-X-SESSION-KEY: 用于解密.
Media or Master Playlist Tags

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

  • EXT-X-INDEPENDENT-SEGMENTS: 表示每个 Media Segment 可以独立解码.
  • EXT-X-START: 标识一个优选的点来播放这个 Playlist.

服务器端与客户端逻辑

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

服务器端逻辑

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

客户端逻辑

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

HLS优化技术

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

302

首先在客户端第一次进行拉流请求的时候,服务器会做一个302的跳转。

uuid

在302跳转后产生的新的url上,服务器会给url加上一个uuid,用来定位可能的问题。

TS文件详解

m3u8只是一个传输格式,一个典型的m3u8文件,最终的音视频数据流都是封装在.ts文件中,ts文件的全称是MPEG-2 Transport Streams,下面来分析一下这个文件的具体结构。

文件结构

视频编码主要格式是h264/mpeg4,音频编码格式主要是acc/mp3

ts文件分为三层:

  • ts(TransportStream)主要是音视频数据
  • pes(PacketElementalStream)在音视频数据(ts)上加了时间戳等数据说明信息
  • es(ElementaryStream)在(pes)上加入数据流的识别和传输必须信息

下面来分析一下每一层具体的格式。

ts(TransportStream)

ts数据大小固定为188个字节,ts又分为三个组成部分,分别是 ts header,adaptation fieldpayload.
其中ts header固定为4个字节,adaptation field可能存在也可能不存在,主要作用是给不足188字节的数据做填充,payload是pes数据。

ts header

ts header中包含一下字段:

ts层的内容是通过pid值来标识的,主要内容包括:PAT表、PMT表、音频流、视频流。解析ts流要先找到PAT表,只要找到PAT就可以找到PMT,然后就可以找到音视频流了。PAT表的pid值固定为0。PAT表和PMT表需要定期插入ts流,因为用户随时可能加入ts流,这个间隔比较小,通常每隔几个视频帧就要加入PAT和PMT。PAT和PMT表是必须的,还可以加入其它表如SDT(业务描述表)等,不过hls流只要有PAT和PMT就可以播放了。

adaptation field

自适应区的长度要包含传输错误指示符标识的一个字节。pcr是节目时钟参考,pcr、dts、pts都是对同一个系统时钟的采样值,pcr是递增的,因此可以将其设置为dts值,音频数据不需要pcr。如果没有字段,ipad是可以播放的,但vlc无法播放。打包ts流时PAT和PMT表是没有adaptation field的,不够的长度直接补0xff即可。视频流和音频流都需要加adaptation field,通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。

PAT & PMT

这2个类型都是ts的一种,通过pid来区分。

PAT主要的作用就是指明了PMT表的pid值,下图是PAT包含的字段:

PMT主要的作用就是指明了音视频流的pid值,下图是PMT包含的字段:

pes(PacketElementalStream)

pes层是在每一个视频/音频帧上加入了时间戳等信息,pes包内容很多,我们只留下最常用的字段,如下:

pts是显示时间戳dts是解码时间戳,视频数据两种时间戳都需要,音频数据的pts和dts相同,所以只需要pts。有pts和dts两种时间戳是B帧引起的,I帧和P帧的pts等于dts。如果一个视频没有B帧,则pts永远和dts相同。从文件中顺序读取视频帧,取出的帧顺序和dts顺序相同。dts算法比较简单,初始值 + 增量即可,pts计算比较复杂,需要在dts的基础上加偏移量。

音频的pes中只有pts(同dts),视频的I、P帧两种时间戳都要有,视频B帧只要pts(同dts)。打包pts和dts就需要知道视频帧类型,但是通过容器格式我们是无法判断帧类型的,必须解析h.264内容才可以获取帧类型。

点播视频dts算法:

dts = 初始值 + 90000 / video_frame_rate,初始值可以随便指定,但是最好不要取0,video_frame_rate就是帧率,比如23、30。

pts和dts是以timescale为单位的,1s = 90000 time scale , 一帧就应该是90000/video_frame_rate 个timescale。

用一帧的timescale除以采样频率就可以转换为一帧的播放时长

点播音频dts算法:

dts = 初始值 + (90000 audio_samples_per_frame) / audio_sample_rate,audio_samples_per_frame这个值与编解码相关,aac取值1024,mp3取值1158,audio_sample_rate是采样率,比如24000、41000。AAC一帧解码出来是每声道1024个sample,也就是说一帧的时长为1024/sample_rate秒。所以每一帧时间戳依次0,1024/sample_rate,…,1024n/sample_rate秒。

直播视频的dts和pts应该直接用直播数据流中的时间,不应该按公式计算。

es(ElementaryStream)

es层指的就是音视频数据,我们只介绍h.264视频和aac音频。

h.264

打包h.264数据我们必须给视频数据加上一个nalu(Network Abstraction Layer unit),nalu包括nalu header和nalu type,nalu header固定为0x00000001(帧开始)或0x000001(帧中)。h.264的数据是由slice组成的,slice的内容包括:视频、sps、pps等。nalu type决定了后面的h.264数据内容。

对h264格式不太了解可以参考h.264文件结构学习笔记