Twilio Clientについて調査しました


## はじめに
お久しぶりです。[ｋ．ｓ．ロジャース](https://www.wantedly.com/companies/ks-rogers)の西谷です。

今回はVoIPを実現したい話があり、2回目のTwilioについて調査しました。
電話とブラウザ、ブラウザとブラウザの通話を実現したいと思います。

Client自体は[チュートリアル](https://jp.twilio.com/docs/voice/client/javascript/quickstart)に従えば、コードを書くことなくサーバレスでデプロイして通話するところまで実現出来ます。

しかし、実務の場合はサーバ側でゴニョゴニョしたいことがあると思いますので、サーバ側の実装も含めて調査しました。
弊社のサーバサイドはKotlinに移行している経緯があり、今回はTwilioのJava SDKを参考にKotlinで実装を進めました。

間違いやより良い実装があればコメントにて教えて頂けたらと思います。


## 開発環境
SDKが古いと一部の機能が実装されていない可能性があるので気をつけてください。

- サーバ
   - Kotlin + Spring Boot
   - com.twilio.sdk v7.50
- フロント
  - JavaScript 
  - [twilio.min.js](https://sdk.twilio.com/js/client/releases/1.10.3/twilio.min.js) v1.10.3

## 前準備
Twilioに登録して準備をします。
必要なものは以下の3つです。

- アカウントSID
- AuthToken
- TwiMLのApplicationSID


アカウントSIDとAuthTokenは[設定画面](https://jp.twilio.com/console)から取得できます。

![スクリーンショット 2020-05-01 13.52.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274674/7ba509d9-bed5-f3a4-7566-52e5e1564135.png)

ApplicationSIDはTwiMLのアプリを作成すると取得できます。

![スクリーンショット 2020-05-01 14.16.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274674/cda693eb-835d-9d78-7116-0de92575a0a2.png)

![スクリーンショット 2020-05-01 14.17.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274674/2da2617f-b471-ae03-d9a6-254a078d3a7e.png)



## 実装の流れ
[公式のこちら](https://jp.twilio.com/docs/voice/client/javascript)に詳細説明があります。
![](https://twilio-cms-prod.s3.amazonaws.com/images/Voice_Client_JS_Overview.width-800.png)

開発で用意するのは`1. You connect to Twilio`と`3. You respond with TwiML instructions`の部分です。

## サーバ側の実装
サーバ側で用意するAPIは2つです。


- ケイパビリティートークンの発行
- 通話時のTwiML発行


### ケイパビリティートークンの発行
ケイパビリティートークンはクライアント毎に発行するアクセストークンのようなものです。

```kotlin
@PostMapping("/twilio/capability/token")
fun createCapabilityToken(): TokenResponse {
    // 電話をかける or かけられるときの名前
    val clientName = UUID.randomUUID().toString()

    // 送受信情報の設定
    val outgoingScope = OutgoingClientScope.Builder(TWILIO_APP_SID).build()
    val incomingScope = IncomingClientScope(clientName)
    val scopes: List<Scope> = Lists.newArrayList(outgoingScope, incomingScope)

    // トークン生成
    val token = ClientCapability.Builder(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
            .scopes(scopes)
            .build()
    
    return TokenResponse(token.toJwt(), clientName)
}
```

### 通話時のTwiML発行
こちらは少しややこしく、電話があったときにTwilioからコールバックされるAPIを実装します。
レスポンスはTwiMLのXMLで、通話先やパラメータを設定出来ます。

callerIdは送信元の電話番号を入れますが、数値である必要がありそうです。
本サンプルのようにクライアント名に英数字が含まれる場合は別途送る必要があります。

```kotlin
/**
* 通話時のTwiML生成
*
* @param To 送信先
* @param From 送信元
* @return xml
*/
@RequestMapping(path = ["/twilio/call"], method = [RequestMethod.POST], produces = ["application/xml"])
fun call(@RequestParam To: String, From: String?): String? {
    return if (isAValidPhoneNumber(To)) {
        val number = Number.Builder(To).build()
        val dial = Dial.Builder().number(number).answerOnBridge(true)
                .callerId("送信元の電話番号").build()

        val response = VoiceResponse.Builder().dial(dial).build()
        response.toXml()
    } else {
        // Parameterを使うことでカスタムパラメータを送信できる
        val parameter = Parameter.Builder().name("from").value(From).build()
        val client = Client.Builder(To).parameter(parameter).build()
        val dial = Dial.Builder().client(client).answerOnBridge(true)
                .callerId("送信元の電話番号").build()

        val response = VoiceResponse.Builder().dial(dial).build()
        response.toXml()
    }
}

/**
* 電話番号とクライアントの判定
*
* @param n 電話番号 or クライアント名
* @return 電話番号であるか
*/
fun isAValidPhoneNumber(n: String): Boolean {
    return """^[\d\+\-\(\) ]+$""".toRegex().containsMatchIn(n)
}
```


後はこのAPIを公開してTwiMLのRequestURLに登録します。
検証用のサーバがない場合は[ngrok](https://ngrok.com/)を使うと便利です！

![スクリーンショット 2020-05-01 15.29.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274674/bdfe2aac-27e8-8f36-818b-07339d505cb7.png)

## フロント側の実装
こちらの実装はHTML一枚だけです。
HTMLはButtonとInputを配置するだけですので割愛します。

```javascript
$(function () {
  $.ajax({
    type: "POST",
    url: 'ケイパビリティートークン取得のURL'
  })
    .done(function (data) {
      console.log('Got a token: ', data);

      $("#name").text(data.name);
      Twilio.Device.setup(data.token);
    })
    .fail(function () {
      alert('Could not authenticate!');
    });
});

//ready状態になった時に実行
Twilio.Device.ready(function (device) {
  $("#status").text("Ready");
});
//エラー状態になった時に実行
Twilio.Device.error(function (error) {
  $("#status").text("Error: " + error.message);
});
//接続状態になった時に実行
Twilio.Device.connect(function (conn) {
  $("#status").text("Successfully established call");
});
//通話終了した時に実行
Twilio.Device.disconnect(function (conn) {
  $("#status").text("Call ended");
});

//Callボタン
$("#call").click(function () {
  Twilio.Device.connect({
    // サーバ側のAPIのを実行するときのパラメータ
    To: $("#phone_number").val(),
    From: $("#name").text(),
  });
});

//Hangupボタン
$("#hangup").click(function () {
  //通話終了
  Twilio.Device.disconnectAll();
});

// Twilioからのコールバック
Twilio.Device.incoming(function (conn) {
  // サーバ側で設定したカスタムパラメータ
  $("#status").text("Incoming connection from: " + conn.customParameters.get("from"));

  // 通話承諾
  $("#answer").click(function () {
    conn.accept();
  });

  // 通話拒否
  $("#reject").click(function () {
    conn.reject();
  });
});
```


## 参考記事
以下記事を参考にさせて頂きました。
https://dev.classmethod.jp/articles/twilio9/#toc-1
https://qiita.com/tmiki/items/fad1a22ba1fe14d32e34

## おわりに
今回はブラウザ間の通話としてTwilio Clientとサーバ側の実装について紹介させて頂きました。
実務での導入を考える場合は同時に複数受信した場合などの処理も考える必要があるかなと思います。


間違い等あればご指摘頂けたらと思います。

### Wantedlyでもブログ投稿してます
Techブログに加えて会社ブログなどもやっているので、気になった方はぜひ覗いてみてください。
https://www.wantedly.com/companies/ks-rogers