Twilio WebRTCハンズオン（発話者を検知しよう編）

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

# はじめに

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

本記事は、以下の記事の内容の続編となります。ただし、以下の記事を実施していなくても大丈夫です。
[Twilio WebRTCハンズオン（マイク・カメラ ON/OFF編）](https://qiita.com/mobilebiz/private/96140ca2b4ab506959ef)

上記記事では、会議中にマイクとカメラのON/OFFを行う方法について說明しました。本記事ではさらに、複数人で会議を行っているときに、話している人を検知してカメラ画像を強調する機能を実装していきたいと思います。
**なお、本機能は Group ルームにのみ対応していますので、WebRTC GO や Peer-to-peer ルームでは利用できない点に注意してください。**

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

![スクリーンショット 2020-12-01 11.44.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/80fc1cab-e292-131e-09a2-591409225e90.png)

上の画像にもあるように、現在話している人のカメラ映像に赤い枠が表示されるようになります。

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

# 準備

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

- 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)
- **完了しました**にチェックを入れて、**終了**ボタンを押します。

## ソースコードの準備

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

```sh
$ git clone -b dominant 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 7.20.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/947bf39b-f7b7-483d-c9e7-f724bcc57b75.png)

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

今回は、この状態をベースに話者を強調させる実装を行っていきます。

# STEP 1. ドミナントスピーカーを有効にする

今回は、Twilio が提供するドミナントスピーカー（ Dominant Speaker ）の API を使って話者が切り替わったことをイベントとして取得していきます。ドミナントスピーカーを有効にするためには、ルームに接続するときにオプションを指定する必要があります。

## オプションを設定する

- assets フォルダ内にある、 `video.js` をエディタで開きます。
- // STEP 1. から // STEP 1. End の間を以下のコードに書き換えます。

```javascript
            // STEP 1. ドミナントスピーカーを有効にする
            dominantSpeaker: true, 
            // STEP 1. End
```

![スクリーンショット 2020-12-01 15.25.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/ae67f07d-78b2-ea1c-735d-552f5e5aa19b.png)

コードを見ていただくとわかるように、`dominantSpeaker` オプションを `true` にしてルームに接続することで、話者が切り替わったことがイベントとして取得できるようになります。

# STEP 2. 話者の画面を強調させる

では次に、話者のカメラ映像の周りに赤い枠線を表示させる部分をつくります。枠線は、 CSS で実現させます。

## CSS にスタイルを追加する

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

```css
/* ドミナントスピーカー */
.dominant-speaker {
    outline: solid 5px #f0506e;
    outline-offset: -5px;
}
```

![スクリーンショット 2020-12-01 15.33.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/1a6591c1-a5f6-9e2d-6741-ab9e2acbafc2.png)

枠線は `outline` で表示させるようにし、`outline-offset: -5px;` を指定することで、内側に枠を引くことでカメラ映像のレイアウトが崩れないようにしています。

## 話者の映像にスタイルを適用させる

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

```javascript
    // STEP 2. ドミナントスピーカーを目立たせる
    const dominantSpeaker = (participant => {
        if (!participant) return;
        console.log(`Dominant speaker changed.　${participant.sid}`);
        // 映像を表示するエリアからDIVタグを検索
        const videoZone = document.getElementById('video-zone');
        videoZone.childNodes.forEach(divNode => {
            if (divNode.nodeName === 'DIV') {
                // さらにその下のVIDEOタグを検索
                divNode.childNodes.forEach(node => {
                    if (node.nodeName === 'VIDEO') {
                        if (node.parentElement.id === participant.sid) {
                            // ドミナントスピーカーなので、スタイルを適用
                            node.classList.add('dominant-speaker');
                        } else {
                            // ドミナントスピーカーではないので、スタイルを削除
                            node.classList.remove('dominant-speaker');
                        }
                    }        
                });
            }
        });
    });
    // STEP 2. End
```

CSS で作成したスタイルは、\<video> タグに適用させるための Function を定義しています。引数には、話者を特定するための `participant` オブジェクトを指定しています。
スタイルを適用するために、現在のドキュメント内を検索し、引数として渡された `participant.sid` と一致する `id` を持った \<video> タグが見つかったらスタイルを適用し、それ以外の \<video> タグからはスタイルを削除しています。
これによって、現在の話者に対してスタイルが適用されるようになります。

# STEP 3. ドミナントスピーカーを検出したときのイベントを定義する

STEP 1. で指定した `dominantSpeaker` オプションを使ってルームに接続すると、ルームの `dominantSpeakerChanged` イベントが発火するようになりますので、それを補足して STEP 2. で定義した Function を呼ぶようにします。

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

```javascript
            // STEP 3. ドミナントスピーカーを検出したときの処理
            room.on('dominantSpeakerChanged', participant => dominantSpeaker(participant));
            // STEP 3. End
```

`dominantSpeakerChanged` イベントが発火すると、引数として `participant` を受け取れるので、それをそのまま先程作成した Function に渡しています。

ここまでの処理で、話者が切り替わったことをイベントとして、話者をハイライトさせることができます。

しかしこのままではあまりうまくありません。なぜなら、自分自身が話し始めたときには、 `dominantSpeakerChanged` イベントが発火しないのです。そのため、最後に話していた人が強調されたままになってしまいます。

これを解決するためには、自分自身でマイクの音量を検出して、自分が話していることを知る必要があります。

# STEP 4. 自身のマイクから発話状態を検知する

自分のPCのマイク音量を取得するには、`AudioContext` から生成する `AnalyserNode` を使って、リアルタイム時間領域/周波数領域分析を一定時間ごとに繰り返す必要があります。

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

```javascript
        // STEP 4. 自身のマイクから発話状態を検知する
        const audioContext = new AudioContext();
        const analyser = audioContext.createAnalyser();
        const timeDomain = new Float32Array(analyser.frequencyBinCount);
        const frequency = new Uint8Array(analyser.frequencyBinCount);
        audioContext.createMediaStreamSource(stream).connect(analyser);

        // 検出ループに入る
        detectOwnSpeak(analyser, timeDomain, frequency);
        // STEP 4. End
```

最初の5行で、ローカルメディアストリームに `analyser` を接続しています。定期的な検知については、`detectOunSpeak` という関数で行うようになっています。

# STEP 5. 音声を検知し、カメラ映像を強調させる

では最後に、定期的に音声を検知しながら一定音量を超えた場合（話している）に、自身のカメラ映像を強調させたいと思います。話し終わったときは、普通のスタイルに戻します。

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

```javascript
    // STEP 5. 検出ループ
    const detectOwnSpeak = ((analyser, timeDomain, frequency) => {
        setTimeout(() => {
            // 音量の平均スコアを計算する
            analyser.getFloatTimeDomainData(timeDomain);
            analyser.getByteFrequencyData(frequency);
            let score = frequency.reduce((p, x) => p + x, 0) / frequency.length;

            // スコアが10を超えたらなにか話していると判断して、スタイルを適用させる
            if (score > 10 && videoRoom && audioOn) dominantSpeaker({ sid: 'my-video'});

            // スコアが10以下の場合は、自身のスタイルを削除
            if (score <= 10) document.getElementById('myStream').classList.remove('dominant-speaker');

            // ループ
            detectOwnSpeak(analyser, timeDomain, frequency);
        }, 100);
    });
    // STEP 5. End
```

検出時間内の音量平均を算出して、スコアが10を超えたら話し中と判断しています。マイクの性能などで常に話し中になってしまったり、逆に検知できないことが多い場合はここのしきい値を変更してください。
なお、この検知ロジックは、100msごとに実施しています。

以上で話者特定に関する作業は終了です。

# サーバーにデプロイ

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

## 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はご自分の環境に合わせてください）にアクセスして、正常にビデオ会議ができることを確認します。

今回の機能については、1台のPCではテストができません。どなたかに協力してもらって、リモートから上記URLにアクセスしてもらうことでテストしてみてください。

# まとめ

今回は、話者の検知について解説をしました。自身の音声レベルの検知方法などは、Twilio に限らず使える技術だと思うので、ぜひ参考にしていただけると幸いです。

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