Google ChromeにH.264映像を送ると再生されない問題の解決

## はじめに

### 記事の内容

(この記事の内容は古いです。最新版では解決されているようです。)

自分がLinuxを使っているので気づいていなかったのですが、Mac版のGoogle ChromeにWebRTCで映像を送る時、H.264でエンコーディングしていると画面に出力されないことがあります。

例えばSkyWay WebRTC Gatewayで送る場合まさにこの問題に引っかかり、問題になっていました。([issue](https://github.com/skyway/skyway-webrtc-gateway/issues/14))
この記事では、この問題の原因と具体的な解決策について記載します

### 想定読者ではない人

- Macなんか絶対使わない人
- 今後Windows/Linux版でも同じ問題が出ても泣かない人
- H.264なんか絶対使わない人

以上の人には関係ない問題なので、気にしなくて大丈夫です。

あと「原因は別にいいから解決策だけ知りたい」という人は解決手順のセクションから読んで下さい。

## 問題の原因調査

### 原因

GstWebRTCというソフトウェアの開発者も同じ問題に引っかかっており、彼らが調査したところ原因は以下のようです。

```
It seems that browsers do not get along with x264 because of SEI NAL units sent with the stream. As a workaround, we set key-int-max=1 and avoid the SEI insertions.
```
(https://developer.ridgerun.com/wiki/index.php?title=GstWebRTC_-_PubNub_Video_Examples)

かいつまんで言うと、x264というエンコーダーを利用して映像をH.264エンコーディングした時、STREAMとしてSEI NALユニットが送られるとうまくいかないということのようです。

で、彼らの言う解決策としては、key-int-maxを1に設定することで、SEI NALユニットが生成されないようにすること。つまりgStreamerのパイプラインは以下のようになります。

```shell
USER_CHANNEL=123
PEER_CHANNEL=123peer
gst-launch-1.0 rrwebrtcsink rtcp-mux=true start-call=true signaler::user-channel=$USER_CHANNEL signaler::peer-channel=$PEER_CHANNEL name=web videotestsrc is-live=true ! x264enc aud=false key-int-max=1 tune=zerolatency intra-refresh=true ! "video/x-h264,profile=constrained-baseline,level=(string)3.1" ! rtph264pay ! web.video
```

### SEI NALユニットとは？を理解するためのH.264のざっくりした理解

動画、人間が動いているように認識するくらい高速に何十枚もの画像を表示することで成り立っています。例えば30FPSだと1秒間に30枚の映像を出力しています。
このとき、全ての映像をそのまま出力すると容量が大きくなってしまうので、圧縮技術を利用します。
H.264は差分圧縮技術です。ざっくり言うと、単体で描画できるIフレームと、Iフレームからの差分のみを含むPフレームで構成されています。
差分フレームは直前のIフレームと同じ情報は含まなくて良いので、変化した部分だけの情報でよく、容量を減らすことができます。
H.264のPフレームは単純な差分ではなく、映像中の画素の動きの予測をうまく行うことで更に圧縮率を高めています(が、詳細を理解すると一冊の本が出来上がるのでここでは省略します)

NALは動画符号化処理とRTP伝送の間に入っている抽象化層で、
この層で抽象化することで、RTPでビデオ符号化データ(VCL)の他にパラメータも転送できるようになります。
端的に言うと、動画データもパラメータも同じように扱って転送するための抽象化です。
SEIはこのパラメータの1種で、動画の復号に必須ではない付加情報を含むものです。
詳しくは[RFC6184](https://tools.ietf.org/html/rfc6184)を読んでください。

[x264encの説明ページ](https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-ugly-plugins/html/gst-plugins-ugly-plugins-x264enc.html#GstX264Enc--key-int-max)を読むと、key-int-max=1にすると全てがキーフレームになるので、再生のために補足情報を含む必要がなくなり、SEIが設定されるパケットはなくなります。

## 内容の精査
### 本当にこれが解決になっているのか？

key-int-max=1にするということは、映像を全てIフレームにするということです。これだとH.264で作り込まれている差分フレームの機能を一切利用しないということで、圧縮率を考えるとmotion jpegと大して変わらなくなってしまいます。
これではH.264を利用する意味が全く無いので、解決策として提示されると微妙です。

正直なところSEI NALフラグが立ってても映像出せよと思うのですが、実際問題として映像が出ないので対処するしかありません。
x264がフラグを立てるのが問題だという話まで戻って、別のエンコーダを利用することにします。

### 別のエンコーダの導入

Google Chromeで映像が出ないということなので、Google Chromeが想定しているエンコーダを利用するのがよいでしょう。
ChromeはOpenH264というCisco製のエンコーダを利用しているようです。
[Chrome Status](https://www.chromestatus.com/feature/6417796455989248)
[chromiumのソースコード](https://chromium.googlesource.com/chromium/src/third_party/+/master/openh264/)

従ってOpenH264を利用することを考えます。

### OpenH264のinstall

[openh264enc](https://gstreamer.freedesktop.org/data/doc/gstreamer/head/gst-plugins-bad/html/gst-plugins-bad-plugins-plugin-openh264.html)というgStreamer pluginがあるようです。
これはgst-plugins-badというpluginパッケージの一つのようですが、apt installしても入りません。

```shell
$sudo apt install gstreamer1.0-plugins-bad
$ gst-inspect-1.0 openh264enc
No such element or plugin 'openh264enc'
```

gst-inspect-1.0というのはインストールしているpluginの情報を出力するコマンドですが、そいつが知らないと言っている…

なんで入らないんだろうと思って調べると以下のようなページがありました。
[why H264 is NOT supported: Plugin 'openh264' not found?](https://github.com/Kurento/bugtracker/issues/255)

```
It's not installed by default because the plugin that implements it downloads a proprietary binary blob from cisco.
```

プロプライエタリ・ソフトウェアなのでデフォルトでは入らないんだよね。と。
なるほど。
ただこの記事にある

```shell
$sudo apt-get install openh264-gst-plugins-bad-1.5 
```

ではUbuntu18.04には入らないようでした。何かのリポジトリを足さないといけないのかな。

## 解決手順

### install

要するにOpenH264とgStreamer Pluginだけ入れればいいのでgithubから落としてきて入れてやります。
ビルドにnasmが必要なのでそれも入れます。

```shell
$sudo apt install nasm
$git clone https://github.com/cisco/openh264.git
$cd openh264
$make && sudo make instlal
```

次にgst-plugins-badを入れますが、そのままビルドしても入らないので、configure.acファイルを編集する必要が有ります。
[この行をtrueにする必要があります](https://github.com/Kurento/gst-plugins-bad/blob/master/configure.ac#L3408)

git cloneしてconfigure.acを編集したら、

```shell
$./autogen.sh
$make && sudo make install
```

make中に

```
error: field ‘aes_ctx’ has incomplete type
   EVP_CIPHER_CTX aes_ctx;
```

というようなエラーがでた場合は、OpenSSLのバージョンが古いので、最新版を入れて下さい。

```shell
$sudo install libssl1.0-dev
```

但し、gstreamer本体をaptでinstallした場合、make installした場合にpluginが入るディレクトリと、gstreamerが利用するpluginを配置しないといけないディレクトリが異なります。実際にどこに入れればいいかは、前述のgst-inspect-1.0コマンドで調べるとよいでしょう。例えば私の手元の環境でudpsrc pluginについて調べると以下のようになります。

```shell
$gst-inspect-1.0 udpsrc
Factory Details:
  Rank                     none (0)
  Long-name                UDP packet receiver
...中略...
Plugin Details:
  Name                     udp
  Description              transfer data via UDP
  Filename                 /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libgstudp.so
  Version                  1.12.5
```

/usr/lib/x86_64-linux-gnu/gstreamer-1.0/ 配下にいれておけば良いようです。
ma
ke installした場合/usr/local/lib/gstreamer-1.0/ 配下にあるためコピーしてやります。

```shell
$sudo cp /usr/local/lib/gstreamer-1.0/libopenh264* /usr/lib/x86_64-linux-gnu/gstreamer-1.0/
```

念の為確認

```shell
$gst-inspect-1.0 openh264enc
Factory Details:
  Rank                     marginal (64)
  Long-name                OpenH264 video encoder
  Klass                    Encoder/Video
  Description              OpenH264 video encoder
...以下省略...
```

### 実行

gStreamer pipelineのx264encの場所をopenh264encに変更すればよいです。例えば以下のようなスクリプトの場合、

```shell
gst-launch-1.0 autovideosrc name=src0 ! video/x-raw,width=640,height=480 ! videoconvert ! x264enc bitrate=90000 pass=quant quantizer=25 rc-lookahead=0 sliced-threads=true speed-preset=superfast sync-lookahead=0 tune=zerolatency ! rtph264pay pt=100 mtu=1400 config-interval=3 ! udpsink port=50001 host=127.0.0.1 sync=false
```

以下のようにすればよいです

```shell
gst-launch-1.0 autovideosrc name=src0 ! video/x-raw,width=640,height=480 ! videoconvert ! openh264enc ! rtph264pay pt=100 mtu=1400 config-interval=3 ! udpsink port=50001 host=127.0.0.1 sync=false
```

実際に実行してみると、Mac版のGoogle Chromeでも映像が出ました。
x264encの場合よりは遅延が小さく、映像品質は低いものが出力されます。OpenH264はハードウェアエンコーダを利用するので、ARMの場合でも比較的高速にエンコーディングされます(ハードウェアエンコーダを搭載していれば)。
映像品質については[pluginの説明ページ](https://thiblahute.github.io/GStreamer-doc/openh264-1.0/openh264enc.html?gi-language=c)を見ながら各パラメータを調整していくといいでしょう。

私の場合は大体以下の感じで動かしています

```
openh264enc enable-denoise=true qp-max=20 complexity=high background-detection=true rate-control=off
```

## まとめ

MacのGoogle Chromeではx264でエンコーディングした映像が表示されないため、openh264(Google Chromeで利用しているエンコーダ)を利用してエンコーディングしてみたところ映像が表示されるようになった。

(*言うまでもないことですが、本記事で出てくるアプリケーションは提供元のライセンスに従って利用してください。)