やりたかったこと
peerjs-serverをシグナリングサーバーに使って、[ブラウザ - iOSネイティヴアプリ]間でWebRTCなビデオチャットしたい。
とりあえずできたもの
iOSネイティヴアプリ用のPeerライブラリPeerObjectiveC作った。作ったというか、WebRTCのiOSアプリ向けサンプルであるAppRTCDemoに変更を加えてpeerjs-serverとやりとりできるようにした。
とりあえずビデオチャットできたというレベルで、色々抜けや間違いがあると思う。
ちなみにPeerJSは一旦XHRStreamingでシグナリングサーバーとの接続オープンしたのちWebSocketオープンにチャレンジしてオープンできたらWebSocketで、だめならWebSocketは諦めてXHRStreamingでシグナリング経路を確保するようだけど、コイツはそれはしてない。
WebSocket一本。WebSocketが繋がらなかったらそれで終了w
大きく表示されているのがMacBookのChrome(http://cdn.peerjs.com/demo/videochat/)から送られてきてるリモートのビデオ映像。小さいのがiPhoneのカメラから取得してるローカルの映像。これは当然、Macのブラウザ側でも表示されてる。
ちなみにNTTコミュニケーションズのSkyWayともやってみたけど、現状ではうまくいかなかった。
WebSocketのリクエスト時、ヘッダに Origin: <SkyWayに登録したドメイン>
とするとWebSocketオープン後の{type:"OPEN"}
受信までいったんだけど、シグナリング中になんか失敗してしまう。
もうちょっと頑張ったらつながりそうだったけど、疲れたのでやめた(´ω`;)
使い方
使い方はREADME.mdに頑張って書いたので見てくだされ。
あとはサンプルアプリのAppDelegate.mとかViewController.mとか。
ハマったとこ
ハマりどころとしては、アプリ開始時に一回だけ [RTCPeerConnectionFactory initializeSSL]
しないとローカルのSessionDescription取得時にエラーがでて先に進めない。気付くのに3時間くらいかかったo<´・ω・`>o
サンプルアプリでいうとココ
わかったこととか
とりあえず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
はデモアプリのコードから借りてきた :p
リクエスト
$ curl http://0.peerjs.com:9000/lwjd5qra8257b9/id
レスポンス
eve816uhqazfflxr
ストリーミング接続開始要求
XHRストリーミングはWebSocketが使用できない環境でストリーミング接続を行うために使用するらしい。
従ってPeerJSの実装では、一旦XHRストリーミングをオープンしておいて、後述のWebSocket接続オープンが成功した時点でXHRストリーミングを破棄している。
HTTPメソッド
POST
URL
- URL
/:key/:id/:token/id
- パラメータ
-
:key
サーバーの起動オプションに渡されたキー。デフォルトはpeerjs
-
:id
id取得
でサーバーから取得したid -
:token
クライアント側で生成したランダムなトークン
-
- レスポンス
- 成功時:
{"type":"OPEN"}
- エラー時:
{"type":"ERROR"}
- 接続はそのまま維持される。
- 成功時:
例
リクエスト
リクエスト
$ curl -X POST http://0.peerjs.com:9000/lwjd5qra8257b9/j2eythggd12lnmi/randomtoken/id
レスポンス
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"}}
- 接続はそのまま維持される。
- 成功時:
例
リクエスト
$ wscat -c "ws://0.peerjs.com:9000/peerjs?key=lwjd5qra8257b9&id=j2eythggd12lnmi&token=randomtoken"
レスポンス
{"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
を設定しておき、イベント発生のたびにピアに送信する。
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"}
この辺まででなんだかできそうな気がしてきたので、これ以上は追ってない。