FFmpegで動画を作成してHLSのストリーミング


先の記事で

  • 動画自体のタイムコードを書き込んだものを作った
  • Shaka Streamer, Shaka Packagerを使ってHLS Liveのストリーミングをローカル環境で構築できた

なので、次は「現在時刻(FFmpegで動画を作っている時の時刻)を書き込んだ動画を作成しながらローカル環境でHLS Liveのストリーミング」をしてみる。

使用する環境

  • macOS 10.14.6(18G3020)
  • FFmpeg version 4.1.4
  • Shaka Packager v2.4.1-c731217-release

Shaka Streamerは使わなくてもよかった。FFmpegからのUDPの入力をするためのShaka Streamerのconfigがわからなかったので、Shaka Streamerを使うのは断念した。

現在時刻を書き込んだ動画を作成

先に書いた記事のコマンドを一部変更して、現在時刻を書き込んだ動画をローカルに保存する。

ffmpeg -re -i 03_caminandes_llamigos_1080p.mp4 -vcodec h264 \
-profile:v main -b:v 1000k -minrate:v 1000k -maxrate:v 1000k \
-bufsize:v 2000k -crf 23 -sc_threshold 0 -keyint_min 24 -r 24 -g 48 \
-vf "scale=640:-1, \
drawtext=fontfile=/Library/Fonts/Courier\ New\ Bold.ttf: \
text='%{localtime\:%X}': r=24: fontsize=60: fontcolor=white: \
x=(w-tw)/2: y=h-(2*lh): box=1: boxcolor=0x00000000@1" \
-acodec aac -ac 2 -ab 64k -ar 44.1k -y output2.mp4

変更した点

  • ffmpegのオプションで-reを加える。
  • drawtextのオプションのtimecode='00\;00\:00\:00'text='%{localtime\:%X}'に変更する。

localtimeの説明については https://ffmpeg.org/ffmpeg-filters.html#Text-expansion を参考に。
ffmpeg drawtime localtimeでググるといろんな人のページも出てくるので参考になると思います。

HLSのストリーミングをしてみる

httpdを起動し、Shaka Packagerを実行し、UDPで入力を待ち受け、UDPにFFmpegで入力をして、HLSを出力するという風なことをしてみます。

httpd起動

sudo brew services start httpd

Shaka Packagerの実行

以下のスクリプトではこんなことをしてます。

  • udp://127.0.0.1:40000 で入力を待つ
  • ./www/packager/hls.m3u8 にHLSを出力する
#!/bin/bash

INPUT="udp://127.0.0.1:40000"
OUT_DIR="./www/packager"

OUT_AUDIO_SEGMENT="${OUT_DIR}/audio_\$Number\$.ts"
OUT_AUDIO_PLAYLIST="${OUT_DIR}/audio.m3u8"

OUT_VIDEO_SEGMENT="${OUT_DIR}/video_\$Number\$.ts"
OUT_VIDEO_PLAYLIST="${OUT_DIR}/video.m3u8"

OUT_MASTER_PLAYLIST="${OUT_DIR}/hls.m3u8"

rm -rf $OUT_DIR
mkdir -p $OUT_DIR

packager \
  "in=${INPUT},stream=audio,segment_template=${OUT_AUDIO_SEGMENT},playlist_name=${OUT_AUDIO_PLAYLIST},hls_group_id=audio" \
  "in=${INPUT},stream=video,segment_template=${OUT_VIDEO_SEGMENT},playlist_name=${OUT_VIDEO_PLAYLIST}" \
  --hls_master_playlist_output ${OUT_MASTER_PLAYLIST} \
  --hls_playlist_type LIVE

FFmpegでの入力

udp://127.0.0.1:40000 でFFmpegから入力をする。
これまでのffmpegのコマンドだとストリーミングをするにはCPUを使用しすぎるので、オプションを変えて試す。

ffmpeg -re -stream_loop -1 -i 03_caminandes_llamigos_1080p.mp4 \
-vcodec h264 -profile:v main -preset ultrafast -tune zerolatency \
-b 500k -sc_threshold 0 -keyint_min 24 -r 24 -g 48 \
-vf "scale=640:-1, \
drawtext=fontfile=/Library/Fonts/Courier\ New\ Bold.ttf: \
text='%{localtime\:%X}': r=24: fontsize=60: fontcolor=white: \
x=(w-tw)/2: y=h-(2*lh): box=1: boxcolor=0x00000000@1" \
-acodec aac -ac 2 -ab 64k -ar 44.1k -f mpegts udp://127.0.0.1:40000

結果

遅延

ffplayで再生をして確認してみたところ、遅延が23秒前後〜28秒程度となった。

出来上がったHLSの確認

出来上がったm3u8の中身を見ると以下の通りだった。

  • m3u8を見る限り、1つのtsファイルの長さが6秒だった
  • m3u8に含まれるtsファイルの数が特定の数ではなかった(5、6個を想定していたが、生成されたtsが全部含まれていた)
video_47.ts
#EXTINF:6.000,
video_48.ts
#EXTINF:6.000,
video_49.ts
#EXTINF:6.000,
video_50.ts
#EXTINF:6.000,
video_51.ts
#EXTINF:6.000,
video_52.ts

また、videoのtsファイルをffprobeで確認し、以下のことは確認できたので、FFmpegでの入力したストリームにはおよそ問題がなさそうだった。

  • 長さが6秒であるのは確かだった
  • 2秒ごとにI-frameが含まれていた

次の調査

調子のいい時は遅延が20秒未満になるのだけれども、どうも安定しない。
FFmpegのコマンドのチューニング、Shaka Packagerのコマンドのオプションの理解が必要そうだ。

(追記)チューニング後

Shaka Packagerのオプションを見直したら安定した。

  • チャンクファイルの長さが2秒になるように。
  • m3u8のチャンクファイルの数が3つになるように。

遅延は8〜9秒ほど。
前のスクリプトからの変更点のみ記述。

# 以下は追加
OPTIONS="${OPTIONS} --segment_duration 2"
OPTIONS="${OPTIONS} --time_shift_buffer_depth 4"
OPTIONS="${OPTIONS} --hls_master_playlist_output ${OUT_MASTER_PLAYLIST}"
OPTIONS="${OPTIONS} --hls_playlist_type LIVE"

# 以下packagerのコマンドに上記のOPTIONSを追加
packager \
  "in=${INPUT},stream=audio,segment_template=${OUT_AUDIO_SEGMENT},playlist_name=${OUT_AUDIO_PLAYLIST},hls_group_id=audio" \
  "in=${INPUT},stream=video,segment_template=${OUT_VIDEO_SEGMENT},playlist_name=${OUT_VIDEO_PLAYLIST}" \
  ${OPTIONS}

CPUのStats等調査はしていなかったけども、Shaka Packagerの動作が安定したのか、HLSの再生も止まることがなくなった。
単純に6秒のチャンクを作るとしたら6秒分のリソースを最低確保するわけだから当たり前だよな。。。
FFmpegで入力するVideoを 720p/1.5Mbps/MP にしても問題なく再生ができるようになった。

(追記2)MP4のHLSにして試してみる

先のShaka Packagerのスクリプトを変更し、

  • 出力をTSからMP4(m4s)に変更
  • Fragmentも指定(1秒)

のHLSに変更してみる。

# SegmentとFragmentを指定
OPTIONS="${OPTIONS} --segment_duration 2"
OPTIONS="${OPTIONS} --fragment_duration 1"
OPTIONS="${OPTIONS} --time_shift_buffer_depth 4"
OPTIONS="${OPTIONS} --hls_master_playlist_output ${OUT_MASTER_PLAYLIST}"
OPTIONS="${OPTIONS} --hls_playlist_type LIVE"

packager \
  "in=${INPUT},stream=audio,init_segment=${OUT_AUDIO_INIT},segment_template=${OUT_AUDIO_SEGMENT},playlist_name=${OUT_AUDIO_PLAYLIST},hls_group_id=audio" \
  "in=${INPUT},stream=video,init_segment=${OUT_VIDEO_INIT},segment_template=${OUT_VIDEO_SEGMENT},playlist_name=${OUT_VIDEO_PLAYLIST}" \
  ${OPTIONS}

結果は、最短で6秒以上〜7秒未満となった。

理由は調べていないが、Fragment化していないとTSで出力した時と同じ程度の遅延であったので・・・
『Fragment化されているので、Shaka Playerはその分m3u8を早く作り、プレイヤー側はそれを読み込んでm4sをダウンロードしながら再生しているんじゃなかろうか』
と思った。適当な見解なので間違っていると思ってください。

本記事はこれで終わり。

実際にサーバーに配置して試してみたくなった。
けどサーバー知識皆無で面倒だしお金かかるの怖い。

(追記3)AndroidとiOSで再生してみた

追記2で作ったストリームをAndroidとiOSで再生してみた。興味深い結果だった。
環境は以下の通り。

  • Android
    • SOV33 (Android 8.0.0)
    • ExoPlayer r2.11.1
  • iOS
    • iOS Simulator (iOS 13.2.2)
    • AVPlayer

結果としては以下の通り。

  • Androidは実機にも関わらず、ffplayの遅延+2秒程度(=8〜10秒)
  • iOSはffplayの遅延+1秒程度(=7〜9秒)、Simulatorなのに頑張っている

Androidで使用しているExoPlayerは以前のバージョンだと再生位置をチューニングすることができた。最新のバージョンでもチューニングができれば遅延を小さくできるかもしれない。
実際、ExoPlayerのデモアプリではシークバーが表示され、シークバーを操作するとffplayと同程度の遅延になることもあったので、可能性は十分にありそう。

画像は、左上がiOSのSimulator, 左下がAndroid, 右がffplay, という感じです。