動画配信(YouTube/Netflix)── エンコードパイプラインと適応ビットレート
扱うCS概念:DAG(有向非巡回グラフ)タスクスケジューリング、アダプティブビットレートストリーミング(ABR)、CDN、チャンク転送エンコーディング

この章で何ができるようになるか:動画ファイルがアップロードされてから視聴できるまでの処理パイプラインを設計できるようになる。回線速度が変わっても途切れない動画再生がどう実現されているかを説明できる。
問題設定
YouTube の動画アップロード〜配信パイプラインを設計する。
規模感:
- 動画アップロード:1分間に500時間分の動画がアップロードされる
- 視聴数:1日あたり数十億回の再生
- サポートするデバイス:スマートフォン(低速回線)〜4Kテレビ(超高速回線)
- アップロード後の公開まで:数分以内(短尺)〜十数分(長尺)
技術的課題:
1. アップロードされた動画を複数の画質・フォーマットに変換する
2. 世界中のユーザーに低レイテンシで配信する
3. 回線速度に合わせて動的に画質を切り替える
フェーズ1:動画アップロードとチャンク転送
大きな動画ファイル(数GB)を1回のHTTPリクエストで送るのは現実的でない。
# クライアント側:チャンク分割アップロード
import requests
def upload_video(file_path: str, chunk_size: int = 5 * 1024 * 1024): # 5MB
# 1. アップロードセッションを開始
session = requests.post("/api/upload/init", json={"filename": file_path})
upload_id = session.json()["upload_id"]
file_size = os.path.getsize(file_path)
with open(file_path, 'rb') as f:
chunk_num = 0
while chunk := f.read(chunk_size):
offset = chunk_num * chunk_size
# 2. 各チャンクを送信
resp = requests.put(
f"/api/upload/{upload_id}/chunk",
headers={
"Content-Range": f"bytes {offset}-{offset+len(chunk)-1}/{file_size}",
"Content-Length": str(len(chunk))
},
data=chunk,
timeout=30
)
if resp.status_code == 308: # Resume Incomplete
chunk_num += 1
elif resp.status_code != 200:
raise Exception(f"Upload failed at chunk {chunk_num}")
# 3. アップロード完了を通知
requests.post(f"/api/upload/{upload_id}/complete")
なぜチャンク分割か:
- ネットワーク障害時に失敗したチャンクだけ再送できる
- 複数チャンクを並列アップロードしてスループットを上げられる
- サーバーはチャンクを受け取りながらすぐに処理を開始できる(パイプライン化)
フェーズ2:動画処理パイプライン(DAG タスクグラフ)
アップロードされた動画は複数の処理ステップを経て配信可能な状態になる。これを DAG(有向非巡回グラフ) として設計する。
graph TD
Upload[動画アップロード完了] --> Validate[メタデータ検証<br/>コーデック確認]
Validate --> Split[チャンク分割<br/>GOP単位で分割]
Split --> T1[トランスコード<br/>240p/30fps]
Split --> T2[トランスコード<br/>480p/30fps]
Split --> T3[トランスコード<br/>720p/30fps]
Split --> T4[トランスコード<br/>1080p/60fps]
Split --> T5[トランスコード<br/>4K/60fps]
T1 & T2 & T3 --> Watermark[ウォーターマーク付与]
T4 & T5 --> Watermark
Watermark --> Manifest[マニフェスト生成<br/>HLS/DASH]
Manifest --> CDN[CDN へプッシュ]
Split --> Thumbnail[サムネイル生成<br/>複数フレームから選択]
Split --> Subtitle[字幕抽出<br/>音声認識]
これは並列処理できる箇所とシリアルに処理する必要がある箇所が混在する。DAG で依存関係を表現することで、最大限並列化できる。
Apache Airflow / AWS Step Functions がこのような DAG タスクスケジューリングを実装するツールだ。
# Airflow での動画処理パイプライン定義(概念コード)
from airflow import DAG
from airflow.operators.python import PythonOperator
dag = DAG("video_processing", schedule_interval=None)
validate = PythonOperator(task_id="validate", python_callable=validate_video, dag=dag)
split = PythonOperator(task_id="split", python_callable=split_into_gops, dag=dag)
transcode_240p = PythonOperator(task_id="transcode_240p", ..., dag=dag)
transcode_1080p = PythonOperator(task_id="transcode_1080p", ..., dag=dag)
generate_manifest = PythonOperator(task_id="manifest", ..., dag=dag)
# 依存関係の定義
validate >> split
split >> [transcode_240p, transcode_480p, transcode_720p, transcode_1080p, transcode_4k]
[transcode_240p, transcode_480p, transcode_720p, transcode_1080p, transcode_4k] >> generate_manifest
GOP(Group of Pictures):並列エンコードの鍵
動画ファイルは全フレームを独立して圧縮しているわけではない。動画圧縮(H.264/H.265/AV1)は:
I フレーム(Intra-coded):完全な画像データ(JPEG のようなもの)
P フレーム(Predicted):前のフレームとの差分
B フレーム(Bi-directional):前後のフレームとの差分
GOP = I フレームから次の I フレームまでの単位
例:30fps 2秒 GOP = 1 I フレーム + 59 P/B フレーム
なぜ GOP 単位で分割するのが重要か:
GOP の境界(Iフレーム)で分割すれば、
各部分が独立してデコードできる
→ 動画を並列にエンコードできる
→ 長時間動画も短時間で処理できる
GOP の中間で分割すると、
そのフレームは他のフレームに依存しているため
独立してデコードできない
フェーズ3:アダプティブビットレートストリーミング(ABR)
「4Kテレビで観るユーザー」と「地下鉄の弱い LTE で観るユーザー」に同じ動画を送れない。ABR は回線速度に合わせて画質を動的に切り替える仕組みだ。
HLS(HTTP Live Streaming)の仕組み
Apple が開発した HLS は現在の標準。動画を小さなチャンク(通常2〜6秒)に分割し、マニフェストファイルで管理する。
# マスタープレイリスト(video.m3u8)
#EXTM3U
#EXT-X-VERSION:3
# 240p
#EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=426x240
240p/playlist.m3u8
# 480p
#EXT-X-STREAM-INF:BANDWIDTH=1000000,RESOLUTION=854x480
480p/playlist.m3u8
# 720p
#EXT-X-STREAM-INF:BANDWIDTH=2500000,RESOLUTION=1280x720
720p/playlist.m3u8
# 1080p
#EXT-X-STREAM-INF:BANDWIDTH=5000000,RESOLUTION=1920x1080
1080p/playlist.m3u8
# 720p のメディアプレイリスト(720p/playlist.m3u8)
#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXTINF:6.0,
segment000.ts
#EXTINF:6.0,
segment001.ts
#EXTINF:5.8,
segment002.ts
...
# クライアント側の ABR ロジック(概念)
class ABRPlayer:
def __init__(self):
self.buffer_seconds = 0
self.current_quality = "480p"
self.download_speeds = [] # 直近のダウンロード速度履歴
def select_quality(self) -> str:
avg_speed_kbps = sum(self.download_speeds[-5:]) / 5
if self.buffer_seconds < 5:
# バッファが少ない → 低画質に下げる(途切れ防止優先)
return "240p"
elif avg_speed_kbps > 5000 and self.buffer_seconds > 15:
# 高速回線+バッファ十分 → 高画質に上げる
return "1080p"
elif avg_speed_kbps > 2500:
return "720p"
elif avg_speed_kbps > 1000:
return "480p"
else:
return "240p"
def download_next_segment(self):
quality = self.select_quality()
start_time = time.time()
segment = download(f"{quality}/segment{self.next_segment}.ts")
elapsed = time.time() - start_time
speed_kbps = len(segment) * 8 / elapsed / 1000
self.download_speeds.append(speed_kbps)
self.buffer_seconds += 6 # 6秒チャンクをバッファに追加
self.current_quality = quality
graph LR
subgraph ネットワーク速度
Fast[高速 >5Mbps] --> Q4[1080p]
Mid[中速 2-5Mbps] --> Q3[720p]
Slow[低速 0.5-2Mbps] --> Q2[480p]
Poor[弱い <0.5Mbps] --> Q1[240p]
end
subgraph バッファ量
High[バッファ >15秒] -->|品質上げる| Fast
Low[バッファ <5秒] -->|品質下げる| Poor
end
CDN(Content Delivery Network)の役割
動画ファイルを世界中のユーザーに低レイテンシで届けるには、ユーザーの近くにキャッシュを配置する必要がある。
graph TD
Origin[オリジンサーバー<br/>(東京)] -->|プッシュまたはプル| Tokyo[東京エッジ]
Origin --> Singapore[シンガポールエッジ]
Origin --> London[ロンドンエッジ]
Origin --> NewYork[ニューヨークエッジ]
UserTokyo[東京のユーザー] --> Tokyo
UserSG[シンガポールのユーザー] --> Singapore
UserUK[英国のユーザー] --> London
プッシュ vs プル:
プッシュ型:
人気コンテンツを事前に全エッジへ配信(プロアクティブ)
→ メリット:キャッシュミスが起きない
→ デメリット:人気でないコンテンツも全エッジに送る(ストレージ無駄)
プル型(オンデマンドキャッシュ):
初めてリクエストされたときだけオリジンから取得してキャッシュ
→ メリット:人気コンテンツだけキャッシュされる(効率的)
→ デメリット:最初のユーザーだけ遅い(キャッシュミスペナルティ)
実際:
新規公開 + 話題性が高い動画 → プッシュ
ロングテールの動画 → プル
Anycast:CDN エッジのIPアドレスをグローバルに同じにし、ルーターが「最も近いエッジ」へ自動的にルーティングする技術。ユーザーは常に最近傍のエッジに接続される。
Netflix の Adaptive Streaming:独自の工夫
Netflix は 2015年に「Per-Title Encoding(タイトル単位エンコード)」を導入し、さらに 2018年に「Shot-based Encoding(ショット単位エンコード)」へと進化させた。
従来:
動画全体を一定のビットレートで均一にエンコード
→ 動きの少ない場面(顔のアップ)に無駄なビットを使っている
→ 動きの激しい場面(アクション)が画質劣化
Shot-based Encoding:
シーンの複雑さを分析し、動きの激しい場面に多く、静止場面に少なくビットを割り当てる
→ 同じデータ量でも体感画質が大幅に向上
また Netflix は AV1 コーデック(2020年に Android 向けで初導入、その後 TV・Web へ拡大)を採用。H.264 に比べて同画質で約50%のデータ量を実現する(計算コストは高い)。
まとめ
| フェーズ | 課題 | 解決策 | CS概念 |
|---|---|---|---|
| アップロード | 大ファイルの転送 | チャンク分割 + 並列送信 | マルチパート転送 |
| 処理 | 複数形式への変換を高速化 | DAG による並列トランスコード | タスクグラフ・GOPベース分割 |
| 配信 | 世界中に低レイテンシで | CDN + Anycast | エッジキャッシング |
| 再生 | 回線速度の変動への対応 | ABR(HLS/DASH) | アダプティブビットレート |
| 画質効率 | 同じデータ量で高画質 | Shot-based Encoding + AV1 | 知覚的コーデック最適化 |
動画配信は「アップロード → 処理 → 保存 → 配信 → 再生」の各レイヤーで異なる技術的問題が積み重なっている。各レイヤーで CS の基礎(グラフアルゴリズム・圧縮理論・キャッシング)が応用されている好例だ。