WebRTCブログ第一回

はじめまして!OISの自社製品『ReDois』開発担当です。
『ReDois』は、WebRTCと呼ばれる技術を利用し、リアルタイム配信を実現しています。
私からは、このWebRTCに関する情報について発信していこうと思います。
今回は、WebRTCの基本として、P2Pでの接続について解説します。
WebRTC公式より公開されているサンプルページを使い、ソースコードの解説も交えながら見ていこうと思います。
ソースコードはこちらになります。
このサンプルページでは、左上から順にボタンを押していけば自分同士でWebRTC接続ができるようになっています。
それぞれのボタンを押したとき、具体的にどのような動作をしているのか掘り下げていきましょう。
①[Get media]ボタン
最初に、マイクやカメラからストリームを取得し、それを画面に表示します
async function getMedia() { (中略) try { const userMedia = await navigator.mediaDevices.getUserMedia(constraints); gotStream(userMedia); } catch (e) { console.log('navigator.getUserMedia error: ', e); } } function gotStream(stream) { console.log('Received local stream'); localVideo.srcObject = stream; localStream = stream; }
具体的には、
navigator.mediaDevices.getUserMedia()メソッドでMediaStreamオブジェクトを取得し、これを画面上のvideo要素にセットすることによってマイクやカメラの音声・映像をブラウザ上で再生することができます。
(サンプルページでは、localVideoが示すvideo要素はmuted属性を持っているので音声は再生されません)
②[Create peer connection]ボタン
WebRTC接続の要となるオブジェクトである、RTCPeerConnectionオブジェクトを作成します。
function createPeerConnection() { (中略) const servers = null; window.localPeerConnection = localPeerConnection = new RTCPeerConnection(servers); console.log('Created local peer connection object localPeerConnection'); localPeerConnection.onicecandidate = e => onIceCandidate(localPeerConnection, e); sendChannel = localPeerConnection.createDataChannel('sendDataChannel', dataChannelOptions); sendChannel.onopen = onSendChannelStateChange; sendChannel.onclose = onSendChannelStateChange; sendChannel.onerror = onSendChannelStateChange; window.remotePeerConnection = remotePeerConnection = new RTCPeerConnection(servers); console.log('Created remote peer connection object remotePeerConnection'); remotePeerConnection.onicecandidate = e => onIceCandidate(remotePeerConnection, e); remotePeerConnection.ontrack = gotRemoteStream; remotePeerConnection.ondatachannel = receiveChannelCallback; localStream.getTracks() .forEach(track => localPeerConnection.addTrack(track, localStream)); console.log('Adding Local Stream to peer connection'); }
今回は自分同士での接続なので、ローカル・リモート共にこの場でRTCPeerConnectionオブジェクトを作成しています。
RTCPeerConnectionオブジェクトを作成する他に、①で取得したMediaStreamオブジェクトからMediaStream.getTracks()メソッドでMediaStreamTrackオブジェクトを取得し、RTCPeerConnection.addTrack()メソッドでこれから送信する音声・映像をセットしています。
③[Create offer]ボタン
SDPと呼ばれる文書をoffer側(今回はローカル側)で作成します。SDPについての詳細な説明は今回省きますが、WebRTC接続にはお互いの内部情報をやりとりする必要があり、SDPはそれを表現するプロトコルです。
async function createOffer() { try { const offer = await localPeerConnection.createOffer(offerOptions); gotDescription1(offer); } catch (e) { onCreateSessionDescriptionError(e); } }
function gotDescription1(description) { offerSdpTextarea.disabled = false; offerSdpTextarea.value = description.sdp; }
SDP offerはRTCPeerConnection.createOffer()メソッドで作成します。
ここでは更に、作成したSDPをtextarea要素で表示しています。
④[Set offer]ボタン
作成したSDP offerをRTCPeerConnectionオブジェクトにセットします。
async function setOffer() { let sdp = offerSdpTextarea.value; const offer = { type: 'offer', sdp: sdp }; console.log(`Modified Offer from localPeerConnection\n${sdp}`); try { // eslint-disable-next-line no-unused-vars const ignore = await localPeerConnection.setLocalDescription(offer); onSetSessionDescriptionSuccess(); } catch (e) { onSetSessionDescriptionError(e); } try { // eslint-disable-next-line no-unused-vars const ignore = await remotePeerConnection.setRemoteDescription(offer); onSetSessionDescriptionSuccess(); } catch (e) { onSetSessionDescriptionError(e); } }
ローカル側はRTCPeerConnection.setLocalDescription()、リモート側はRTCPeerConnection.setRemoteDescription()でSDP offerをセットします。
今回は自分同士の接続なので気にしなくてよいのですが、本来リモートはSDP offerを知らないはずなので、SDPを伝達するためにシグナリングサーバーと呼ばれるものが必要になってきます。これについては次回以降で詳しく解説する、かも…?
また、SDP offerをセットした後、リモート側のonTrackイベントが発火します。イベント発火時の処理gotRemoteStream()は②でリモート側に設定しています。
function gotRemoteStream(e) { if (remoteVideo.srcObject !== e.streams[0]) { remoteVideo.srcObject = e.streams[0]; console.log('Received remote stream'); } }
onTrackイベントが発火すると、通信相手が送信するMediaStreamオブジェクトの配列を受け取れるので、それをリモート側のvideo要素にセットしています。
ただし、この時点ではWebRTC接続が確立されていないので、セットした音声・映像が再生されることはありません。
⑤[Create answer]ボタン
SDPをanswer側(今回はリモート側)で作成します。
offerとanswerについての説明も次回以降をお楽しみに…!!
async function createAnswer () { (中略) try { const answer = await remotePeerConnection.createAnswer(); gotDescription2(answer); } catch (e) { onCreateSessionDescriptionError(e); } }
function gotDescription2(description) { answerSdpTextarea.disabled = false; answerSdpTextarea.value = description.sdp; }
SDP answerはRTCPeerConnection.createAnswer()メソッドで作成します。
③同様、作成したSDPをtextarea要素で表示しています。
⑥[Set answer]ボタン
作成したSDP answerをRTCPeerConnectionオブジェクトにセットします。
async function setAnswer() { let sdp = answerSdpTextarea.value; const answer = { type: 'answer', sdp: sdp }; try { // eslint-disable-next-line no-unused-vars const ignore = await remotePeerConnection.setLocalDescription(answer); onSetSessionDescriptionSuccess(); } catch (e) { onSetSessionDescriptionError(e); } console.log(`Modified Answer from remotePeerConnection\n${sdp}`); try { // eslint-disable-next-line no-unused-vars const ignore = await localPeerConnection.setRemoteDescription(answer); onSetSessionDescriptionSuccess(); } catch (e) { onSetSessionDescriptionError(e); } }
ローカル側はRTCPeerConnection.setRemoteDescription()、リモート側はRTCPeerConnection.setLocalDescription()でSDP answerをセットします。
ここまで完了すると後はWebRTCが勝手に接続試行を行ってくれ、成功するとリモート側のvideo要素で音声・映像の再生が始まります。
接続成功!
⑦[Hang up]ボタン
WebRTC接続を終了します。
function hangup() { remoteVideo.srcObject = null; console.log('Ending call'); localStream.getTracks().forEach(track => track.stop()); sendChannel.close(); if (receiveChannel) { receiveChannel.close(); } localPeerConnection.close(); remotePeerConnection.close(); (以下略)
ローカル・リモート共にRTCPeerConnection.close()メソッドでWebRTC接続を閉じます。
お疲れ様でした。
さて、いかがでしたでしょうか。
単純なP2Pであれば、WebRTCは誰でも簡単に利用することができる優れものです。
今後も『ReDois』やWebRTCについて継続的に発信していきます。
お楽しみに!