SkyWay 配信者側で加工した映像を配信する

## 概要

弊社のインターンのお手伝いで、たまたまWebRTCを触る機会があったので、

気軽に加工した映像を配信したりできるのかなと思って試したので記します :pencil: 

※ 本来なら、MCU ( _Multipoint Control Unit_ )とかで映像加工するのかなと思うんですが、

今回は、インターン期間中に気軽に実装できそうなものくらいの感覚なので

配信者側（クライアント側）で映像を加工して配信する想定です。

## 本題

今回は、[SkyWay](https://webrtc.ecl.ntt.com/) さんのサービスを利用します。（簡単にビデオ通話が実装できます）

結論から言うと、 **カメラから取得した映像ストリームをCanvasに描画し、**

**Canvasで加工したものを再び映像ストリームとして取得したものを配信すれば可能** です。

なので、👇のように、白黒モザイク加工を施した上で、Websocketで受け取ったコメントを映像に載せて配信なんかもできます
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547621/6f681f00-3869-d783-4fdd-2e059b4477f7.png" width="480">



簡易版として、[チュートリアル 1:1ビデオ通話のサンプルコード](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#_2-%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E4%BD%9C%E6%88%90)を元に、

カメラから取得した映像を **白黒加工して配信** していきます  

カメラから映像を取得したら発火する処理に追記してく感じです :pencil: 

```js
<script>
let localStream;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then( stream => {
  /**
   * ここに映像を加工する処理を書く
   */

  // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
  localStream = stream;
```

最初にカメラから取得した映像を流すためのVideoタグに加えて、加工用のCanvasタグを追加しておきます

```html
<canvas id="my-canvas" width="640" height="360"></canvas>
<video id="my-video" width="640" height="360" autoplay></video>
```

追加したCanvasに Videoに流しているカメラから取得した映像を描画しながら、ピクセル単位で白黒にします

```js
navigator.mediaDevices.getUserMedia(
..
)
.then( stream => {
    const videoElm = document.getElementById('my-video')
    const canvasElm = document.getElementById('my-canvas')
    const ctx = canvasElm.getContext('2d'); // CanvasRenderingContext2D インスタンス

    setInterval(() => {
        ctx.drawImage(videoElm, 0, 0, canvasElm.width, canvasElm.height); // Videoに描画されている内容をCanvasにも描画する
        var frame = ctx.getImageData(0, 0, canvasElm.width, canvasElm.height); // ImageData インスタンス
        var pixels = frame.data.length / 4; // ピクセルサイズ長 (今回の場合 640 x 360)

        for (var i = 0; i < pixels; i++) {
            const grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3;

            frame.data[i * 4 + 0] = grey // Red
            frame.data[i * 4 + 1] = grey // Green
            frame.data[i * 4 + 2] = grey // Blue
        }
        ctx.putImageData(frame, 0, 0);
    }, 10000/120);

    videoElm.srcObject = stream;
    videoElm.play();

    // Canvasから映像ストリームを取得する
    localStream = canvasElm.captureStream(120); // 120fps
```

順に見ていきます

カメラから映像を取得できたら、Canvasから [`CanvasRenderingContext2D`](https://developer.mozilla.org/ja/docs/Web/API/CanvasRenderingContext2D) という２次元のレンダリングコンテキストを取得してきます。

これに手を加えていくことでCanvasを加工していくことが可能です。

```js
    const ctx = canvasElm.getContext('2d');
```

次に、 `setInterval` で定期的に、さきほどの `ctx` に対して加工を加えていきます

（今回の場合、120fps で加工を加えていきます）

```js
    setInterval(() => {
        /**
         * ここに ctx に対して加工を加えていく処理を書く
         */
    }, 10000/120);
```

`setInterval` 内での処理に入り、 最初に Videoに描画されている内容をCanvasにも描画します

```js
    ctx.drawImage(videoElm, 0, 0, canvasElm.width, canvasElm.height);
```

Videoの映像が描画された `ctx` を加工していきたいので、 `getImageData` を実行し [`ImageData`](https://developer.mozilla.org/ja/docs/Web/Guide/HTML/Canvas_tutorial/Pixel_manipulation_with_canvas) を取得してきます

`ImageData` の `data` を実行すると、 1ピクセルごとのRGBAの値が格納された一次元配列（正確には `Uint8ClampedArray`）を取得できます。
RGBA: 赤・緑・青・アルファの値が `0 ~ 255` で格納されてます

なので配列の中身としては以下のようなものになります

```
[
  1ピクセル目の赤, 1ピクセル目の緑, 1ピクセル目の青, 1ピクセル目のアルファ,
  2ピクセル目の赤, 2ピクセル目の緑, 2ピクセル目の青, 2ピクセル目のアルファ,
  ..
]
```

`4` で割ってCanvasのピクセルサイズ長を算出しているのはそのためです

```js
        var frame = ctx.getImageData(0, 0, canvasElm.width, canvasElm.height);
        var pixels = frame.data.length / 4;
```

後は、ピクセルサイズ長だけ繰り返しで、RGBの平均値を算出し、値を代入していきます

```js
        for (var i = 0; i < pixels; i++) {
            const grey = (frame.data[i * 4 + 0] + frame.data[i * 4 + 1] + frame.data[i * 4 + 2]) / 3;

            frame.data[i * 4 + 0] = grey // Red
            frame.data[i * 4 + 1] = grey // Green
            frame.data[i * 4 + 2] = grey // Blue
        }
```

`setInterval` の最後に `ctx` に `putImageData` を実行すれば、Canvasに白黒が反映されます

```js
        ctx.putImageData(frame, 0, 0);
```

加工したCanvasから映像ストリームを取得するには `captureStream` を利用します

(`setInterval` に合わせて 120fps にしてあります）

```js
    localStream = canvasElm.captureStream(120);
```


これで、加工した映像ストリームを取得できたので、カメラから取得した映像ストリームをP2P通信に流すのと同じようにすればできます。

```js
    const mediaConnection = peer.call(theirID, localStream);
```

　　

最後に、今回はSkyWayを利用して、さくっとクライアント側で映像を加工して配信する方法を試してみました。

おそらく本来はこういう感じにはしないのでは（？）と思ったりしているので、詳しい方がいらっしゃれば教えていただきたいところです :bow_tone1: 

　　

あと、 `setInterval` を利用しているので、ChromeやFirefoxなどのブラウザを利用していて、別タブに移動したりすると、非アクティブと認識され、映像がカクカクするようになるので注意です :rotating_light: 

MDNの [`setInterval`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval) にも以下のような記載があります
> Starting in Gecko 5.0 (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), intervals are clamped to fire no more often than once per second in inactive tabs.