FFmpegによるM3U8ストリーム操作:セグメント分割、結合、トランスコード、ライブ配信の応用
やあ、戻ってきました。最近またストリーミングの沼にハマって、M3U8形式とがっつり向き合っています。FFmpegがどれほど凄いかはもう言うまでもないですが、新しい場面に出くわすたびにドキュメントを必死に漁る羽目になるのは相変わらずです。そこで、自分がよく使ういくつかのパターンをメモ代わりに残しておきます。必要とする人の参考にもなれば幸いです。
まず白状すると、初めて .m3u8 を見たときは何のことかさっぱりわからず、動画を探しているはずが .ts の小さなファイルが大量に出てきて困惑しました。後になって、これが Apple の HLS ライブストリーミングプロトコルであり、要するにインデックスと細切れのセグメントの集まりだと知りました。ただし、もし単に特定の m3u8 リンクの中身を一時的に見たいだけなら、私みたいにわざわざ FFmpeg をインストールしてコマンドラインをゴチャゴチャやる必要はありません。そんな時は M3U8Player(https://m3u8player.link/)というオンラインサイトにURLを放り込めば再生できます。手間いらずで楽ちんです。でも私と同じように、自分で処理・ダウンロード・トランスコード、あるいは配信したいというなら、その先を読んでください。
まずセグメント分割
手持ちの動画を HLS ストリームに分割するのは日常的な操作です。私はたいていこんな感じで書きます。
ffmpeg -i input.mp4 -c copy -hls_time 10 -hls_list_size 0 -hls_segment_filename "seg%d.ts" playlist.m3u8
パラメータをいくつか説明すると、-hls_time 10 は10秒ごとにセグメントを区切る指定、-hls_list_size 0 はすべてのセグメントを保持する(ライブ配信なら通常は数字を指定して最新数個のみ残す)、-hls_segment_filename でファイル名をカスタマイズ(デフォルトだと output%03d.ts みたいになる)。-c copy で再エンコードなし、処理は爆速です。ただし元動画のオーディオストリームが不適切な場合、単純コピーだと音が出ないことがあるので、-bsf:a aac_adtstoasc を追加することがありますが、普通のmp4のセグメント分割では不要です。
以前、長い動画をセグメント分割した際、出力ディレクトリを変更し忘れて、ファイル名が前回のものをそのまま上書きしてしまったことがありました。痛い経験です。セグメント分割のたびに新しいフォルダを作ってそこに出力することをおすすめします。
結合とダウンロード
最近のオンライン動画の多くは m3u8 アドレスを直接提供しています。それを mp4 としてダウンロードするのはこんなに簡単です。
ffmpeg -i "https://...playlist.m3u8" -c copy -bsf:a aac_adtstoasc output.mp4
一行で完了、デフォルトで最もビットレートの高いストリームが選ばれます。ただしホットリンク防止対策がある場合は -user_agent "Mozilla/5.0" を追加しないと 403 エラーが続出します。以前、デフォルトのユーザーエージェントがブロックされているのに気づかず半日ほど原因究明に費やしましたが、UA を変更したら一瞬で解決しました。
もしマルチビットレートのマスタープレイリスト(内部に複数の解像度のストリームが含まれている)を入手した場合、上のコマンドだと最も高いものだけが選ばれます。特定のバージョンを選択したいなら、まず ffmpeg -i "master.m3u8" で情報を表示させます。すると #EXT-X-STREAM-INF 以下にある各ストリームのインデックス番号が分かります。その後 -map 0:v:0 -map 0:a:0? で取得したい動画と音声のストリームを指定し、最後に単一ファイルにまとめます。
トランスコードの応用
元の m3u8 の解像度が高すぎたり、形式が合わなかったりする場合、再エンコードして新しい m3u8 に出力したいことがあります。
ffmpeg -i "input.m3u8" -vf scale=1280:720 -c:v libx264 -crf 23 -c:a aac -hls_time 10 out.m3u8
入力として m3u8 リンクを直接指定すれば、FFmpeg がダウンロードしながら処理します。ただしこの方法は速度が遅く、一時的なディスク使用量も大きいので、一度一時ファイルに結合してからセグメント分割する方が良いでしょう。しかし一度で完結するのは便利です。
アダプティブなマルチビットレストリームを構築するには、通常は元の動画をあらかじめ異なる解像度・ビットレートにトランスコードし、各バージョンを個別にセグメント分割した後、マスタープレイリストを手書きする必要があります。FFmpeg 単体ではアダプティブストリームを一度に生成できないので、スクリプトで制御する必要があります。
トランスコードで落とし穴になりがちなのが、異なるビットレート間でセグメントの切れ目をキーフレームに揃えることです。そうしないと、解像度切り替え時に画面が一瞬乱れることがあります。私は -g 48 を付けて、48フレームごとにキーフレームを配置(24fpsなら約2秒間隔)し、同じ長さの -hls_time 2 を組み合わせて、セグメントの切れ目をきっちり揃えています。
ライブ配信の応用
これは純粋にテストで遊んでいるものです。FFmpeg だけで HLS をローカルディレクトリに配信します。
ffmpeg -re -i input.mp4 -c copy -f hls -hls_time 4 -hls_list_size 10 -hls_flags delete_segments live.m3u8
-re は元のフレームレートで読み込むオプション(これがないと高速で処理が進んでしまう)。こうするとカレントディレクトリに更新され続ける m3u8 と ts ファイルが生成され、さらに nginx でそのディレクトリを公開すれば簡単なライブ配信ソースになります。小規模なテストに適しています。
より一般的なのは、まず RTMP でサーバー(nginx-rtmp-module など)にプッシュし、サーバー側で HLS に変換する方法です。ただしサーバーを立てたくない場合でも、FFmpeg で直接クラウドプラットフォームにプッシュ配信できます。パラメータはほぼ同じです。
以前ローカルでプッシュ配信を試したとき、プレイヤーが頻繁にバッファリングしていたのですが、-hls_time を2秒に短縮し、-hls_list_size を3つだけにしたところ、遅延が明らかに改善しました。ただしセグメントを短くするとサーバー負荷が高くなるので、トレードオフです。
もう一つの落とし穴はタイムスタンプです。同じファイルをループで配信する場合(例えば -stream_loop -1 を追加)、タイムスタンプがリセットされて HLS ストリームが途切れることがあります。対策として -fflags +genpts を追加するか、-re を正しいタイムベースと組み合わせて使います。この問題に2時間も費やしてしまいました。
実は今のライブ配信のほとんどは OBS で直接行っていますが、自動化スクリプトを組むときはやはり FFmpeg に頼らざるを得ません。仕方ないですね。
さて、M3U8 と FFmpeg に関するよく使う操作は以上です。それぞれほんの少数のコアパラメータで成り立っていますが、組み合わせは無限大です。そういえば、生成した m3u8 が正しいかどうかさっと確認したいとき、私はたまに前述の M3U8Player サイトにURLを放り込んで一目でチェックしています。いちいちローカルプレイヤーを立ち上げる手間が省けます。とにかく何度も試せば勝手がわかってきます。新しいハマりどころがあれば、ぜひ情報交換しましょう。