DockerでMP4をHLS(M3U8)に変換する方法|再エンコードなしで高速に配信形式へ変換

はじめに:この記事で得られること

結論から書きます。
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 が扱いやすい選択肢です。

前提条件

項目バージョン・条件
OSmacOS / Ubuntu 22.04 / Windows 11 (WSL2) で動作確認済み
Docker25.0 以上
Docker Composev2 系(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_mp4toannexbMP4コンテナのH.264をTS互換のAnnex B形式に変換するビットストリームフィルタ。コピー時にこれがないと入力によっては再生に失敗する
-hls_time 101セグメントあたり約10秒で分割
-hls_list_size 0m3u8プレイリストにすべてのセグメントを記載
-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.m3u8test.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 イメージのバージョンを固定するには?

DockerfileFROM を以下のように変更します。

FROM jrottenberg/ffmpeg:4.4-ubuntu

バージョン一覧は Docker Hub の jrottenberg/ffmpeg で確認できます。本番環境ではバージョンを固定しておくことをお勧めします。


まとめ

  • Docker + ffmpeg で MP4 を HLS(M3U8)へ変換する環境を、ホストへのインストールなしで構築できる
  • -c copy による再エンコードなし変換は、H.264/AAC の MP4 に対して有効で、変換時間を大幅に短縮できる
  • セグメント長、コーデック変換、一括処理はオプション変更で柔軟に対応可能
  • 変換後は .m3u8.ts ファイルを同じディレクトリに置いて HTTP 配信するだけで再生できる

次の行動

  1. data/input.mp4 を用意して docker compose run --rm --entrypoint bash ffmpeg ./convert.sh を実行する
    → まず手を動かして動かしてみましょう。理解は後からついてきます。
  2. セグメント長を変えて体感速度の違いを確認する
    -hls_time を 2〜10 秒の間で変えて再生してみてください。数値ひとつで体験がかなり変わります。
  3. 生成した output.m3u8 を静的ホスティング(S3 + CloudFront など)に置いて配信してみる
    → 実際の配信環境への応用に挑戦してみてください。

参考情報

天前智矢

天前智矢

企画開発部 マネージャー 愛犬ファーストのエンジニア

関連記事