Twilio Client の同時着信問題に立ち向かう

こんにちは、[@kimihom](https://twitter.com/kimihom) です。

Twilio Client はブラウザで電話ができてしまう優れものですが、一般的な電話の課題を全て JavaScript でなんとかしなければならない難しさがあります。私が Twilio Client を実装する上で起きた課題とその解決方法の提案を本記事でご紹介します。

本記事は Twilio Client の相当マニアックな記事ですので、Twilio 初心者の方は読まないことをお勧めします。Twilio の良さは簡単に電話とインターネットを繋げられる！ ということですので。

## 同時着信問題とは

一口に「同時着信」とはいえ、あらゆるパターンが存在します。そんなに着信量が多くない場合は考える必要のないことですが、次第に Twilio Client を使ってくると必ずいつか同時着信の問題に突き当たります。本記事で紹介するのは、以下のような同時着信のパターンです。

- 着信が同時に2件入ってきた
- 1つの着信に対して2人が同時に電話に出た
- キューにためていた着信をオペレーターが同時に発信した

私が行った解決方法に関して以下で詳しくご紹介します。

### 着信が同時に2件入ってきた場合

基本的に Twilio Client は、着信を受けた時に `connection` オブジェクトがコールバックで渡ってきて、その `connection` で `accept()` を呼ぶことで通話が開始します。以下のソースの場合は、着信が来たのを問答無用で電話に出るというやや強引なやり方です。大抵はこの `connection` をクロージャとして持っておき、「応答」ボタンがクリックされたタイミングで、`accept()`を呼ぶのが一般的でしょう。

```
Twilio.Device.incoming(function(connection) {
  connection.accept();
  // do awesome ui stuff here
  // $('#call-status').text("you're on a call!");
});
```

そして通話が終われば、`disconnect` のイベントが呼ばれます。

```
Twilio.Device.disconnect(function (conn) {
    // Called for all disconnections
    console.log(conn.status);
});
```

さて実際に通話を開始する前に、さらに他の着信がきた場合にはどうなるでしょうか。これはとても単純で、`incoming`イベントが再度発生します。それぞれが別の `connection` オブジェクトを持つので、どっちかを `accept` すれば通話が始まります。

オペレーターA,Bがいて、さらに同時着信 X,Y が来た場合にどうなるでしょうか。
私のサービスでは、同時着信があったとしても、着信に出られる表示になるのは1件のみです。そのため、AさんBさんともに着信 Y が表示されることになります。ここで A が Y に `accept()` した時、 B には X の着信を次に表示させたいところです。

そこで Twilio Client の `cancel` イベントの出番です。これは、着信に対してキャンセルを行った場合に呼ばれるイベントであり、他の方が着信に出た場合でも呼ばれるイベントです。

```
  var connection;

  Twilio.Device.cancel(function (conn) {
    if (！Twilio.Device.activeConnection()) {
      initView();  // 初期画面に戻す
    } else { // receive call at the same time
      connection = null; // 
      connection = Twilio.Device.activeConnection(); //他のアクティブな connection を取得
      showIncoming(); // その connection としての着信を表示
    }
  });
```

`Twilio.Device.activeConnection()` がポイントです。 `cancel` が呼ばれた時に、他のアクティブなコネクションがあった場合には、それを着信として表示するようにしています。こうすることで、同時着信で A さんが Y の着信を受け取った後に、 B さんには X の着信を表示することが可能になります。

同時着信が頻発するようであれば、そもそもキューに入れておくのも良いでしょう。しかし、それはそれでまた問題があるのですが・・。次に進みます。

### 1つの着信に対して2人が同時に電話に出た場合

着信に出る順番をオペレーター間で予め決めて置けば問題ないのですが、オペレーター同士がそれぞれ別の場所で待機していて、1つの着信に対して同時に出てしまった、ということはよくあります。

普通に Twilio Client を使っていれば、A が電話に出た時には B はすぐに通話が切断されます。そのため、あまり問題になることはないかもしれません。しかし、例えば通話が終わった後に後処理のような機能を実装している場合に大きな問題となります。後処理とは、通話が終わった後にその通話に紐づくメモや顧客情報を入力することです。A には正しく後処理の UI を表示したいのだけど、電話に出られなかった B には後処理の UI を表示せずに、そのまま着信待機の初期画面に戻したいところです。

さて何が問題になるのかというと、 A と B とでどちらが通話に出られたのかを Twilio Client で把握できないのです。電話に出られなかった B　さんも、Twilio Client 上では `connect`イベントが呼ばれた後に `disconnect` イベントが呼ばれます。これは当然、通話ができた A さんでも同じ流れを踏みます。

Twilio Client だけで、どちらが正しく通話ができたのかを把握することは不可能です。`connection`オブジェクトに何らかの違いが出てくるのではないかと思われるかもしれませんが、違いが全く見つかりません。つまり、電話に出られなかった B さんも後処理の表示を出てしまうという問題がずっと続きました。今まではそもそも**オペレーターが同時に電話に出てしまった**というケース自体がそこまで多くなかったので、長い間"仕様"として置いていた問題だったのですが、いよいよ対策しなければならないということで、先日対応しました。

この問題の根本は、`Twilio.Device.connect()`イベントが発生した時というのは、実際には **Client(ブラウザ) と Twilio が接続できた場合であり、Client から対象の相手と電話が繋がったイベントではない** という点です。だからこそ `connect`イベントが電話に出られなかった方にも呼ばれてしまうのです。

本当に悩んで、今は暫定対応となっていますが、純粋にフラグを立てて対応することにしました。`connect`と`disconnect` の間にあるタイムラグを利用しています。それを利用して、`connect`イベントが発生した時にすぐに"電話に出た"という状態にはしないようにしました。`setTimeout` を利用して数秒待ったのちに、それでも `disconnect`が呼ばれていなければ、相手との通話が確実に確立された、と判断するようにしています。

```
var connectedFlg = false;

Twilio.Device.connect(function (conn) {
  setTimeout(function() {
    if (Twilio.Device.activeConnection()) {
      connectedFlg = true;
      // 通話中の UI
    }
  }, 3000);
});

Twilio.Device.disconnect(function (conn) {
  if (connectedFlg) {
    // 同時着信で出られた方の切断処理
  } else {
    // 同時着信で出られなかった方の切断処理
  }
  connectedFlg = false;
});
```

同時着信で電話に出られなかった場合には、`connect`が呼ばれた後すぐに `disconnect`イベントが呼ばれます。この性質を利用しました。`3000` という数字は要チューニングです。暫定対応ではありますが、これで同時着信次に B さんが出られなかった場合に後処理の UI を表示しないという対策がなんとか取れました。


**2017/01/13 更新** Twilio Client 1.4 の最新では、後から出た方の通話がエラーを吐くようになりました！ 31003 のエラーコードを出すので、error 時にハンドリングすれば OK です。
https://www.twilio.com/docs/api/client/errors


### キューにためていた着信をオペレーターが同時に発信した場合

最後にキューを使った時に起きた問題と解決を共有します。これもまた同時着信と似たような問題ではありますが、キューに2件 (X,Y) 通話が溜まっていた場合に、A,B が同時に通話開始を押すとどうなるか、という問題です。

実際にどうなるかというと、A, B それぞれがキューに溜まった通話X, Y と通話ができます。 Twilio の動作としては全く問題ないことがわかります。

しかし、たいていの場合は以下のような仕様であることが多いのではないでしょうか。
> キューにためていた情報の先頭を Web 上で表示させておいて、それと接続する

Twilio の Queue の API で、Queue　の[先頭を取ってこれる API](https://jp.twilio.com/docs/api/rest/member) があります。これを使えば、キューに待っている先頭情報を取得できて便利です。

これを実装してしまうと、途端にキューの扱いが難しくなります。それは、同時に2人待ちのキューに発信してしまった時に起こります。例えばキューの先頭には Y がいたとしましょう。そうすると同時に発信した A, Bには、通話の相手が Y の表示のままとなってしまうのです。B さんは Y さんと話しているつもりだったのに実際は X さんだったということが起こってしまいます。

この問題を解決するには、「キューで本当に通話できた相手というのは、実際に通話が始まるまでわからない」という事実を受け入れる必要があります。その上でどんな対策ができるのでしょうか。

TwiML の Dial にある [Queue](https://jp.twilio.com/docs/api/twiml/queue) を読んでみると、`url` を指定できるのがわかります。そしてこの url に指定した後のアクションで、`DequeingCallSid` というキューから外れた CallSid が取ってこれます。これを利用しました。

具体的には、ここで渡ってくる `DequeingCallSid` をキーとして、 `CallSid` の値を Redis に保存しました。 Redis の `setex` を使えば、一時的な保存領域として使うのにとても便利だったからです。そして、実際に Client からキューへ通話する時に、その Client の `CallSid` が Redis に保存された `DequeingCallSid`　となるので、オペレーターとキューで待っている人の通話をつなげることが可能となります。

注意したいのが、キューの`action`が呼ばれるタイミングは、オペレーターが対象のキューに発信した時となります。そのため、オペレーター側の Redis へのアクセスにしばらく時間をおかないと、 Twilio の action で Redis に CallSid が保存される前に、キューの外れた Call 情報を取ってこようとしてしまう(キーが見つからない)ケースが発生するので、ここの呼び出しのタイミングを少し遅らせる必要があります。

これにより、2人以上がキューに待っていた時にオペレーターが同時に発信したとしても、それぞれが正しい通話相手の情報を表示することが可能になりました。

## 終わりに

電話というのは "同時に何か起きる" 可能性があるものです。同時着信問題を完全に解決するためには、 Twilio の Call の概念そのものの深い理解が必要です。

本記事が、より本格的な Twilio Client の実装を試みようとしている方の役になれば幸いです。