Canvas APIを活用してWebRTCによるビデオ通話の帯域を削減する

## はじめに
この記事は、WebRTCのビデオ通話の帯域幅を自分で削減したい、という方向けの記事です。
WebRTCにはユーザの回線状況に合わせて動的にビットレートを調整する機能がありますが、それとは別軸の話です。

ビデオ通話の通信量が想定していたより大きいので節約したい...
解像度は低くていいから、細い回線でもビデオ通話できるようにしたい...

こんな声を開発者からいただくことがあります。
その回答としてこの記事が役立てばと思いこの記事を書きました。

## なぜCanvas APIなのか
帯域削減の解となる選択肢はいくつかあると思います。以下にその例を列挙してみました。

1. `getUserMedia()`の`MediaTrackConstraintsを`使って解像度・フレームレートを調整する
2. Canvas APIを使って映像の解像度・フレームレートを調整する
3. 帯域削減に有効な映像コーデックを指定する（例:VP8からVP9に変える）
4. 機械学習アプローチを活用して送信する情報量を抑える

1.ですが、gUMのConstraintsの挙動はブラウザ実装依存により結構変わり、Chromeだと動くのにFirefoxだと動かない...といったことが発生します。
また、Webカメラの仕様にも依存しており、Constraintsで指定した解像度が使えない場合もあります。
Constraintsの仕様や挙動については以下の記事が素晴らしいので参考にしてみてください。

[getUserMedia()で指定できるMediaTrackConstraintsのよもやま](https://lealog.hateblo.jp/entry/2017/08/21/155211)
[getUserMedia()のconstraintsでカメラデバイスおよび出力解像度の選択に使用されるfitness distanceアルゴリズムについて](https://qiita.com/gtk2k/items/d5fdc55cb96f485e7913)

3.は指定したコーデックに対応していないデバイスであれば当然この手法は使えません。
また、新しいコーデックを採用することにより帯域幅を抑えられるが、その分CPU使用率が上がります。なので、ある程度高いスペックの端末が必要になってくるかもしれません。

4.はめっちゃ良さそうなんですが、何かとつよいGPUが必要だったりするので、一般的なスペックのPCやスマホでは現状気軽に取り扱えないかも...。

そこで、今回はデバイス依存がなく、ブラウザ依存も少ないと思われ、かつ気軽に扱えそうな2.のCanvas APIによるクライアントサイドでの解像度・フレームレート調整方法を紹介します。

## 手順

まず、HTMLでcanvas要素を追加します。

```html
<canvas id="js-canvas"></canvas>
```

次に、canvasを使った映像加工に移ります。

```javascript
// 解像度を調整する
const context = canvas.getContext('2d');
canvas.width = 100;
canvas.height = 100;
_canvasUpdate();

function _canvasUpdate() {
  context.drawImage(localVideo, 0, 0, canvas.width, canvas.height);
  requestAnimationFrame(_canvasUpdate);
}

// フレームレートを調整する
const newStream = canvas.captureStream(30);
```

canvas自体はあくまでピクセルを制御できる矩形領域です。
自分で描画する能力を持っているわけではないので、`canvas.getContext('2d')` により`CanvasRenderingContext2D`インスタンスを取得し、canvas上に描画できるようにします。

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D

`CanvasRenderingContext2D.drawImage()`は指定したイメージを描画するメソッドです。
今回使っている`drawImage(image, x, y, width, height)`は拡大・縮小したイメージを描画できるやつです。
切り抜きもできるので、アスペクト比を固定したいなどの要件に応じて適宜使い分けてください。

https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D/drawImage
https://developer.mozilla.org/ja/docs/Web/Guide/HTML/Canvas_tutorial/Using_images

ちなみに、Canvas APIは`HTMLImageElement`、`HTMLVideoElement`、`HTMLCanvasElement`のデータ形式を画像ソースとして扱えます。
なので、普通にストリーム描画用の変数（上のコードだと`localVideo`）を引数に代入してOKです。

ビデオフレームは刻一刻と更新されるので、再帰的に更新処理を行います。
上のコードでは`requestAnimationFrame`を使って約60fps間隔で更新処理を行っています。

`canvas.captureStream(frameRate)`は指定したフレームレートでcanvasからMediaStreamを取得するメソッドです。

https://developer.mozilla.org/ja/docs/Web/API/HTMLCanvasElement/captureStream

以上で解像度・フレームレートの調整は完了です。
簡単ですね。

## 実際どれくらい帯域削減できるのか

[SkyWay JS SDKのサンプルコード](https://github.com/skyway/skyway-js-sdk/tree/master/examples/p2p-media)をベースに、送信帯域幅が削減できているかを検証しました。
下のケースについて、webrtc-internalsのRTCOutboundRTPVideoStreamから秒間伝送ビットレートを確認してみました。

1. gUMするだけ、何も制約しない
2. Canvas APIで解像度を100x100に、フレームレートを30に設定
3. Canvas APIで解像度を100x100に、フレームレートを5に設定

結論から言うと、解像度・フレームレートの調整はちゃんとできており、秒間伝送ビットレートも下がっていることが確認できました。

### 1. gUMするだけ、何も制約しない

![スクリーンショット 2020-10-08 16.17.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148447/c2d3b5ae-dc9d-3b44-baf1-4ee67e3114e1.png)


WebRTCらしく動的にビットレートが変わってます。平均すると1.5Mbpsってとこでしょうか。
解像度はビットレートの浮き沈みに応じて1280x720になったり640x360になったりしています。
想定はできますが、改めてグラフ見ると面白いですね。
FPSはほぼずっと30をキープしてるように見えます。

### 2. Canvas APIで解像度を100x100に、フレームレートを30に設定

![スクリーンショット 2020-10-08 16.28.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148447/01e71b14-c015-4260-2ad1-1d6f633da8ec.png)

結構ビットレートが揺れてますが、だいたい400kbpsまで落ちました。
解像度はしっかり100x100になってますね。
意外だったのはFPSがめっちゃ揺れてること。ビットレートの浮き沈みに追従しているようなグラフになっています。後半は落ち着いてほぼ30をキープしています。

### 3. Canvas APIで解像度を100x100に、フレームレートを5に落とす

![スクリーンショット 2020-10-08 16.39.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148447/85e5df96-b7c1-bbf9-a456-592450125539.png)

ビットレートは約100kbpsまで落ちました。
フレーム数が1/6になったのに比例してビットレートも100kbps以下になると想定してましたが、完全に比例するわけではなさそうです。

とは言え、解像度・ビットレートの調整は帯域削減にガッツリ効いてくることが分かりました。

## まとめ
Canvas APIは帯域削減に有効な手段と言えそうです。
映像加工する上で非常な優秀なAPIでもあるので、videoタグでの表示から脱却してcanvasを活用してみても良いかもですね！

## ネタ枠

限界を超えるとどうなるのか。
### Canvas APIで解像度を1x1に、フレームレートを1に落とす

WebRTC的には繋がりましたが、
伝送ビットレートは0、解像度とFPSに至っては表示されないという結果に。
frameSentも0だったので、1x1だとそもそもフレームを送れないっぽい。

### Canvas APIで解像度を2x2に、フレームレートを1に落とす

![スクリーンショット 2020-10-08 17.00.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148447/017df2d4-3f27-36b0-de01-0abdd537aaad.png)

もはや実態が何なのかまるで分からない映像を対向に届けられました。
ビデオ通話してるはずなのに、伝送ビットレートは250bpsまで落ちました。すごい。（使えるどうかはアイデア次第...？）