# VOD 深度剖析（五）：流媒体协议——HLS 和 DASH 到底是怎么工作的

> 为什么渐进式下载会失败、HLS 两级清单和 DASH MPD 如何工作、CMAF 双清单最佳实践、面向低延迟的 LL-HLS，以及何时该考虑 WebRTC。

- 作者: zhuermu
- 发布: 2026-05-10
- 网页版: https://zhuermu.com/blog/vod-deep-dive-part-5-streaming-protocols-hls-dash/

---
> *这是 [VOD 流媒体深度剖析](/blog/vod-deep-dive-part-1-video-fundamentals/) 系列的第 5 篇。*

---

## 为什么不能直接下载一个 MP4？

最简单的做法：把 `video.mp4` 放到 HTTP 服务器上，让用户直接下载。这就是**渐进式下载（progressive download）**。它有几个致命缺陷：

1. 如果没有做 faststart，**必须等整个文件下载完才能开始播放**（参见[第 4 篇](/blog/vod-deep-dive-part-4-container-formats-mp4-fmp4-cmaf/)）。
2. 当带宽下降时，播放器**无法切换到更低的清晰度**——只能一直卡顿缓冲。
3. 拖动进度条要在一个大文件上使用 HTTP Range 请求——**对 CDN 缓存不友好**。
4. 你**无法为不同设备提供不同的版本**（一台老手机拿到的和 4K 电视一样是 1080p）。

解决方案：**把视频切成很多个小片段，再写一个"目录文件"告诉播放器接下来该下载什么**。这就是**流媒体协议**。

---

## 核心思路

现代流媒体由三个部分组成：

```
1. Cut the video into small segments
   ┌──┐ ┌──┐ ┌──┐ ┌──┐ ┌──┐ ...
   │s1│ │s2│ │s3│ │s4│ │s5│
   └──┘ └──┘ └──┘ └──┘ └──┘
   Each 2-6 seconds

2. Produce a set of segments for each quality level
   360p:  ┌──┐ ┌──┐ ┌──┐ ...
   720p:  ┌──┐ ┌──┐ ┌──┐ ...
   1080p: ┌──┐ ┌──┐ ┌──┐ ...

3. Write a manifest telling the player where to find everything
   manifest:
     "360p, 720p, and 1080p available"
     "Each tier: seg_001.m4s through seg_100.m4s"
```

播放器读取清单后：

- **第 1 秒**：选择一个保守的档位，开始下载
- **第 2 秒**：测量实际吞吐量，决定下一个片段该用哪个档位
- **持续地**：带宽好就升档，带宽差就降档，卡顿严重时进一步降档

这个决策过程被称为**自适应码率（Adaptive Bitrate，ABR）**——将在[第 6 篇](/blog/vod-deep-dive-part-6-adaptive-bitrate-streaming/)中讲解。本篇聚焦协议层。

---

## HLS（HTTP Live Streaming）

**创造者**：Apple，2009 年，随 iOS 3.0 一起发布。

**标准**：IETF RFC 8216（在 `draft-pantos-hls-rfc8216bis` 中持续更新）。

### 两级 M3U8 播放列表

M3U8 是一种 UTF-8 文本文件（扩展的 M3U 格式）。HLS 使用两级结构：

```
         Master Playlist              Media Playlist
         (top-level directory)        (per-tier segment list)
         master.m3u8
         │
         ├─► 360p/index.m3u8  ──► 360p/seg1.m4s, seg2.m4s ...
         │
         ├─► 720p/index.m3u8  ──► 720p/seg1.m4s, seg2.m4s ...
         │
         └─► 1080p/index.m3u8 ──► 1080p/seg1.m4s, seg2.m4s ...
```

### 主播放列表（Master Playlist）示例

```m3u8
#EXTM3U
#EXT-X-VERSION:7

#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=640x360,CODECS="avc1.640016,mp4a.40.2"
360p/index.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8

#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
```

关键字段：

- `BANDWIDTH`：该档位的最大码率（**必填**）
- `RESOLUTION`：像素尺寸
- `CODECS`：RFC 6381 编解码器字符串——告诉浏览器该用哪个解码器

### 媒体播放列表（Media Playlist）示例（fMP4，VOD）

```m3u8
#EXTM3U
#EXT-X-VERSION:7
#EXT-X-TARGETDURATION:4
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="init.mp4"

#EXTINF:4.000,
seg_00001.m4s
#EXTINF:4.000,
seg_00002.m4s
#EXTINF:4.000,
seg_00003.m4s
...
#EXTINF:3.120,
seg_00150.m4s

#EXT-X-ENDLIST
```

- `EXT-X-TARGETDURATION:4`：片段最大时长为 4 秒
- `EXT-X-PLAYLIST-TYPE:VOD`：VOD 模式（相对于 EVENT 或 LIVE）
- `EXT-X-MAP:URI="init.mp4"`：fMP4 初始化片段的位置
- `EXTINF:4.000,`：下一个片段时长为 4 秒
- `EXT-X-ENDLIST`：播放列表已完整（VOD 必填；直播中不出现）

### 多音轨与字幕

```m3u8
#EXTM3U
#EXT-X-VERSION:7

#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="en",NAME="English",DEFAULT=YES,URI="audio_en/index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="zh",NAME="中文",URI="audio_zh/index.m3u8"

#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="en",NAME="English",URI="subs_en/index.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",LANGUAGE="zh",NAME="中文",URI="subs_zh/index.m3u8"

#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2",AUDIO="audio",SUBTITLES="subs"
720p/index.m3u8
```

HLS 的优势：在所有 iOS/Safari 上原生支持、基于 HTTPS（对防火墙友好）、人类可读的文本格式，以及**全球最高的市场占有率**。

---

## DASH（Dynamic Adaptive Streaming over HTTP）

**创造者**：MPEG（ISO/IEC 23009-1），2012 年标准化。它被设计为 Apple 私有 HLS 之外的一个**开放标准**替代方案。

它的清单是一个 XML 文件：`.mpd`（**M**edia **P**resentation **D**escription，媒体呈现描述）。

### 简化的 MPD 示例

```xml
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011"
     type="static"
     mediaPresentationDuration="PT10M30S"
     minBufferTime="PT2S">

  <Period>
    <AdaptationSet mimeType="video/mp4" codecs="avc1.64001f">
      <Representation id="360p" bandwidth="800000" width="640" height="360">
        <BaseURL>360p/</BaseURL>
        <SegmentTemplate initialization="init.mp4"
                         media="seg_$Number$.m4s"
                         timescale="1000"
                         duration="4000"
                         startNumber="1"/>
      </Representation>
      <Representation id="720p" bandwidth="2500000" width="1280" height="720">
        <BaseURL>720p/</BaseURL>
        <SegmentTemplate initialization="init.mp4"
                         media="seg_$Number$.m4s"
                         timescale="1000"
                         duration="4000"
                         startNumber="1"/>
      </Representation>
    </AdaptationSet>

    <AdaptationSet mimeType="audio/mp4" codecs="mp4a.40.2" lang="en">
      <Representation id="audio_en" bandwidth="128000">
        <BaseURL>audio_en/</BaseURL>
        <SegmentTemplate initialization="init.mp4"
                         media="seg_$Number$.m4s"
                         duration="4000"/>
      </Representation>
    </AdaptationSet>
  </Period>
</MPD>
```

关键概念：

- **Period**：整个呈现内容。电影通常只有一个；插入广告的内容会有多个。
- **AdaptationSet**：一种媒体类型（视频 / 音频 / 字幕 / 特定语言）。
- **Representation**：一个 AdaptationSet 内的具体版本（360p、720p、1080p）。
- **SegmentTemplate**：基于模板的片段 URL——比逐个列出每个片段更紧凑。

### HLS vs DASH

| | HLS | DASH |
|--|-----|------|
| 清单格式 | M3U8（文本） | MPD（XML） |
| 片段容器 | TS / fMP4（现代） | fMP4 / WebM |
| iOS/Safari | 原生支持 | 需要 MSE（JS） |
| Android | 支持 | 支持 |
| Web（Chrome/Firefox） | 通过 hls.js | 通过 dash.js / Shaka |
| 开放标准 | IETF 标准化中 | ISO/IEC 标准 |

---

## CMAF + 双清单：行业最佳实践

正如[第 4 篇](/blog/vod-deep-dive-part-4-container-formats-mp4-fmp4-cmaf/)所述，CMAF 让 HLS 和 DASH 能够共享同一套 fMP4 片段。

典型的目录结构：

```
/vod/episode-01/
  init.mp4              ← CMAF init segment
  seg_00001.m4s         ← Shared video data
  seg_00002.m4s
  seg_00003.m4s
  ...

  hls/
    master.m3u8         ← HLS manifest (references shared segments)
    360p/index.m3u8
    720p/index.m3u8

  dash/
    manifest.mpd        ← DASH manifest (references same segments)
```

- iPhone 用户 → 拿到 `master.m3u8` → 下载 `seg_*.m4s`
- Android 用户 → 拿到 `manifest.mpd` → 下载**同一套** `seg_*.m4s`

**CDN 缓存命中率最大化。**

---

## 延迟问题：为什么直播会滞后 30 秒

传统的 HLS/DASH 有 **20～30 秒的启播延迟**：

```
Encoder:   [produce 6s segment]──────►
HLS spec:  Client waits for 3 segments before playing = 3 × 6 = 18s
Add:       Playback buffer + network = 25-30s total
```

对于体育直播、电商直播和互动直播来说，这个延迟太大了。

### LL-HLS（Low-Latency HLS，低延迟 HLS）

Apple 在 2019 年推出的方案。目标：端到端**小于 2 秒**。

关键技术：

1. **部分片段（Partial Segments）**：把 6 秒的片段切成 200～500 毫秒的"部分（parts）"，让播放器更早拿到数据。
2. **阻塞式播放列表重载（Blocking Playlist Reload）**：播放器带着 `?_HLS_msn=X&_HLS_part=Y` 发起请求，服务器在有新数据之前一直挂起响应（类似长轮询）。
3. **预加载提示（Preload Hint）**：告诉播放器接下来会有什么。

### LL-DASH / CMAF-LL

DASH 通过 **HTTP 分块传输编码（Chunked Transfer Encoding）** 实现同样的效果——编码器每产生一个 CMAF 块（约 300 毫秒）就立即推送，无需等待整个片段完成。

### 延迟对比

| 协议 | 典型延迟 | 复杂度 |
|----------|----------------|-----------|
| 传统 HLS（TS，6s × 3） | 20～30 秒 | 低 |
| 现代 HLS（fMP4，4s × 3） | 8～15 秒 | 低 |
| **LL-HLS / CMAF-LL** | **2～5 秒** | 中 |
| **WebRTC** | **&lt;500ms** | 高 |

---

## WebRTC：另一条路

WebRTC **并不是** HLS/DASH 的升级版——它是一种从根本上不同的技术：

| | HLS/DASH | WebRTC |
|--|----------|--------|
| 传输 | HTTP/HTTPS | UDP + DTLS + SRTP |
| CDN 友好 | 是（HTTP 缓存） | 否（点对点或专用 SFU） |
| 延迟 | 2～30 秒 | &lt;500ms |
| 规模 | 轻松支持数百万并发 | 受限于 SFU 容量 |
| 使用场景 | 电影、VOD、一对多直播 | 视频通话、互动、云游戏 |

**VOD 不使用 WebRTC。** 本系列聚焦于 HLS/DASH/CMAF。

---

## 协议选型指南

```
What are you building?
│
├── ① Pure VOD
│     → HLS + DASH dual manifest + CMAF fMP4
│
├── ② Standard live streaming (<10s latency OK)
│     → HLS + DASH + CMAF fMP4
│
├── ③ Low-latency live (sports, e-commerce, <3s)
│     → LL-HLS + LL-DASH + CMAF-LL
│
├── ④ Ultra-low latency (interactive, <500ms)
│     → WebRTC or RTMP-over-QUIC
│
└── ⑤ IPTV (carrier set-top boxes)
      → MPEG-TS over UDP/HTTP
```

---

## 动手实践：用 ffmpeg 和 Shaka Packager 生成 HLS + DASH

### 单档位 HLS（fMP4）

```bash
ffmpeg -i input.mp4 \
  -c:v libx264 -preset slow -crf 22 -g 96 -keyint_min 96 -sc_threshold 0 \
  -c:a aac -b:a 128k \
  -f hls \
  -hls_time 4 \
  -hls_segment_type fmp4 \
  -hls_playlist_type vod \
  -hls_list_size 0 \
  -hls_segment_filename "hls/seg_%04d.m4s" \
  hls/index.m3u8
```

### 多码率 HLS（一条命令生成三个档位）

```bash
ffmpeg -i input.mp4 \
  -filter_complex "[0:v]split=3[v1][v2][v3]; \
    [v1]scale=640:360[v1out]; \
    [v2]scale=1280:720[v2out]; \
    [v3]scale=1920:1080[v3out]" \
  -map "[v1out]" -c:v:0 libx264 -b:v:0 800k -maxrate:v:0 850k -bufsize:v:0 1600k \
  -map "[v2out]" -c:v:1 libx264 -b:v:1 2500k -maxrate:v:1 2650k -bufsize:v:1 5000k \
  -map "[v3out]" -c:v:2 libx264 -b:v:2 5000k -maxrate:v:2 5300k -bufsize:v:2 10000k \
  -map 0:a -c:a aac -b:a 128k \
  -g 96 -keyint_min 96 -sc_threshold 0 \
  -f hls -hls_time 4 -hls_segment_type fmp4 -hls_playlist_type vod -hls_list_size 0 \
  -master_pl_name master.m3u8 \
  -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \
  "hls/v%v/index.m3u8"
```

### 生产级方案：用 Shaka Packager 生成 CMAF + HLS + DASH

```bash
packager \
  in=input_360p.mp4,stream=video,init_segment=cmaf/v0/init.mp4,segment_template=cmaf/v0/seg_\$Number\$.m4s \
  in=input_720p.mp4,stream=video,init_segment=cmaf/v1/init.mp4,segment_template=cmaf/v1/seg_\$Number\$.m4s \
  in=input_1080p.mp4,stream=video,init_segment=cmaf/v2/init.mp4,segment_template=cmaf/v2/seg_\$Number\$.m4s \
  in=input_720p.mp4,stream=audio,init_segment=cmaf/a0/init.mp4,segment_template=cmaf/a0/seg_\$Number\$.m4s \
  --segment_duration 4 \
  --hls_master_playlist_output=cmaf/master.m3u8 \
  --mpd_output=cmaf/manifest.mpd
```

输出：一套片段，同时生成 HLS 和 DASH 两份清单。

---

## 核心要点

1. 流媒体 = 片段 + 清单 + 由客户端驱动的按需拉取。
2. **HLS**（Apple）使用 M3U8 文本清单；**DASH**（MPEG）使用 MPD XML。
3. iOS/Safari 只原生支持 HLS；其他平台两者都支持。
4. **CMAF** 让 HLS 和 DASH 共享同一套 fMP4 文件——这是行业最佳实践。
5. 传统 HLS 延迟为 20～30 秒；**LL-HLS / CMAF-LL** 可达到 2～5 秒。
6. **WebRTC** 是另一条独立的路径（&lt;500ms）——不适用于 VOD。
7. 在生产环境中，使用 Shaka Packager 或云服务（MediaPackage）来完成打包。

---

*上一篇：* [第 4 篇：容器格式](/blog/vod-deep-dive-part-4-container-formats-mp4-fmp4-cmaf/)

*下一篇：* **[第 6 篇：自适应码率流媒体](/blog/vod-deep-dive-part-6-adaptive-bitrate-streaming/)**

---

## 常见问题

### HLS 和 DASH 有什么区别，该怎么选？

HLS 由 Apple 于 2009 年推出，清单是人类可读的 M3U8 文本，是 iOS/Safari 唯一原生支持的协议；DASH 由 MPEG 于 2012 年标准化，是开放的 ISO/IEC 标准，清单是 XML 格式的 MPD，网页端需要 dash.js 等播放器配合 MSE。两者原理相同：把视频切成小分片、按档位组织、通过 HTTP 分发。最佳实践是用 CMAF 让两个协议共享同一套 fMP4 分片，同时输出两份清单。

### 为什么直播会延迟二三十秒？LL-HLS 是怎么解决的？

传统 HLS 规定客户端要攒够 3 个分片才开始播放，按 6 秒一片算光这一步就是 18 秒，加上播放缓冲和网络传输，总延迟达 25～30 秒。Apple 2019 年推出的 LL-HLS 用三招压低延迟：把分片再切成 200～500 毫秒的部分片段、阻塞式播放列表重载（服务器挂起请求直到有新数据）、预加载提示。配合 DASH 侧的 CMAF 分块传输，延迟可降到 2～5 秒。

### 做视频流媒体该不该用 WebRTC 取代 HLS/DASH？

只有视频通话、连麦互动、云游戏这类要求 500 毫秒以内延迟的场景才需要 WebRTC。它基于 UDP 而非 HTTP，无法利用 CDN 缓存，并发规模受 SFU 容量限制；而 HLS/DASH 走 HTTP，轻松支撑数百万并发观看。点播和一对多直播应该用 HLS + DASH 配 CMAF；低延迟直播用 LL-HLS 就能做到 2～5 秒，不必换掉 HTTP 体系。


---

## 参考资料

- [RFC 8216: HTTP Live Streaming](https://datatracker.ietf.org/doc/html/rfc8216) — IETF
- [DASH-IF guidelines](https://dashif.org/guidelines/) — DASH Industry Forum
- [HTTP Live Streaming](https://developer.apple.com/documentation/http-live-streaming) — Apple Developer
