Twilio WebRTCハンズオン（ホワイトボード編）

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

# はじめに

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

本記事は、以下の記事の内容の続編となります。ただし、以下の記事を実施していなくても大丈夫です。
[Twilio WebRTCハンズオン（発話者を検知しよう編）](https://qiita.com/mobilebiz/items/7bbfa8e794bd97d6f927)

上記記事では、複数人で会議を行っているときに、話している人を検知してカメラ画像を強調する機能を実装しましたが、この記事では複数人で同時に書き込みが可能なホワイトボード機能を実装していきたいと思います。

完成予想図は以下の通りです。
![スクリーンショット 2021-05-31 13.49.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/ad80fbfc-b33a-7fe4-cdaf-9f2019496747.png)

上の画像では分かりづらいですが、カメラ映像の下に表示されたキャンバス上に、複数人が同時に書き込みが可能です。あくまで複数人での同時書き込みを実現するだけですので、消しゴムとかペンの色を変えるなどの機能はありません。

今回は、こちらの機能を実装するために Twilio Video のデータトラックを利用します。データトラックは、音声や動画データとは別に、データのみをそれぞれの参加者と双方向でやり取りができるものです。応用次第で、テキストデータや絵文字を送り合ったりすることができます。

では早速始めていきましょう。
すでに過去のWebRTC関連記事を実施している方は、前半部分については割愛することができますますので、[ソースコードの準備](#ソースコードの準備)から実施してください。

# 準備

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

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

## ソースコードの準備

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

```sh
$ git clone -b data-track 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. ホワイトボートのエリアの準備をする

ホワイトボードのエリアとして、HTML の \<canvas> タグを利用します。

## video.html に \<canvas> タグを追加する

- assets フォルダ内にある、 `video.html` をエディタで開きます。
- 17行目の後ろ（`video-zone` の後）に以下のコードを追加します。

```html
    <div id="canvas-zone">
      <canvas id='common-pics' width='560' height='360'></canvas>
    </div>
```

以下のような感じになります。

![スクリーンショット 2021-05-31 15.02.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/e49c67ab-536d-52bb-c808-1249e021392f.png)

## video.css にホワイトボードエリアのスタイルを設定します。

- assets フォルダ内にある、`video.css` をエディタで開きます。
- 行末に以下のスタイルを追加します。

```css
/* キャンバス */
#common-pics {
  background-color: rgb(217, 217, 235);
  width: 560px;
  height: 360px;
  visibility: hidden;
}
```

ホワイトボードの背景色（`background-color`）は自由に変えてください。
visibility 属性を `hidden` にすることで、通常はホワイトボードを見えないようにしておき、入室したときに `visible`　に切り替えます。

# STEP 2. 描画コンテキストの準備

ホワイトボードとして利用するコンテキストを準備します。
ここからは、`assets/video.js`を編集していきます。

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

```javascript
  // STEP 2. 描画エリア
  let context;
  // STEP 2. End
```

描画用に用意した \<canvas> タグは、JavaScript を使ってすべてのピクセルを制御できるエリアで、実際に線や円などを描画するために、内部に getContext("2d") オブジェクトを持っています。ここで指定した context には後ほど、getContext("2d") で取得した描画オブジェクトが格納されます。

# STEP 3. ローカルデータトラックを取得する

ホワイトボード機能は、マウスの座標情報をリモートに配信することで実現します。座標情報の送受信には、[DataTrack API](https://jp.twilio.com/docs/video/javascript-v1-using-the-datatrack-api) を利用します。データトラックには、ローカルから相手に送るローカルデータトラックと、リモートユーザのデータトラックを受け取るリモートデータトラックがあります。
ここではまず、ローカルデータトラックを用意します。

- // STEP 3. から // STEP 3 End. までを以下のコードで置き換えます。

```javascript
  // STEP 3. ローカルデータトラックの取得
  let dataTrack = new Video.LocalDataTrack();
  // STEP 3 End.
```

ローカルデータトラックを取得するのは簡単です。上記のように `new Video.LocalDataTrack()` を実行するだけです。このようにして取得したローカルデータトラックを、この後ルームに接続した後に自分自身のストリームに追加します。
取得するのは最初の1回だけでいいので、今回はプログラムのはじめの方で取得しておきます。

# STEP 4. ローカルデータトラックの割り当てとマウスイベントの定義

次に行うのは、自分自身がビデオルームに接続したときに、先程取得したローカルデータトラックを自分自身に割り当てることです。
さらに描画エリアの初期設定と、マウスイベントを設定します。

- // STEP 4. から // STEP 4 End. までを以下のコードで置き換えます（STEP 4 は、 STEP 5 より後ろにありますので気をつけてください）。

```javascript
        // STEP 4. 
        // ローカルデータトラックを割り当て
        room.localParticipant.publishTrack(dataTrack);

        // キャンバスを表示
        const commonPics = document.getElementById("common-pics");
        commonPics.style.visibility = "visible";
        context = commonPics.getContext("2d");
        let isDrawing = false;
        const mouse = { newPos: {}, oldPos: {} };

        // マウスダウンで描画開始
        commonPics.addEventListener("mousedown", (e) => {
          mouse.newPos = mouse.oldPos = { x: e.offsetX, y: e.offsetY };
          isDrawing = true;
        });

        // マウスを動かしたらキャンバスに描画してデータトラックで送信
        commonPics.addEventListener("mousemove", (e) => {
          if (isDrawing) {
            mouse.oldPos = mouse.newPos;
            mouse.newPos = { x: e.offsetX, y: e.offsetY };
            drawLine(context, mouse);
            dataTrack.send(JSON.stringify(mouse));
          }
        });

        // マウスアップで描画終了
        commonPics.addEventListener("mouseup", (e) => {
          if (isDrawing) {
            mouse.newPos = { x: e.offsetX, y: e.offsetY };
            drawLine(context, mouse);
            dataTrack.send(JSON.stringify(mouse));
            mouse.newPos = mouse.oldPos = { x: 0, y: 0 };
            isDrawing = false;
          }
        });
        // STEP 4 End.
```

先程取得したローカルデータトラックを割り当てるのは以下のコードとなります。
`room.localParticipant.publishTrack(dataTrack);`
これにより、自身のデータトラックがリモートユーザにパブリッシュされるようになります。ルームに接続してからでないと実行できないので、Connect した後の一連の処理の中で実施するようにします。

その後、初期状態で非表示となっていた \<canvas> を表示し、`getContext("2d")`を取得しています。

マウスの操作については、以下の3つのイベントに処理を割り当てています。
- `mousedown` マウスが押された（描画の開始）
- `mousemove`　マウスが移動した
- `mouseup`　マススを離した（描画の終了）

# STEP 5. 退出したときにキャンバスを非表示にする

ルームから退出すると、リモートデータトラックも届かなくなりますので、ホワイトボードを非表示にします。

- // STEP 5. から // STEP End. までを以下のコードで置き換えます。

```javascript
    // STEP 5. キャンバスを非表示
    const commonPics = document.getElementById("common-pics");
    commonPics.style.visibility = "hidden";
    // STEP 5 End.
```

`commonPics.style.visibility = "hidden";` 
によってホワイトボード自体が非表示になります。ただし、`context`オブジェクト自体は削除されているわけではないので、この状態で再度ルームに入ると、退出したときのホワイトボードの状態が復活します。
退出した後にリモートユーザが書いたものは、こちらでは取得できていませんので、再入室したときには反映されませんので気をつけてください。

# STEP 6. リモートのデータトラックを購読する

次に、リモートユーザが送信してくるデータトラックを購読する処理を記載します。今回は送られてきたデータトラックの内容をもとに、自分のホワイトボードに描画行うようにします。

- // STEP 6. から // STEP 6 End. の間にある既存のコードを、以下のコードで置き換えてください。

```javascript
    // STEP 6. 
    // データトラックをアタッチする
    if (track.kind === "data") {
      track.on("message", (data) => {
        // リモートの描画を表示
        drawLine(context, JSON.parse(data));
      });
    } else {
      // トラックをアタッチする
      const child = div.appendChild(track.attach());
      // 映像トラックにCSSを設定
      if (track.kind === "video") {
        child.classList.add("video-style");
      }
    }
    // STEP 6 End.
 ```

データトラックには、`attach()`メソッドは用意されていないため、データトラックである場合と、それ以外の場合で処理を切り替えています。
データトラックで送られてくるデータは、ローカルデータトラックで送信しているマウスの座標情報となりますので、それを使って自身のホワイトボードに描画します。

# STEP 7. ホワイトボードに描画する

最後にキャンバス上の描画部分を実装します。

- // STEP 7. 〜 // STEP 7 End. の間に以下のコードを記述します。

```javascript
  // STEP 7. キャンバス上に描画
  const drawLine = (context, mouse) => {
    context.beginPath();
    context.strokeStyle = "black";
    context.lineWidth = 1;
    context.moveTo(mouse.oldPos.x, mouse.oldPos.y);
    context.lineTo(mouse.newPos.x, mouse.newPos.y);
    context.stroke();
    context.closePath();
  };
  // STEP 7 End.
```

今回は線の色を黒、太さを1にして描画しています。

# テスト

以上で実装はすべて終了です。
では、以下のコマンドでローカル実行を行い、ルームに接続してみましょう。

```
npm start
```

![スクリーンショット 2021-06-01 8.25.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/1ee83558-0fc4-bc3f-8d8f-fd82a924151c.png)

ルームに入室すると画面下部にホワイトボードが現れ、マウスで描画ができることがわかります。
複数タブを開いて、同時に入室してホワイトボードが共有されていることも確認しましょう。

# まとめ

今回は、非常に簡易的なホワイトボード機能をご紹介しました。データトラックを使うことで、音声や動画以外のデータをやり取りできることがわかったかと思います。また、Twilioのデータトラックには料金はかかりません（Room の利用料に含まれます）。

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