Raspberry PIとffmpegとWebカメラ、USBマイクを利用してHLSストリーミング＆保存

#まずはHLSストリーミン　その1
参考1：https://signal-flag-z.blogspot.com/2016/10/raspberry-pi-3ffmpegusb.html
参考2：https://homemadegarbage.com/raspi-ffmpeg-rtmp
参考3：https://signal-flag-z.blogspot.com/2018/03/ffmpeg-hls-streaming.html
参考4：https://www.ffmpeg.org/ffmpeg-all.html
参考5：http://mobilehackerz.jp/archive/wiki/index.php?%BA%C7%BF%B7FFmpeg

参考のページのコマンドを見ながら、カメラやマイクに依存してそうなところをすこしカスタマイズ。
長くなったので、シェルスクリプト化した。
また、カレントディレクトを`~/Public/web_server`に設定。この設定はストリーミング再生するためのWebサーバを立ち上げたので、その公開用ディレクトリによるもの。

```bash:streaming1
#! /bin/bash
httpDir=~/Public/web_server
cd $httpDir

ffmpeg -f alsa -ac 1 -thread_queue_size 16384 -i hw:1,0 \
  -f v4l2 -thread_queue_size 16384 -s 640x480 -r 30 -vsync 2 -i /dev/video0 \
  -c:v h264_omx -b:v 1000k -bufsize 1000k -g 60 \
  -c:a aac \
  -flags +cgop+global_header \
  -f hls \
     -hls_time 2 \
     -hls_list_size 3 \
     -hls_allow_cache 0 \
     -hls_segment_filename $httpDir/stream/stream_%06d.ts \
     -hls_base_url stream/ \
     $httpDir/playlist.m3u8
```
とりあえず、これでストリーミング再生はできるようになった。
セグメント化されたtsファイルは「stream000001.ts」のような名前で保存される。
再生するためのマニュフェストファイルはplaylist.m3u8。
thread_queue_sizeやb:v、b:aはかなり大きめ。
音声サンプリングレートは44.1kHz。
`-hls_time`で各セグメントの時間が設定できる。今回は2秒/セグメント。
`-hls_list_size`でマニュフェストに書かれるファイルの数を制限。ライブ配信の時に、スタートまで遡って再生できるのか、直近3個のセグメントまでしか戻れないのか。


実は、これで再生はできるのはできるが、実は誤りが含まれている。
これをこのままやると、音がずれていく。
どうすればいいんだろうか？といろいろと悩んだ結果、`-r 30`と`-g 60`を除き、`-vsync 2` を`-vsync -1`にしてみたら上手くいったっぽい。
けど、この時点ではまだこれらのオプション指定が何を意味しているのかをあまり理解できておらず（マニュアルから意味は分かっていたが、使い方がわかってなかった）


#HLSストリーミング　その2
参考:http://nico-lab.net/hls_muxer_with_ffmpeg/

その1の方はHLS Muxerをつかってストリーミングをしているが、Segment Muxerを使うのが一般的らしい（その1の参考1）ので、そうしてみた。

```bash:streaming2
#! /bin/bash
httpDir=~/Public/web_server
cd $httpDir

ffmpeg  -f alsa -ac 1 -thread_queue_size 16384 -i hw:1,0 \
  -f v4l2 -thread_queue_size 16384 -s 640x480 -vsync -1 -i /dev/video0 \
  -c:v h264_omx -b:v 1000k -bufsize 1000k \
  -c:a aac -b:a 256k -ar 44100 -bufsize 256k \
  -flags +cgop+loop-global_header \
  -bsf:v h264_mp4toannexb \
  -f segment -segment_format mpegts -segment_time 10 -segment_list playlist.m3u8 stream_%06d.ts
```
その1と違い、最初から`-r 30`や`-gop 60`は除いていて、`-vsync -1`にしている。
`-segment_time 10`とあるように、1セグメントは10秒。
また` -b:a 256k -ar 44100 -bufsize 256k`を付加。
これは、つけずにやると「バリバリバリ」というような音われがひどく、例えば声とかまったく聞き取れない状態だったので。
なんでかな？と考えながら、streamig1の時の再生開始後に表示される情報を見ると、ビットレートが69kとかすんごい低かったのと、サンプリングレートも48khzやったので、これが原因かな（量子化の際のノイズ）と考えて、ビットレートを256kをにし、逆にサンプリングレートは44.1kHzに。これで、音は安定。
音と映像がずれていく、ということもなくうまくいったっポイ。

ただ、問題がストリーミング再生を実際にやってみると、ある程度再生が進んだ後にブラウザを再読み込みして、もう一回再生をすると音は聞こえるのだが、画面は真っ白に。
生成される各セグメント自体を個別に再生する分は問題ないので、気持ち悪いのだが、とりあえずは不用意に再読み込みはしないようにしよう、ということでスルーしておく。



#ストリーミングと同時に映像を保存　その1
参考1：http://nico-lab.net/hls_muxer_with_ffmpeg/
参考2：https://matome.naver.jp/odai/2150723389118167201
参考3：https://signal-flag-z.blogspot.com/2016/12/raspberry-pi-3ffmpegyoutube.html

もともと目的は、被験者実験で、実験室中の被験者を録画をしながらモニターしたい、というのだったので、ストリーミングするとともに動画を保存したい。
さて、どうしようか。

ストリーミングその2でも参考にしたページを見てみると、いくつか方法が書いてある。
`tee`というMuxerを使って、出力を分岐させて、、、ということが書いてあるんだが、なんかよくわからん。`tee`のマニュアルを読むのも面倒。
じゃあ、どうしようか・・・と考えていきついたのは、「各セグメントファイルがあるんなら、動画撮影終了したタイミングで、くっつけたらよくないか」というもの。
ということで参考2や参考3をもとに、書いたのが以下のもの。
参考3は`-i`の部分をシェルで渡すときのスクリプトの参考にした。

```bash:streaming2に追記した内容
#ffmpeg -i "playlist.m3u8" -movflags faststart -c copy output.mp4
ffmpeg -f concat -safe 0  \
    -i <(for file in ~/Public/web_server/stream_*.ts ; do echo file "$file" ; done)  \
    -movflags  faststart -c copy  output.mp4

mv output.mp4 output_rpi_2_$(date +%Y%m%d_%H%M%S).mp4

rm $httpDir/stream_*.ts
rm $httpDir/playlist.m3u8
```

1行目について、マニュフェストから結合させることができるはずなのだが、どうもうまくいかなかったのでコメントアウトした。
`mv`や`rm`は後始末として、ファイル名を日付時間が入ったものにリネームするのと、セグメントファイルを消したり、マニュフェストを消したり。

動画撮影後にファイル結合処理が入ってくるので、撮影終了してもすぐには復帰しない点が問題。
特に長時間撮影してたりすると結構な時間待たされる。

ストリーミングの箇所そのものはいじっていないので、再読み込みかけると映像が表示されなくなる問題は変わらない。

#ストリーミングと同時に映像を保存　その2
参考1：http://nico-lab.net/hls_muxer_with_ffmpeg/

長時間待たされるのがどうにも気に食わないし、ストリーミング中に映像表示がされない問題も変わらない。
そこで参考1にあったストリーミングと同時に保存する方法をもう一度詳しく見る。
おススメは4つ目と書いてあるが、5つ目が一番コマンドとしてはシンプル。
なので、5つ目をやってみる。

```bash:streaming3
#! /bin/bash
httpDir=~/Public/web_server
cd $httpDir

ffmpeg  -f alsa -ac 1 -thread_queue_size 16384 -i hw:1,0 \
  -f v4l2 -thread_queue_size 16384 -s 640x480 -vsync -1 -i /dev/video0 \
  -c:v h264_omx -b:v 1000k -bufsize 1000k \
  -c:a aac -b:a 256k -ar 44100 -bufsize 256k \
  -flags +cgop+loop-global_header \
  -bsf:v h264_mp4toannexb \
  -hls_flags single_file output.m3u8
```
これはそもそもセグメントを作らないやり方。出力されるのはoutput.tsっていうひと固まりになった動画ファイルと、時間単位で区切られたマニュフェスト。
非常にシンプル！tsファイルはvlcで普通に再生できるしいい感じ！

やけど、ストリーミング中に再読み込みして、もう一度ストリーミング再生させると、映像が表示されなくなる問題は変わらない。
一体何が原因なんだ？

#ストリーミングと同時に映像を保存　その3
参考1：http://nico-lab.net/hls_muxer_with_ffmpeg/
参考2：https://trac.ffmpeg.org/wiki/Creating%20multiple%20outputs
参考3：https://ffmpeg.org/ffmpeg-formats.html#tee
参考4：https://signal-flag-z.blogspot.com/2016/10/raspberry-pi-3ffmpegusb.html
参考5：https://ffmpeg.org/ffmpeg.html
参考6：https://ja.stackoverflow.com/questions/12736/ffmpegのsegment-timeオプションが効かないのか

とりあえずいろいろ試そうということで、参考1でおススメと書いているtee muxcerなるものも試してみる。

分岐されてきちんと動画も保存されるんだが、今度は最初からストリーミングで映像が表示されない。
それよりも何よりも、ターミナルに大量のWarningsが出る。以下はその一例。

```terminal
Non-monotonous DTS in output stream 0:1; previous: 0, current: 0; changing to 1. This may result in incorrect timestamps in the output file.
```
うん？つまり映像のフレームのDTS(decoding time stamp)が連続して同じのがやってきたので、あとの方を強制的に1つ進めたってこと？
このWarningsがすべてのフレーム対して出てる。

ここで一つピンときた。
いろいろと方法を探っている時に見かけていた参考4のサイトで、UDPでストリーミングするときに、
「RPi3が送る最初のキーフレームをうまく受信できないと映像が表示されないかもしれません。vlcとffmpegを止め、もう一度試してみましょう。」
と書いてあった。
さらに`-g`オプションの意味を調べてるときに勉強したgopの構造、というかmpeg形式の動画のデータ構造から、
「つまり、タイムスタンプが1つずつずれたせいで、本来キーフレームであるべきフレームがBフレームかPフレームになり、本来キーフレームではないBフレームやPフレームがキームレームになってしまってて問題を引き起こしたんとちゃうか？それがストリーミングでうまくいかない原因にもなってるんちゃう？」
という考えが出てきた。

じゃあ、なんでフレームのDTSがずれるの？

なんとなく心当たりはvsyncオプション。
これは一番最初のストリーミングを試すときに、カメラからのフレームレートをコントロールしたいなと思って`-r 30`と一緒に入れてたんだけど、これってカメラからフレームを取得するときにすることというよりは、エンコードをかけるときにすべきことなんちゃうんか？
実際に参考6だと、`-c:v`に対するオプションで`-vsync`を入れてるやん？

そこで、`-vsync`の位置を修正。
さらにそこから派生させて、上にあるとおり、フレームレートを30にするために`-r 30`を最初のストリーミングのコマンドでは入れていたんだが、これを入れてると音ずれが発生した。なんで結局外したんだけど、これの位置も悪かったんではないか？と思い、`-c:v`の後、`-vsync`の前に入れてみた。

うん、なんとなくこれで問題解決できそうなんじゃない？？

ついでに、いろいろと今までデッドコピーしていたオプションについても調べてみた。
すると、`-flags`で指定されているものので`+loop-global_header`なるオプション値指定が、あちこちの「ffmpegでストリーミングやってみた」サイトで書かれてるんだけど、公式マニュアルをどれだけ調べてもそんなオプション指定が存在しない。
あるのは`loop`と`global_header`という別々のオプション値であって、`-flags`はこれらのオプション値を`+`で結び付けていく物らしい。
となると、これって日本語のいろいろなサイトが間違った情報を載せてるんじゃない？と思って`+loop-global_header`を`+loop+global_header`にしてみた。


#ストリーミングと同時に映像を保存　最終版

ということで、いろいろと修正してみたのが以下のもの。

```bash:streaming4
#! /bin/bash
httpDir=~/Public/web_server
cd $httpDir

ffmpeg  -f alsa -ac 1 -thread_queue_size 16384 -i hw:1,0 \
  -f v4l2 -thread_queue_size 16384 -s 640x480 -i /dev/video0 \
  -c:v h264_omx -b:v 1000k -bufsize 1000k -r 30 -vsync cfr -g 300\
  -c:a aac -b:a 256k -ar 44100 -bufsize 256k \
  -flags +cgop+loop+global_header \
  -bsf:v h264_mp4toannexb \
  -f tee -map 1:v -map 0:a "[f=hls:onfail=ignore]output.m3u8|[onfail=ignore]Record.ts"

mv Record.ts Record_rpi_2_$(date +%Y%m%d_%H%M%S).ts

rm $httpDir/output*.ts
rm $httpDir/output.m3u8
```

一つ注目してほしいのは`-map 1:v`というところ。一つ上のセクションの参考1のページでは`-map 0:v`となっているが、これは参考1ではinputが1つしかなくて、その1つから音声と動画の両方を取得する想定になっているので、`-map 0:v -map 0:a`となっている。
やけど、今回はWebカメラとUSBマイクという別々のソース。
またコマンドを書く順序として最初に音声ソースを書いて、あとに映像ソースを書いているので音声が0、映像が1になる。

これで、ストリーミングも再読み込みしても問題なく表示されるようになったし、Warningも全くでないし、全部うまく回るようになった！