23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PeerJSのObjectiveC版 PeerObjectiveC書いてみた

Last updated at Posted at 2015-01-14

やりたかったこと

peerjs-serverをシグナリングサーバーに使って、[ブラウザ - iOSネイティヴアプリ]間でWebRTCなビデオチャットしたい。

とりあえずできたもの

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

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

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

こんな感じ。
IMG_4546.jpg

大きく表示されているのが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: データのシリアライズ方式(binarybinary-utf8jsonのいずれか) 。
    • metadata: ユーザーからoptionとして渡される。
    • browser: ブラウザの種類(FirefoxChromeSupportedUnsupportedのいずれか)

{"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: ブラウザの種類(FirefoxChromeSupportedUnsupportedのいずれか)

{"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"}

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

23
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?