Twilio WebRTCハンズオン（バーチャル背景編）

:::note alert
2023年5月1日を持ちまして、株式会社KDDIウェブコミュニケーションズのTwilioリセール事業が終了したため、本記事に記載されている内容は正確ではないことを予めご了承ください。
:::

# はじめに

みなさん、こんにちは。
KDDIウェブコミュニケーションズの Twilio 事業部エバンジェリストの高橋です。

本記事は、以下の記事の内容をリメイクして、バーチャル背景を追加したものです。

https://qiita.com/mobilebiz/items/a3e59f295f5e4c963638

上記記事は、Twilio を初めて触る方などを対象としており、管理コンソール上のエディタを使って WebRTC Go を理解する目的で書かれています。
一方、Twilio をある程度理解されているエンジニアの方にとっては、ローカル環境で開発をしたいというニーズも多いかと思われます。そこで本記事では、WebRTC Go のアプリケーションをローカル環境で構築する方法について説明したいと思います。

# 準備

## 環境
本記事を実施するにあたって、以下の環境が必要です。

- Twilio アカウント : [こちら](https://cloudapi.kddi-web.com/signup)から作成しましょう（トライアルアカウントでも OK です）
- Twilio CLI のセットアップ : [こちらの記事](https://qiita.com/mobilebiz/items/456ce8b455f6aa84cc1e)を参考にしてください
- Twilio CLI Serverless plugins : [こちらの記事](https://qiita.com/mobilebiz/items/fb4439bf162098e345ae)を参考にしてください
- VSCodeなどのエディタ

## Twilio 側の準備

### サブアカウントの作成

:::note info
すでに作業用のサブディレクトリがある場合は、そちらで作業をしていただいても問題ありません。
その場合は、作業ディレクトリのAccountSidを控えておいてください。
:::

- Twilio 管理コンソールにログインします。
- **Dashboard** > **設定** > **サブアカウント** を選択します。

![WebRTCGoCLI_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b497ce6a-cb33-111e-5e23-e4af5ecee5d1.png)
- 赤い＋アイコンを押します。
- **わかりやすい名前**欄に、「WebRTC Go CLI」と入力して、**Create**ボタンを押します。

![スクリーンショット 2020-11-24 8.15.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/959e2205-2ecd-da17-214a-98d3b05fd72b.png)
- アカウント名の一覧に今作成した WebRTC Go CLI が表示されるので、そちらをクリックしてサブアカウントに移動します。
- 表示されたサブアカウントの**アカウントSID**と、**AUTHTOKEN**（View を押すと表示されます）を控えておきます。

![WebRTCGoCLI_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/a06ff5b2-73c4-c6e1-d5df-b433af935fc2.png)

## API キーの作成

- サブアカウントの **Dashboard** > **設定** > **API キー** を選択します。
- 赤い**新しい API キーを作成する**ボタンを押します。
- **わかりやすい名前**欄に、「Video」と入力して、**キータイプ**は「Standard」を選択し、**API キーを作成する**ボタンを押します。

:::note info
新コンソールでは、API Keyの作成は画面上部の**Account**メニューから行うようになります。
また、リージョンの指定ができるようになりますが、**US**を選択してください。
:::

![スクリーンショット 2020-11-24 8.26.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/fd603bc7-7eb1-5892-41c8-7c181a53aa17.png)
- 生成された SID（これが API Key になります）と、SECRET を控えておきます。

:::note warn
SECRET はこの画面でしか確認できませんのでご注意ください。
:::

![WebRTCGoCLI_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/0be6af70-b3ea-c7a6-b65b-6ce177215fe1.png)
- **完了しました**にチェックを入れて、**終了**ボタンを押します。

## ソースコードの準備

この記事のソースコードは以下に準備してあります（ virtual-background ブランチ）。
https://github.com/mobilebiz/twilio-video-handson-webrtc-go/tree/virtual-background

```sh
$ git clone -b virtual-background https://github.com/mobilebiz/twilio-video-handson-webrtc-go.git
$ cd twilio-video-handson-webrtc-go
$ npm install 
```

## 環境変数の設定

ダウンロードしたフォルダの、`.env.sample` を `.env` にコピーしてご自分の環境に併せて編集します。

```sh
$ cp .env.sample .env
```

```text:.env
ACCOUNT_SID=AC から始まる文字列（先程控えた作業アカウントのものと一致していることを確認）
AUTH_TOKEN=（設定しなくてOKです）

# Variables for function ".env"
# ---
TWILIO_VIDEO_KEY=上記、ACCOUNT_SIDと同じ SK から始まる文字列
TWILIO_VIDEO_SECRET=上記、AUTH_TOKENと同じ、API SECRET
```

## ローカル環境での実行

ここまでの状態で、ローカル環境での実行環境が整いましたので、`npm start` コマンドを使って早速起動してみましょう。

```sh
$ npm start
> twilio-video-handson-webrtc-go@1.0.0 start
> twilio-run --env


│ WARNING Different Node.js Version Found
│ 
│ You are currently running Node.js 12.8.0 on this local machine. The production environment for Twilio Serverless currently supports versions 10.x.
│ 
│ When you deploy to Twilio Serverless, you may encounter differences between local development and production.
│ 
│ For a more accurate local development experience, please switch your Node.js version.
│ A tool like nvm (https://github.com/creationix/nvm) can help.

┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                                                                                                          │
│   Twilio functions available:                                                                                                                                            │
│   └── /video-token | http://localhost:3000/video-token                                                                                                                   │
│                                                                                                                                                                          │
│   Twilio assets available:                                                                                                                                               │
│   ├── /video.html | http://localhost:3000/video.html                                                                                                                     │
│   ├── /video.js | http://localhost:3000/video.js                                                                                                                         │
│   ├── /images/background.jpeg | http://localhost:3000/images/background.jpeg                                                                                             │
│   ├── /twilio-video-processors-assets/selfie_segmentation_landscape.tflite | http://localhost:3000/twilio-video-processors-assets/selfie_segmentation_landscape.tflite   │
│   ├── /twilio-video-processors-assets/tflite-1-0-0.wasm | http://localhost:3000/twilio-video-processors-assets/tflite-1-0-0.wasm                                         │
│   ├── /twilio-video-processors-assets/tflite-simd-1-0-0.js | http://localhost:3000/twilio-video-processors-assets/tflite-simd-1-0-0.js                                   │
│   ├── /twilio-video-processors-assets/tflite-1-0-0.js | http://localhost:3000/twilio-video-processors-assets/tflite-1-0-0.js                                             │
│   ├── /twilio-video-processors-assets/tflite-simd-1-0-0.wasm | http://localhost:3000/twilio-video-processors-assets/tflite-simd-1-0-0.wasm                               │
│   ├── /twilio-video-processors-assets/twilio-video-processors.js | http://localhost:3000/twilio-video-processors-assets/twilio-video-processors.js                       │
│   └── /twilio-video-processors-assets/twilio-video-processors.min.js | http://localhost:3000/twilio-video-processors-assets/twilio-video-processors.min.js               │
│                                                                                                                                                                          │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

```

無事に起動が確認できたら、早速ブラウザ上で以下のURLを起動しましょう。

http://localhost:3000/video.html

※初回は、カメラとマイクのアクセス許可が出るかもしれませんが、もし出た場合は許可してください。

![スクリーンショット 2022-05-29 7.04.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/db551612-81b4-0470-5149-3a9977ffac63.png)

**入室**ボタンを押してルームに入ります。
ターミナル画面には、以下のような HTML ファイル、 JS ファイルへのアクセスと、アクセストークン取得用 Function である `video-token?roomName=VideoRoom` が表示されます。

```sh
200 GET /video.html │ Response Type text/html; charset=UTF-8
200 GET /video.js?version=1.01 │ Response Type application/javascript; charset=UTF-8
200 GET /twilio-video-processors-assets/twilio-video-processors.js │ Response Type application/javascript; charset=UTF-8
200 GET /images/background.jpeg │ Response Type image/jpeg
200 GET /twilio-video-processors-assets/tflite-simd-1-0-0.js │ Response Type application/javascript; charset=UTF-8
200 GET /twilio-video-processors-assets/selfie_segmentation_landscape.tflite │ Response Type application/octet-stream
200 GET /twilio-video-processors-assets/tflite-simd-1-0-0.wasm │ Response Type application/wasm
```

ローカルサーバーを停止するには、`Ctrl+C` を押下します。

## コードの解説

バーチャル背景は少し難解なので、ここでコード解説をします。

```video.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Twilio Video</title>
  </head>
  <body>
    <div id="room-controls">
      <video id="myStream" autoplay muted="true"></video>
      <button id="button-join">入室</button>
      <button id="button-leave" disabled>退室</button>
    </div>
  </body>
  <script src="./twilio-video-processors-assets/twilio-video-processors.js"></script>
  <script src="//media.twiliocdn.com/sdk/js/video/releases/2.21.1/twilio-video.min.js"></script>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  <script src="./video.js?version=1.01"></script>
</html>
```

下から5行目、`twilio-video-processors.js`は、CDNで一般公開されていないので、今回はAssestsから読み込むようにしています。
次の行の`twilio-video`のSDKは、バージョンが古いと動作しないので、最新版（この記事執筆時は、2.21.1）を読み込むようにしてください。

```video.js
(async () => {
  'use strict';

  const TWILIO_DOMAIN = location.host; // 現在のURL
  const ROOM_NAME = 'VideoRoom'; // 部屋の名前
  const Video = Twilio.Video; // Twilio Video JS SDK
  const VideoProcessors = Twilio.VideoProcessors; // Twilio Video Processors
  const BACKGROUND_IMAGE = './images/background.jpeg'; // 背景画像
  let videoRoom;
  let localTracks, virtualBackground;
  const img = new Image();

  // プレビュー画面の表示
  localTracks = await Video.createLocalTracks();
  const localVideo = document.getElementById('myStream');
  localTracks.forEach((track) => {
    if (track.kind === 'video' || track.kind === 'audio') {
      track.attach(localVideo);
    }
  });

  // バーチャル背景を設定
  img.onload = () => {
    virtualBackground = new VideoProcessors.VirtualBackgroundProcessor({
      assetsPath: `${document.location.protocol}//${TWILIO_DOMAIN}/twilio-video-processors-assets/`,
      backgroundImage: img,
    });
    virtualBackground.loadModel().then(async () => {
      if (localTracks) {
        const videoTrack = localTracks.filter(
          (track) => track.kind === 'video',
        );
        if (!videoTrack[0].processor) {
          videoTrack[0].addProcessor(virtualBackground);
        }
      }
    });
  };
  img.src = BACKGROUND_IMAGE;

  // ボタンの準備
  const btnJoinRoom = document.getElementById('button-join');
  const btnLeaveRoom = document.getElementById('button-leave');

  // 入室ボタンが押されたときの処理
  btnJoinRoom.onclick = () => {
    // アクセストークンを取得
    axios
      .get(
        `${document.location.protocol}//${TWILIO_DOMAIN}/video-token?roomName=${ROOM_NAME}`,
      )
      .then(async (body) => {
        const token = body.data.token;
        console.log(`Token got. ${token}`); // 本番環境ではコメントアウトしましょう

        connectRoom(token); // ルームに接続
      });
  };

  // 退出ボタンが押されたときの処理
  btnLeaveRoom.onclick = () => {
    // 部屋から退室
    videoRoom.disconnect();
    console.log(`Disconnected to Room ${videoRoom.name}`);
    btnJoinRoom.disabled = false;
    btnLeaveRoom.disabled = true;
  };

  // ルームに接続
  const connectRoom = (token) => {
    // 部屋に入室
    Video.connect(token, { name: ROOM_NAME, tracks: localTracks })
      .then((room) => {
        console.log(`Connected to Room ${room.name}`);
        videoRoom = room;

        // すでに入室している参加者を表示
        room.participants.forEach(participantConnected);

        // 誰かが入室してきたときの処理
        room.on('participantConnected', participantConnected);

        // 誰かが退室したときの処理
        room.on('participantDisconnected', participantDisconnected);

        // 自分が退室したときの処理
        room.once('disconnected', (error) =>
          room.participants.forEach(participantDisconnected),
        );

        btnJoinRoom.disabled = true;
        btnLeaveRoom.disabled = false;
      })
      .catch((err) => console.error(err));
  };

  // 他の参加者が入室したとき
  const participantConnected = (participant) => {
    console.log(`Participant ${participant.identity} connected'`);

    // 参加者を表示する
    const div = document.createElement('div');
    div.id = participant.sid;

    // 参加者のトラック（映像、音声など）を処理
    participant.tracks.forEach((publication) => {
      if (publication.isSubscribed) {
        trackSubscribed(div, publication.track);
      }
    });

    // 参加者の映像が届いたとき
    participant.on('trackSubscribed', (track) => trackSubscribed(div, track));

    // 参加者の映像が切れたとき
    participant.on('trackUnsubscribed', trackUnsubscribed);

    document.body.appendChild(div);
  };

  // 他の参加者が退室したとき
  const participantDisconnected = (participant) => {
    console.log(`Participant ${participant.identity} disconnected.`);

    // 他の参加者の画面を削除する
    document.getElementById(participant.sid).remove();
  };

  // トラックの購読
  const trackSubscribed = (div, track) => {
    // トラックをアタッチする
    div.appendChild(track.attach());
  };

  // トラックの非購読
  const trackUnsubscribed = (track) => {
    // トラックのデタッチ
    track.detach().forEach((element) => element.remove());
  };
})();
```

表示したい画像ファイルは、コードの上の方で指定します。
```javascript:背景画像の指定
const BACKGROUND_IMAGE = './images/background.jpeg'; // 背景画像
```

:::note warn
画像のソースを外部サーバーに指定した場合、外部サーバーがクロスサイトスクリプティングチェックをしていると、画像が取得できません。その場合は、
img.src = '外部サイトの画像'
img.crossorigin = 'anonymous';
のように指定ができます。ただし、不正なコードを実行してしまう可能性もあるので、利用するときは気をつけてください。
:::

バーチャル背景を表示させるためには、Twilio の`localVideoTrack.addProcessor()`が使えなくてはいけないので、今回はプレビュー画面の表示に`navigator.mediaDevices.getUserMedia()`を利用せず、以下のように`Video.createLocalTracks()`を使ってローカルトラックを取得します。
`Video.createLocalTracks()`では、ビデオやオーディオ以外にも、データトラックなども一括で取得できるので、念の為にビデオとオーディオトラックだけをDOMにアタッチしています。

```javascript:プレビュー画面の取得
  localTracks = await Video.createLocalTracks();
  const localVideo = document.getElementById('myStream');
  localTracks.forEach((track) => {
    if (track.kind === 'video' || track.kind === 'audio') {
      track.attach(localVideo);
    }
  });
```

バーチャル背景の設定するには、`VideoProcessors.VirtualBackgroundProcessor()`を利用してプロセッサを作る必要があります。

:::note info
プロセッサというのは、画像などを加工するための仕組みで、適用したいトラックに追加して利用します。
:::

https://twilio.github.io/twilio-video-processors.js/classes/virtualbackgroundprocessor.html

ちなみに、バーチャル背景に画像ではなく背景ぼかしを使いたい場合は、`VideoProcessors.GaussianBlurBackgroundProcessor()`を利用します。

https://twilio.github.io/twilio-video-processors.js/classes/gaussianblurbackgroundprocessor.html

`assetsPath`には、バーチャル背景を表示させるための機械学習モデルを指定します。今回、このモデルはAssetsフォルダの`twilio-video-processors-assets`に格納されていて、これが外部から参照できる必要があります。
学習モデルが適用できたら、次にモデルを利用できるようにします（`loadModel()`）。ここは場合によっては少し時間がかかることもあります。
ロードが完了したら、最後にローカルビデオトラックに`addProcessor()`することによって、以後このローカルビデオトラックにプロセッサによる加工がされるようになります。
なお、今回は実装には入れていませんが、バーチャル背景を解除したい場合は、`removeProcessor()`を利用します。

```javascript:バーチャル背景を表示
  img.onload = () => {
    virtualBackground = new VideoProcessors.VirtualBackgroundProcessor({
      assetsPath: `${document.location.protocol}//${TWILIO_DOMAIN}/twilio-video-processors-assets/`,
      backgroundImage: img,
    });
    virtualBackground.loadModel().then(async () => {
      if (localTracks) {
        const videoTrack = localTracks.filter(
          (track) => track.kind === 'video',
        );
        if (!videoTrack[0].processor) {
          videoTrack[0].addProcessor(virtualBackground);
        }
      }
    });
  };
  img.src = BACKGROUND_IMAGE;
```

以上がバーチャル背景のコード詳細となります。

# サーバーにデプロイ

ローカル環境でのテストが終わりましたので、いよいよサーバーにデプロイします。

## Twilio CLI の設定

:::note info
このステップは、すでにプロファイルが作成されている場合は不要です。
:::

まずは、Twilio CLI の Profile を設定します。
`twilio profiles:create` コマンドを使って新しく Profile を作成してください。

```sh
$ twilio profiles:create
You can find your Account SID and Auth Token at https://www.twilio.com/console
 » Your Auth Token will be used once to create an API Key for future CLI access to your Twilio Account or Subaccount, and then forgotten.
? The Account SID for your Twilio Account or Subaccount: <-- 控えておいたACから始まるアカウントSID を入力
? Your Twilio Auth Token for your Twilio Account or Subaccount: [hidden] <-- 控えておいたAuthTokenを入力
? Shorthand identifier for your profile: WebRTC Go CLI <--　プロファイル名として「WebRTC Go CLI」を入力
Created API Key SKxxxxxxxxxxxxxxxxxxxxxxxx and stored the secret in your keychain. See: https://www.twilio.com/console/runtime/api-keys/SKxxxxxxxxxxxxxxxxxxxxxxxx
twilio-cli configuration saved to "/Users/katsumi/.twilio-cli/config.json"
Saved WebRTC Go CLI.
```

このような感じで Profile が作成されますので、早速切り替えてみましょう。

```sh
$ twilio profiles:use 'WebRTC Go CLI'
set "WebRTC Go CLI" as active profile
twilio-cli configuration saved to "/Users/katsumi/.twilio-cli/config.json"
```

※プロファイル名にスペースを入れた場合、シングルコーテーションで囲う必要があります。

## デプロイ

ではいよいよサーバーへのデプロイです。
`twilio serverless:deploy` コマンドを使ってデプロイをしてみましょう。

```sh
$ twilio serverless:deploy

Deploying functions & assets to the Twilio Runtime

Account		SK****************************
Token		pQRS****************************
Service Name	twilio-video-handson-webrtc-go
Environment	dev
Root Directory	/Users/katsumi/Documents/workspace/webRTC/twilio-video-handson-webrtc-go
Dependencies
Env Variables	MAIN_ACCOUNT_SID, TWILIO_VIDEO_KEY, TWILIO_VIDEO_SECRET

✔ Serverless project successfully deployed

Deployment Details
Domain: twilio-video-handson-webrtc-go-XXXX-dev.twil.io
Service:
   twilio-video-handson-webrtc-go (ZS****************************)
Environment:
   dev (ZE****************************)
Build SID:
   ZB****************************
View Live Logs:
   Open the Twilio Console
Functions:
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/video-token
Assets:
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/images/background.jpeg
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/selfie_segmentation_landscape.tflite
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/tflite-1-0-0.js
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/tflite-1-0-0.wasm
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/tflite-simd-1-0-0.js
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/tflite-simd-1-0-0.wasm
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/twilio-video-processors.js
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/twilio-video-processors-assets/twilio-video-processors.min.js
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/video.html
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/video.js
```

上記のように、正常にデプロイが完了すると URL が払い出されます。
では、払い出された https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/video.html （XXXXはご自分の環境に合わせてください）にアクセスして、正常にビデオ会議ができることを確認します。

## Assets の キャッシュについて 

このように Twilio CLI を使うと、ローカルでテストしてからサーバーにデプロイができるので、開発がよりやりやすくなります。
ただ、今回のように、HTML ファイル内から同じ Assets 内にある JavaScript ファイルを参照しているようなケースですと、Assets にデプロイしたはずの JavaScript ファイルがうまく読み取られないことがあります。
これは、Assets のキャッシュが効いてしまうことにより発生します。

対応方法としては、`video.html` 側の JavaScript の指定部分に以下のような`version` パラメータなどを付加し、それをデプロイの度に更新することで常に最新の `JavaScript` が読まれるようになります。

```html:video.html
<script src="./video.js?version=1.00"></script>
```

# まとめ

今回の記事では Twilio Video のルームタイプについては触れませんでしたが、デフォルトのルームタイプを Go にしておくことで、無駄なコストをかけずにテストができます。

---
### Twilio（トゥイリオ）とは

https://cloudapi.kddi-web.com
Twilioは音声通話、メッセージング（SMS/チャット）、ビデオなどの 様々なコミュニケーション手段をアプリケーションやビジネスへ容易に組み込むことのできるクラウドAPIサービスです。初期費用不要な従量課金制で、各種開発言語に対応しているため、多くのハッカソンイベントやスタートアップなどにも、ご利用いただいております。

自己紹介|　
:--|:--
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b51a8b88-abd5-ce5e-195a-de3564ba2ae6.png" height='200' width='150'/>|高橋克己（Katsumi Takahashi） 自称「<font color='red'>赤い芸人</font>」<br />グローバル・インターネット・ジャパン株式会社　代表取締役<br />株式会社KDDIウェブコミュニケーションズ　Twilio事業部エバンジェリスト<br /><br />2001年より大手通信事業者の法人サービスの教育に携わり、企業における電話のしくみや重要性を研究。2016年よりTwilio事業部にジョインし、Twilioを使ったスマートコミュニケーションの普及活動を精力的に行っている。<br />2019 Twilio Champions