PeerJSのObjectiveC版 PeerObjectiveC書いてみた


## やりたかったこと

[peerjs-server](https://github.com/peers/peerjs-server)をシグナリングサーバーに使って、<b>[ブラウザ - iOSネイティヴアプリ]</b>間でWebRTCなビデオチャットしたい。

## とりあえずできたもの

iOSネイティヴアプリ用のPeerライブラリ[PeerObjectiveC](https://github.com/hiroeorz/PeerObjectiveC)作った。作ったというか、WebRTCのiOSアプリ向けサンプルであるAppRTCDemoに変更を加えてpeerjs-serverとやりとりできるようにした。

* [github:PeerObjectiveC](https://github.com/hiroeorz/PeerObjectiveC)

とりあえずビデオチャットできたというレベルで、色々抜けや間違いがあると思う。

ちなみにPeerJSは一旦XHRStreamingでシグナリングサーバーとの接続オープンしたのちWebSocketオープンにチャレンジしてオープンできたらWebSocketで、だめならWebSocketは諦めてXHRStreamingでシグナリング経路を確保するようだけど、コイツはそれはしてない。
WebSocket一本。WebSocketが繋がらなかったらそれで終了w

こんな感じ。
![IMG_4546.jpg](https://qiita-image-store.s3.amazonaws.com/0/13835/07c9412c-67d8-eafe-f323-bbc486acd79f.jpeg "IMG_4546.jpg")

大きく表示されているのがMacBookのChrome([http://cdn.peerjs.com/demo/videochat/](http://cdn.peerjs.com/demo/videochat/))から送られてきてるリモートのビデオ映像。小さいのがiPhoneのカメラから取得してるローカルの映像。これは当然、Macのブラウザ側でも表示されてる。

ちなみにNTTコミュニケーションズの[SkyWay](http://nttcom.github.io/skyway/)ともやってみたけど、現状ではうまくいかなかった。
WebSocketのリクエスト時、ヘッダに ```Origin: <SkyWayに登録したドメイン>```とするとWebSocketオープン後の```{type:"OPEN"}```受信までいったんだけど、シグナリング中になんか失敗してしまう。
もうちょっと頑張ったらつながりそうだったけど、疲れたのでやめた(´ω`;)

## 使い方

使い方は[README.md](https://github.com/hiroeorz/PeerObjectiveC/blob/master/README.md)に頑張って書いたので見てくだされ。

あとはサンプルアプリの[AppDelegate.m](https://github.com/hiroeorz/PeerObjectiveC/blob/master/PeerObjectiveC/AppDelegate.m)とか[ViewController.m](https://github.com/hiroeorz/PeerObjectiveC/blob/master/PeerObjectiveC/ViewController.m)とか。

## ハマったとこ

ハマりどころとしては、アプリ開始時に一回だけ ```[RTCPeerConnectionFactory initializeSSL]``` しないとローカルのSessionDescription取得時にエラーがでて先に進めない。気付くのに3時間くらいかかったo<´・ω・｀>o

サンプルアプリでいうと[ココ](https://github.com/hiroeorz/PeerObjectiveC/blob/master/PeerObjectiveC/AppDelegate.m#L22)

## わかったこととか

とりあえずAppRTCDemoをpeerjs-server向けに変更するにあたって、peerjs-serverのシグナリングプロトコルを追ってみた。

ここから先は単なる覚書。下記のシグナリング経路を使って、リモートのSessionDescriptionをローカルのPeerConnectionにセットし、ローカルのSessionDescriptionをリモートに送り、さらにお互いにICE CANDIDATEを投げ合えばWebRTCによるピアツーピア通信が始まる（乱暴。


## HTTP API

PeerJSは最初にidが与えられなかったらサーバーからランダムなidを取得する。これは単純にHTTP GETでサーバーから取得する。

### id取得

サーバーからランダムなidを取得する。
クライアント側でidを生成する場合は、このAPIを呼び出す処理を省くことができる。

#### HTTPメソッド
GET

#### URL

* URL:
    * ```/:key/id ```
* パラメータ
    * ```:key``` サーバーの起動オプションに渡されたキー。デフォルトは```peerjs```
* レスポンス
    * ```id``` サーバー側で生成したランダムな文字列
        * 例: ```eve816uhqazfflxr``` 


#### 例

以下の```key```は[デモアプリ](http://cdn.peerjs.com/demo/videochat/#)のコードから借りてきた :p

<b>リクエスト</b>

```
$ curl http://0.peerjs.com:9000/lwjd5qra8257b9/id
```

<b>レスポンス</b>

```
eve816uhqazfflxr
```

----

### ストリーミング接続開始要求

XHRストリーミングはWebSocketが使用できない環境でストリーミング接続を行うために使用するらしい。
従ってPeerJSの実装では、一旦XHRストリーミングをオープンしておいて、後述のWebSocket接続オープンが成功した時点でXHRストリーミングを破棄している。

* [参考1](https://github.com/peers/peerjs/blob/master/lib/socket.js#L33)
* [参考2](https://github.com/peers/peerjs/blob/master/lib/socket.js#L70)

#### HTTPメソッド
POST

#### URL

* URL
    * ```/:key/:id/:token/id```
* パラメータ
    * ```:key``` サーバーの起動オプションに渡されたキー。デフォルトは```peerjs```
    * ```:id``` ```id取得```でサーバーから取得したid
    * ```:token``` クライアント側で生成したランダムなトークン
* レスポンス
    * 成功時:
        * ```{"type":"OPEN"}```
    * エラー時:
        * ```{"type":"ERROR"}```
    * 接続はそのまま維持される。

#### 例

リクエスト

<b>リクエスト</b>

```
$ curl -X POST http://0.peerjs.com:9000/lwjd5qra8257b9/j2eythggd12lnmi/randomtoken/id
```

<b>レスポンス</b>

```
00000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000
...(中略)...
00000000000000000000000000000000000000000000000000000000000
{"type":"OPEN"}
```
そのまま接続維持。後述のWebSocketが接続成功したらクローズされる。成功しなかった場合はこの接続を用いて下記のWebSocket APIの通信を行う。


## WebSocket API

### WebSocket接続開始要求

#### URL

* URL
    * ```ws://<ホスト名>:<ポート番号>/:path/peerjs?key=:key&id=:id&token=:token```
* パラメータ
    * ```:key``` サーバーの起動オプションに渡されたキー。デフォルトは```peerjs```
    * ```:id``` ```id取得```でサーバーから取得したid
    * ```:token``` クライアント側で生成したランダムなトークン
* レスポンス
    * 成功時: 
        * ```{"type":"OPEN"}```
    * エラー時: 
        * ```{"type":"ERROR", "payload":{"msg":"No id, token, or key supplied to websocket server"}}```
    * 接続はそのまま維持される。

#### 例

<b>リクエスト</b>

```
$ wscat -c "ws://0.peerjs.com:9000/peerjs?key=lwjd5qra8257b9&id=j2eythggd12lnmi&token=randomtoken"
```

<b>レスポンス</b>

```
{"type":"OPEN"}
```

----

## WebRTC開始要求: OFFER

送信先クライアントにP2P接続のオープンを要求する。
シグナリングサーバーは受信したデータをそのまま送信先クライアントに転送する。

### パラメータ

* ```type```: "OFFER" 固定
* ```src```: 送信元id
* ```dst```: 送信先id
* ```payload```:
    * ```sdp```: ```pc.setLocalDescription```のコールバックに引数として渡されたsdp
    * ```type```: ストリームのタイプ。```media```または```data```
    * ```label```: コネクションを識別するための文字列。デフォルトは```id```と同じ。
    * ```connectionId```: クライアント側で生成したコネクション毎のid(```pc_xxxx```)
    * ```reliable```: data channelに信頼性をもたせるか、もたせないかの指定(デフォルトは```false```)。
    * ```serialization```: データのシリアライズ方式(```binary```、```binary-utf8```、```json```のいずれか) 。
    * ```metadata```: ユーザーからoptionとして渡される。
    * ```browser```: ブラウザの種類(```Firefox```、```Chrome```、```Supported```、```Unsupported```のいずれか)

### 例

```
{"type": "OFFER", 
 "src": "05u1k6gkhjwi2j4i", 
 "dst": "haruhf6uvr9vygb9", 
 "payload": {"sdp": {"type": "media", 
                     "connectionId": "pc_abc", 
                     "reliable": false, 
                     "serialization": "binary", 
                     "browser": "Chrome", 
                     "sdp": "(下記に例を記載)"}}
```

### sdpの例

```
v=0\r\no=- 7418734785428027033 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE audio video\r\na=msid-semantic: WMS yuuHBnpn1dCjPmqLMDoylFMGwUNX5V7POuu4\r\nm=audio 1 RTP/SAVPF 111 103 104 0 8 106 105 13 126\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=ice-ufrag:Hw3NWnb12md3lR6d\r\na=ice-pwd:otSaCLf4lCJ/thdp4vrZDBeT\r\na=ice-options:google-ice\r\na=fingerprint:sha-256 3C:7C:7F:50:12:01:52:1E:68:82:60:96:4F:E3:34:8E:93:6D:49:D7:FA:6C:C5:97:82:A9:17:F6:99:71:BB:B7\r\na=setup:actpass\r\na=mid:audio\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=recvonly\r\na=rtcp-mux\r\na=rtpmap:111 opus/48000/2\r\na=fmtp:111 minptime=10\r\na=rtpmap:103 ISAC/16000\r\na=rtpmap:104 ISAC/32000\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:106 CN/32000\r\na=rtpmap:105 CN/16000\r\na=rtpmap:13 CN/8000\r\na=rtpmap:126 telephone-event/8000\r\na=maxptime:60\r\nm=video 1 RTP/SAVPF 100 116 117 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:1 IN IP4 0.0.0.0\r\na=ice-ufrag:Hw3NWnb12md3lR6d\r\na=ice-pwd:otSaCLf4lCJ/thdp4vrZDBeT\r\na=ice-options:google-ice\r\na=fingerprint:sha-256 3C:7C:7F:50:12:01:52:1E:68:82:60:96:4F:E3:34:8E:93:6D:49:D7:FA:6C:C5:97:82:A9:17:F6:99:71:BB:B7\r\na=setup:actpass\r\na=mid:video\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=sendrecv\r\na=rtcp-mux\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=rtcp-fb:100 goog-remb\r\na=rtpmap:116 red/90000\r\na=rtpmap:117 ulpfec/90000\r\na=rtpmap:96 rtx/90000\r\na=fmtp:96 apt=100\r\na=ssrc-group:FID 2413349061 3209926379\r\na=ssrc:2413349061 cname:LFa7v78rabKhSORN\r\na=ssrc:2413349061 msid:yuuHBnpn1dCjPmqLMDoylFMGwUNX5V7POuu4 6f957125-c30c-45f8-85a6-5ac644736662\r\na=ssrc:2413349061 mslabel:yuuHBnpn1dCjPmqLMDoylFMGwUNX5V7POuu4\r\na=ssrc:2413349061 label:6f957125-c30c-45f8-85a6-5ac644736662\r\na=ssrc:3209926379 cname:LFa7v78rabKhSORN\r\na=ssrc:3209926379 msid:yuuHBnpn1dCjPmqLMDoylFMGwUNX5V7POuu4 6f957125-c30c-45f8-85a6-5ac644736662\r\na=ssrc:3209926379 mslabel:yuuHBnpn1dCjPmqLMDoylFMGwUNX5V7POuu4\r\na=ssrc:3209926379 label:6f957125-c30c-45f8-85a6-5ac644736662\r\n
```

## 応答: ANSWER

ピアからの```OFFER```に対する応答。

### パラメータ

* ```type```: "ANSWER" 固定
* ```src```: 送信元id
* ```dst```: 送信先id
* ```payload```:
    * ```sdp```: ```pc.setLocalDescription```のコールバックに引数として渡されたsdp
    * ```type```: ストリームのタイプ。```media```または```data```
    * ```connectionId```: クライアント側で生成したコネクション毎のid(```pc_xxxx```)
    * ```browser```: ブラウザの種類(```Firefox```、```Chrome```、```Supported```、```Unsupported```のいずれか)

### 例

```
{"type": "ANSWER", 
 "src": "05u1k6gkhjwi2j4i", 
 "dst": "haruhf6uvr9vygb9", 
 "payload": {"sdp": {"type": "media", 
                     "connectionId": "pc_abc", 
                     "browser": "Chrome", 
                     "sdp": "(前項に例を記載)"}}
```

## エンドポイント情報の伝達: ICE CANDIDATE

エンドポイント同士がSTUNサーバーとTURNサーバーを用いて接続するための情報を相互に送信する。
送信は非同期におこなわれる。

### パラメータ

* ```type```: "CANDIDATE" 固定
* ```src```: 送信元id
* ```dst```: 送信先id
* ```payload```:
    * ```candidate```: ```onicecandidate```のコールバックに引数として渡されたイベントオブジェクトのcandidate(```evt.candidate```)
    * ```type```: ストリームのタイプ。```media```または```data```
    * ```connectionId```: クライアント側で生成したコネクション毎のid(```pc_xxxx```)

### 例

```
{"type": "CANDIDATE", 
 "src": "05u1k6gkhjwi2j4i", 
 "dst": "haruhf6uvr9vygb9", 
 "payload": {"candidate": {"type": "media", 
                           "connectionId": "pc_abc", 
                           "candidate": {"sdpMLineIndex":1,
                                         "sdpMid":"video",
                                         "candidate":"candidate:3013953624 2 udp 2122260223 192.168.1.123 60258 typ host generation 0"}}}
```

### candidateの例

```
{"sdpMLineIndex":1,
 "sdpMid":"video",
 "candidate":"candidate:3013953624 2 udp 2122260223 192.168.1.123 60258 typ host generation 0"}
```

### 送信コード例

```onicecandidate``` を設定しておき、イベント発生のたびにピアに送信する。

```javascript
pc.onicecandidate = function(evt) {
  if (evt.candidate) {
    util.log('Received ICE candidates for:', connection.peer);
    provider.socket.send({
      type: 'CANDIDATE',
      dst: peerId,
      payload: {
        candidate: evt.candidate,
        type: connection.type,
        connectionId: connection.id
      }
    });
  }
};
```


## 通信終了: LEAVE

通信相手の接続が閉じた。この通知はクライアントからは送らず、サーバーが判断して接続が閉じたクライアントと通信中だったクライアントに送信します。

### 送信データ

```
{"type": "LEAVE", "src": "送信元id", "dst": "送信先id"}
```

### 例

```
{"type": "LEAVE", "src": "05u1k6gkhjwi2j4i", "dst": "jghpgwypfucba9k9"}
```

----

## OFFERに対する応答がない: EXPIRE

リモートにOFFERを送ったが応答がなかった。この通知はクライアントからは送らず、サーバーが判断してOFFER送信元のクライアントに送信します。

### 送信データ

```
{"type": "EXPIRE", "src": "送信元id", "dst": "送信先id"}
```

### 例

```
{"type": "EXPIRE", "src": "05u1k6gkhjwi2j4i", "dst": "jghpgwypfucba9k9"}
```

----


この辺まででなんだかできそうな気がしてきたので、これ以上は追ってない。