Twilioを使ってWeb通話を実現するまで（React）


Twilio APIを使って、ReactでWeb通話を実装してみました。
SNSに搭載されているのが当たり前になったWeb通話ですが、Twilioを使えば案外簡単に自社サービスにも取り入れることができると分かりました。

今回はTwilioの`Programmable Video` APIを使って、Web通話を実現するまでを解説していきたいと思います。

# Twilioとは
![twilio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/116375/fcdf94e6-a030-1f9d-9428-a225435412d5.png)
一言でいうと、「電話のAPI」です。2008年に創業で、2016年にはニューヨーク証券取引所にも上場しています。聞いたことがある人からすると、Webから電話かけたり、代替の電話番号使えるみたいなイメージが強いかもしれませんが、今回取り扱うWeb通話（Web RTC）や、SMS、音声ボットなど展開している領域は幅広いです。

ドキュメントも重厚で、信頼も厚いので通話系を実装したいという方にはオススメです。気になるお値段ですが、グループ通話の場合は0.01ドル、P2Pの場合は1参加者あたり0.0015ドル。（いずれも1分間あたり）詳しくは[こちらの記事](https://qiita.com/mobilebiz/items/db133aeb21f880ddfcdd#comment-9ee20f776d763364a365)などみてみてください。

# Web通話を実現する

ドキュメントに出てくる概念が新しかったので最初は戸惑いもあったのですが、最終的には[このベストプラクティス](https://github.com/twilio/video-quickstart-js/blob/master/quickstart/src/index.js)を写経してみたら、ちゃんと動くところまで辿りつくことができました。

## SID、API KEYと、API SECRETを取得する
まずは、[公式サイト](https://www.twilio.com/)にいってユーザー登録を済ませます。ログインしたあとに、プロジェクトを作成すると、`ACCOUNT SID`が同時に生成されます。

そこから、`Settings > API Keys`に入って、新しくAPI Keyを生成します。`API KEY`と、`API SECRET`が表示されるのでメモを取っておいてください。ちなみに、`API SECRET`は生成された時しか表示されないので注意してください。

```javascript:APIキーの例
TWILIO_ACCOUNT_SID = 'ACXXXXXXXXXXXXXXXXXXXXXXXXX'
TWILIO_API_KEY_SID = 'SKXXXXXXXXXXXXXXXXXXXXXX'
TWILIO_API_KEY_SECRET = 'XXXXXXXXXXXXXXXXX'
```

暗号化して`.env`ファイルあたりに格納して呼び出せるようにしておきましょう。

## Twilio Tokenを設定する

Twilioの通話部屋に入る前に、入室ルームや、入室するユーザーの情報を揃えてトークン化しなければなりません。コンソール画面の`Tooks > Testing Tools`にも記載がありますが、下記のようになります。

```JavaScript:twilio.js
const AccessToken = twilio.jwt.AccessToken;
const VideoGrant = AccessToken.VideoGrant;

// 最初に取得したSID、API KEYと、API SECRETを設定
const ACCOUNT_SID = 'accountSid';
const API_KEY_SID = 'apiKeySid';
const API_KEY_SECRET = 'apiKeySecret';

// アクセストークンの生成
const accessToken = new AccessToken(
  ACCOUNT_SID,
  API_KEY_SID,
  API_KEY_SECRET
);

// オリジナルのユーザー名を設定
accessToken.identity = 'example-user';

// ビデオにアクセス
const grant = new VideoGrant();
grant.room = 'DailyStandup';
accessToken.addGrant(grant);

// Serialize the token as a JWT
const jwt = accessToken.toJwt();
console.log(jwt);
```

## プレビュー画面を生成する

TwilioのWeb通話ルームに繋ぐ前に、まずローカルで画像と動画を配信するトラックを用意していきます。自分の画面を映し出す鏡的な機能も果たしてくれます。Google HangoutやZoomでも自分の顔も表示されますよね。それと同じようなイメージです。

```JavaScript:video_chat.js
// Preview LocalParticipant's Tracks.
const localTracksPromise = previewTracks
  ? Promise.resolve(previewTracks)
  : createLocalTracks();

localTracksPromise.then(
  function(tracks) {
    currentComponent.setState({ previewTracks: tracks });
    const previewContainer = document.getElementById("local-media");
    if (!previewContainer.querySelector("video")) {
      currentComponent.attachTracks(tracks, previewContainer);
    }
  },
  function(error) {
    console.error("Unable to access local media", error);
    console.log("Unable to access Camera and Microphone");
  }
```

ビューの中に、`local-media`を仕込んでおいて、あとは`video`に出現させてるだけです。
ここで、`previewTracks`というstateを用意して、これからTwilioのWeb通話ルームに送り込むトラックを設定します。

## ルームに入室する

先ほど生成したtwilioのトークンと、`previewTracks`を使ってルームに入室していきます。

```JavaScript
  onJoin = id => {
    const { previewTracks } = this.state;

    const AccessToken = twilio.jwt.AccessToken;
    const VideoGrant = AccessToken.VideoGrant;

    // Grant access to Video
    const grant = new VideoGrant();
    grant.room = id;
    accessToken.addGrant(grant);

    // Set the Identity of this token
    accessToken.identity = "guest";

    const twilioToken = accessToken.toJwt();
    const connectOptions = {
      name: id
    };
    if (previewTracks) {
      connectOptions.tracks = previewTracks;
    }

    connect(twilioToken, connectOptions).then(
      room => this.roomJoined(room),
      error => {
        console.error(`Unable to connect to Room: ${error.message}`);
      }
    );
  };
```

ルームに入るためには、その人と特定できる`identity`を指定する必要があります。ここでは、`guest`としていますが基本ルームは2人以上が参加することが前提となりますので、random関数を使うなり、動的に作成すると良いかと思います。トークンと、ルームの設定をして、通話ルームに繋ぎます。

## ルームの状態を管理する

Web通話が始まると、常にルームが開かれているような状態となり、参加者が抜けたり入ったり、既にいる参加者の情報がリアルタイムで更新されることになります。

```JavaScript
  // Successfully connected!
  roomJoined(room) {
    let self = this;
    this.setState({ activeRoom: room });

    // Attach LocalParticipant's Tracks, if not already attached.
    var previewContainer = document.getElementById("local-media");
    if (!previewContainer.querySelector("video")) {
      this.attachTracks(
        this.fetchTracks(room.localParticipant),
        previewContainer
      );
    }

    // Attach the Tracks of the Room's Participants.
    var remoteMediaContainer = document.getElementById("remote-media");
    room.participants.forEach(function(participant) {
      console.log("Already in Room: '" + participant.identity + "'");
      self.participantConnected(participant, remoteMediaContainer);
    });

    // When a Participant joins the Room, log the event.
    room.on("participantConnected", function(participant) {
      console.log("Joining: '" + participant.identity + "'");
      self.participantConnected(participant, remoteMediaContainer);
    });

    // When a Participant leaves the Room, detach its Tracks.
    room.on("participantDisconnected", function(participant) {
      console.log(
        "RemoteParticipant '" + participant.identity + "' left the room"
      );
      self.detachParticipantTracks(participant);
    });

    // Once the LocalParticipant leaves the room, detach the Tracks
    // of all Participants, including that of the LocalParticipant.
    room.on("disconnected", function() {
      const { previewTracks } = self.state;
      const { onDisconnect } = self.props;
      console.log("Left");
      if (previewTracks) {
        previewTracks.forEach(function(track) {
          track.stop();
        });
        self.setState({ previewTracks: null });
      }
      // self.detachParticipantTracks(room.localParticipant);
      // room.participants.forEach(self.detachParticipantTracks);
      self.setState({ activeRoom: null });
      onDisconnect();
    });
  }
```

`room.on()`の関数はルームに動きがあるたびに実行されることになります。

## ルームを退室する

ルームを退室する時は、ルームを`disconnect`するだけです。

```JavaScript
  onLeave = () => {
    const { activeRoom } = this.state;
    if (activeRoom) {
      activeRoom.disconnect();
    }
  };
```

# さいごに

若干、価格設定は高めに感じたりもしますが、試してもらった知り合いの方には「通信環境、安心してるね！」と感想をいただけたりもしました。本格的なWeb通話アプリがこんなにも簡単に作れると思わなかったので、驚きました！

ルームの部分や、細かいReact部分でスタックしてしまったりもしたのですが、友人が主催している「[川島のITスキルマガジン](https://note.com/kawashimaken/m/me13f2d6e0ab8)」より、随時質問して解決していくことができました。

このサロンは、機械学習だけでなく、Java Scriptや、Pythonをはじめ、IoTまで最新の技術で分からないところまで細かく指導してくれます！分からないことがあって検索してもなかなか回答が得られず、何日もスタックしてしまうことありますよね。そういう時には、ここのサロンで聞いてみるのも良いかもしれません！