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を使うのは断念した。


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

[先に書いた記事](https://qiita.com/yoshua/items/e72d75a32f29faef9c84)のコマンドを一部変更して、現在時刻を書き込んだ動画をローカルに保存する。

```
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秒程度となった。

<img width="480" alt="スクリーンショット 2020-02-27 0.32.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375661/d7be329d-8e5f-a280-1a93-f3e135a928c7.png">

### 出来上がった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}
```

<img width="480" alt="スクリーンショット 2020-02-27 8.05.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375661/fc7e63d0-5f68-48a7-bd4a-262fd8a2baeb.png">


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秒未満**となった。

<img width="480" alt="スクリーンショット 2020-02-27 22.09.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375661/9b549b91-23b0-622b-d60e-98ec6f9baf8a.png">

理由は調べていないが、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, という感じです。
<img width=480 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375661/ce3bc9be-5c33-062e-cf37-8fcca50aa14a.png">