tips

DASH ディープ:SegmentTemplate、ContentProtection、視点切り替え MPD 構造

MPD 入門のその先——SegmentTemplate vs SegmentTimeline、$Number$ vs $Time$、ContentProtection シグナリング、そして DASH が HLS より解析が難しいのにダウンロードしやすい構造的理由。
FlowPick チーム
15 分で読了
# dash # mpd # ディープ # segmenttemplate # drm # ストリーミング

MPD 入門ガイド では MPD とは何か、なぜ MP4 ではなく 12KB の XML が出てくるのかを説明した。この記事はその続き——MPD を開いて、400 行の <SegmentTemplate><SegmentTimeline><ContentProtection><AdaptationSet> を見て、何が本当に使えるのかを知りたい人向け。

DASH パーサーを書いているか、ダウンロードツールが取れるストリームと取れないストリームがある理由を知りたいなら、この記事が地図になる。

MPD の階層

すべての MPD はこのネスト構造:

MPD
├── Period                  # コンテンツの 1 セグメント(本編、広告など)
│   ├── AdaptationSet       # 交換可能なバージョンのグループ(映像、音声など)
│   │   ├── Representation  # 具体的な 1 バージョン(1080p、720p など)
│   │   │   ├── SegmentList     # セグメント URL を明示的に列挙
│   │   │   ├── SegmentTemplate # セグメント URL をテンプレート化
│   │   │   └── SegmentBase     # 単一セグメント + バイト範囲
│   │   └── ContentProtection   # DRM シグナリング(Widevine、PlayReady)
│   └── EventStream         # 帯域外イベント(ID3 のようなもの)

典型的な映画の MPD は Period 1 つ、AdaptationSet 2 つ(映像 + 音声)、各セットに Representation が 5-10 個。広告付きライブは Period が 4 つ以上になる。マルチカメラのスポーツライブは各カメラに 1 つの AdaptationSet。

核心:DASH はすべてのセグメント URL を列挙しないように設計されている。 HLS は 1 つずつ列挙する。DASH はテンプレートを使う。

SegmentTemplate:90% のケース

<SegmentTemplate
  initialization="video/init.mp4"
  media="video/$Number$.m4s"
  startNumber="1"
  timescale="1000"
  duration="6000" />

訳すと:

  • 初期化セグメントは video/init.mp4(fMP4 の初期化ボックス)
  • 各メディアセグメントは video/$Number$.m4s$Number$ は 1 から
  • 各セグメントは 6000 ミリ秒(timescale=1000 なので 6 秒)

この Representation をダウンロードするには、$Number$ を置換して URL を生成する:

const startNumber = 1
const duration = 6000        // ms
const timescale = 1000       // 1 単位 = 1 ms
const totalDuration = 7200000 // ms——Representation の @duration 属性から

const segmentCount = Math.ceil(totalDuration * timescale / (duration * timescale))
// = 1200 セグメント

const urls = []
for (let n = startNumber; n < startNumber + segmentCount; n++) {
  urls.push(`video/${n}.m4s`)
}

HLS よりずっとコンパクト——1200 行の #EXTINF + URL が 1 つの <SegmentTemplate> に。代償はパーサーが自分で計算しないといけないことで、エッジケース(最後のセグメントが短い、セグメント長が変化する)に注意。

$Number$ vs $Time$

$Number$ はセグメントインデックス(1、2、3……)。$Time$ はセグメントの開始時刻(timescale 単位)。両方使うもの、どちらかだけのものがある:

<SegmentTemplate
  media="video/$Time$.m4s"
  timescale="1000"
  duration="6000" />

$Time$ を解析するには、セグメントインデックスに長さを掛ける:

const segmentTime = (n - startNumber) * duration
const url = `video/${segmentTime}.m4s`

timescale を掛け忘れると、URL が 1000 倍ずれて全部 404 になる。これが DASH パースで最も一般的なバグ。

SegmentTimeline:セグメント長が固定でない時

ライブや動的コンテンツは <SegmentTimeline> を使う:

<SegmentTemplate timescale="1000" initialization="init.mp4" media="$Number$.m4s">
  <SegmentTimeline>
    <S t="0" d="6000" />
    <S t="6000" d="6000" />
    <S t="12000" d="4000" />
    <S t="16000" d="6000" r="3" />  <!-- r=3 は 3 回反復:計 4 セグメント -->
    <S t="40000" d="6000" />
  </SegmentTimeline>
</SegmentTemplate>
  • t——開始時刻(timescale 単位)
  • d——長さ(timescale 単位)
  • r——反復回数(r+1 個のセグメントを意味する)

ライブの timeline は時間と共に伸びる。典型的なフロー:

  1. クライアントが MPD を取得。セグメント 1-50 が見える。minimumUpdatePeriod は 2 秒
  2. クライアントは 1-50 を再生
  3. クライアントは MPD を再取得。今は 1-52(timeline が伸びている)
  4. MPD@endOfAvailability またはストリーム終了まで繰り返し

ダウンローダーの選択肢:

  • ストリームが完全に終わるのを待つ(VOD に切り替わる)——常に可能とは限らない
  • 現在の timeline をスナップショットして、後から追加されるコンテンツは諦める

FlowPick はスナップショット方式:現在の timeline を取得、セグメントをダウンロード、マージ。ストリームがまだ生きていれば、ユーザーはもう一度トリガーして新しいセグメントを取得できる。

ContentProtection:DRM シグナリング

<ContentProtection
  schemeIdUri="urn:mpeg:dash:mp4protection:2011"
  value="cenc"
  cenc:default_KID="12345678-1234-1234-1234-123456789012" />

<ContentProtection
  schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"
  value="Widevine">
  <cenc:pssh>AAAA...</cenc:pssh>
</ContentProtection>

<ContentProtection
  schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95"
  value="PlayReady">
  <cenc:pssh>BBBB...</cenc:pssh>
</ContentProtection>

UUID が DRM システムを識別する:

  • edef8ba9-79d6-4ace-a3c8-27dcd51d21ed——Widevine(Google)
  • 9a04f079-9840-4286-ab92-e65be0885f95——PlayReady(Microsoft)
  • f239e769-efa3-4850-9c16-a9036d3fe1c1——Adobe Primetime
  • 94ce86fb-07ff-4f43-adb8-93d2fa968ca2——FairPlay(Apple、DASH 文脈では)

<cenc:pssh> は Protection System Specific Header——base64 の blob で、key ID とシステム固有データを含む。ブラウザはこれを EME(Encrypted Media Extensions)に渡し、EME がシステムの CDM(Content Decryption Module)と会話する。

ここで線を引く: FlowPick は DRM で保護された DASH コンテンツを解読しないし、しないし、できない。正当なブラウザ拡張もオープンソースツールもできない。CDM はブラックボックス——暗号化バッファを渡すと、表示用の復号フレームを返すが、エクスポートは永久にしない。これを迂回するのは反規避法(米国 DMCA §1201、他の地域も同等)に違反し、CDM の契約にも違反する。

DASH 仕様には「保護されていない」パスがある:ある AdaptationSet が ContentProtection を持たなければ、セグメントは暗号化されておらず、ダウンローダーは自由にフェッチしてマージできる。だから FlowPick は Bilibili、Udemy の無料コース、たいていの企業 LMS を処理できる。Netflix/Disney+/HBO Max は処理できない。

「保護っぽい」けど DRM ではない一般的なパターン

「ダウンロードできない」ことがすべて DRM というわけではない。ダウンロードを難しくするが、実際には暗号化しないパターン:

  1. 短い TTL のトークン URL——?token=abc&expires=1234567890。トークンが数秒で期限切れになるので、清単を解析したらすぐにセグメントをフェッチしないといけない。FlowPick はそうしている。「何時間も前に URL を解析して今からダウンロード」系のバッチダウンローダーは失敗する。
  2. CDN の CORS 制限——でも拡張はページ origin からリクエストを送るので CORS に縛られず、スニッファーを実質止められない。
  3. <BaseURL> の書き換え——MPD の BaseURL 要素は文書の途中で変えられる。各 BaseURL は親に対して相対的に解決されるので、URL の連鎖を追跡する必要がある。これは DASH 版の HLS 相対 URI だが、エラーを起こしやすい。

マルチ Period MPD:広告とライブイベント

<Period id="ad1" start="PT0S" duration="PT30S">...</Period>
<Period id="main" start="PT30S">...</Period>
<Period id="ad2" start="PT1800S" duration="PT30S">...</Period>
<Period id="main2" start="PT1830S">...</Period>

各 Period は独自の AdaptationSet を持つ。コンテンツ全体をダウンロードするには:

  1. @start で Period をソート
  2. 各 Period で映像 + 音声セグメントをすべて取得
  3. Period 間で結合——ただし codec が Period 間で変わる場合に注意

広告ブレークはよく別 codec(広告は H.264 baseline、本編は H.264 high)。無脑に結合すると、本編は普通に再生されるが広告部分でグリッチが出る。

修正方法:

  1. 広告 Period をスキップ(FlowPick のアプローチ——ユーザーは広告を欲しくないので)
  2. 広告 Period を本編と同じ codec に再エンコード(遅い、トランスコードが必要)
  3. より賢いマルチプレクサで codec 切り替えを処理(FFmpeg はできるが複雑)

視点切り替えと trick-mode AdaptationSet

<AdaptationSet id="1" contentType="video" group="1">
  <!-- メインカメラ -->
</AdaptationSet>
<AdaptationSet id="2" contentType="video" group="1">
  <!-- バックアップカメラ、同じ group -->
</AdaptationSet>
<AdaptationSet id="3" contentType="video" group="2">
  <!-- Trick mode(シークバーのサムネイル) -->
</AdaptationSet>

group="1" は同じコンテンツの異なる視点を意味する。プレイヤーはユーザーにカメラ切り替えを提供する。ダウンローダーはどれか 1 つを選ぶ——大抵は最初だが、理想はユーザーに選ばせる。

Trick-mode AdaptationSet(ここでは group=2)はシークバーでドラッグした時のプレビュー用、低ビットレートの低フレームレート版。これをダウンロードしても無意味——パーサーがこれを「映像」と誤認しないように。

プリフェッチと並列化

DASH ダウンロードで最も実用的な最適化:セグメントを並列フェッチ。

async function downloadRepresentation(rep, concurrency = 6) {
  const segmentUrls = computeSegmentUrls(rep)
  const initBytes = await fetch(rep.initUrl).then(r => r.arrayBuffer())

  const results = new Array(segmentUrls.length)
  let nextIndex = 0

  async function worker() {
    while (nextIndex < segmentUrls.length) {
      const i = nextIndex++
      const resp = await fetch(segmentUrls[i])
      results[i] = new Uint8Array(await resp.arrayBuffer())
    }
  }

  await Promise.all(Array.from({ length: concurrency }, () => worker()))

  // init + セグメントを順番に結合
  const total = initBytes.byteLength + results.reduce((s, b) => s + b.byteLength, 0)
  const out = new Uint8Array(total)
  let offset = 0
  out.set(new Uint8Array(initBytes), offset); offset += initBytes.byteLength
  for (const buf of results) {
    out.set(buf, offset); offset += buf.byteLength
  }
  return out
}

6 並列接続がスイートスポット——それ以上は CDN のレート制限をトリガーし、以下は帯域を無駄にする。Promise.all の worker プールパターンで、セグメントが順不同で完了しても正しい配列位置に落ちる。

これが FlowPick 内部で使っているパターン。ブラウザ内マージの記事 で最終バッファをどうマルチプレクスするかを説明している。

踏んだ抜け

抜け 1:BaseURL を無視。 MPD が https://cdn.example.com/v/manifest.mpd にあって、中身が <BaseURL>https://other-cdn.example.com/</BaseURL> なら、このセクションの相対 URL は other-cdn.example.com で解決される。MPD の origin ではない。これを忘れると「全部 404」バグの頭号原因。

抜け 2:timescale の解析間違い。 duration="6000"timescale="1000" なら 6 秒。timescale="90000"(映像でよくある)なら 0.067 秒。timescale を必ず確認——同じ MPD 内で AdaptationSet ごとに異なることがある。

抜け 3:init セグメントの忘れ。 HLS(TS の場合)は各セグメントが自己記述的だが、DASH の fMP4 セグメントは init セグメントがないと役立たず。「ダウンロード成功した」ように見えて再生できないファイルは、initialization="init.mp4" を忘れていることが多い。

抜け 4:ライブ MPD は更新される。 一度解析してスナップショットでダウンロードすると、その後に追加されたセグメントを見逃す。VOD は問題ない。ライブは定期的に再取得するか、ストリームの終わりを待つ。

参考資料

まとめ

DASH と HLS は同じ問題——HTTP ベースのアダプティブストリーミング——を逆の考え方で解決する。HLS は各セグメントを列挙。DASH はテンプレートを使う。HLS はデフォルトで MPEG-TS。DASH は fMP4。HLS の DRM は Apple 専有(FairPlay)。DASH の DRM はマルチベンダー(Widevine、PlayReady、FairPlay)で CENC を通る。

ダウンローダーの観点では、DASH は構造的に扱いやすい:fMP4 セグメントは直接結合できる、init セグメントが codec 情報を明示する、$Number$ 置換は清単を追加フェッチせずに URL を計算できる。複雑さは XML 解析にある——でも SegmentTemplateSegmentTimelineBaseURLContentProtection を扱えれば、現実の MPD の 95% をカバーできる。

次は fMP4 の結合がなぜ動くのか、moof ボックスに何が入っているのか、なぜ「リマックスでトランスコードしない」が正しい哲学なのか


おすすめ記事