JA EN
体系現場のメディア実務
·無料·14分で読めます

HLSとDASH — 動画が届くまでのプロトコル

配信されている動画は「流れて」いるのではなく、数秒ぶんの小さなファイルを順番にダウンロードしているだけです。セグメントとマニフェストの正体、プレイヤーが段を選ぶ仕組み、遅延がどこから生まれるか、低遅延化の手口、そしてCDNの役割までを1から解説します。

対象textタスクcompression

比喩: 一本の川ではなく、短冊の束

「動画を配信する」と聞くと、蛇口をひねると水が出るように映像が流れてくる絵を思い浮かべます。実際に起きているのはずっと素っ気ない話で、サーバに置かれた小さなファイルを、プレイヤーが1つずつ順番にダウンロードしているだけです。

分冊百科を想像してください。全何巻かの目録が1枚の紙にあり、読者はそれを見て次の巻を買いに行く。書店は本を棚に並べておくだけで、どの巻をどの順で買うかは読者が決めます。目録にあたるのがマニフェスト、1巻にあたるのが数秒ぶんのセグメントです。

この「短冊に切る」という一手が、配信の性質をほぼ全部決めています。途中で画質を切り替えられるのも、世界中のキャッシュに置けるのも、遅延が数十秒に膨らむのも、すべてここから出てきます。

なぜHTTPで運ぶことになったのか

かつての配信は専用プロトコルの世界でした。Flash時代のRTMPや、監視カメラで今も見かけるRTSP/RTPです。サーバとクライアントが接続を張り続け、サーバが「今この瞬間の映像」を押し込むプッシュ型です。

理屈では効率的ですが、運用で3つの壁にぶつかりました。専用サーバが要る専用ポートがファイアウォールで塞がれる、そして決定的だったのがキャッシュできないことです。接続ごとに状態を持つ通信は中継機から見ればただの不透明なデータの流れで、「同じものを求めている100人にコピーを配る」ができません。

HLS(Apple、2009年)とMPEG-DASH(ISO/IEC 23009-1、2012年)は発想を裏返しました。映像を普通のファイルに切って、普通のHTTPで置く。サーバは何も判断せず、クライアントが必要な順に取りに来ます。この瞬間、Webのために20年かけて作られたインフラ——CDN、キャッシュ、TLS、どこでも通る443番ポート——が、そのまま動画配信に使えるようになりました。今の規模の配信が成立しているのは、コーデックの進歩と同じくらい、この乗り換えの結果です。

セグメントとマニフェストの実体

HLSのマニフェストはテキストファイルで、2階建てです。上の段(マルチバリアントプレイリスト)が画質の一覧、下の段(メディアプレイリスト)がセグメントの一覧になります。

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
v1080/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
v720/index.m3u8
#EXTM3U
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:1024
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4.000,
1024.m4s
#EXTINF:4.000,
1025.m4s

DASHのマニフェスト(MPD)はXMLで、Period(時間区間)→ AdaptationSet(映像・音声・字幕といったトラック群)→ Representation(1つの画質)という入れ子です。セグメントを1つずつ書き並べる代わりに、テンプレートで済ませられます。

<AdaptationSet mimeType="video/mp4" segmentAlignment="true">
  <SegmentTemplate media="$RepresentationID$/$Number$.m4s"
                   initialization="$RepresentationID$/init.mp4"
                   duration="4" startNumber="1024" timescale="1"/>
  <Representation id="v1080" bandwidth="5000000" width="1920" height="1080" codecs="avc1.640028"/>
  <Representation id="v720"  bandwidth="2500000" width="1280" height="720"  codecs="avc1.64001f"/>
</AdaptationSet>

違いは表面的です。片方はテキスト、片方はXML。片方はAppleの端末が黙って再生し、もう片方はブラウザのMSE(Media Source Extensions)経由で再生する。中身の映像ファイルは、いまやどちらも同じものにできます。それがCMAF(ISO/IEC 23000-19)で、fMP4という共通の器に入れておけば、マニフェストを2種類生成するだけで両方に配れる。ストレージもキャッシュも1セットで済みます。

前提が1つあります。全画質でセグメントの切れ目が揃っていることです。切れ目はキーフレームの位置で決まるので、エンコード時にGOP長を固定し、全解像度で同じ位置に打つ必要があります。詳細は動画圧縮の基礎に譲りますが、揃っていないと切り替えのたびに映像が飛びます。

プレイヤーは何を見て段を選ぶのか

サーバが何も判断しないということは、判断はすべてプレイヤーがしているということです。これがABR(Adaptive Bitrate、適応ビットレート)で、中身は驚くほど単純な足し算と引き算でできています。

プレイヤーは「まだ再生していない映像を何秒ぶん持っているか」というバッファ残量を見ています。kk 番目のセグメントを取り終えた時点の残量を BkB_k 秒、セグメント1本の長さを dd 秒、選んだ段のビットレートを RkR_k、実際に出た回線速度を CkC_k とすると、

Bk+1=max ⁣(BkdRkCk,  0)+dB_{k+1} = \max\!\left(B_k - \frac{d\,R_k}{C_k},\; 0\right) + d
(1)

dRkd R_k はセグメント1本のデータ量、それを CkC_k で割ったものがダウンロードにかかった秒数です。落としている間も再生は進むのでその秒数だけ減り、落とし終えれば dd 秒ぶん増える。つまりこの式は「取るのにかかった時間より、取れた中身のほうが長ければバッファは増える」と言っているだけです。Rk>CkR_k > C_k になった瞬間から残量は減り始め、0に達すると再生が止まる——これがリバッファ(読み込み中のぐるぐる)の正体です。

だから次の段の選び方は、素朴には次のようになります。

Rk+1=max{RL  :  RγC^}R_{k+1} = \max\{\,R \in \mathcal{L} \;:\; R \le \gamma \hat{C}\,\}
(2)

L\mathcal{L} は用意された段の集合、C^\hat{C} は直前のダウンロードから推定した回線速度、γ\gamma は1未満の安全係数です。要するに「推定した速度に安全率をかけて、それを超えない範囲でいちばん良い段を選ぶ」。γ\gamma を1に近づけると画質は上がりますが、推定が少し外れただけで止まります。実際のプレイヤーはこれにバッファ残量そのものも足して判断し、細ってきたら推定に関係なく段を下げます。

段を1つ下げるとは、同じ絵をより粗く量子化したものへ切り替えることです。それが視聴者の画面でどう見えるかを、静止画で確かめておきます。

FIG 1品質を下げるとまず細部が消え、ある点から急にブロック状の崩れに変わる。ABRで段が落ちたときに起きているのはこれ。段の間隔設計は「落ちたことに気づかせない」ための設計でもある

段をいくつ、どの間隔で用意するかという設計そのものは配信のビットレート設計で扱っています。

遅延はどこから生まれるか

HTTPに乗り換えた代償が遅延です。ライブで「テレビより30秒遅い」と言われるあれは、プロトコルの構造からほぼ計算で出てきます。

プレイヤーは、セグメントがサーバ上に完成してからでないと取りに行けません。1本4秒なら、最新の映像は平均2秒ぶん「まだファイルになっていない」状態で待たされます。さらに再生開始前に、途切れないよう数本をバッファに溜めます。溜める本数を nn とすると、

Ltenc+d+nd+tnetL \approx t_{\text{enc}} + d + n\,d + t_{\text{net}}
(3)

tenct_{\text{enc}} はエンコードとパッケージングにかかる時間、dd はセグメントが出来上がるまでの待ち、ndnd はバッファに溜めるぶん、tnett_{\text{net}} はCDNを経由して届くまでの時間です。遅延の大半は「セグメント長 × 溜める本数」だと分かります。6秒を3本溜めればそれだけで18秒、素朴に組むと20〜30秒台になります。

ではセグメントを短くすればいいかというと、そう単純ではありません。セグメントごとにキーフレームが要るので、短いほどキーフレーム密度が上がり、同じ画質に必要なビットレートが増えます。リクエスト数もマニフェストの更新頻度も増える。遅延・画質・オーバーヘッドの三すくみです。

低遅延化: 完成を待たずに配る

そこで両者が採ったのが、「セグメントの完成を待たずに、出来た先頭から配る」という手です。

DASH側は素直です。CMAFのfMP4はさらに細かい「チャンク」の連なりとして書き出せるので、サーバはセグメントを作りながらHTTPのチャンク転送(Transfer-Encoding: chunked)で流し込みます。プレイヤーは4秒のファイルを、完成前から少しずつ受け取れます。

HLS側は、部分セグメントを本物のファイルとして扱う方式です。4秒を0.5秒ずつに分け、#EXT-X-PART としてマニフェストに載せる。加えて、プレイヤーが次の番号を指定して要求するとサーバがそれが出来るまで応答を保留するブロッキング要求と、次に来るURLを先に知らせる #EXT-X-PRELOAD-HINT を組み合わせ、往復の待ちを削ります。

ここに、知らないと必ず踏む罠があります。低遅延にすると回線速度の推定が壊れるのです。通常なら「4秒ぶんを0.5秒で落とせた」から速度が分かりますが、チャンク転送では配信ペースをエンコーダが握っているので、回線がどれだけ太くてもダウンロードには4秒かかります。素朴な推定器は「回線速度=いまのビットレート」と結論し、上の段へ上がれなくなる。低遅延対応のプレイヤーがチャンクの到着間隔を見る作りになっているのは、この問題を避けるためです。

CDNがいなければ成立しない

HTTPに乗り換えた最大の見返りがこれです。セグメントは一度作れば中身が変わらない不変のファイルなので、CDNのエッジに置いておけば、同じ番組を見ている1万人には1回のオリジン取得で足ります。

運用上の勘所はキャッシュ時間(TTL)の付け分けに集約されます。セグメントは長く、マニフェストは短く。ライブのマニフェストは数秒ごとに中身が変わるので、長くキャッシュすると全視聴者が古い目録を見続けて更新が止まります。逆にセグメントのTTLを短くすると、ヒット率が落ちてオリジンに負荷が戻ります。

もう1つはリクエストの束ねです。人気の生放送では、新しいセグメントが出た瞬間に同じURLへ数千の要求が同時に来ます。CDNがこれを1本にまとめてオリジンへ問い合わせる仕組みを持っていないと、オリジンが自分の視聴者に殺されます。低遅延配信では、ブロッキング要求を保留したまま扱えるか、チャンク転送をエッジで溜め込まずに流せるかも、CDNの対応可否として効いてきます。

現場ではこう使う

誰がいつ触るか。 配信基盤のパッケージング担当が新規サービスの構成を決めるとき、ライブ運用が「遅延を縮めたい」と言われたとき、フロントの再生担当が特定端末で再生できない苦情を追うとき。この3者が同じマニフェストを見て話すので、タグの意味を知っているかどうかが会話の速度をそのまま決めます。

素材の作り方(実際に打つコマンド)

ffmpeg -i in.mp4 -c:v libx264 -b:v 2500k -g 96 -keyint_min 96 -sc_threshold 0 \
  -c:a aac -b:a 128k \
  -hls_time 4 -hls_playlist_type vod -hls_segment_type fmp4 \
  -hls_segment_filename 'v720/%d.m4s' v720/index.m3u8

キモは -g 96 -keyint_min 96 -sc_threshold 0 です。24fpsで96フレーム=4秒、つまりセグメント長の整数倍にGOPを固定し、シーンチェンジでの自動キーフレーム挿入を止めています。これを外すと全画質で境界がずれ、切り替えのたびに映像が飛びます。 本番のパッケージングはShaka PackagerやBento4を使うのが一般的ですが、この制約は変わりません。

触るパラメータ名

知らないと事故になる落とし穴

設計レビューで出る問い

「HLSとDASHのどちらを使うべきか」——CMAFで素材を共通化し、マニフェストを2種類出すのが標準解で、二択の問題ではありません。「遅延を5秒にしたい」——セグメントを短くするだけでは画質とリクエスト数を払うだけなので、部分セグメントかチャンク転送を入れ、同時にプレイヤーのバッファ段数とCDNの対応可否を見ます。「キャッシュヒット率が上がらない」——まずセグメントURLに毎回変わるクエリ文字列が付いていないかを疑う。コーデックそのものの選択はH.264とAV1の議論になります。

まとめ

コメント

コメントにはログインが必要です