ffmpeg で Apple HTTP Live Streaming(HLS)形式に出力する方法。各セグメントの動画ファイルは .ts(ffmpeg 3.4 からは .m4s(fragmented MP4, fmp4)も可能)でプレイリストとなるマニフェストは .m3u8 で出力される。また segment muxer のオプションを使うことでより詳細な設定ができる。

詳細な分割設定ができる Segment

MPEG-DASH は以下より
ffmpeg で MPEG-DASH を扱う

マニフェスト作成の参考になる::HTTP Live Streaming Examples – Apple Developer
HTTP Live Streaming(HLS)- Apple Developer
(PDF)Apple の HTTPライブストリーミングの概要(日本語): web.archive.org
HLS のドキュメントを簡潔にしたもの:General Authoring Requirements
HLS の公式ドキュメント:HTTP Live Streaming
HLS 2nd Edition の草案ドキュメント:HTTP Live Streaming 2nd Edition

基本コマンド

動画ファイルからライブ配信する場合には -re をつけて、VOD 配信には -re はつけない。.mp4 ファイルから .ts ファイルで出力するときには -bsf:v h264_mp4toannexb をつける。

ffmpeg -i input -c:v libx264 -crf 23 -c:a aac -b:a 128k -f hls output.m3u8

.mp4 を HLS 形式にコーデックコピーする例。(-bsf:v h264_mp4toannexb を付けないとエラーで処理が止まっていたが止まらなくなった)詳しくはここ-hls_list_size 0 ですべてのセグメントをマニフェストに書き込む。
ffmpeg -i input.mp4 -bsf:v h264_mp4toannexb -c copy -f hls -hls_list_size 0 output.m3u8
ffmpeg -i input.mp4 -c copy -f hls -hls_list_size 0 output.m3u8

.mp4 から暗号化とセグメントの時間指定(60秒)をする例(file.keyinfo は後述)。
ffmpeg -i input.mp4 -bsf:v h264_mp4toannexb -c:v libx264 -c:a aac -f hls -hls_key_info_file file.keyinfo -hls_list_size 0 -hls_time 60 output.m3u8

H.265(HEVC)の fmp4 での HLS 出力コマンド例。
wiki: Encode / H.265 – trac.ffmpeg.org
movenc: allow alternative hvc1 h.265 codec tag
ffmpeg -i input -c:v libx265 -c:a aac -tag:v hvc1 -f hls -hls_list_size 0 -hls_segment_type fmp4 -movflags +faststart output.m3u8

公式ドキュメント

オプション

ヘルプコマンド ffmpeg -h muxer=hls

  • -start_number [int64]
    セグメントの開始番号。開始番号を変更するのであり、映像の開始をずらすのではない。-hls_flags +second_level_segment_index を使うときにつける
    範囲:0 から I64_MAX まで
    既定値:0
  • -hls_time [float]
    各セグメントの動画時間。指定した時間が来たら次のキーフレームで分割される。任意の時間で分割する場合にはGOPのフレーム間隔(group of picture size)を決めるエンコードオプションの -g(keyint)、-keyint_min を併用する。-sc_threshold 0 をつけるとキーフレームがシーンチェンジにで入らなくなり意図したセグメント分割になる
    範囲:0 から FLT_MAX まで
    既定値:2
  • -hls_init_time[float]
    最初のセグメントの動画時間。0 だと最初のキーフレームが来たら分割する。挙動は -hls_time と同じ
    既定値:0
  • -hls_list_size [int]
    マニフェストに記載されるセグメント数。0 はすべて
    範囲:0 から INT_MAX まで
    既定値:5
  • -hls_delete_threshold [int]
    ディスクから削除せずに残すセグメント数。-hls_flags +delete_segments と併用。詳しくは後述
    範囲:1 から INT_MAX まで
    既定値:1
  • -hls_ts_options [string]
    Set output format options using a :-separated list of key=value parameters. Values containing : special characters must be escaped.
  • -hls_vtt_options [string]
    set hls vtt list of options for the container format used for hls
  • -hls_wrap [int]
    セグメント数の上限を指定する。上限に達すると最初のセグメントから上書きするので容量の削減になる。しかし 廃止予定 なので代わりに -hls_list_size、-hls_flags +delete_segments を併用する
    範囲:0 から INT_MAX まで
    既定値:0
  • -hls_allow_cache [int]
    キャッシュを許可するかどうかを明示的に指定する
    1 は許可するかもしれない、0 は許可してはならない
    範囲:-1, 0, 1
    既定値:-1
  • -hls_base_url [string]
    各セグメントに URL をつける。各セグメントに絶対パスを指定するときに使う
    既定値:なし
  • -hls_segment_filename [string]
    -hls_flags single_file を使うとそのファイル名で出力し、それ以外は各セグメント番号を含む文字列形式のファイル名で出力する。字幕のセグメント名は指定できない。無指定だとマニフェストのファイル名に連番が振られる。Windows 環境では drawtext フィルタと同じように %s が使えず、組み合わせによってもエラーになる
    Windows の ffmpeg で生放送する方法 : drawtext(テキスト描写)
    例:file%03d.ts(file000.ts, file001.ts, file002.ts, …)
    既定値:なし
  • -hls_segment_size [int]
    1セグメント当たりのバイト単位の最大ファイルサイズ値。マニフェストはバイトレンジ(#EXT-X-BYTERANGE)分割になり、マニフェストのバージョンは 4 になる
    範囲:0 から INT_MAX まで
    既定値:0(無制限)
  • -hls_key_info_file [string]
    セグメントの暗号化で使うファイルを指定する。key には 16バイトのバイナリ、または16文字の16進数の文字列を、IV に32文字の16進数の文字列を指定する。書式は後述
    既定値:なし
  • -hls_enc [boolean]
    有効にするとオプションで指定したキーファイルの URI と IV をマニフェストに書き込む。-hls_enc_key-hls_enc_iv を指定する。無効にすると暗号化しない。-hls_key_info_file と併用すると -hls_key_info_file が優先される
    規定値:0
  • -hls_enc_key [string]
    指定した16文字の16進数の文字列からキーファイルを作成する
  • -hls_enc_key_url [string]
    -hls_enc 1 で暗号化したときに既定値ではマニフェストと同じ場所に「マニフェスト名.m3u8.key」が出力されるが、パスとキーファイル名を指定するとそれがマニフェストに記載される。マニフェストの保存先をカレント以外で出力したときにキーファイルが参照できないときはこのオプションでファイル名を指定する
    例:-hls_enc_key_url “L:\hoge/\piyo.key”
    例:-hls_enc_key_url “hoge/\piyo.key”
  • -hls_enc_iv [string]
    指定した32文字の16進数の文字列からIV を作成する。無指定だと IV=0x00000000000000000000000000000000 がマニフェストに記載される
  • -hls_subtitle_path [string]
    字幕マニフェストのファイル名とパス指定。無指定だと映像と音声のセグメント、マニフェストと同じ場所になる
    既定値:なし
  • -hls_segment_type [int]
    セグメントのファイルタイプの指定。詳しくはここ1, fmp4 にするとマニフェストのバージョンは 7 になる
    範囲:0(mpegts), 1(fmp4)
    既定値:0
  • -hls_fmp4_init_filename [string]
    #EXT-X-MAP タグの mp4 のファイル名の指定。出力ファイルと同じ場所に出力される
    既定値:init.mp4
  • -hls_flags[flags]
    フラグの指定。+で複数のオプションをつなげられる

    • single_file
      #EXT-X-BYTERANGE タグを追加する。セグメントを分割せずに1つのファイルで出力する。マニフェストのバージョンは 4 になる。いわゆるバイトレンジ
    • temp_file
      セグメントを .temp で一時出力し、1セグメント分の出力が終わるとリネームして通常のセグメント名になる
    • delete_segments
      マニフェストに記載されていないセグメントを順番に削除していく。-hls_list_size 0 を併用すると効果が無い
    • round_durations
      マニフェストに記載されている1秒未満の時間を四捨五入する
    • discont_start
      #EXT-X-DISCONTINUITY タグを追加する。セグメントが不連続してるフラグ。配信が止まったとき仕切り直すときにつける。エンコードが正常終了すると #EXT-X-DISCONTINUITY タグは削除される
    • omit_endlist
      最終行に追記される #EXT-X-ENDLIST タグを省略する
    • split_by_time
      時間指定で分割するのでキーフレーム以外で分割されることもある。特定のプレイヤーで挙動がよくなるかもしれないが一般的にはよくならない。-hls_time と併用する
    • append_list
      最終行に記載されている #EXT-X-ENDLIST タグを削除し、新しいセグメントを追記し再び最後に #EXT-X-ENDLIST タグを追加する
    • program_date_time
      #EXT-X-PROGRAM-DATE-TIME タグを追加する
    • second_level_segment_index
      -use_localtime または -strftime を使ったときにセグメントのファイル名にインデックス、つまり連番 %d を含める
    • second_level_segment_duration
      -use_localtime または -strftime を使ったときにセグメントのファイル名に動画時間、つまり %t を含める。単独で使うとセグメント時間が変更にならないと同じファイル名になるので他のフラグを併用する
    • second_level_segment_size
      -use_localtime または -strftime を使ったときにセグメントのファイル名にファイルサイズ、つまり %s を含める
    • periodic_rekey
      キーファイルが動的に書き換えられるのに対応して再読み込みする。現時点では ffmpeg 単体で -hls_key_info_file をセグメントの途中で書き換えることは出来ない(はず)
    • independent_segments
      #EXT-X-INDEPENDENT-SEGMENTS タグを追加する。それぞれのセグメントが独立しデコードするのに他のセグメントを参照しない。マニフェストのバージョンは 6 になる
    • iframes_only
      #EXT-X-I-FRAMES-ONLY タグを追加する。Iフレームだけのセグメントを明示するが実際に使うにはエンコード設定もそれに直す必要がある。詳しくは後述
  • -use_localtime[boolean]
    セグメントのファイル名に strftime を割り当てる。廃止予定なので -strftime を使う。-use_localtime_mkdir、-hls_segment_filename を併用する
    既定値:0
  • -strftime[boolean]
    セグメントのファイル名に strftime を割り当てる。-use_localtime の代わりに使う。-strftime_mkdir、-hls_segment_filename で併用する
    例:-strftime 1 -hls_segment_filename %Y%m%d%H%M%S.ts(20160420231015.ts)
    既定値:0
  • -use_localtime_mkdir[boolean]
    セグメントの出力フォルダ名の指定に strftime を割り当てる。廃止予定なので -strftime_mkdir を使う。-use_localtime、-hls_segment_filename で併用する。フォルダがなければつくる
    既定値:0
  • -strftime_mkdir[boolean]
    セグメントの出力フォルダ名の指定に strftime を割り当てる。-use_localtime_mkdir の代わりに使う。-strftime, -hls_segment_filename を併用する。フォルダがなければつくる
    例:-strftime 1 -strftime_mkdir 1 -hls_segment_filename “%Y%m%d/%H%M%S.ts”(20160420/231015.ts)
    そのほかに、%H%M%S、時間分秒も使える
    既定値:0
  • -hls_playlist_type[int]
    • 0:何もつけない。既定値
    • 1, event
      #EXT-X-PLAYLIST-TYPE:EVENT タグを追加し、-hls_list_size 0(マニフェストにすべてのセグメント名を書き込む)も強制される
    • 2, vod
      #EXT-X-PLAYLIST-TYPE:VOD タグを追加し、-hls_list_size 0 も強制される
  • -method[string]
    指定した HTTP メソッドでセグメントを作る。これを使うには HTTP サーバがそのメソッドに対応していなければならない
    例:HTTP PUT メソッドでセグメントをアップロードし、refresh 時間毎に m3u8 ファイルが同じメソッドで更新される
    既定値:PUT
    ffmpeg -re -i in.ts -f hls -method PUT http://example.com/live/out.m3u8
  • -hls_start_number_source [int] 各セグメントのファイル名の様式
    • 0, generic:各セグメントのファイル名に連番が付く。既定値
    • 1, epoch:各セグメントのファイル名に unix time が付く(ライブ配信向け)
    • 2, datetime:各セグメントのファイル名に年月日と時間が付く(ライブ配信向け)
  • -http_user_agent[string]
    HTTP ヘッダの User-Agent フィールドを優先させる
    既定値:なし
  • -var_stream_map[string]
    マニフェストにどの映像や音声を記載するのかを指定する
    1入力の映像と音声と、2入力の映像と音声をそれぞれのマニフェストで出力する例。スペースを空けると別マニフェストになる
    例:”v:0,a:0 v:1,a:1″
    既定値:1入力の映像と音声
  • -cc_stream_map[string]
    Closed captions をマニフェストに記載するのかを指定する
    既定値:無指定(指定しないと出力しない、未確認)
  • -master_pl_name[string]
    複数のマニフェストを参照するマスタープレイリストのファイル名を指定する。出力パスは出力ファイルと同じ場所になるので、同じように相対パスを指定するとエラーになる
    既定値:無指定(指定しないと出力しない)
  • -master_pl_publish_rate[int]
    最初の何セグメントを出力したときにマスタープレイリストを出力するか
    既定値:0
    範囲:0 から INT_MAX まで
  • -http_persistent[boolean]
    HTTP 接続を継続する
    既定値:0
  • -timeout[duration]
    I/O 処理のタイムアウト時間を指定する
    既定値:-0.000001
  • -ignore_io_errors[boolean]
    ネットワーク越しの長時間配信で IO エラーが出たときに無視する
    既定値:0
  • -headers[string]
    既定値のヘッダから書き換える

エンコード設定

うまく配信するには GOP サイズであったり、映像と音声の同期問題であったり、遅延だったり色々と解決しなければならない問題も多い。

GOPサイズなら -g と、オープンGOP、クローズドGOP の設定。通常は圧縮率の高いオープンGOP、つまり設定変更しない。
-flag +cgop、-flag -cgopCompressor 4 ユーザーズマニュアル: MPEG-2 に関する参考情報

シーンチェンジにキーフレームを入れずにキーフレーム間隔を固定するなら、-sc_threshold 0ffmpeg Documentation。実際に運用するなら -g をフレームレートのN倍でN*fpsフレームごとにキーフレームを入れて、シーンチェンジにキーフレームを入れさせない -sc_threshold 0 を併用する。何フレーム間隔にするかは1セグメント時間の間に1キーフレームを最低限入れる。

つまり意図した通りにセグメント分割するには -hls_time で分割時間を指定するだけではなく、映像を再エンコードし -g、-sc_threshold を併用しなければならない。

映像と音声の同期問題は固定フレームの指定 -r と、-vsync cfrffmpeg でのフレームレート設定の違い

遅延はエンコード設定とオーバーヘッド、マニフェストにいくつのセグメントを記載する -hls_list_size と、セグメント時間 -hls_time が大きく依存する。また segment muxer にあるキーフレームで分割しない -break_non_keyframes を使う方法もある。

またバッファの調整では、エンコーダ側(配信側)の設定とデコーダ側(視聴者側)の設定も考える。

Raspberry PIとffmpegとWebカメラ、USBマイクを利用してHLSストリーミング&保存 – Qiita
ffmpeg – ffmpegのsegment_timeオプションが効かないのか – スタック・オーバーフロー

再生確認

「mp4をFFmpegでhlsに変換し、ストリーミング再生」(記事削除:https://qiita.com/korsmic/items/fac1d737f48aabb1294f#%E5%AE%9F%E8%A3%85%E3%82%B3%E3%83%BC%E3%83%89)を参考にし、hls.js, plyr.js を使って再生する。

コマンド例(video フォルダに出力)
ffmpeg -re -i input.mp4 -bsf:v h264_mp4toannexb -c copy -f hls -hls_list_size 0 -flags +global_header video/video.m3u8

セグメントファイル名の設定

設定できるのは任意の桁数で指定できる連番 %d、セグメントのマイクロ時間 %t、セグメントのファイルサイズ %s に、マニフェスト名の後に続くUNIX時間と年月日時と -strftime 1 で使える一部の strftime() 書式である。-start_number を指定すると連番の %d の値が0以外から開始する。-hls_flags+ で複数のオプションがつけられる。コマンドプロンプトに直接実行するときは %d などの % は一つで、バッチファイルなどで実行するときは %%d の2つにする。

%d で0から始まる連番になる。

fmpeg -i input -c:v libx264 -c:a aac output.m3u8
output0.ts
output1.ts
output2.ts
:

second_level_segment_index フラグは上と同じ連番になる。

fmpeg -i input -c:v libx264 -c:a aac -strftime 1 -hls_flags +second_level_segment_index -hls_segment_filename output%d.ts output.m3u8
output0.ts
output1.ts
output2.ts
:

%03d は0を埋めた3桁の数になる。

ffmpeg -i input -c:v libx264 -c:a aac -strftime 1 -hls_flags +second_level_segment_index -hls_segment_filename output-%03d.ts output.m3u8
output-000.ts
output-001.ts
output-002.ts
:

second_level_segment_duration フラグはセグメントが固定間隔だと同じ値になるので他のオプションと併用する。

ffmpeg -i input -c:v libx264 -c:a aac -strftime 1 -hls_flags +second_level_segment_index+second_level_segment_duration -hls_segment_filename output-%03d-%t.ts output.m3u8
output-000-10000000.ts
output-001-10000000.ts
output-002-10000000.ts
:

second_level_segment_size フラグはセグメントごとのファイルサイズがファイル名につく。

ffmpeg -i input -c:v libx264 -c:a aac -strftime 1 -hls_flags +second_level_segment_index+second_level_segment_duration+second_level_segment_size -hls_segment_filename output-%03d-%t-%s.ts output.m3u8
output-000-10000000-472632.ts
output-001-10000000-481092.ts
output-002-10000000-477520.ts
:

-hls_start_number_source 0 は通常の連番。

ffmpeg -i input -c:v libx264 -c:a aac -hls_start_number_source 0 output.m3u8
output0.ts
output1.ts
output2.ts
:

-hls_start_number_source 1 は UNIX時間。

ffmpeg -re -i input -c:v libx264 -c:a aac -hls_start_number_source 1 output.m3u8
output1575032400.ts
output1575032410.ts
output1575032420.ts
:

-hls_start_number_source 2 は年月日と時間分秒。

ffmpeg -re -i input -c:v libx264 -c:a aac -hls_start_number_source 2 output.m3u8
output20191129220000.ts
output20191129220010.ts
output20191129220020.ts
:

-strftime 1 で使える strftime() 書式。

フォーマット 説明 返り値の例
%A 曜日の名前 Wednesday
%a 短縮された曜日の名前 Wed
%B 月の名前 January
%b 省略された月の名前 Jan
%d 2桁の日付 08
%H 24時間制の時(00-23) 20
%I 12時間制の時(01-12) 11
%j 年中の通算日(001-366) 008
%M 分(00-59) 53
%m 月を表す数字(01-12) 01
%p 午前または午後(AM,PM) AM
%S 秒(00-60) (60はうるう秒) 06
%U 週を表す数。最初の日曜日が第1週の始まり(00-53) 01
%W 週を表す数。最初の月曜日が第1週の始まり(00-53) 01
%w 曜日を表す数(0-6) 日曜日が0 3
%x 日付(%m/%d/%y) 12/20/19
%Y 西暦を表す数 2014
%y 西暦の下2桁(00-99) 14
%Z タイムゾーンの略称 東京 (標準時)
%z タイムゾーンの略称 東京 (標準時)

ここでは連番で使われていた %d が2桁の日数扱いになる。

ffmpeg -re -i input -c:v libx264 -c:a aac -strftime 1 -hls_segment_filename output-%Y%m%d%H%M%S.ts output.m3u8
output-20191129220000.ts
output-20191129220010.ts
output-20191129220020.ts
:

-strftime_mkdir 1 で任意の日時のフォルダの中にセグメントを入れられるが、マニフェストの出力場所に strftime() 書式が使えないので直接指定することになる。

ffmpeg -re -i input -c:v libx264 -c:a aac -strftime 1 -strftime_mkdir 1 -hls_segment_filename %Y%m%d/output-%Y%m%d%H%M%S.ts output.m3u8
20191129/output-20191129220000.ts
20191129/output-20191129220010.ts
20191129/output-20191129220020.ts
:

月/日/年(2桁)になる %x は、スラッシュがフォルダ内扱いになるので、-strftime_mkdir 1 と併用するとフォルダも同時作成して出力エラーが無くなる。しかしこれもマニフェストの出力場所に strftime() 書式が使えないので直接指定することになる。12フォルダの23フォルダの19フォルダの中に %Y%m%d%H%M%S.ts のセグメントが出力される。

ffmpeg -re -i input -c:v libx264 -c:a aac -strftime 1 -strftime_mkdir 1 -hls_segment_filename %x/%Y%m%d%H%M%S.ts output.m3u8
12/23/19/20191223220000.ts
12/23/19/20191223220010.ts
12/23/19/20191223220020.ts
:

削除するセグメント数を調整する

-hls_flags +delete_segments でマニフェストに記載されていないセグメントを削除できる。以前までは消されずに残っているセグメント数は -hls_list_size + 1 だったのが、-hls_flags +delete_segments を併用すると残すセグメント数を調整できる。

マニフェストに記載されるセグメント数は -hls_list_size の 10。
ffmpeg -i input -c copy -f hls -hls_list_size 10 output.m3u8

マニフェストに記載されるセグメント数は -hls_list_size の 10 で、データが残っているのは -hls_list_size の 10 + 1 = 11。
ffmpeg -i input -c copy -f hls -hls_list_size 10 -hls_flags +delete_segments output.m3u8

マニフェストに記載されるセグメント数は -hls_list_size の 10 で、データが残っているのは -hls_list_size 10-hls_delete_threshold 5 の 10 + 5 = 15。
ffmpeg -i input -c copy -f hls -hls_list_size 10 -hls_flags +delete_segments -hls_delete_threshold 5 output.m3u8

マスタープレイリストを出力する

複数のプレイリストをまとめたマスタープレイリストを出力する例。-map で指定した順番が -var_stream_map で指定する順番になる。出力マニフェスト名は連番 %v の output_0.m3u8、output_1.m3u8 と -master_pl_name で指定したファイル名、例えば master.m3u8 になる。master.m3u8 を入力するときに -map 0:a:0, -map 0:v:0 で音声や映像を指定する方法と、-map p:0 (p はプレイリスト)のようにプレリストを指定する方法がある。

2種類の動画をそれぞれコーデックコピーして2つの個別マニフェストとそれを合わせたマスタープレイリストを出力する例。
ffmpeg -i input1 -i input2 -c copy -map 0:v -map 0:a -map 1:v -map 1:a -f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 -var_stream_map "v:0,a:0 v:1,a:1" %v-output.m3u8

ffmpeg -i input1 -i input2 -c copy -map 0:v -map 0:a -map 1:v -map 1:a \
-f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 \
-var_stream_map "v:0,a:0 v:1,a:1" %v-output.m3u8

1つの動画を複数の解像度にリサイズしてマスタープレイリストを出力する例。「Cannot use rename on non file protocol, this may lead to races and temporary partial files.」のエラーは2019年7月時点で出なくなってた。
ffmpeg -i input -filter_complex split[0][1];[0]scale=1280:-2[0v];[1]scale=640:-2[1v] -map [0v] -map 0:a -c:v:0 libx264 -c:a:0 copy -map [1v] -map 0:a -c:v:1 libx264 -c:a:1 aac -b:a:1 96k -f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 -var_stream_map "v:0,a:0 v:1,a:1" %v-output.m3u8

ffmpeg -i input \
-filter_complex split[0][1];[0]scale=1280:-2[0v];[1]scale=640:-2[1v] \
-map [0v] -map 0:a -c:v:0 libx264 -c:a:0 copy \
-map [1v] -map 0:a -c:v:1 libx264 -c:a:1 aac -b:a:1 96k \
-f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 \
-var_stream_map "v:0,a:0 v:1,a:1" %v-output.m3u8

上の -filter_complex を使わずに個別にフィルタを当てる方法もある。
ffmpeg -i input -filter:v:0 scale=1280:-2 -c:v:0 libx264 -c:a:0 copy -filter:v:1 scale=640:-2 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k -map 0:v -map 0:v -map 0:a -map 0:a -f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 -var_stream_map "v:0,a:0 v:1,a:1" %v-output.m3u8

ffmpeg -i input \
-filter:v:0 scale=1280:-2 -c:v:0 libx264 -c:a:0 copy \
-filter:v:1 scale=640:-2  -c:v:1 libx264 -c:a:1 aac -b:a:1 96k \
-map 0:v -map 0:v -map 0:a -map 0:a \
-f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 \
-var_stream_map "v:0,a:0 v:1,a:1" %v-output.m3u8

tee デマクサを使いマスタープレイリストとセグメントを1つの動画に出力する例。
ffmpeg -i input -filter:v:0 scale=1280:-2 -c:v:0 libx264 -c:a:0 copy -filter:v:1 scale=640:-2 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k -map v:0 -map v:0 -map a:0 -map a:0 -flags +global_header -f tee "[select=\'v:0,a:0,v:1,a:1\':f=hls:master_pl_publish_rate=4:hls_list_size=0:var_stream_map=\'v:0,a:0 v:1,a:1\':master_pl_name=playlist.m3u8:hls_segment_filename=out_%v_%d.ts]out_%v.m3u8|[select=\'v:0,a:0\':movflags=+faststart]out1.mp4|[select=\'v:1,a:1\':movflags=+faststart]out2.mp4"

ffmpeg -i input \
-filter:v:0 scale=1280:-2 -c:v:0 libx264 -c:a:0 copy \
-filter:v:1 scale=640:-2 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k \
-map v:0 -map v:0 -map a:0 -map a:0 -flags +global_header -f tee \
"[select=\'v:0,a:0,v:1,a:1\':f=hls:master_pl_publish_rate=4:hls_list_size=0:\
var_stream_map=\'v:0,a:0 v:1,a:1\':master_pl_name=playlist.m3u8:\
hls_segment_filename=out_%v_%d.ts]out_%v.m3u8|\
[select=\'v:0,a:0\':movflags=+faststart]out1.mp4|\
[select=\'v:1,a:1\':movflags=+faststart]out2.mp4"

詳しい書式は 1つの出力内容を複数に振り分ける tee を参照。

-var_stream_map で指定するストリームに同じ出力内容の複数指定はできないので、

  1. tee個別のプレイリストを出力して、それらをのせたマスタープレイリストを手動でつくる方法。利点は同じエンコードを流用できるので負荷は小さくなるがマスタープレイリストは手動作成になるので、BANDWIDTH の値がビットレート指定のエンコードでないと固定値にならない。個別のマニフェスト名は任意に指定できる
  2. teeプレイリスト出力ではなく個別のファイルを出力して、マスタープレイリストをつくる方法。利点は同じエンコードを流用できるので負荷は小さくマスタープレイリストも作られるが、個別のマニフェスト名は同名の連番指定になる
  3. 同じ設定を出力数だけエンコードして -var_stream_map で指定する方法。利点は1番目の逆で負荷は大きくなるがエンコードと同時にマスタープレイリストも作られ BANDWIDTH の計算も不要で個別のプレイリスト名は同名の連番指定になる
VOD LIVE
1
2 ×
3
  1. ffmpeg -re -i input -filter:v:0 scale=-2:720 -c:v:0 libx264 -c:a:0 aac -b:a:1 128k -filter:v:1 scale=-2:360 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k -filter:v:2 scale=-2:240 -c:v:2 libx264 -map v:0 -map v:0 -map v:0 -map a:0 -map a:0 -flags +global_header -f tee "[select=\'v:0,a:0\':f=hls:start_number=0:hls_time=10:hls_list_size=0]high.m3u8|[select=\'v:1,a:1\':f=hls:start_number=0:hls_time=10:hls_list_size=0]medium.m3u8|[select=\'v:2,a:1\':f=hls:start_number=0:hls_time=10:hls_list_size=0]low.m3u8"
    ffmpeg -re -i input \
    -filter:v:0 scale=-2:720 -c:v:0 libx264 -c:a:0 aac -b:a:1 128k \
    -filter:v:1 scale=-2:360 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k \
    -filter:v:2 scale=-2:240 -c:v:2 libx264 \
    -map v:0 -map v:0 -map v:0 -map a:0 -map a:0 -flags +global_header -f tee \
    "[select=\'v:0,a:0\':f=hls:start_number=0:hls_time=10:hls_list_size=0]high.m3u8|\
     [select=\'v:1,a:1\':f=hls:start_number=0:hls_time=10:hls_list_size=0]medium.m3u8|\
     [select=\'v:2,a:1\':f=hls:start_number=0:hls_time=10:hls_list_size=0]low.m3u8"

    master.m3u8 を以下のようにつくる

    #EXTM3U
    #EXT-X-VERSION:3
    #EXT-X-STREAM-INF:BANDWIDTH=3382474,RESOLUTION=1280x720
    high.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=1250067,RESOLUTION=640x360
    medium.m3u8
    #EXT-X-STREAM-INF:BANDWIDTH=508669,RESOLUTION=320x180
    low.m3u8
  2. ffmpeg -re -i input -filter:v:0 scale=-2:720 -c:v:0 libx264 -c:a:0 aac -b:a:0 128k -filter:v:1 scale=-2:360 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k -filter:v:2 scale=-2:240 -c:v:2 libx264 -map v:0 -map v:0 -map v:0 -map a:0 -map a:0 -flags +global_header -f tee "[select=\'v:0,a:0\']high.mp4|[select=\'v:1,a:1\']medium.mp4|[select=\'v:2,a:1\']low.mp4"
    ffmpeg -i high.mp4 -i medium.mp4 -i low.mp4 -c copy -map 0:v -map 0:a -map 1:v -map 1:a -map 2:v -map 2:a -f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" %v-output.m3u8

    ffmpeg -re -i input \
    -filter:v:0 scale=-2:720 -c:v:0 libx264 -c:a:0 aac -b:a:0 128k \
    -filter:v:1 scale=-2:360 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k \
    -filter:v:2 scale=-2:240 -c:v:2 libx264 \
    -map v:0 -map v:0 -map v:0 -map a:0 -map a:0 -flags +global_header -f tee \
    "[select=\'v:0,a:0\']high.mp4|[select=\'v:1,a:1\']medium.mp4|[select=\'v:2,a:1\']low.mp4"
    
    ffmpeg -i high.mp4 -i medium.mp4 -i low.mp4 -c copy \
    -map 0:v -map 0:a -map 1:v -map 1:a -map 2:v -map 2:a -f hls \
    -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 \
    -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" %v-output.m3u8
  3. ffmpeg -re -i input -filter:v:0 scale=-2:720 -c:v:0 libx264 -c:a:0 aac -b:a:0 128k -filter:v:1 scale=-2:360 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k -filter:v:2 scale=-2:240 -c:v:2 libx264 -c:a:2 aac -b:a:2 96k -map v:0 -map v:0 -map v:0 -map a:0 -map a:0 -map a:0 -f hls -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" %v-output.m3u8
    ffmpeg -re -i input \
    -filter:v:0 scale=-2:720 -c:v:0 libx264 -c:a:0 aac -b:a:0 128k \
    -filter:v:1 scale=-2:360 -c:v:1 libx264 -c:a:1 aac -b:a:1 96k \
    -filter:v:2 scale=-2:240 -c:v:2 libx264 -c:a:2 aac -b:a:2 96k \
    -map v:0 -map v:0 -map v:0 -map a:0 -map a:0 -map a:0 -f hls \
    -hls_list_size 0 -master_pl_name master.m3u8 -master_pl_publish_rate 4 \
    -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" %v-output.m3u8

分割ファイルと連結ファイルを同時出力する

セグメントを出力して連結した動画も出力するコマンド例は5通りある。セグメントを連結するのにマニフェストがない場合は concat することで連結できる。1,2,3は segment demuxerを使っている。

concat を使い分ける
詳細な分割設定ができる Segment
Creating multiple outputs – trac.ffmpeg.org

  1. セグメントと連結した動画の2種類エンコードする。つまり負荷も2倍
    ffmpeg -i input.mp4 -c:v libx264 -c:a aac -flags +loop+global_header -bsf:v h264_mp4toannexb -f segment -segment_format mpegts -segment_time 10 -segment_list output.m3u8 output_%04d.ts -c:v libx264 -c:a aac -flags +loop+global_header -bsf:v h264_mp4toannexb output.ts
  2. 上の2種類エンコードせずに、udp などにエンコードした映像を流して再度 ffmpeg で -c copy で取り込みセグメントと連結した動画の2種類を出力する
    ffmpeg -re -i input.mp4 -c:v libx264 -c:a aac -flags +loop+global_header -bsf:v h264_mp4toannexb -f mpegts "udp://localhost:8080"
    ffmpeg -i "udp://localhost:8080" -c copy -f segment -segment_format mpegts -segment_time 10 -segment_list output.m3u8 -c copy output_%04d.ts -c copy output.ts
  3. -hls_list_size 0 ですべてのセグメントを書き込むように設定し、各セグメントをエンコードした後に ffmpeg でマニフェストを読み込み -c copy で連結する
    ffmpeg -i input.mp4 -c:v libx264 -c:a aac -flags +loop+global_header -bsf:v h264_mp4toannexb -f segment -hls_list_size 0 -segment_format mpegts -segment_time 10 -segment_list output.m3u8 output_%04d.ts
    ffmpeg -i "output.m3u8" -c copy output.ts
  4. tee muxer を使って1度のエンコードで2出力する(これが一番おすすめ)
    1つの出力内容を複数に振り分ける tee
    ffmpeg -i input.mp4 -c:v libx264 -c:a aac -map 0:v -map 0:a -flags +global_header -f tee "[f=hls:hls_time=10:onfail=ignore]output0.m3u8|[bsfs/v=h264_mp4toannexb:onfail=ignore]output.ts"
  5. 配信で個別セグメントは出力せずにバイトレンジで出力するなら -hls_flags single_file を使う。出力ファイルは output.m3u8 と output.ts だけ
    ffmpeg -i input -f hls -hls_flags +single_file output.m3u8

中断したのを途中から再開する

エンコード内容の変更や宣伝を入れたりするのに使う。配信中に中断、正常終了しないとマニフェストの最終行に #EXT-X-ENDLIST がつかないので、-hls_flags +discont_start+append_list をつけて正常終了したマニフェストに変換する。

-hls_flags omit_endlist をつけて意図的に #EXT-X-ENDLIST を省略したマニフェストを作るコマンド例。
ffmpeg -i output.mp4 -c:v libx264 -hls_flags +omit_endlist output.m3u8

-hls_flags +discont_start+append_list をつけて上の output.m3u8 に追記する。実際にエンコード中に output.m3u8 を開いてみると書き込み内容がわかる。append_list をつけないと上書きになる。
ffmpeg -re -i output.mp4 -c:v libx264 -hls_flags discont_+start+append_list output.m3u8

iframes_only のエンコード設定

単にフラグをつけてもIフレームだけの動画にならないので、それにあわせてエンコードの設定をする。このフラグがついている動画は Apple で公開されている。この動画は通常の動画のIフレーム部分だけの動画なのでそれ以外のフレームがない。

HEVC Video with Alpha – WWDC 2019 – Videos – Apple Developer
https://devstreaming-cdn.apple.com/videos/wwdc/2019/506lqy7sprpfyo800/506/hls_vod_mvp.m3u8
https://devstreaming-cdn.apple.com/videos/wwdc/2019/506lqy7sprpfyo800/506/0640/0640_I-Frame.m3u8

HLS で使える映像コーデックは H.264, H.265 なので ffmpeg で使えるコーデックの一覧は以下になる。

  • H.264:libx264, libopenh264, h264_qsv, nvenc_h264, h264_amf
  • H.265:libx265, libkvazaar, hevc_qsv, hevc_nvenc, hevc_amf

QSV 対応の ffmpeg をつくる
ffmpeg に nvenc(cuda)をインストールする
AMD VCE 対応の ffmpeg をつくる

例えば 25fpsの映像に5秒間隔、つまり25*5=125フレーム間隔、のキーフレームを入れて、同様に5秒ごとにキーフレームだけの動画も出力する。キーフレームだけの動画にするにはキーフレーム以外の映像はドロップさせるので select フィルタでそれ以外のフレームを選ばないようにしている。だたしこれで HLS 向けに正しくエンコードできているのかは確認できていない。

1ファイル出力なら問題ないのだがセグメント出力すると意図した通りに出力していないのでコマンド例を削除した。

Iフレームだけにエンコードする設定例

-c:v libx264 -intra -crf 23
-c:v h264_qsv -intra -g 1 -look_ahead 0 -q:v 20
-c:v libopenh264 -g 1 -q:v 20
-c:v libx265 -x265-params frame-threads=4:keyint=1:ref=1:no-open-gop=1:weightp=0:weightb=0:cutree=0:rc-lookahead=0:bframes=0:scenecut=0:b-adapt=0:repeat-headers=1

hls_key_info_file の書式

書式

key URI
key file path
IV (optional)

http://example.com/file.key
/path/to/file.key
0123456789ABCDEF0123456789ABCDEF

file.key を openssl で作って暗号化するシェルスクリプト。

#!/bin/sh
BASE_URL=${1:-'.'}
openssl rand 16 > file.key
echo $BASE_URL/file.key > file.keyinfo
echo file.key >> file.keyinfo
echo $(openssl rand -hex 16) >> file.keyinfo
ffmpeg -f lavfi -i testsrc2 -t 10 -c:v libx264 -hls_key_info_file file.keyinfo output.m3u8

上のシェルスクリプトで作られる file.keyinfo の例。

./file.key
file.key
50d6dc6e65d9aa0f810a3f699950c8a0

openssl で復号する例。K の指定値は file.key をバイナリエディタで読むことが出来る。
openssl aes-128-cbc -d -in output0.ts -out output.ts -K A4856CAC48F1E48D -iv 50d6dc6e65d9aa0f810a3f699950c8a0

ffmpeg で復号する例。
ffmpeg -i output.m3u8 -c copy output.ts

ffmpeg で復号する例(mp4)。
ffmpeg -i output.m3u8 -c copy -bsf:a aac_adtstoasc output.mp4

output.m3u8 の中身。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="./file.key",IV=0x50d6dc6e65d9aa0f810a3f699950c8a0
#EXTINF:10.000000,
output0.ts
#EXT-X-ENDLIST

ffmpeg でローカルファイルを復号してプレビューする。
ffmpeg -re -allowed_extensions ALL -i output.m3u8 -f sdl -
ffplay -allowed_extensions ALL -i output.m3u8

hls_enc, hls_enc_key, hls_enc_iv で暗号化する

コマンド例、output.m3u8.key が -hls_enc_key で作られる。
ffmpeg -i input.ts -hls_enc 1 -hls_enc_key A4856CAC48F1E48D -hls_enc_iv 50d6dc6e65d9aa0f810a3f699950c8a0 -c copy -f hls output.m3u8

output.m3u8 の中身。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="output.m3u8.key",IV=0x35306436646336653635643961613066
#EXTINF:3.920844,
output0.ts
#EXTINF:1.251333,
output1.ts
略
#EXT-X-ENDLIST

キーファイルのファイル名とパスの設定は -hls_enc_key_url を使う。
ffmpeg -i input.ts -c copy -f hls -hls_enc 1 -hls_enc_key A4856CAC48F1E48D -hls_enc_iv 50d6dc6e65d9aa0f810a3f699950c8a0 -hls_enc_key_url hoge.key output.m3u8

output.m3u8 の中身。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="hoge.key",IV=0x35306436646336653635643961613066
#EXTINF:3.920844,
output0.ts
#EXTINF:1.251333,
output1.ts
略
#EXT-X-ENDLIST

保存した m3u8 やセグメントから動画に変換する

HLS 配信している動画でマニフェストやセグメントを保存した場合にマニフェストを書き換えて ffmpeg で動画にする方法。主な用途はライブ配信で過去のセグメントがマニフェストに載っていない場合に、セグメントが連番で推測しやすくアクセスも出来る場合である。

まずマニフェストに記載してあるセグメントを絶対パスにし、最後の行に #EXT-X-ENDLIST がなければ追加する。同様に暗号用の key も絶対パスにする。各セグメントを保存した場合はファイルパスに注意する。

ffmpeg -protocol_whitelist http,https,tls,tcp,crypto,file -allowed_extensions ALL -analyzeduration 30M -probesize 30M -i master.m3u8 -c copy output.ts
ffmpeg -protocol_whitelist http,https,tls,tcp,crypto,file -allowed_extensions ALL -analyzeduration 30M -probesize 30M -i master.m3u8 -movflags +faststart -c copy -bsf:a aac_adtstoasc output.mp4

セグメントが多いとコンソールのログが長くなり処理が遅くなるので、-loglevel 0
で非表示にすることもできる。
ffmpeg -protocol_whitelist http,https,tls,tcp,crypto,file -allowed_extensions ALL -analyzeduration 30M -probesize 30M -i master.m3u8 -c copy output.ts -loglevel 0
ffmpeg -protocol_whitelist http,https,tls,tcp,crypto,file -allowed_extensions ALL -analyzeduration 30M -probesize 30M -i master.m3u8 -movflags +faststart -c copy -bsf:a aac_adtstoasc output.mp4 -loglevel 0

セグメントの開始がキーフレームでは無くて音声は流れるのに映像がすぐに動かない場合は入力ファイルの前に -ss 0 を追加するとキーフレームまでの映像をカットする。
ffmpeg -protocol_whitelist http,https,tls,tcp,crypto,file -allowed_extensions ALL -analyzeduration 30M -probesize 30M -ss 0 -i master.m3u8 -c copy output.ts
ffmpeg -protocol_whitelist http,https,tls,tcp,crypto,file -allowed_extensions ALL -analyzeduration 30M -probesize 30M -ss 0 -i master.m3u8 -movflags +faststart -c copy -bsf:a aac_adtstoasc output.mp4

m3u8 を読み込む

HLS配信されているマニフェストを保存せずにそのまま読み込んで復号し動画で出力するコマンド。cookie や UA を指定して接続もできる。それらを調べるには開発ツールを有効にしてマニフェストだけを開き「Copy as CURL」で調べられる。

リファラや cookie、UA が必要な場合は -headers-user_agent を指定する。-user_agent"Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0" のように指定する。-headers
"Cookie: _alid_=hogehoge==; hdntl=exp=15123456..." のように指定する。

コマンド例
ffmpeg -user_agent "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0" -headers "Cookie: _alid_=hogehoge==; hdntl=exp=15123456..." -i "https://example.com/master.m3u8" -c copy output.ts
ffmpeg -user_agent "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:56.0) Gecko/20100101 Firefox/56.0" -headers "Cookie: _alid_=hogehoge==; hdntl=exp=15123456..." -i "https://example.com/master.m3u8"-movflags +faststart -c copy -bsf:a aac_adtstoasc output.mp4

FFmpegを使ってradikoを録音する方法 – Qiita を参考に、-headers は以下の設定が可能。

-headers "Origin: https://radiko.jp"
-headers "Accept-Encoding: gzip, deflate"
-headers "Accept-Language: ja,en-US;q=0.8,en;q=0.6"
-headers "Accept: */*"
-headers "X-Radiko-AuthToken: foobar"
-headers "Referer: https://radiko.jp/"
-headers "Connection: keep-alive"
-headers "Cookie: _alid_=hogehoge==; hdntl=exp=15123456..."

How to enable cookies in ffmpeg HLS – Stack Overflow

更新履歴

オプションが追加されていたので追加した。2016年7月30日
オプションが追加されていたので追加し文章を直した。2016年9月6日
オプションが追加されていたので追加した。2016年9月11日
オプションが追加されていたので追加し順番を直した。2017年2月12日
セグメントを出力して連結した動画も出力するコマンド例。2017年3月26日
hls_key_info_file の書式をわかりやすく書き直した。2017年4月17日
hls_enc, hls_enc_key, hls_enc_key_url, hls_enc_iv のオプションを追加した。 2017年6月8日
concat する方法のリンクが間違ってたのを直した。2017年6月12日
fragment mp4 のオプションを追加した。2017年7月20日
fragment mp4 の記述が間違っているので一度削除。2017年7月22日
fragment mp4 のコマンド例を追加したが、#EXT-X-MAP:URI=”(null)” になるのを回避する方法が分からない。periodic_rekey のフラグ追加。2017年8月10日
ネットから保存した m3u8 から動画に変換するを追加。2017年10月11日
livestreamer、streamlink では使えなかったので記述を削除した。streamlink は file プロトコルが使える 2017年10月12日
新しいオプションを追加し、マスタープレイリストを出力するコマンド例を追加。2017年12月9日
cookie 指定を headers のオプションに変更しコマンド例を追加。2018年2月21日
use_localtime, use_localtime_mkdir が廃止予定になったので代替のオプションを追加。再生確認の項目追加。オプションの追加。2018年9月10日
GOP、映像音声同期、遅延について追記。2018年11月24日
loop-global_header を loop+global_header に直した。2019年6月19日
.ts 出力には movflags +faststart を消した。2019年7月1日
hls_delete_threshold の説明を追加した。2019年7月5日
記事の順番を見直し目次を細分化した。2019年7月6日
master_pl_name, tee の併用例を追記した。2019年7月30日
headers の利用例を追加した。2019年10月18日
マスタープレイリストの出力例を追加した。2019年10月19日
hls_segment_filename を誤訳していたのを直した。2019年11月18日
中断したのを途中から再開する項目を追加した。2019年11月24日
セグメントファイル名の設定の項目を追加した。2019年11月29日、2019年11月30日
セグメントファイル名の設定の項目の%%の部分を直した。2019年12月23日

One thought on “ffmpeg で Apple HTTP Live Streaming(HLS)を扱う”

コメントを残す

メールアドレスが公開されることはありません。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)