tips

HLS ディープ:暗号化、マルチトラック、覚えておくべき EXT-X タグ

M3U8 入門のその先——EXT-X-KEY、EXT-X-MEDIA、不連続性、そして素朴な HLS ダウンローダーをクラッシュさせる境界ケース。実際の清単スニペットと FlowPick の処理ロジック付き。
FlowPick チーム
16 分で読了
# hls # m3u8 # ディープ # 暗号化 # マルチトラック # ストリーミング

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. の後の最初の 2 桁の 16 進数が profile(64 = High)、constraint flag、level(28 = 4.0)をエンコードする。
  • mp4a.40.2——AAC-LC。

なぜ重要か:あるバリアントが avc1.640028 と宣言しているのに、セグメントを codec box が間違った MP4(avc1 ではなく hvc1)にマルチプレクスすると、ファイルは再生できない。.ts セグメントの生の結合はこの問題を回避できる——MPEG-TS は自己記述的だから。MP4 にマルチプレクスするなら、このフィールドを解析して stsd ボックスを正しく書く必要がある。

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 がブラウザでの正しいツール——AES を WASM で書く必要はなく、ネイティブ WebCrypto は速くて constant-time。

チュートリアルが教えてくれないこと:METHOD=AES-128 は簡単なケース。 大多数の主要プラットフォームは METHOD=SAMPLE-AES を使う——Apple の FairPlay DRM に近いもの。SAMPLE-AES は:

  • 各セグメントの一部だけが暗号化(映像 NAL unit、音声と PES ヘッダーは暗号化なし)
  • key URI は通常、生の 16 バイト鍵ではなく、FairPlay license 交換で包まれた EXT-X-KEY レスポンスを返す
  • ブラウザタブでは復号できない。これが DRM であり、「FlowPick がこの Netflix 動画をダウンロードできない」のはバグではなく仕様。ストリーミングダウンロードの合法性ガイド でこの法的・技術的一線を説明した。

私の見解: FairPlay、Widevine、PlayReady を処理できると主張するダウンローダーは、嘘をついているか、ほとんどの司法管轄区で違法。FlowPick は明示的に SAMPLE-AES コンテンツを拒否する。正しい挙動は、黙って壊れたファイルを出すことではなく、明確にエラーを投げること。

EXT-X-MEDIA:音声と字幕トラック

EXT-X-MEDIA タグは HLS のマルチ音声メカニズム。各 GROUP-ID は交換可能な音声バージョンのセット。バリアント playlist の AUDIO 属性が使うグループを指す。

ダウンローダーにとって意味:「完全なダウンロード」は映像セグメントだけではない:

  1. 選んだバリアント playlist の映像セグメント
  2. 1 つの EXT-X-MEDIA エントリの音声セグメント(通常は DEFAULT=YES のもの、ただしユーザーに選ばせるべき)
  3. オプションで TYPE=SUBTITLES の字幕セグメント
  4. これらを 1 つの MP4 にマルチプレクスし、正しいトラックメタデータを書く

マルチプレクスが一番失敗しやすい。異なる playlist の MPEG-TS セグメントはそれぞれ独自の PTS タイムラインを持つ。映像だけの .ts と音声だけの .ts を 1 つの MP4 にまとめるには:

  • 各 TS パケットの PES ヘッダーを解析して PTS を抽出
  • PTS でパケットをソート
  • 各トラックの mdia/minf/stbl 構造を正しく書く

これが FlowPick の WebAssembly FFmpeg の仕事——ブラウザ内マージのディープ記事 を参照。重要なのは:独立した音声トラックが現れた瞬間に「ファイルをただ連結する」は機能しなくなる。

不連続性タグ

#EXT-X-DISCONTINUITY

意味:「この後のタイムスタンプはリセットされる」。よくあるケース:

  • 広告ブレークのあるライブ(広告は独自のタイムラインを持つ)
  • 異なるエンコーダでエンコードされたセグメントの結合

無脑な結合ダウンローダーは、不連続点でプレイヤーのタイムラインを巻き戻す。回避策:

  1. 不連続性を処理できるマルチプレクサを使う(FFmpeg は OK)
  2. 不連続性を strip し、タイムスタンプのドリフトを受け入れる
  3. 広告セグメントを完全にスキップ(FlowPick のデフォルト——広告はたいてい 2 秒の .ts セグメントでストリームを汚すだけなので、v1.0.0 リリースノート のサイズフィルタを参照)

EXT-X-VERSION:覚えておくべきバージョンタグ

バージョン何が追加されたかなぜ気にするか
2I-frame playlistトリックモード(早送り/巻き戻し)対応
3浮動小数点 EXTINF低遅延ストリームのサブ秒単位の長さ
4独立音声/字幕の EXT-X-MEDIAマルチ音声——上記参照
5EXT-X-KEY の IV が省略可能鍵からセグメント番号で IV を導出可能
6EXT-X-MAPfMP4 の 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 が解釈に必要な ftypmoov ボックスを含む。init セグメントがないと、手元にあるのは生の moof/mdat ボックスだけで、codec 情報がない。

fMP4 HLS をマージ:

  1. init.mp4 を取得——これが出力ファイルのヘッダー
  2. セグメントの moof/mdat ペアを番号順に結合
  3. シークインデックスが必要なら sidx ボックスをオプションで書き直す

TS リマックスよりずっと簡単——fMP4 は結合のために設計されている。Apple の LL-HLS は今やほぼ完全に fMP4 を使う。

Low-Latency HLS(LL-HLS)

Apple の 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

意味:「セグメントは大きなファイルの 1 つのバイト範囲」。Range ヘッダーなしでこの URI を fetch すると、ファイル全体を取得する。多くの CDN ホスト HLS playlist はアーカイブコンテンツに対してこうする——数千の小さなファイルではなく、1 つの大きなファイルとバイト範囲。

抜け 3:CODECS でバリアントを選ぶ。 master playlist が avc1.*(H.264)と hvc1.*(HEVC)の両方のバリアントを持つ場合、プレイヤーは HEVC を選ぶかもしれない。ダウンローダーが最初のバリアントだけを取得すると、HEVC が来るが H.264 が欲しいかもしれない。必ずユーザーに選ばせ、CODECS を解析してバリアントにラベルを付ける。

抜け 4:トークンの期限切れ。 ライブの URI のトークンはたいてい数分で期限切れ。1 時間前に取得した清単 URL を今からダウンロードすると、全部 403。FlowPick はダウンロード時に清単を再取得する。これが「URL を先に取得して後でダウンロード」がライブコンテンツでよく失敗する理由。

参考資料

まとめ

M3U8 入門の物語——「プレイリストで、セグメントを取って、結合する」——は実際のストリームの約 60% をカバーする。残り 40% はマルチ音声、暗号化、fMP4 init セグメント、不連続性、バイト範囲、期限切れトークンを含む。堅牢な HLS ダウンローダーは EXT-X-MEDIAEXT-X-KEYEXT-X-MAPEXT-X-BYTERANGEEXT-X-DISCONTINUITY を正しく解析する必要がある。「正しい」がどんな見た目かは FlowPick のソースコード で見られる。

次は DASH/MPD ディープ記事 で、MPEG-DASH の対応メカニズム——SegmentTemplate$Number$ 置換、ContentProtection、そして DASH が構造的に HLS より複雑なのにダウンロードしやすい理由を説明。


おすすめ記事