DASH ディープ:SegmentTemplate、ContentProtection、視点切り替え MPD 構造
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 は時間と共に伸びる。典型的なフロー:
- クライアントが MPD を取得。セグメント 1-50 が見える。
minimumUpdatePeriodは 2 秒 - クライアントは 1-50 を再生
- クライアントは MPD を再取得。今は 1-52(timeline が伸びている)
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 Primetime94ce86fb-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 というわけではない。ダウンロードを難しくするが、実際には暗号化しないパターン:
- 短い TTL のトークン URL——
?token=abc&expires=1234567890。トークンが数秒で期限切れになるので、清単を解析したらすぐにセグメントをフェッチしないといけない。FlowPick はそうしている。「何時間も前に URL を解析して今からダウンロード」系のバッチダウンローダーは失敗する。 - CDN の CORS 制限——でも拡張はページ origin からリクエストを送るので CORS に縛られず、スニッファーを実質止められない。
<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 を持つ。コンテンツ全体をダウンロードするには:
@startで Period をソート- 各 Period で映像 + 音声セグメントをすべて取得
- Period 間で結合——ただし codec が Period 間で変わる場合に注意
広告ブレークはよく別 codec(広告は H.264 baseline、本編は H.264 high)。無脑に結合すると、本編は普通に再生されるが広告部分でグリッチが出る。
修正方法:
- 広告 Period をスキップ(FlowPick のアプローチ——ユーザーは広告を欲しくないので)
- 広告 Period を本編と同じ codec に再エンコード(遅い、トランスコードが必要)
- より賢いマルチプレクサで 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 は問題ない。ライブは定期的に再取得するか、ストリームの終わりを待つ。
参考資料
- ISO/IEC 23009-1 — DASH 仕様——正式仕様、有料だが権威
- DASH-IF 実装ガイドライン——DASH Industry Forum のガイド、無料、ISO ドキュメントより実用的
- GPAC MP4Box ドキュメント——リファレンス級の MP4/DASH ツール。パーサーの検証用
- Bento4 MP4 ドキュメント——オープンソースの MP4 ツールキット、コンテナフォーマットの理解に役立つ
まとめ
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 解析にある——でも SegmentTemplate、SegmentTimeline、BaseURL、ContentProtection を扱えれば、現実の MPD の 95% をカバーできる。
次は fMP4 の結合がなぜ動くのか、moof ボックスに何が入っているのか、なぜ「リマックスでトランスコードしない」が正しい哲学なのか。
おすすめ記事
- DASH ストリーミングとは?MPD ファイルを解説——この記事の入門版
- HLS ディープ:暗号化、マルチトラック、覚えておくべき EXT-X タグ——HLS 側の対応物
- ブラウザでの動画リマックス:fMP4、ISOBMFF、トランスコードしない理由——DASH セグメントをダウンロードした後、何が起きるか
- ストリーミングのダウンロードは合法か?技術と法律のチェックリスト——ContentProtection タグがなぜハードな一線なのか
- FlowPick はどうやってブラウザだけで数百の動画セグメントを 1 つの MP4 にまとめるのか——ここでのパターンが FlowPick でどう実装されているか
- WebCodecs + Web Workers + OPFS:ブラウザ動画処理実戦——FFmpeg WASM を補完するより低レベルなブラウザ API