Twilio WebRTCハンズオン（マイク・カメラ ON/OFF編）

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

# はじめに

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

本記事は、以下の記事の内容の続編となります。ただし、以下の記事を実施していなくても大丈夫です。
[Twilio WebRTCハンズオン（CSSで見栄えをよくする編）](https://qiita.com/mobilebiz/private/0d6f763b47fdeeb4740d)

上記記事では、CSS を使って映像のサイズを動的に変更したり、ボタンの見栄えを良くする方法について解説しました。今回は、より実践的な機能として、会議中にマイクとカメラのON/OFFを行う方法について說明します。

完成予想図は以下の通りです。

![スクリーンショット 2020-11-30 8.14.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b6720ced-d7c1-aaa8-7bd8-57abfb3e0f33.png)

カメラとマイクのON/OFFをするためのアイコンと、それぞれが押された場合の処理を実装します。

すでに上記記事を実施している方は、前半部分については割愛することができますますので、[ソースコードの準備](#ソースコードの準備)から実施してください。

# 準備

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

- 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 側の準備

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

- 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 キーを作成する**ボタンを押します。

![スクリーンショット 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 を控えておきます（<font color='red'>SECRET はこの画面でしか確認できません</font>）。

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

## ソースコードの準備

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

```sh
$ git clone -b device-on-off 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=先程控えておいた API Key（SK から始まる文字列）
AUTH_TOKEN=先程控えておいた API Secret（本当の AuthToken ではないので注意）

# Variables for function ".env"
# ---
MAIN_ACCOUNT_SID=先程控えておいたアカウント SID （AC から始まる文字列）
TWILIO_VIDEO_KEY=上記、ACCOUNT_SIDと同じ SK から始まる文字列
TWILIO_VIDEO_SECRET=上記、AUTH_TOKENと同じ、API SECRET
```

本来は、最初の `ACCOUNT_SID`と`AUTH_TOKEN`にそれぞれ正規のアカウント SID と AUTH TOKEN を入れたいのですけど、Twilio CLI の関係で API Key と SECRET を入れています。

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

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

✋🏼 <font color='blue'>ヒント</font>
*`contents`フォルダに完成版の `video.html`、`video.js`、`video.css`が用意されていますので、ハンズオンをやらずに結果を確かめたい場合は、これらのファイルを`assets`フォルダにコピーしてから実行してください。*

```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.js | http://localhost:3000/video.js                             │
│   ├── /video.html | http://localhost:3000/video.html                         │
│   ├── /video.css | http://localhost:3000/video.css                           │
│   ├── /images/camera_off.png | http://localhost:3000/images/camera_off.png   │
│   ├── /images/camera_on.png | http://localhost:3000/images/camera_on.png     │
│   ├── /images/mic_on.png | http://localhost:3000/images/mic_on.png           │
│   └── /images/mic_off.png | http://localhost:3000/images/mic_off.png         │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
```

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

http://localhost:3000/video.html

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

![スクリーンショット 2020-11-30 8.29.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/a5505046-70b2-4ad5-ee5f-849e7664754e.png)

**入室**ボタンを押してルームに入ります。

ブラウザのタブを複製して、2枚めのタブでも入室してみましょう。

![スクリーンショット 2020-11-30 8.30.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/bca3a575-6f18-8525-627e-5acc315d2a17.png)

今回は、この状態にカメラとマイクのON/OFF機能を実装していきます。

# STEP 1. 切り替え用のボタンを表示させる

まずはカメラとマイクのON/OFFをするボタンを作ります。ボタンは CSS を使って自分のカメラ映像の上に表示させるようにしましょう。

## CSS ファイルにボタンのスタイルを追加する

- assets フォルダ内にある、 `video.css` をエディタで開きます。
- `:root` クラスを以下のように書き換えます。

```css
/* rootクラス */
:root {
    --video-width: 640px;   /* デフォルトの横幅 */
    --camera-icon: url('./images/camera_on.png');  /* カメラアイコン */
    --mic-icon: url('./images/mic_on.png');  /* マイクアイコン */
}
```

![スクリーンショット 2020-11-29 11.32.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/687b8b3f-2384-126e-a8f0-6699d1c0f844.png)

4行目と5行目を追加して、アイコンファイルの場所を指定しています。
今回はアイコンファイルとして以下の4つを予め準備しておき、これらを動的に切り替える予定です。

- images/camera_on.png
- images/camera_off.png
- images/mic_on.png
- images/mic_off.png

- さらに以下の行を CSS ファイルの最後に追加します。

```css
/* カメラボタン */
.toggle-camera {
    position: absolute;
    bottom: 10px;
    left: 8px;
    border: 0;
    padding: 0;
    width: 30px;
    height: 30px;
    background: rgba(0, 0, 0, 0);
}

.camera-image {
    background-image: var(--camera-icon);
    background-size: cover;
    display: inline-block;
    margin: 0;
    width: 30px;
    height: 30px;
    z-index: 10;
}

/* マイクボタン */
.toggle-mic {
    position: absolute;
    bottom: 10px;
    left: 45px;
    border: 0;
    padding: 0;
    width: 30px;
    height: 30px;
    background: rgba(0, 0, 0, 0);
}

.mic-image {
    background-image: var(--mic-icon);
    background-size: cover;
    display: inline-block;
    margin: 0;
    width: 30px;
    height: 30px;
    z-index: 10;
}
```

これらは、それぞれのアイコンを表示するスタイルになります。
カメラアイコンが自分自身のカメラ映像の左下にくるように、`bottom` を10px、`left`を8pxに設定します。
マイクアイコンは、カメラアイコンの右側にくるように、`left` を45pxにしています。
それぞれのアイコンに、`position: absolute;` を指定することで、カメラ映像の上にアイコンが表示されますが、そのためにはカメラ映像側に `position: relative;` の指定と、重ね順の指定である、`z-index` が必要になる点に注意してください。

- `video.js` をエディタで開きます。
- // STEP 1. から // STEP 1. End までの間を以下のコードで置き換えます。

```javascript
    // STEP 1. カメラ・マイクのON/OFFボタンを表示
    const root = document.documentElement;
    // カメラボタン
    const toggle_camera = document.createElement("button");
    toggle_camera.id = "btn-toggle-camera";
    toggle_camera.classList.add("toggle-camera");
    const toggle_camera_image = document.createElement("span");
    toggle_camera_image.classList.add("camera-image");
    toggle_camera.append(toggle_camera_image);
    document.getElementById('my-video').append(toggle_camera);

    // カメラボタンが押されたときの処理
    toggle_camera.addEventListener('click', () => {
        if (videoRoom) {    // ルームに接続済みの場合のみ
            videoRoom.localParticipant.videoTracks.forEach(videoTrack => 
                videoOn ? videoTrack.track.disable() : videoTrack.track.enable());  
        }
        // フラグを反転
        videoOn = !videoOn;
        // ボタンのアイコンを切り替え
        root.style.setProperty('--camera-icon', `url('./images/camera_${videoOn ? 'on' : 'off'}.png')`);
    });

    // マイクボタン
    const toggle_mic = document.createElement("button");
    toggle_mic.id = "btn-toggle-mic";
    toggle_mic.classList.add("toggle-mic");
    const toggle_mic_image = document.createElement("span");
    toggle_mic_image.classList.add("mic-image");
    toggle_mic.append(toggle_mic_image);
    document.getElementById('my-video').append(toggle_mic);

    // マイクボタンが押されたときの処理
    toggle_mic.addEventListener('click', () => {
        if (videoRoom) {    // ルームに接続済みの場合のみ
            videoRoom.localParticipant.audioTracks.forEach(audioTrack => 
                audioOn ? audioTrack.track.disable() : audioTrack.track.enable());  
        }
        // フラグを反転
        audioOn = !audioOn;
        // ボタンのアイコンを切り替え
        root.style.setProperty('--mic-icon', `url('./images/mic_${audioOn ? 'on' : 'off'}.png')`);
    });
    // STEP 1. End
```

これらのコードでは、カメラとマイクのアイコンを動的に生成し、それらが押下されたときの処理を記述しています。
とくに重要なのが、ローカルストリームの映像と音声のトラックの取得方法です。
`videoRoom` には、入室済みのルーム情報が入っていますので、そこから `localParticipant` を使って自分自身の映像トラックと音声トラックを取得しています。そして、それらのトラックに対して有効（`.enable()`）、無効（`.disable()`）を指定することで自信からの配信トラックを制御します。

アイコンの切り替えについては、CSS の `:room` クラスで指定したアイコンのデータの参照場所を動的に変えることで対応しています。

- 設定を変更します。
- ローカル環境でテストします。

![スクリーンショット 2020-11-30 7.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/ff3c34ba-3212-b2ed-3245-ad7825fc5000.png)

自信のカメラ映像の左下にカメラとマイクのアイコンが表示されます。
それぞれをクリックすると、アイコンが切り替わることも確認しておきます。

# STEP 2. 入室時の処理を記述する

STEP 1. では、ルームに入室後のストリームの切り替えを記述しましたが、ルームに入室する前にカメラとマイクを切り替えていたときの処理がないため、ここではその処理を追加します。具体的には、ルームに接続した直後に先程と同じ切り替えの処理を行えばよさそうです。

- `video.js` をエディタで開きます。
- // STEP 2. から // STEP 2. End までの間を以下のコードで置き換えます。

```javascript
            // STEP 2. 入室時のローカルデバイスON/OFF制御
            room.localParticipant.videoTracks.forEach(videoTrack => 
                videoOn ? videoTrack.track.enable() : videoTrack.track.disable());  
            room.localParticipant.audioTracks.forEach(audioTrack => 
                audioOn ? audioTrack.track.enable() : audioTrack.track.disable());  
            // STEP 2. End
```

コード自体は STEP 1. で出てきたものと同じです。ちなみに、`videoOn` と `audioOn` という状態を保持するフラグについては、`video.js` の先頭部分（12〜14行目）に定義されています。

- 変更を保存します。
- カメラをOFFにした状態で、ルームに入室します。
- この状態で別のタブで同じルームに入室します。
- 先に入っていたカメラの映像が送られてこないことを確認します。

![スクリーンショット 2020-11-30 7.32.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/763812df-83cd-1579-cb70-2f925e150a31.png)

# STEP 3. リモート側のカメラ・マイクが切り替わったときの処理

では次に、他の参加者がカメラやマイクのON/OFFをしたときの処理を追加していきましょう。
とはいえ、そもそも他の参加者のトラック制御はその参加者側で行われるため、特別になにかする必要はなさそうです。ただ、カメラのON/OFFについては画面を見ればわかるのですが、マイクのON/OFFは視覚的に表示したほうがわかりやすいです。
そこで、リモート側のマイクがOFFになったら、相手のカメラ映像の上にマイクOFFのアイコンを表示させたいと思います。

- `video.js` をエディタで開きます。
- // STEP 3. から // STEP 3. End の間を以下のコードで書き換えます。

```javascript
    // STEP 3. リモート側のカメラ・マイクが切り替わったときの処理
    const handleTrackChanged = ((track, participant) => {
        const dom = document.getElementById(participant.sid);

        // ミュートアイコンを表示
        const muteIcon = (dom => {
            const remote_mic = document.createElement('div');
            remote_mic.id = 'remote-mic';
            remote_mic.classList.add('remote-mic');
            const mic = document.createElement('sp');
            mic.classList.add('mic-image');
            mic.style.backgroundImage = "url('./images/mic_off.png')";
            remote_mic.append(mic);
            dom.append(remote_mic);
        });

        if (track.kind === 'audio' && !track.isEnabled) {
            // 参加中のメンバーがすでにマイクをOFFにしているのでミュートアイコンを表示
            muteIcon(dom);
        }
        // 参加中のメンバーがマイクをOFFにしたときの処理
        track.on('disabled', () => {
            if (track.kind === 'audio') {
                // ミュートアイコンを表示
                muteIcon(dom);
            }
        });
        // 参加中のメンバーがマイクをONにしたときの処理
        track.on('enabled', () => {
            if (track.kind === 'audio') {
                // ミュートアイコンを削除
                dom.childNodes.forEach(node => {
                    if (node.id === 'remote-mic') node.remove();
                });
            }
        });
    });
    // STEP 3. End
```

ここでは、`handleTrackChanged` という関数を定義し、引数として参加者情報（`participant`）と参加者のトラック情報（`track`）を受け取ります。
まず、すでに参加者がマイクをOFFにしているときはミュートアイコンを表示します。
さらに、参加者のトラックに対して、`disabled` と `enabled` の２つのイベントが発火したときの処理を記述しています。
ミュートアイコンを表示するときは、やはり CSS を使います。

- `video.css` エディタで開きます。
- 以下のコードを追加します。

```css
/* リモートマイク*/
.remote-mic {
    position: absolute;
    bottom: 10px;
    left: 8px;
    border: 0;
    padding: 0;
    width: 30px;
    height: 30px;
    background: rgba(0, 0, 0, 0);
}
```

- 変更を保存します。

この時点では関数を作成しただけですので、まだテストは行いません。

# STEP 4. トラックの状態を監視する

では最後に、他の参加者がカメラやマイクをON/OFFしたときに STEP 3. の関数をコールするようにコードを修正します。

- `video.js` をエディタで開きます。
- // STEP 4. から // STEP 4 End. までを以下のコードで置き換えます。

```javascript
            // STEP 4. トラックの状態を監視する
            if (publication.isSubscribed) {
                trackSubscribed(div, publication.track);
                handleTrackChanged(publication.track, participant);
            }
            publication.on('subscribed', track => handleTrackChanged(track, participant));
            // STEP 4. End
```

STEP 4. は、他の参加者が参加してきたときに、`publication` の `subscribed` イベントで取得できます。`participant` のイベントではなく、`publication` のイベントとして定義する必要がある点に注意してください。
自分がルームに入室したときにすでに参加者が入室していたときの処理も併せて記述します。

- 変更を保存します。
- ローカル環境でテストしてみます。

![スクリーンショット 2020-11-30 8.14.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/b6720ced-d7c1-aaa8-7bd8-57abfb3e0f33.png)

複数タブでテストして、カメラとマイクのON/OFFをテストしてみます。
以上でカメラ・マイクのON/OFFに関する作業は終了です。

# サーバーにデプロイ

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

## Twilio CLI の設定

※前回の記事ですでにCLIの設定が終わっている方は、[デプロイ](#デプロイ)まで飛ばしていただいて構いません。

まずは、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 --force

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-****-dev.twil.io
Service:
   twilio-video-handson-webrtc-go (ZS****************************)
Environment:
   dev (ZE****************************) 
Build SID:
   ZB****************************
View Live Logs:
   https://www.twilio.com/console/assets/api/ZS****************************/environment/ZE****************************
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/camera_off.png
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/images/camera_on.png
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/images/mic_off.png
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/images/mic_on.png
   https://twilio-video-handson-webrtc-go-XXXX-dev.twil.io/video.css
   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はご自分の環境に合わせてください）にアクセスして、正常にビデオ会議ができることを確認します。

# まとめ

今回は、トラックの制御について解説をしました。カメラやマイクだけでなく、画面共有などでもトラックを使いますので、トラックの扱い方については理解をしておくとよいでしょう。

---
### 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