Phoenix Channelを使ってWebRTCのシグナリングをするメモ #beamlangtokyo


[Elixir初心者向けハンズオン vol2](https://beam-lang.connpass.com/event/60655/)で学んだことを振り返りつつメモ書いています。

前に[WebRTCのシグナリングサーバーにMilkcocoaを使って見るメモ #webrtcjp #mlkcca](https://qiita.com/n0bisuke/items/2d88d5b31b9bb7e7ac1e)という記事を書いたのですが、これのPhoenix.channel版をやってみます。

内容的にはWebRTC側の要素が多いかも

## 作ったもの

ビデオチャット - Phoenix Channelでシグナリングをするようにしています。

![](https://i.gyazo.com/7706deae06e5dd280d1ff9d9602183ca.png)

Elixirハンズオンでやったこれと
![](https://i.gyazo.com/0306c9832f689d0409dd5e38d026459a.png)

WebRTCハンズオンでやったこれ

![](https://camo.qiitausercontent.com/778a05be724b665167190475728e5b309737515a/68747470733a2f2f692e6779617a6f2e636f6d2f31666464626539343266633666396265353264393163336539383562333565322e706e67)

二つの組み合わせです。

## Phoenix Channel

[ハンズオンの資料](PhoenixはWebSocketをそのまま使うのではなく、 WebSocket上に作られた複数のChannel(PhoenixChannel)を使って クライアントとサーバーの双方向通信を行います。)を参照。

>PhoenixはWebSocketをそのまま使うのではなく、 WebSocket上に作られた複数のChannel(PhoenixChannel)を使って クライアントとサーバーの双方向通信を行います。

* WebSocket上に仮想的なレイヤーを作って通信を行う仕組み
* Socket.ioのルーム機能みたいなものを実装している
* JS以外にも[Swift](https://github.com/davidstump/SwiftPhoenixClient)などのクライアントが存在する

## WebRTCのシグナリングの修正点

基本は[こちらの記事の手順](https://qiita.com/n0bisuke/items/2d88d5b31b9bb7e7ac1e)で進めたあとの話になります。

### イベント追加

`lib/demo_web/channels/chat_channel.ex`に`signaling`イベントを追加

```lib/demo_web/channels/chat_channel.ex
・
・(省略)
・
    def handle_in("signaling", payload, socket) do
        broadcast! socket, "signaling", payload
        {:reply, {:ok, payload}, socket}
    end
・
・(省略)
・
```

サーバーサイドはこれだけ。

### UUIDの生成

今回は`demo`という名前のPhoenixアプリケーションを作成し、`assets/js/demo.js`に追記していきました。

Milkcocoa版の時と同様ですが、SDPを送る際に、自分自身に情報が送られると`Failed to set remote answer sdp: Called in wrong state: STATE_INPROGRESS`などのエラーがでるため自分自身には情報が送られてこないようにする必要があります。

アクセスごとにUUIDを付与して自分自信への情報は省くようにします。

Phoenix Channelでそういう機能ないのかな (ありそう)

```assets/js/demo.js

'use strict';

import {Socket} from "phoenix"

const UUID = uuid();

function uuid() {
  let uuid = "", i, random;
  for (i = 0; i < 32; i++) {
    random = Math.random() * 16 | 0;

    if (i == 8 || i == 12 || i == 16 || i == 20) {
      uuid += "-"
    }
    uuid += (i == 12 ? 4 : (i == 16 ? (random & 3 | 8) : random)).toString(16);
  }
  return uuid;
}


・
・(省略)
・

```

### sdpとIceCandidateの送信

どちらも`chan.push()`を使って送信するようにします。

```assets/js/demo.js

・
・(省略)
・

function sendSdp(sessionDescription) {
        console.log('---sending sdp ---');
        $textForSendSdp.value = sessionDescription.sdp;
        const message = JSON.stringify(sessionDescription);
        console.log('sending SDP=' + message);
        //ws.send(message);
        chan.push('signaling', {body: message, uuid:UUID})
}

・
・(省略)
・

 function sendIceCandidate(candidate) {
        console.log('---sending ICE candidate ---');
        const message = JSON.stringify({ type: 'candidate', ice: candidate });
        console.log('sending candidate=' + message);
        // ws.send(message);
        chan.push('signaling', {body: message, uuid:UUID})
    }

・
・(省略)
・
```

### signalingイベントを受け取る処理を追加

SDP情報が送られてきたときの処理です。

`if(msg.uuid === UUID) return;`では先ほどのUUID話で送られてきたUUIDが自分のものだった場合は何もしないようにしています。

```assets/js/demo.js

・
・(省略)
・
    chan.on("signaling", msg => {

      const message = JSON.parse(msg.body);
      if(msg.uuid === UUID) return; //送られてきたUUIDが自分のものだった場合は何もしない

      if (message.type === 'offer') {
        // offer 受信時
        console.log('Received offer ...');
        $textToReceiveSdp.value = message.sdp;
        const offer = new RTCSessionDescription(message);
        setOffer(offer);
      }
      else if (message.type === 'answer') {
          // answer 受信時
          console.log('Received answer ...');
          $textToReceiveSdp.value = message.sdp;
          const answer = new RTCSessionDescription(message);
          setAnswer(answer);
      }
      else if (message.type === 'candidate') {
          // ICE candidate 受信時
          console.log('Received ICE candidate ...');
          const candidate = new RTCIceCandidate(message.ice);
          console.log(candidate);
          addIceCandidate(candidate);
      }
    })
・
・(省略)
・
```

これでPhoenix Channel経由でシグナリングができるようになりました。

## 所感

Phoenix自体今日初めて触りましたが使い勝手よくErlang VMを利用できる仕組みはすごいですね。（小並感

Socket.ioなどの既存システムとうまく連携できる仕組みがあると過去のシステムの乗り換えハードルが下がって良さそう。(ブリッジする仕組みありそうな気がする）