HLS動画配信
はじめに
動画配信では多くの技術要素が協調して動いています。動画ファイル(コーデック・コンテナ)、ストリーミング向けの分割形式、配信プロトコル(HLS/DASH)、サーバ側でのトランスコード、ブラウザでの再生(hls.js/video.js)、ABRによる最適化…。
本記事では これら動画配信周りのことを整理してみています。
動画とストリーミング
動画ファイル
映像データ → エンコード ┐
|→ コンテナ(動画ファイル)
音声データ → エンコード ┘
エンコード
エンコードとは、映像・音声を コーデック規格に従って圧縮・変換する処理 です。
- 映像コーデック:H.264 / VP9 / AV1 など
- 音声コーデック:AAC / MP3 / Opus など
コーデックは 「どう圧縮するか(圧縮規格)」 を定める技術で、画質・音質・ファイルサイズ・再生負荷などに影響します。一部のコーデック(H.264など)にはライセンスが存在するため、利用する場合には注意が必要です。
また多くの映像コーデックでは効率的に圧縮するために I / P / B フレーム という種類のフレームを使っています。
- Iフレーム(Intra):静止画として独立して再生できる基準フレーム
- Pフレーム(Predicted):過去のフレームとの差分だけを記録
- Bフレーム(Bi-predictive):前後のフレーム両方から予測してさらに圧縮
これらを組み合わせることで、高画質を保ちつつデータ量を減らす 仕組みになっています。
コンテナ
コンテナは、エンコードされた映像データと音声データをまとめて格納する「箱」です。コンテナの種類ごとに格納可能なコーデックに制限があります。
例:MP4 / WebM / TS / fMP4
コンテナの中には以下が含まれます:
- 実データ(映像・音声フレームの集合)
- メタデータ(コーデックの種類、解像度、サンプリングレートなど)
ストリーミング向けの「分割コンテナ」
HLS / DASH では動画を 小さなチャンク(数秒単位) に分割して配信するため、
TS(MPEG-TS)/fMP4(fragmented MP4)など専用のコンテナが使われることが多いです。
これらの 分割されたコンテナ を順次取得し、
ブラウザで滑らかに再生するのが HLS / DASH の基本的な仕組みです。
MP4の構造
MP4 File
├── ftyp (ファイルタイプ・互換性情報)
├── moov (メタデータ:再生に必要な情報)
│ ├── mvhd (動画全体の情報)
│ ├── trak (トラック:映像/音声ごとに1つ)
│ └── ...
├── mdat (実データ本体:映像・音声フレーム)
│ ├── [video frame 1]
│ ├── [video frame 2]
│ ├── ...
│ ├── [audio frame 1]
│ ├── [audio frame 2]
│ └── ...
└── free / skip(パディング:任意)
TS(MPEG-TS)の構造
TS Packet (188 bytes)
├── Header
└── Payload(映像/音声データ)
特徴:
- 常に 188バイト固定長
- 音声・映像が細かく分割されて流れる
- 破損に強く、ライブ配信や HLS(初期)で多用
fMP4(fragmented MP4 / CMAF)の構造
Segment.m4s(1チャンク)
├── styp(セグメントタイプ)
├── sidx(セグメントインデックス)
├── moof(フラグメントのメタデータ)
└── mdat(フラグメント実データ)
特徴:
- MP4 を「フラグメント化」して小さく分割できる
- moov が先頭に固定されず、チャンクごとに moof + mdat を持つ
- HLS(CMAF)や DASH の標準的形式として現在主流
大雑把には、MP4 は通常再生向け、TS は古くからある壊れにくい配信用、fMP4 はその両方を両立した分割配信用フォーマットと立ち位置です。
配信プロトコル
動画のコンテナ(箱)だけではストリーミングはできません。どの画質のセグメントを、どの順番で、どう取得するかという「配信ルール」を定めたのが「配信プロトコル」です。
代表的なものは次の2つがあります。
HLS(HTTP Live Streaming)
- プレイリスト形式:m3u8
- セグメント:TS → 現在は fMP4(CMAF)
- 特徴:実装が比較的シンプル、iOS/Safari でネイティブ再生可能
- LL-HLS による低遅延対応も強化
DASH(Dynamic Adaptive Streaming over HTTP)
- プレイリスト形式:MPD
- セグメント:fMP4 が主流
- 特徴:標準化が進んでいる汎用的プロトコル
小さく分割された動画コンテナの配信で用いられるのが、HLS や DASH という配信プロトコルです。かつては「一括ダウンロード → 再生」でしたが、現代は ストリーミング(分割して逐次取得) が主流です。
まとめると、ストリーミングは
コーデック(中身)+ 分割コンテナ(小さな箱) + 配信プロトコル
という組み合わせで成り立っていると言えます。
HLSについて
HLS(HTTP Live Streaming)は Apple が提案し、
現在は RFC 8216 として IETF によって標準化されています。
公式仕様:
https://www.rfc-editor.org/rfc/rfc8216
HLSでは動画を中断せず安定的に再生し続けるため、ネットワークの状態に応じてビットレートを動的に上げたり下げたりします。これを アダプティブストリーミング と呼びます。ネットワーク状況が悪くなると、画質は悪化するものの再生の継続が可能になります。
仕組みとしては、
- HTTPプロトコルを使用して配信する
- プレイリスト(m3u8)で再生可能なビットレートのリストを渡す
- 小さなセグメント(ts/m4s)を逐次取得しながら再生する
というシンプルなアプローチです。
サーバの処理
HLS / DASH のセグメントをブラウザが受け取って再生するまでには、
サーバ側で動画がストリーミング向けに加工・最適化する工程が存在します。
ここではその典型的なステップをまとめてみます。
【アップロード】
multipart / tus
↓
【ジョブ登録】
非同期キューに投入
↓
【トランスコード】
デコード→再エンコード
マルチビットレート生成
↓
【アセット生成】
m3u8 / MPD プレイリスト生成
TS / fMP4 セグメント生成
サムネイル生成
↓
【保存】
オブジェクトストレージ(S3/GCS)
↓
【配信最適化】
CDN キャッシュ
↓
【ブラウザへ】
HLS/DASH プレイヤー(hls.jsなど)
動画ファイルアップロード
動画はブラウザからサーバへアップロードされます。
動画ファイルはサイズが大きいため、マルチパートアップロードやtus(断続アップロード)といった手法が用いられます。不安定なネットワークでも途中再開できるプロトコルであるtusを採用するケースが増えているようです。その後のトランスコード処理は重いため非同期ジョブキューに追加されるのが一般的だと思います。
トランスコード
アップロードされた動画は必ずしもストリーミング向けではないため、
ストリーミングで扱いやすい形に変換(トランスコード)します。
HLS / DASH でビットレートを切り替えるため、
複数解像度・複数ビットレートの動画を生成 します。
ブラウザでの再生 ― hls.js の役割
Chrome / Firefox / Edge は、HLS をそのまま理解しません。つまり、m3u8 も TS も fMP4 も直接 HLS として再生できません。そのためvideoタグ等で再生ができないということになります。
そこで hls.js が登場します。
m3u8 → セグメント取得 → TSの場合は fMP4 へ transmux → MSE SourceBuffer に投入
この「MSE (Media Source Extensions)」が
ブラウザがストリームを受け取る標準 API です。
hls.jsがブラウザが処理できる形式への橋渡しを行います。
⚠️ Safari は特殊
SafariはHLSをブラウザで処理するため、Chromeなどと挙動が異なります。
個人的経験では、Safari は
- MSE の挙動が独自
- 再生バッファ処理の癖
- fMP4 の細かい仕様差
などの理由で、HLS再生では注意が必要です。
video.jsとの関係
WebでHLS再生するときにはvideo.jsを用いる方が簡単です。
以前のvideo.jsではHLS再生できず、video.js + hls.js を併用していました。
この時はvideo.js = 動画プレイヤーのUI、hls.js = HLSの処理という住み分けでした。
現在のvideo.jsでは、videojs-http-streaming(VHS)として本体に統合されています。HLSだけではなく、DASH の一部機能も提供しています。
VHSではhls.jsに比べてよりシンプルなABRロジックで、
各セグメントのダウンロード時間とセグメント長からスループットを計算して、どの程度のビットレートなら許容できるかを計算します。
まとめると:
- hls.js → HLS 専用・チューニング余地が大きい
- VHS → UI と統合された一般用途の ABR(シンプルで安定だが深い制御には向かない)
eラーニングの動画
eラーニング用途は映画やゲーム配信と異なり、
以下の特徴があります:
- 動きが比較的少ない(講義・スライド中心)
- ユーザのネットワーク品質がバラバラ
- 再生停止は UX に大きく影響する
- 長時間視聴(5〜30分)が一般的
- 動画のテキストが読めることが重要
字幕・スライド・画面キャプチャがあるため「テキストが読める」画質を最低限キープし、その上で「停止しない」ことを優先したビットレート設計が必要になってきます。
こうした「文字が読める画質」と「止まらない安定性」が 重要であるeラーニングの動画再生では、HLS の ビットレート選択(ABR)・ブラウザ互換性・安定性が適していると判断して採用しています。
また、ストリーミングの話とは異なりますが講座の動画をきちんと視聴してもらうためシークや倍速の制御や動画が長時間であることが多いため、途中から再生やセクション管理も機能として求められます。
ビットレート選択(ABR)
HLS 規格は選択方法は定めない
HLS の仕様には、
「どのビットレートを選ぶべきか」
という記述は一切ありません。
つまり ABR(Adaptive Bitrate Algorithm)は完全に実装依存となります。
HLSを利用する各サービスごとに独自の方法を持っています。
hls.js の ABR ロジック
帯域推定(EWMA)
hls.js は 指数加重移動平均(EWMA) で帯域を推定します。
EWMAとは、直近の測定値に重みを置きつつ、過去の値も少しだけ考慮する平均手法です。
帯域推定では、簡易的には次のように計算できます。
bwt = α * xt + (1 - α) * bw(t-1)
-
bw_t:時刻 t の帯域推定値 -
x_t:直近セグメントの実測ダウンロード帯域 -
α:重み(0.3~0.9 を使うことが多い)- 大きいと最新の帯域変化に敏感(fast EWMA)
- 小さいと長期安定を重視(slow EWMA)
具体的に計算しているソースコードを抜粋します。この処理により「推定帯域 × 安全係数 >= ビットレート」を満たす最大のレベルを選んでいます。
// abr-controller.js
bw = this.bwEstimator.getEstimate();
if (bw * this.abrBandWidthFactor >= level.bitrate) {
nextLevel = levelIndex;
}
一方で再生バッファが枯渇しそうなとき、現在のダウンロードを中断し、
より低いビットレートのセグメントに切り替えます。
if (fragLoadDelay > bufferStarvationDelay) {
this.hls.nextAutoLevel = findLowerLevel();
loader.abort(); // 今のフラグメントを中止
}
「読み終わるまでの時間 > バッファが尽きる時間」
なら即座にダウンスイッチを行う方式です。
2つの視点
hls.js の特徴は「帯域推定を短期と長期で分けて持つ点」です。
- 短期 EWMA(fast EWMA) → 最近の帯域変動に素早く反応する
- 長期 EWMA(slow EWMA) → 安定した帯域の基準値として使う
ABRロジックは
「短期で急落していないか」「長期で十分な余裕があるか」
を両方参照し、安全係数をかけた上でレベル選択を行っています。
→ 時間スケールの異なる2系統で安全性を確保しています。
さらに詳細では、ABRControllerが現在のバッファ量、セグメントサイズの履歴、前回の切り替えからの経過時間なども含めて複数の情報を組み合わせて判断しています。
他動画サービスのABR
YouTube や Netflix の ABR(Adaptive Bitrate)アルゴリズムについては、
完全な内部実装が公式に公開されているわけではありません。
あくまで公開されている情報から推察される内容です。
YouTube(BOLA)
- バッファ量に基づいてビットレートを決める
- 「バッファの価値」を最大化する思想
Netflix(MPC)
- 数秒先までの QoE を予測し最適化する
- 未来を“読む”アルゴリズム
強化学習 ABR
- 過去の視聴ログから最適戦略を学習
- 状況に応じて ABR 判断を自動学習
ABR は何を最適化の基準にするかがそのままコードに現れます。
HLS と LLM
少し視点を変えて動画ストリーミングを抽象的に捉えてLLMとの共通性を考えてみます。
動画ストリーミングと LLM は、一見するとまったく異なる技術領域です。しかし両者の内部には 「未来が不確実な状態で、逐次的に最適化し続ける」という共通の構造があります。
- HLS → ネットワーク帯域が刻々と変わる中で、「次にダウンロードする最適な画質」を選ぶ
- LLM → 次に出てくる単語が確率的に揺らぐ中で、「次に生成する最適なトークン」を選ぶ
どちらも以下の 3 つを必要とする点で共通します。
- ストリーム処理である(データは逐次到着/逐次生成)
- 未来はわからない(帯域もトークンも確率的)
- その瞬間の状態から “次の一手” を最適化する
この抽象化を踏まえて比較すると
動画ストリーミングと LLM が同じ「オンライン最適化問題」という同じ課題に取り組む技術と捉えることができます。
不確実性の中での逐次的な最適化
HLS の ABR
- ネットワーク帯域は未来が読めない
- その時点でのダウンロード速度やバッファから
「次に安全に再生できるビットレート」 を決める - → 不確実性下での逐次意思決定
LLM のサンプリング
- 次にどのトークンが“正しい”かは確率的
- 文脈と確率分布を見ながら
「破綻なく、かつ目的に適した単語」 を選ぶ - → 不確実性下での逐次意思決定
HLS vs LLM のパイプライン比較図
[ HLS Streaming Pipeline ]
m3u8
↓
Segment Fetcher
↓
(Decrypt / Transmux)
↓
ABR Decision ← 推定帯域・現在のバッファ(不確実なネットワーク)
↓
MSE SourceBuffer
↓
Browser Decoder
↓
Video Playback
[ LLM Inference Pipeline ]
Prompt
↓
Tokenizer
↓
Transformer Layers
↓
Sampling Decision ← トークン確率分布・文脈(不確実な次トークン)
↓
Generated Token
↓
Streaming Output
対応関係
| HLS | LLM |
|---|---|
| 次に取得するセグメントを選ぶ | 次に生成するトークンを選ぶ |
| 帯域・バッファから未来の余裕を推測 | 文脈と確率分布から未来の文を推測 |
| ABR(adaptive bitrate) | Sampling(top-k, nucleus 等) |
| ネットワーク帯域は揺らぎ不確実 | トークン確率も確率的で不確実 |
| 再生停止を避けつつ画質最適化 | 破綻を避けつつ創造性と一貫性を最適化 |
- HLS(特に hls.js の ABR)は、帯域推定とバッファ状況に基づく逐次最適化
- LLM の推論(sampling)は、確率分布と文脈に基づく逐次最適化
- どちらも「未来が不確実」の中で動作し、
現在の情報から動的に最適な判断を瞬時に積み重ねていくアーキテクチャ - 両者の最適化の目的関数が異なり、HLSは再生停止を避ける、LLMは言語品質の最大化
つまり、動画ストリーミングと LLM は、
異なる領域でありながら、同じ抽象パターンに属します。
まとめるとHLS はネットワークの不確実性、LLM は言語の不確実性を扱っている。
どちらも「確実ではない未来に対して最適な一手を選び続ける問題」と捉えられます。
Discussion