動画をCDNで配信したいと思ったとき、最初に壁になるのが「どのフォーマットで出力すべきか」という問題だ。MP4をそのまま置けばいいのでは、と思いがちだが、それだと回線速度に関係なく1つのビットレートしか出せないし、シークのたびにサーバーへのリクエストが発生してCDNの恩恵も受けにくい。
HLS(HTTP Live Streaming)はこの問題を解決するために設計されたフォーマットだ。FFmpegを使えば既存の動画ファイルをHLSに変換し、CloudFrontやCloudflare R2経由で低コストかつ高速に配信できる。
この記事では、シングルビットレートのHLS変換から始めて、アダプティブビットレート(ABR)のマルチビットレート構成、CDNへのアップロード、CORS設定、そしてブラウザでhls.jsを使って再生するところまで、一通りの流れを僕が解説する。
HLSとは何か
HLS(HTTP Live Streaming)はAppleが開発したストリーミングプロトコルだ。動画を数秒単位の小さなセグメント(.ts ファイル)に分割し、そのリストを記述したプレイリストファイル(.m3u8)と組み合わせて配信する仕組みになっている。
通常のMP4配信と比べたHLSの主なメリットは次の3点だ。
- セグメント単位のキャッシュ: 各セグメントが独立したHTTPリクエストなので、CDNが細かく効率よくキャッシュできる
- アダプティブビットレート(ABR): 回線状況に応じて自動的に画質を切り替えられる(マスタープレイリストを使う場合)
- シーク効率: 特定のセグメントだけ取得すればよいので、長尺動画でも任意の位置にすぐジャンプできる
プレイリストには2種類ある。セグメント一覧を列挙するメディアプレイリスト(.m3u8)と、複数のメディアプレイリストをまとめるマスタープレイリスト(.m3u8)だ。ABRを使う場合はマスタープレイリストが起点になる。
シングルビットレートのHLS変換
まず最もシンプルな形から始める。1本の動画ファイルを1つのビットレートでHLSに変換するコマンドだ。
ffmpeg -i input.mp4 \
-c:v libx264 \
-preset fast \
-crf 22 \
-c:a aac \
-b:a 128k \
-hls_time 6 \
-hls_list_size 0 \
-hls_segment_filename "output/segment_%04d.ts" \
output/index.m3u8
各オプションの意味を整理しておく。
| オプション | 意味 |
|---|---|
-hls_time 6 | 1セグメントの長さ(秒)。一般的には4〜10秒が多い |
-hls_list_size 0 | プレイリストに全セグメントを残す(ライブ配信では別設定が必要) |
-hls_segment_filename | セグメントの出力パスとファイル名パターン |
-crf 22 | 品質。値が小さいほど高品質・大ファイルサイズ。18〜28が実用範囲 |
-preset fast | エンコード速度と圧縮率のバランス。slow で小さく、fast で速く |
実行後、output/ ディレクトリに以下のファイルが生成される。
output/
├── index.m3u8 # プレイリスト
├── segment_0000.ts # 1セグメント目(約6秒)
├── segment_0001.ts
├── segment_0002.ts
└── ...
これでシングルビットレートのHLSは完成だ。このまま静的ホスティングに置けば再生できる。
アダプティブビットレート(ABR)の作り方
シングルビットレートでは、低速回線のユーザーに高ビットレートの動画を送りつけてしまう。ABRを使えば、プレイヤーがリアルタイムで回線速度を測定し、最適な画質を選んで再生してくれる。
3段階のビットレートラダーを作るコマンドはこうなる。
# 低品質(360p, 500kbps)
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 28 \
-vf "scale=640:360" \
-c:a aac -b:a 96k \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "output/360p/segment_%04d.ts" \
output/360p/index.m3u8
# 中品質(720p, 1500kbps)
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 23 \
-vf "scale=1280:720" \
-c:a aac -b:a 128k \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "output/720p/segment_%04d.ts" \
output/720p/index.m3u8
# 高品質(1080p, 3000kbps)
ffmpeg -i input.mp4 \
-c:v libx264 -preset fast -crf 20 \
-vf "scale=1920:1080" \
-c:a aac -b:a 192k \
-hls_time 6 -hls_list_size 0 \
-hls_segment_filename "output/1080p/segment_%04d.ts" \
output/1080p/index.m3u8
3つのエンコードが終わったら、マスタープレイリストを手動で作成する。
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=500000,RESOLUTION=640x360,CODECS="avc1.42e01e,mp4a.40.2"
360p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720,CODECS="avc1.64001f,mp4a.40.2"
720p/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=1920x1080,CODECS="avc1.640028,mp4a.40.2"
1080p/index.m3u8
このファイルを output/master.m3u8 として保存する。プレイヤーはこのマスタープレイリストを最初に読み込み、回線速度に応じていずれかのメディアプレイリストに切り替えながら再生する。
出力ディレクトリ構成は次のようになる。
output/
├── master.m3u8 # マスタープレイリスト(プレイヤーの起点)
├── 360p/
│ ├── index.m3u8
│ ├── segment_0000.ts
│ └── ...
├── 720p/
│ ├── index.m3u8
│ └── ...
└── 1080p/
├── index.m3u8
└── ...
CDNにアップロードして配信する
生成したHLSファイルをS3互換のオブジェクトストレージ(AWS S3、Cloudflare R2など)にアップロードし、CloudFrontやCloudflareのCDNで配信する構成が一般的だ。
AWS CLIを使ったS3へのアップロードコマンドは以下のとおりだ。
# m3u8ファイルは text/plain または application/vnd.apple.mpegurl で配信する
aws s3 sync output/ s3://your-bucket-name/videos/sample/ \
--exclude "*" \
--include "*.m3u8" \
--content-type "application/vnd.apple.mpegurl" \
--cache-control "no-cache, no-store"
# tsセグメントは長期キャッシュを設定してCDNにキャッシュさせる
aws s3 sync output/ s3://your-bucket-name/videos/sample/ \
--exclude "*" \
--include "*.ts" \
--content-type "video/mp2t" \
--cache-control "public, max-age=31536000, immutable"
ポイントはキャッシュ戦略の使い分けだ。
- m3u8:
no-cacheにする。プレイリストはセグメントの追加(ライブ)や再エンコードで変わる可能性があるため、常に最新を取得させる - tsセグメント:
max-age=31536000で1年間キャッシュさせる。ファイル名にシーケンス番号が入っているため、内容が変わることはない
CloudFront を使う場合は、ビヘイビアで m3u8 と ts を分けてキャッシュ設定を使い分けると効果的だ。
CORSとセキュリティ設定
hls.jsなどのJavaScriptプレイヤーがCDN上のファイルを読み込む際、クロスオリジンリクエストが発生する。CORS設定を忘れると、ブラウザがセグメントの取得をブロックして再生が止まる。
S3バケットのCORSルールを設定する。
# cors-config.json を作成してから適用する
cat > cors-config.json << 'EOF'
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["GET", "HEAD"],
"AllowedOrigins": ["https://your-site.com"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}
]
EOF
aws s3api put-bucket-cors \
--bucket your-bucket-name \
--cors-configuration file://cors-config.json
CloudFrontを使う場合は、レスポンスヘッダーポリシーで Access-Control-Allow-Origin を追加する必要がある。AWSコンソールから「CloudFront → レスポンスヘッダーポリシー → CORSポリシーを作成」で設定できる。
セキュリティ面では、バケットをパブリックアクセスしない構成(OAC: Origin Access Control)にして、CloudFront経由でのみアクセスできるようにするのが正しい設計だ。特定ドメインからのアクセスのみ許可したい場合は署名付きURLや署名付きCookieも活用できる。
hls.jsでブラウザ再生する
SafariとiOSはHLSをネイティブサポートしているが、ChromeとFirefoxでは hls.js が必要だ。以下はNext.jsでの実装例だ。
"use client";
import { useEffect, useRef } from "react";
import Hls from "hls.js";
interface HlsPlayerProps {
src: string;
poster?: string;
}
export function HlsPlayer({ src, poster }: HlsPlayerProps) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const video = videoRef.current;
if (!video) return;
// SafariはHLSネイティブ対応なのでhls.jsは不要
if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = src;
return;
}
// hls.jsが使えるか確認してから初期化
if (!Hls.isSupported()) {
console.error("HLS is not supported in this browser");
return;
}
const hls = new Hls({
// バッファの設定(デフォルトで多くの場合問題ない)
maxBufferLength: 30,
maxMaxBufferLength: 600,
});
hls.loadSource(src);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play().catch(() => {
// autoplayポリシーで再生がブロックされる場合がある(無音でmutedなら通る)
});
});
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
hls.startLoad(); // ネットワークエラーは再試行
break;
case Hls.ErrorTypes.MEDIA_ERROR:
hls.recoverMediaError(); // デコードエラーはリカバリ試行
break;
default:
hls.destroy(); // 回復不能なエラーはインスタンスを破棄
}
}
});
// クリーンアップ
return () => {
hls.destroy();
};
}, [src]);
return (
<video
ref={videoRef}
controls
poster={poster}
style={{ width: "100%", aspectRatio: "16/9" }}
/>
);
}
hls.js のインストールは npm install hls.js で完了する。TypeScript型定義は @types/hls.js で別途インストールするか、最近のバージョンでは同梱されている。
使い方はシンプルだ。
// app/video/page.tsx
import { HlsPlayer } from "@/components/HlsPlayer";
export default function VideoPage() {
return (
<main>
<h1>サンプル動画</h1>
<HlsPlayer
src="https://cdn.your-site.com/videos/sample/master.m3u8"
poster="https://cdn.your-site.com/videos/sample/thumbnail.jpg"
/>
</main>
);
}
トラブルシューティング
HLS配信でよく踏む問題と対処法をまとめる。
セグメントが取得できない(404 or CORS Error)
- S3バケットのCORSルールが正しく設定されているか確認する
- CloudFrontのレスポンスヘッダーポリシーに
Access-Control-Allow-Originが入っているか確認する - tsファイルのContent-Typeが
video/mp2tになっているか確認する(application/octet-streamだと一部プレイヤーで問題が出る)
再生が途中で止まる(バッファリング)
- セグメントサイズが大きすぎる可能性がある。
-hls_timeを6秒に下げてみる - CDNのキャッシュが効いていない可能性がある。CloudFrontのキャッシュヒット率をメトリクスで確認する
- 低品質ストリームのビットレートが高すぎる可能性がある。360pのCRFを30まで上げてビットレートを下げてみる
Safariで再生できない
- m3u8のContent-Typeが
application/vnd.apple.mpegurlになっているか確認する - S3のリダイレクト設定が邪魔している場合がある。直接URLにアクセスして確認する
hls.jsでMANIFEST_LOAD_ERRORが出る
- マスタープレイリストのURLが正しいか確認する
- CORS設定の
AllowedOriginsに開発環境(http://localhost:3000)が含まれているか確認する(本番と開発で別エントリを追加)
エンコードが遅い
-presetをultrafastに変えると速くなるが品質は落ちる- GPU エンコードを使う場合は
-c:v h264_nvenc(NVIDIA)または-c:v h264_videotoolbox(Mac)に置き換える - 3段階のエンコードを並列で走らせるには
&とwaitを使う
ffmpeg -i input.mp4 -vf "scale=640:360" ... output/360p/index.m3u8 &
ffmpeg -i input.mp4 -vf "scale=1280:720" ... output/720p/index.m3u8 &
ffmpeg -i input.mp4 -vf "scale=1920:1080" ... output/1080p/index.m3u8 &
wait
echo "全エンコード完了"
FFmpegの基本的な使い方については FFmpegの使い方入門 も参考にしてほしい。
まとめ
FFmpegでHLS動画をCDN配信する流れをまとめると次のとおりだ。
ffmpegコマンドで動画をHLSセグメントに変換する(-hls_time 6で6秒セグメント)- ABRが必要なら3段階(360p / 720p / 1080p)でエンコードし、マスタープレイリストを作る
- S3にアップロード。m3u8は
no-cache、tsはmax-age=31536000でキャッシュを使い分ける - CORSを S3とCloudFrontの両方に設定する
- ブラウザでは hls.js(Safari以外)を使って再生する
MP4をそのまま配信するのと比べると、セットアップの手間は確かにかかる。ただ、CDNのキャッシュ効率が上がり、低速回線のユーザーでも途切れなく再生できるようになるメリットは大きい。動画コンテンツを本番に乗せるなら、僕は最初からHLS+CDNの構成で設計しておくことを強くすすめる。