HLS 深度:加密、多音轨、值得记住的 EXT-X 标签
HLS 入门指南 讲了"什么是 M3U8、怎么下载"。这篇是另一篇——给那些试着下过一个流、拿到一个 200KB 的 .m3u8、里面引用了 8 条音轨、4 种字幕、还有一个让 VLC 吐垃圾的 #EXT-X-KEY 标签的人。
你打开过 Netflix、Disney+ 或付费课程平台的真实 HLS 清单,纳闷为什么你的下载器下出来的是无声视频或损坏文件,原因就在这里。
真实 HLS 清单长什么样
多数教程用这个当例子:
#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-VERSION:3
#EXTINF:6.0,
segment0.ts
#EXTINF:6.0,
segment1.ts
#EXTINF:4.2,
segment2.ts
#EXT-X-ENDLIST
可爱。真实清单不长这样。这是一个主流平台的真实 master playlist,精简过但能代表全貌:
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-INDEPENDENT-SEGMENTS:yes
# 音频组
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac-64",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio/eng_64.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac-128",NAME="English",LANGUAGE="en",DEFAULT=YES,AUTOSELECT=YES,CHANNELS="2",URI="audio/eng_128.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac-64",NAME="Español",LANGUAGE="es",DEFAULT=NO,AUTOSELECT=YES,CHANNELS="2",URI="audio/spa_64.m3u8"
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",LANGUAGE="en",DEFAULT=NO,AUTOSELECT=YES,URI="subs/en.m3u8"
#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="CC",LANGUAGE="en",DEFAULT=NO,AUTOSELECT=YES,INSTREAM-ID="CC1"
# 视频变体
#EXT-X-STREAM-INF:BANDWIDTH=1200000,RESOLUTION=854x480,CODECS="avc1.64001f,mp4a.40.2",AUDIO="aac-64",SUBTITLES="subs",CLOSED-CAPTIONS="cc"
v_480.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1280x720,CODECS="avc1.640020,mp4a.40.2",AUDIO="aac-128",SUBTITLES="subs",CLOSED-CAPTIONS="cc"
v_720.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=8000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2",AUDIO="aac-128",SUBTITLES="subs",CLOSED-CAPTIONS="cc"
v_1080.m3u8
实际媒体分片根本不在这里。master playlist 指向变体 playlist,每个变体再指向分片。音频还单独在自己的 playlist 里。你的下载器要是只抓 v_1080.m3u8 然后拼接分片,得到的就是无声视频。这是"HLS 下载没声音"反馈的头号原因。
CODECS 字段不是装饰
CODECS="avc1.640028,mp4a.40.2" 不是写着好看:
avc1.640028— H.264 High profile、level 4.0。avc1.后面前两个十六进制位编码 profile(64= High)、constraint flag、level(28= 4.0)。mp4a.40.2— AAC-LC。
为什么重要:某个变体声明 avc1.640028,你却把分片封装进一个 codec box 写错的 MP4(hvc1 而不是 avc1),文件不能播。.ts 分片裸拼接能避开这个问题,因为 MPEG-TS 自带 codec 信息;封装进 MP4 就得解析这个字段、写对 stsd box。
EXT-X-KEY:加密,以及"直接解密"不是一个句子
清单里有这行:
#EXT-X-KEY:METHOD=AES-128,URI="https://cdn.example.com/key.bin",IV=0x1a2b3c4d5e6f7890abcdef1234567890
意味着之后每个分片都用 URI 处的密钥和指定的 IV 做 AES-128-CBC 加密。解密:
const keyResp = await fetch(keyUri)
const keyBytes = await keyResp.arrayBuffer() // 16 字节
const iv = new Uint8Array(16)
// IV 可能是字面值(如上),也可能从序号推导
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-CBC', iv },
await crypto.subtle.importKey('raw', keyBytes, 'AES-CBC', false, ['decrypt']),
encryptedSegment
)
crypto.subtle.decrypt 是浏览器里的正确工具——不需要 WASM 写 AES,原生 WebCrypto 又快又是 constant-time。
教程不会告诉你的是:METHOD=AES-128 是简单情况。 大多数主流平台用 METHOD=SAMPLE-AES,苹果的 FairPlay DRM 邻近方案。SAMPLE-AES:
- 每个分片只有部分被加密(视频 NAL unit,音频和 PES 头不加密)
- key URI 通常返回的是一个被 FairPlay license 交换包过的
EXT-X-KEY响应——不是裸 16 字节密钥 - 浏览器标签页里无法解密。这就是 DRM,"FlowPick 下不了这个 Netflix 视频"是特性不是 bug。我们在 流媒体下载合法性指南 里讲了这条法律和技术底线。
我的看法: 哪个下载器声称能处理 FairPlay、Widevine 或 PlayReady,要么在撒谎,要么在大多数司法管辖区违法。FlowPick 明确拒绝碰 SAMPLE-AES 内容。正确行为是大声报错,不是静默产出损坏文件。
EXT-X-MEDIA:音频和字幕轨道
EXT-X-MEDIA 标签是 HLS 多音轨的机制。每个 GROUP-ID 是一组可互换的音频版本;变体 playlist 的 AUDIO 属性指向它用的那组。
对下载器意味着:一次"完整下载"不只是视频分片。是:
- 选定变体 playlist 的视频分片
- 一条
EXT-X-MEDIA条目的音频分片(通常是DEFAULT=YES那条,但应该让用户选) - 可选的,
TYPE=SUBTITLES的字幕分片 - 把它们封装进一个 MP4,写正确的轨道元数据
封装这一步最容易翻车。来自不同 playlist 的 MPEG-TS 分片各有自己的 PTS 时间线;把只有视频的 .ts 和只有音频的 .ts 拼进一个 MP4,你得:
- 从每个 TS 包解析 PES 头抽 PTS
- 按 PTS 排序包
- 把它们写进 MP4,每个轨道的
mdia/minf/stbl结构要正确
这是 FlowPick 的 WebAssembly FFmpeg 干的活——见 浏览器内合并深度篇。重点:"拼文件"在出现独立音轨那一刻就不灵了。
不连续性标签
#EXT-X-DISCONTINUITY
意思是"时间戳在这之后重置"。常见于:
- 带广告位的直播(广告有自己的时间线)
- 不同编码器拼接的清单
无脑拼接分片的下载器,在不连续点会让播放器时间倒流。要么:
- 用会处理不连续性的重封装器(FFmpeg 行)
- 把不连续性 strip 掉,接受时间戳漂移
- 直接跳过广告分片(FlowPick 默认这样做,反正广告多是 2 秒的
.ts污染流——见 v1.0.0 发布说明 里的尺寸过滤器)
EXT-X-VERSION:值得记住的版本标签
| 版本 | 加了什么 | 为什么你在意 |
|---|---|---|
| 2 | I-frame playlist | Trick mode(快进/倒退)支持 |
| 3 | 浮点 EXTINF | 低延迟流的亚秒级时长 |
| 4 | 独立音频/字幕的 EXT-X-MEDIA | 多音轨——见上 |
| 5 | EXT-X-KEY 的 IV 可选 | 密钥能从序号推导 IV |
| 6 | EXT-X-MAP | fMP4 的 init 分片——见下一节 |
看到 #EXT-X-VERSION:6 而变体 playlist 里没有 #EXT-X-MAP,哪里怪怪的。fMP4 HLS 必须有 init 分片;没有它,分片不能播。
EXT-X-MAP 与 fragmented MP4
#EXT-X-MAP:URI="init.mp4"
意思是分片不是 MPEG-TS——是 fragmented MP4(fMP4),init.mp4 含解读它们所需的 ftyp 和 moov box。没有 init 分片,你手里就是裸 moof/mdat box,没有 codec 信息。
合并 fMP4 HLS:
- 拉
init.mp4——这是输出文件的文件头 - 按序号顺序拼接分片的
moof/mdat对 - 想要 seek 索引的话可选重写
sidxbox
这比 TS 重封装简单得多,因为 fMP4 就是为拼接设计的。苹果的 LL-HLS 现在几乎全用 fMP4。
Low-Latency HLS(LL-HLS)
苹果 2019 年的规范扩展加了:
EXT-X-PART— 部分分片(通常 200ms 而不是 6s)EXT-X-PRELOAD-HINT— 告诉客户端开始抓还不存在的分片EXT-X-RENDITION-REPORT— 让变体 playlist 直接互相引用
对下载器来说,LL-HLS 主要意味着"更多请求、更小文件"。合并逻辑跟普通 HLS 一样——只是除了 EXTINF 还要处理 EXT-X-PART。FlowPick 处理得了;你自己撸的下载器不处理,就会漏掉部分分片。
我真见过的坑
坑 1:相对 URI。 M3U8 大量用相对 URI。master playlist 在 https://cdn.example.com/v/master.m3u8,里面写 v_720.m3u8,变体就是 https://cdn.example.com/v/v_720.m3u8。但 master 在 https://cdn.example.com/v/master.m3u8?token=abc,token 不会自动传过去。query string 继承要手动处理。
坑 2:字节范围寻址。
#EXT-X-BYTERANGE:522752@1024
意思是"分片是大文件里的一个字节范围"。不带 Range 头去 fetch 这个 URI,你会拿到整个文件。很多 CDN 托管的 HLS playlist 对归档内容这么做——不是几千个小文件,而是一个大文件加字节范围。
坑 3:按 CODECS 选变体。 master playlist 同时有 avc1.*(H.264)和 hvc1.*(HEVC)变体,你的播放器可能选 HEVC。你的下载器只抓第一个变体,可能拿到 HEVC 而你想要的是 H.264。一定让用户选,并解析 CODECS 给变体打标签。
坑 4:token 过期。 URI 里的直播 token 通常几分钟过期。一小时前抓的清单 URL 现在去下,全是 403。FlowPick 在下载时重拉清单,这就是"先抓 URL 后下"在直播内容上经常失败的原因。
参考资料
- RFC 8216 — HTTP Live Streaming, 2nd Edition — 正式规范,107 页
- Apple HLS 创作规范 — 苹果 App Store 上架要求
- FFmpeg MPEG-TS 格式文档 — 理解
.ts文件里到底有什么 - MP4RA — MP4 注册机构 — CODECS 属性的 codec 到 FourCC 映射
小结
M3U8 入门叙事——"它是个 playlist,抓分片,拼起来"——覆盖大概 60% 的真实流。剩下 40% 涉及多音轨、加密、fMP4 init 分片、不连续性、字节范围、过期 token。一个稳健的 HLS 下载器得正确解析 EXT-X-MEDIA、EXT-X-KEY、EXT-X-MAP、EXT-X-BYTERANGE、EXT-X-DISCONTINUITY。"正确"长什么样,看 FlowPick 源码。
下一篇是 DASH/MPD 深度篇,讲 MPEG-DASH 对应的机制——SegmentTemplate、$Number$ 替换、ContentProtection,以及为什么 DASH 结构上比 HLS 复杂、实操上却更好下。
推荐阅读
- 如何下载 M3U8/HLS 流:完整入门指南 — 这篇 60% 的简单情况,那篇做基础
- FlowPick 是怎么在浏览器里把几百个视频切片合成 MP4 的 — FlowPick 怎么处理多音轨+视频的封装
- DASH 深度:SegmentTemplate、ContentProtection 与多视角 MPD — 这篇的 DASH 对应物
- 流媒体下载到底合不合法?一份技术和法律清单 — 为什么 FlowPick 拒绝 SAMPLE-AES / DRM 内容
- 浏览器端视频重封装:fMP4、ISOBMFF 和不转码的理由 — 容器格式深度篇
- WebCodecs + Web Workers + OPFS:浏览器视频处理实战 — FFmpeg WASM 不合适时的更底层浏览器 API