はじめに:この記事で得られること
結論から書きます。docker compose run --rm --entrypoint bash ffmpeg ./convert.sh の1コマンドで、MP4ファイルをHLS形式(.m3u8 + .tsセグメント群)へ変換できます。ffmpegのホストへのインストールは不要で、macOS・Linux・Windowsどこでも同じ手順が使えます。
自分自身が動画配信機能の実装で試行錯誤した内容を、備忘録も兼ねてまとめています。実際に手元で動かして確認した手順なので、そのまま追えば同じ結果が得られるはずです。
この記事でできること:
- Docker環境だけでffmpegを使ったHLS変換を実行できる
- 再エンコードなし(コーデックコピー)で変換時間を大幅に短縮できる
- 変換後のM3U8ファイルを動画プレイヤーで再生できる状態へ
この記事が対象外の人:
ffmpegをホストマシンに直接インストールして使いたい方、またはAWSやGCPのマネージドトランスコードサービスを探している方には向いていません。
対象読者
- 動画配信機能を初めて実装するWeb開発者
- Dockerは日常的に使えるが、ffmpegやHLSの経験はほぼゼロの方
- 到達ゴール:
data/output.m3u8を生成し、HLSプレイヤーで再生できる状態にする
HLS(HTTP Live Streaming)とは
HLS(HTTP Live Streaming)とは、Apple が策定した動画配信プロトコルです。
1本の動画を一定時間ごとに分割した .ts セグメントファイルと、その再生順序を記述した .m3u8 プレイリストファイルの組み合わせで構成されます。
実は仕組み自体はシンプルで、HTTPサーバーがあれば配信できるので、CDNとの相性もよく、Webでの動画配信では定番の選択肢になっています。
補足: 本記事ではセグメント形式として従来の TS(MPEG-2 Transport Stream)を使用しています。近年は fMP4(Fragmented MP4 /
.m4s)を採用するケースも増えていますが、互換性の広さと設定のシンプルさから、初めての HLS 構築には TS が扱いやすい選択肢です。
前提条件
| 項目 | バージョン・条件 |
|---|---|
| OS | macOS / Ubuntu 22.04 / Windows 11 (WSL2) で動作確認済み |
| Docker | 25.0 以上 |
| Docker Compose | v2 系(docker compose コマンド) |
| 変換対象 | MP4コンテナ、H.264映像 + AAC音声のファイル |
注意: 映像コーデックが H.265(HEVC)の場合、ブラウザによっては再生できないことがあります。その場合は再エンコードオプションを使う必要があります(後述のFAQを参照してください)。
プロジェクト構成
ffmpeg-mp4/
├── Dockerfile
├── docker-compose.yml
├── convert.sh
└── data/ ← 入力ファイルと出力ファイルを置くディレクトリ
└── input.mp4
ファイル数は少ないですが、それぞれの役割がはっきり分かれているのがポイントです。
補足: ファイル名は
Dockerfile(先頭大文字)が広く使われている慣習で、Docker のデフォルト検索対象でもあります。dockerfile(小文字)でもビルドは通りますが、チームで運用する場合はDockerfileに統一しておくと混乱を防げます。
各ファイルの役割と内容
Dockerfile
# ffmpegを含むUbuntuベースのイメージを使用
FROM jrottenberg/ffmpeg
# 作業ディレクトリの設定
WORKDIR /workspace
# 変換スクリプトをコンテナにコピー
COPY convert.sh /workspace/convert.sh
# スクリプト実行権限の付与
RUN chmod +x /workspace/convert.sh
jrottenberg/ffmpeg は FFmpeg 入りの定番 Docker イメージです。ホストマシンに ffmpeg をインストールせずに済むため、バージョン違いによるトラブルを気にしなくてよくなります。本番やチームでの運用では、FROM jrottenberg/ffmpeg:4.4-ubuntu のようにタグを固定しておくと、意図しないバージョンアップを防げます。
docker-compose.yml
services:
ffmpeg:
build: .
volumes:
- ./data:/data
./data をコンテナ内の /data にマウントしています。Dockerfile の WORKDIR(/workspace)とは別のパスにしているのは、COPY したスクリプトがマウントで上書きされるのを防ぐためです。変換後のファイルは自動的にホスト側の data/ に書き出されるので、コンテナの中を覗く必要はありません。
補足:
version: '3'は Docker Compose v2 系では非推奨(無視される)ため、記載を省略しています。
convert.sh
#!/bin/bash
# MP4ファイルをHLS(M3U8)形式に変換
ffmpeg -i /data/input.mp4 \
-c copy \
-bsf:v h264_mp4toannexb \
-hls_time 10 \
-hls_list_size 0 \
-hls_flags independent_segments \
-hls_playlist_type vod \
-f hls /data/output.m3u8
入出力ファイルのパスは、docker-compose.yml でマウントした /data ディレクトリを基準にしています。なお、スクリプトはコンテナ内の bash で実行されるため、ホスト側に bash や ffmpeg をインストールする必要はありません。
各オプションの意味を整理しておきます:
| オプション | 意味 |
|---|---|
-c copy | 映像・音声を再エンコードせずそのままコピー(高速・劣化なし) |
-bsf:v h264_mp4toannexb | MP4コンテナのH.264をTS互換のAnnex B形式に変換するビットストリームフィルタ。コピー時にこれがないと入力によっては再生に失敗する |
-hls_time 10 | 1セグメントあたり約10秒で分割 |
-hls_list_size 0 | m3u8プレイリストにすべてのセグメントを記載 |
-hls_flags independent_segments | 全セグメントがキーフレーム開始であることが保証できる場合に #EXT-X-INDEPENDENT-SEGMENTS を付与する。コピー運用(-c copy)では入力の GOP 次第で保証できない場合がある |
-hls_playlist_type vod | プレイリストに #EXT-X-PLAYLIST-TYPE:VOD を付与し、全セグメントが揃った録画済みコンテンツであることを明示する |
-f hls | 出力フォーマットをHLSに指定 |
補足: コピー運用(
-c copy)では入力動画のキーフレーム配置がそのまま使われるため、independent_segmentsの前提を満たせない場合があります。確実に独立セグメントにしたい場合は、再エンコードしてキーフレーム間隔(GOP)をセグメント長に合わせます。-gはフレーム数指定なので、fps × セグメント秒数で計算します(例:30fps で 10 秒なら-c:v libx264 -g 300 -keyint_min 300)。
手順:ゼロからHLS変換を実行する
1. リポジトリをクローンする
git clone https://github.com/yourname/ffmpeg-mp4.git
cd ffmpeg-mp4
2. 変換したいMP4を配置する
cp /path/to/your/video.mp4 ./data/input.mp4
3. Dockerイメージをビルドする
docker compose build
初回はイメージのダウンロードが入るため、数分かかることがあります。ここだけ少し待ちましょう。
4. 変換を実行する
前回の変換結果が残っている場合は、先に削除しておきます。出力ファイル名が固定のため、残っていると混在する原因になります。
rm -f ./data/output.m3u8 ./data/output*.ts
変換を実行します。
docker compose run --rm --entrypoint bash ffmpeg ./convert.sh
docker compose runは--entrypointでサービスの ENTRYPOINT を一時的に上書きできます(Docker公式リファレンス)。ここでは bash 経由でスクリプトを確実に実行するために指定しています。--rmは実行後にコンテナを自動削除するオプションで、不要なコンテナが残らないので習慣にしておくと便利です。
5. 出力ファイルを確認する
ls ./data/
# 出力例:
# input.mp4
# output.m3u8
# output0.ts
# output1.ts
# output2.ts
# ...
output.m3u8 と連番の .ts ファイルが生成されていれば成功です。
6. 再生確認する
まず、data/ ディレクトリに移動してから簡易HTTPサーバーを起動します。サーバーはカレントディレクトリをドキュメントルートとして配信するので、output.m3u8 や test.html がある data/ で実行する必要があります。
# プロジェクトルートから実行する場合
cd data && python3 -m http.server 8080
Safari の場合
Safari は HLS をネイティブ再生できるので、URLバーに以下を直接入力するだけで再生できます。
http://localhost:8080/output.m3u8
再生が始まれば変換成功です。
Chrome / Firefox / Edge の場合
これらのブラウザは基本的に HLS をネイティブ再生できないため、hls.js を使ったテストページをローカルに用意するのが確実です。以下の内容を data/test.html として保存してください。
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>HLS Test</title></head>
<body>
<video id="video" controls width="720"></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.0"></script>
<script>
const video = document.getElementById('video');
if (Hls.isSupported()) {
const hls = new Hls();
hls.loadSource('output.m3u8');
hls.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = 'output.m3u8';
}
</script>
</body>
</html>
同じサーバーから配信しているので、http://localhost:8080/test.html を開けば再生されます。
なぜ外部デモページではなくローカルのテストページを使うのか: hls.js のデモページ は HTTPS で配信されているため、
http://localhostの動画を読み込むと Mixed Content としてブロックされます。また、python3 -m http.serverはデフォルトで CORS ヘッダー(Access-Control-Allow-Origin)を返さないため、クロスオリジンの fetch も失敗します。同一オリジンのローカルテストページを使えば、どちらの問題も回避できます。
検証記録(実測)
筆者環境(macOS M1 Pro / 16GB)での実測値を載せておきます。計測は time docker compose run ... でリアルタイム経過(real)を確認しています。
| ファイル | サイズ | 変換時間(-c copy) |
|---|---|---|
| 5分のMP4(H.264/AAC) | 約180 MB | 約4秒 |
| 30分のMP4(H.264/AAC) | 約1.1 GB | 約22秒 |
再エンコード(-c:v libx264)と比べると約10〜30倍の速度差が出ました。コーデックコピーが使える場面では、積極的に活用してみてください。
失敗例と回避策
失敗1:No such file or directory: /data/input.mp4
原因: コンテナ内の /data(= ホスト側の data/)にファイルが存在しないか、ファイル名が input.mp4 と一致していない。
回避策:
ls ./data/
# input.mp4 があることを確認してから実行する
convert.sh 内のファイル名を変更するか、ファイルを input.mp4 という名前でコピーして配置してください。
失敗2:Muxer hls does not support non seekable output
原因: 出力先をパイプや標準出力にしようとしている場合に発生します。
回避策: 出力はファイルパスで指定してください。output.m3u8 のようにファイル名を直接書くだけで大丈夫です。
失敗3:.ts ファイルは生成されるが output.m3u8 が空
原因: スクリプトが途中で中断されたか、書き込み権限がない。
回避策:
# dataディレクトリの権限を確認
ls -la ./data/
# 書き込み権限がない場合
chmod 755 ./data/
失敗4:変換後にブラウザで再生できない
原因: コーデックが H.264/AAC 以外(例:H.265、VP9、Opus)の場合、主要ブラウザでは直接再生できません。
回避策: convert.sh のオプションを変更して、H.264/AACに再エンコードしてください。
ffmpeg -i /data/input.mp4 \
-c:v libx264 -c:a aac \
-hls_time 10 \
-hls_list_size 0 \
-hls_flags independent_segments \
-hls_playlist_type vod \
-f hls /data/output.m3u8
再エンコードは変換時間が大幅に増加します(上記実測で30分動画が10〜30分かかる場合があります)。再エンコード時は
-bsf:v h264_mp4toannexbは不要です(エンコーダが適切な形式で出力します)。
失敗5:permission denied でスクリプトが実行できない
原因: convert.sh に実行権限がない。
回避策:
chmod +x convert.sh
Dockerfileの RUN chmod +x はコンテナ内での権限付与なので、ホスト側の権限は別途設定が必要です。この点は少しハマりやすいので注意してください。
よくある質問(FAQ)
Q1. Docker を使わずに直接 ffmpeg コマンドで変換できますか?
はい、できます。convert.sh のコマンドをそのまま ffmpeg がインストールされた環境で実行すれば動きます。/data/input.mp4 と /data/output.m3u8 のパスをホスト側の絶対パスに置き換えてください。なお、スクリプトのシバン(#!/bin/bash)の通り bash が必要です。macOS や主要な Linux ディストリビューションでは標準搭載されていますが、Alpine などの軽量環境では別途インストールが必要な場合があります。
Q2. セグメントの長さ(10秒)を変えるにはどうすればいいですか?
convert.sh 内の -hls_time 10 の数値を変更するだけです。配信目的では2〜6秒が一般的ですが、短くするとファイル数が増える点は覚えておいてください。
Q3. 複数のMP4ファイルを一括変換できますか?
convert.sh を以下のように書き換えることで対応できます。
#!/bin/bash
cd /data
for f in *.mp4; do
base="${f%.mp4}"
ffmpeg -i "$f" \
-c copy \
-bsf:v h264_mp4toannexb \
-hls_time 10 \
-hls_list_size 0 \
-hls_flags independent_segments \
-hls_playlist_type vod \
-f hls "${base}.m3u8"
done
Q4. 変換後の .ts ファイルはどこに置けばいいですか?
.m3u8 と同じディレクトリに置く必要があります。CDNやS3に配置する場合も、M3U8と.tsファイルのパス関係を維持してください。
なお、複数の動画を処理する場合やS3に配置する場合は、デフォルトの output0.ts のような連番名だとファイル名が衝突しやすくなります。-hls_segment_filename オプションで命名パターンをカスタマイズできます:
ffmpeg -i /data/input.mp4 \
-c copy \
-bsf:v h264_mp4toannexb \
-hls_time 10 \
-hls_list_size 0 \
-hls_flags independent_segments \
-hls_playlist_type vod \
-hls_segment_filename '/data/video1_seg%03d.ts' \
-f hls /data/video1.m3u8
この例では video1_seg000.ts, video1_seg001.ts, … という名前で生成されます。
Q5. HLSの .m3u8 はどのプレイヤーで再生できますか?
- macOS/iOS の Safari はネイティブ対応
- Chrome/Firefox/Edge は
hls.jsライブラリを使うことで対応可能 - ffplay(ffmpegに同梱)でもローカル再生できます:
ffplay output.m3u8
Q6. 変換中にコンテナを止めるとどうなりますか?
生成途中の .ts ファイルと不完全な .m3u8 が残ります。data/ 内の output* ファイルを削除してから再実行してください。
Q7. jrottenberg/ffmpeg イメージのバージョンを固定するには?
Dockerfile の FROM を以下のように変更します。
FROM jrottenberg/ffmpeg:4.4-ubuntu
バージョン一覧は Docker Hub の jrottenberg/ffmpeg で確認できます。本番環境ではバージョンを固定しておくことをお勧めします。
まとめ
- Docker + ffmpeg で MP4 を HLS(M3U8)へ変換する環境を、ホストへのインストールなしで構築できる
-c copyによる再エンコードなし変換は、H.264/AAC の MP4 に対して有効で、変換時間を大幅に短縮できる- セグメント長、コーデック変換、一括処理はオプション変更で柔軟に対応可能
- 変換後は
.m3u8と.tsファイルを同じディレクトリに置いて HTTP 配信するだけで再生できる
次の行動
data/input.mp4を用意してdocker compose run --rm --entrypoint bash ffmpeg ./convert.shを実行する
→ まず手を動かして動かしてみましょう。理解は後からついてきます。- セグメント長を変えて体感速度の違いを確認する
→-hls_timeを 2〜10 秒の間で変えて再生してみてください。数値ひとつで体験がかなり変わります。 - 生成した
output.m3u8を静的ホスティング(S3 + CloudFront など)に置いて配信してみる
→ 実際の配信環境への応用に挑戦してみてください。