contentWebRTC知見まとめJavaScriptWebRTCWebRTCで1対1ビデオチャットシステムを実装して得られた知見のまとめ。1対nとかn対nの場合はまた別なのでご注意を。処理の全体の流れ(TrickleICE時)双方がRTCPeerConnectionを作成する双方がnavigator.mediaDevices.getUserMediaを実行し、取得したMediaStreamからMediaStreamTrackMediaStreamTrackを取得してConnectionにaddするどちらか一方がOfferSdpを作成する。(どちらでも構わない。以下送信側と呼ぶ)送信側は作成したSdpをRTCPeerConnectionのlocalDescriptionとして登録する送信側は、onIceCandidate,onTrackにイベントハンドラを設定する送信側は、何らかの手段(通常はシグナリングサーバーを使うが何でも良い)でofferSdpをもう片方に送るofferSdpを受け取った側(以降受信側と呼ぶ)は受けっとたsdpをRTCPeerConnectionのremoteDescriptionにsetする。受信側はRTCPeerConnection.createAnswerでanswerSdpを作る受信側は作成したanswerSdpをlocalDescriptionにセットする。iceCandidateイベントが発火した場合、そのcandidateをもう片方に送る(手段は問わない)iceCandidateを受け取った側はRTCPeerConnection.addCandidateで受け取ったcandidateを追加する上記のICE交換を行っているうちに上手く行くとonTrackイベントが呼ばれるのでそこからMediaStreamを取得してvideoなどに投入する実装手順STUN/TURNサーバーを用意するWebRTCで接続する両社が同一LANにいるとか、PublicIPアドレスを持っているとかでない限り、STUNサーバーまたはTURNサーバーは必要。STUNサーバー、TURNサーバー自体についてはhttps://qiita.com/okyk/items/a405f827e23cb9ef3bdeの記事が分かりやすい。STUNサーバーは非常に処理が軽いので無料でGoogleなどが提供してくれていたり後述のcoTurnがデフォルトでSTUN機能も持っているので、あまり意識する必要はない。一方で、TURNサーバーはWebRTCで利用するデータをリレーする性質上、データ通信のコストが大きい。無料では利用できないので、有料サービス(あまり多くない)を利用するか自分で立てる必要がある。自分で立てる場合はcoTurnがデファクトスタンダード。いえ、Turnサーバー自体がいまいちメジャーではないので、構築は結構面倒。(パッケージマネージャーから一発では入らずビルドが必要、構築済みのDockerイメージもないなど)新規に対応が必要な場合は、多めに工数を見積もっておくべき。TURNサーバーの稼働チャックに関するTIPSTrickleICEをまず使う。candidateの取得シミュレーションが出来るページ。ここでtype:relayでcandidateに出てこない場合は確実に無理である。candidateとして選出されるかどうかの保証にしかならないので、ここが上手く通ったからと言って確実に本番のWebRTCも通るわけではないのは注意。coTurnはデフォルトでstun機能もONになっている。純粋にTURNサーバーとして動くかどうか確認したい場合、turnserver.confにno-stunを付けてSTUN機能を切るべし。これをやっておかないとTURNでテストしているつもりが実際はSTUNで接続していたということになりかねない。基本WebRTCでは開発者は接続方法の選定に直接タッチできないので、確実にTURNでテストしたい場合は対応必須。TURNサーバー)の情報からRTCConfigurationを作る。最低限だとこんな感じ。]}STUNはURLのみ、TURNの場合は認証情報が必須。(認証情報はturnserver側で設定する)navigator.mediaDevices.getUserMedia周りMDN結構事故る可能性の高い部分。実は対応ブラウザが意外と狭く、特にiOS系は純正のSafari以外では動かない。(2019年11月現在)ChromeやFireFoxなどはAndroidではOKだが、iOSでは無理。WebView系も全滅である。PC版Chrome非常にカメラ周りがデリケートで、別タブや他アプリがカメラを利用している状態でgetUserMediaをするとすぐにDOMExceptionを吐くので注意が必要。最近のブラウザではhttpsではないとgetUserMediaが出来ないケースが多い。本番環境がHttpsではないことは今日日ないと思われるが、開発環境の場合は注意。localhostの場合はhttpでもOK。SDPのやり取りRTCPeerConnection.createOffer()を呼ぶと、正確にはSDPではなくRTCSessionDescriptionInitという型のObjectが返る。これはsetLocalDescripitonする場合にはそのまま引数で渡してもOK。このObjectはtype(基本的にofferまたはanswer)とsdp文字列で構成されている。offerかanswerかは自明なケースが多いので、通信の双方でやり取りを行う場合はsdpを文字列としてやり取りすればOKである。Candidateのやり取りの方法であるが、ここに関しては全くの自由である。実際にはWebSocketを使ってタイムラグがないように実装するのが普通であるが、ハンズオン系のサンプルで行われているようにコピー&ペーストで手動伝達しても構わない。(凡そ文字列のやり取りが出来るのならば何でも通る)システム構成に応じて一番やりやすい方法を選べばよいと思われる。(例えばFirebaseを利用したサーバーレスだったらFireStoreとquerySnapshotが良いし、サーバーがあるならそこでWebSocket通信を行うのがやりやすいはず)ICEのやり取りTrickleとVanillaICEのやり取り方としてTrickleICEとVanillaICEがあります。Candidateの収集が完了するまでSDPの送付を遅らせて、SDPと一緒にまとめて送ってしまう方式。ICEを使うものだと思っていて問題ない。基本的に、onIceCandidateの中で非同期で取得する全て集めきった場合にも呼ばれるので、event.candidateの存在確認は必要である。}ICEを文字列としてやり取りするIceCandidateを文字列に変換する場合は以下のように行うJSON.stringify(event.candidate.toJSON())event.candidateで取得できるRTCIceCandidateはそのまま文字列出来ないので、toJSONを呼んでRTCIceCandidateInitに変換した上で、JSON.stringifyで文字列化すればOK。復号は以下のように行う。RTCIceCandidate(JSON.parse(iceString))rtcPeerConnection.addCandidate(iceCandidate);このようにすることで、同様に文字列→init→Candidateと変換できる。streamの取り出しonTrackで取り出す基本的にはこの方法でOKonTrackにはRTCTrackEventが渡ってくるので、event.streams[0]でMediaStreamが取り出せる。後はvideoのsrcObjectに入れればOKReceiverから引っこ抜く何らかの理由でonTrackで上手く行かない場合WebRTCの通信準備を始めた時点で、内部的にRTCRtpReceiverが生成されている。内部的に、これがよろしく受信処理を行っているわけだが、これは受信したMediaStreamTrackを保持している。}videoにsrcObjectを投入する時にやらかしたこと投入後video.play()を呼び忘れるplay()系で再生を開始しない限り画面が黒いままplaysInlineを付けておらずiOS系で動かない特にWindowで開発している場合は気づきにくいので注意Debugに役立つものchromeでchrome://webrtc-internalsをアドレスバーに入れるとWebRTC用のDebugツールが起動する。requestStockLike3@xx2xyyy技術チーム新設に伴い、新人CTOになりました。Followイングリッシュブートキャンプ株式会社2日20時間の超短期集中英会話スクールを運営。Webやアプリへの展開も計画中です。