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