WebRTC 勉強会資料
概要
JavaScriptに触ったことがない人が読めるように解説しながら、
WebRTCのPeer to Peer通信の仕組みについて学ぶ。
環境構築
skip
Section. 4
ウェブカメラの映像をストリーミングする
このセクションでは、WebRTC APIでメディアストリームをつかむ
通信はしない
getUserMedia()
の使い方
MediaDevices.getUserMedia()
メソッドは、MediaStream
を戻り値として返す。
MediaStream
に引数constraints
に指定されたメディアを持たせるため、メディアの使用許可をユーザに求める。
そのため、非同期処理を記述しなければならない。
- 使い方1
async function getMedia(constraints) { let stream = null; try { stream = await navigator.mediaDevices.getUserMedia(constraints); /* ストリームを使用 */ } catch(err) { /* エラーを処理 */ } }
- 使い方2 (Promiseで非同期処理のコールバックを記述)
navigator.mediaDevices.getUserMedia(constraints) .then(function(stream) { /* ストリームを使用 */ }) .catch(function(err) { /* エラーを処理 */ });
今回のconstraints
は、
// On this codelab, you will be streaming only video (video: true).
const mediaStreamConstraints = {
video: true,
};
であり、映像のみをメディアとして使用する。
また、使い方2のとおり非同期処理のコールバックに、MediaStream
を引数に取る関数と、エラーをハンドルするコールバックを指定している。
メディアの使用許可が得られれば、ローカルストリームに格納する。
// Local stream that will be reproduced on the video.
let localStream;
// Handles success by adding the MediaStream to the video element.
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
そして、こちらのコードでHTMLのvideo
タグがlocalVideo
に関連付けされており、
// Video element where stream will be placed.
const localVideo = document.querySelector('video');
<video autoplay playsinline></video>
ストリームに格納されたビデオが再生される。
メディアの使用許可が得られなかった場合は、エラーメッセージをブラウザのデバッグコンソールに表示する。
// Handles error by logging a message to the console with the error message.
function handleLocalMediaStreamError(error) {
console.log('navigator.getUserMedia error: ', error);
}
Bonus points
-
constraints
の記述を変更することで、追加の設定を定義できるconst hdConstraints = { video: { width: { min: 1280 }, height: { min: 720 } } }
constraints
が持つことができるプロパティはこちら。
-
MediaStream
は複数のMediaStreamTrack
を持つ。MediaStreamTrack
のメソッドを確認すれば、何が起こるかわかる。
- 映像はHTMLの
video
タグに記述されているので、cssでvideo
タグにスタイルを指定すれば、フィルタなどをかけることができる。video { filter: blur(4px) invert(1) opacity(0.5); }
Section. 5
RTCPeerConnectionは、WebRTCで映像、音声、データを通信するために作られるAPI。
この例では、同一ページ上に2つのRTCPeerConnectionを作成して、接続する。
HTMLには、2つのビデオ要素とボタンを配置する。
<video id="localVideo" autoplay playsinline></video>
<video id="remoteVideo" autoplay playsinline></video>
<div>
<button id="startButton">Start</button>
<button id="callButton">Call</button>
<button id="hangupButton">Hang Up</button>
</div>
localVideo
にはgetUserMedia()
で取得したビデオトラックが表示され、remoteVideo
にはRTCPeerConnection経由で送られてきた映像が表示される。
How it works
WebRTCは、クライアント間の通信をセットアップする際に、RTCPeerConnection APIを使用する。
つまり、各クライアントに一つpeerが使われる。
peerはセットアップに際して、3つのタスクを実行する。
- RTCPeerConnectionを作成し、
getUserMedia()
からローカルストリームを追加する。 - 自身のネットワーク情報を把握し、共有する。
- 自身のメディア情報を把握し、共有する。
コードを順に見ていく
リスナーの設定
JavaScriptの処理を見ていく際、処理の開始タイミングは主にイベント発火で決まっている。そのため、どの要素に、どのようなリスナー関数が設定されているかを知ることで、後の処理を追っていける。
localVideo.addEventListener('loadedmetadata', logVideoLoaded);
remoteVideo.addEventListener('loadedmetadata', logVideoLoaded);
remoteVideo.addEventListener('onresize', logResizedVideo);
...
// Define action buttons.
const startButton = document.getElementById('startButton');
const callButton = document.getElementById('callButton');
const hangupButton = document.getElementById('hangupButton');
// Set up initial action buttons status: disable call and hangup.
callButton.disabled = true;
hangupButton.disabled = true;
...
// Add click event handlers for buttons.
startButton.addEventListener('click', startAction);
callButton.addEventListener('click', callAction);
hangupButton.addEventListener('click', hangupAction);
getUserMedia()
の実行
初期化ステップとして、startButton
がクリックされると、メディアストリームの使用許可を取り、localStream
にセットされる。
// Handles start button action: creates local MediaStream.
function startAction() {
startButton.disabled = true;
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
trace('Requesting local stream.');
}
RTCPeerConnectionの生成
callButton
がクリックされると、callAction()
が呼び出される。
// Handles call button action: creates peer connection.
function callAction() {
...
// Get local media stream tracks.
const videoTracks = localStream.getVideoTracks();
const audioTracks = localStream.getAudioTracks();
...
const servers = null; // Allows for RTC server configuration.
// Create peer connections and add behavior.
localPeerConnection = new RTCPeerConnection(servers);
trace('Created local peer connection object localPeerConnection.');
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener(
'iceconnectionstatechange', handleConnectionChange);
...
// Add local stream to connection and create offer to connect.
localPeerConnection.addStream(localStream);
trace('Added local stream to localPeerConnection.');
trace('localPeerConnection createOffer start.');
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
}
ここでは、シグナリング用のサーバは使用しないため、server
情報はnull
のまま。
そして、localPeerConnection
には'icecandidate'
と'iceconnectionstatechange'
イベントのリスナー関数を定義する。
次に、localStream
をlocalPeerConnection
にセットしcreateOffer
を開始する。
createOffer()
について
createOffer()
はWeb APIで、自身のSDPを取得する。
実際に取得するとこんな感じ。
758.439 Offer from localPeerConnection:
v=0
o=- 6055839967588719905 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
a=extmap-allow-mixed
a=msid-semantic: WMS 0zjtJBPZY9aHaZSZ5HRYX7oZnLIbV9p3WXie
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 124 119 123 118 114 115 116
c=IN IP4 0.0.0.0
a=rtcp:9 IN IP4 0.0.0.0
a=ice-ufrag:3ElD
a=ice-pwd:ccwMmnowPNsfPt1Fbcii5Zun
a=ice-options:trickle
a=fingerprint:sha-256 C8:94:85:05:51:33:8A:B8:83:09:5E:94:D7:A4:6E:0A:E8:C8:F7:10:56:55:A3:48:55:66:6D:53:51:AC:47:36
a=setup:actpass
a=mid:0
a=extmap:1 urn:ietf:params:rtp-hdrext:toffset
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=extmap:3 urn:3gpp:video-orientation
a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01
a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay
a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type
a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing
a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space
a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid
a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id
a=sendrecv
a=msid:0zjtJBPZY9aHaZSZ5HRYX7oZnLIbV9p3WXie 4bdef3f2-471b-4c6a-83ef-d9870bf25d83
a=rtcp-mux
a=rtcp-rsize
a=rtpmap:96 VP8/90000
a=rtcp-fb:96 goog-remb
a=rtcp-fb:96 transport-cc
a=rtcp-fb:96 ccm fir
a=rtcp-fb:96 nack
a=rtcp-fb:96 nack pli
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
a=rtpmap:98 VP9/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 transport-cc
a=rtcp-fb:98 ccm fir
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
a=fmtp:98 profile-id=0
a=rtpmap:99 rtx/90000
a=fmtp:99 apt=98
a=rtpmap:100 VP9/90000
a=rtcp-fb:100 goog-remb
a=rtcp-fb:100 transport-cc
a=rtcp-fb:100 ccm fir
a=rtcp-fb:100 nack
a=rtcp-fb:100 nack pli
a=fmtp:100 profile-id=2
a=rtpmap:101 rtx/90000
a=fmtp:101 apt=100
a=rtpmap:102 H264/90000
a=rtcp-fb:102 goog-remb
a=rtcp-fb:102 transport-cc
a=rtcp-fb:102 ccm fir
a=rtcp-fb:102 nack
a=rtcp-fb:102 nack pli
a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=fmtp:121 apt=102
a=rtpmap:127 H264/90000
a=rtcp-fb:127 goog-remb
a=rtcp-fb:127 transport-cc
a=rtcp-fb:127 ccm fir
a=rtcp-fb:127 nack
a=rtcp-fb:127 nack pli
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:120 rtx/90000
a=fmtp:120 apt=127
a=rtpmap:125 H264/90000
a=rtcp-fb:125 goog-remb
a=rtcp-fb:125 transport-cc
a=rtcp-fb:125 ccm fir
a=rtcp-fb:125 nack
a=rtcp-fb:125 nack pli
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:107 rtx/90000
a=fmtp:107 apt=125
a=rtpmap:108 H264/90000
a=rtcp-fb:108 goog-remb
a=rtcp-fb:108 transport-cc
a=rtcp-fb:108 ccm fir
a=rtcp-fb:108 nack
a=rtcp-fb:108 nack pli
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=fmtp:109 apt=108
a=rtpmap:124 H264/90000
a=rtcp-fb:124 goog-remb
a=rtcp-fb:124 transport-cc
a=rtcp-fb:124 ccm fir
a=rtcp-fb:124 nack
a=rtcp-fb:124 nack pli
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=fmtp:119 apt=124
a=rtpmap:123 H264/90000
a=rtcp-fb:123 goog-remb
a=rtcp-fb:123 transport-cc
a=rtcp-fb:123 ccm fir
a=rtcp-fb:123 nack
a=rtcp-fb:123 nack pli
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:118 rtx/90000
a=fmtp:118 apt=123
a=rtpmap:114 red/90000
a=rtpmap:115 rtx/90000
a=fmtp:115 apt=114
a=rtpmap:116 ulpfec/90000
a=ssrc-group:FID 2707868699 4063280442
a=ssrc:2707868699 cname:N8HvhdkMJe/MuIO3
a=ssrc:2707868699 msid:0zjtJBPZY9aHaZSZ5HRYX7oZnLIbV9p3WXie 4bdef3f2-471b-4c6a-83ef-d9870bf25d83
a=ssrc:2707868699 mslabel:0zjtJBPZY9aHaZSZ5HRYX7oZnLIbV9p3WXie
a=ssrc:2707868699 label:4bdef3f2-471b-4c6a-83ef-d9870bf25d83
a=ssrc:4063280442 cname:N8HvhdkMJe/MuIO3
a=ssrc:4063280442 msid:0zjtJBPZY9aHaZSZ5HRYX7oZnLIbV9p3WXie 4bdef3f2-471b-4c6a-83ef-d9870bf25d83
a=ssrc:4063280442 mslabel:0zjtJBPZY9aHaZSZ5HRYX7oZnLIbV9p3WXie
a=ssrc:4063280442 label:4bdef3f2-471b-4c6a-83ef-d9870bf25d83
これをsetLocalDescription()
にて、使用するSDPとして登録する。
また、登録したローカルピアのSDPはリモートピアに送信される。そして、リモートピアのSDPを待ち受ける状態となる。
リモートピアの挙動
リモートピアもlocalPeerConnectionと同様にgetLocalMedia()
を実施し、・・・
・・・
SDPを受信したリモートピアは、setRemoteDescription()
によりSDPを登録し、同時にcreateAnswer()
によりリモートピアのSDPを作成する。
そして、作成したSDPをリモートピアのsetLocalDescription()
で登録し、SDPをローカルピアに送信する。
SDPの交換完了とIce Connection
ローカルピア、リモートピア共にSDPの交換が完了すると、接続経路候補(IceCandidate)の交換が実施される。
これはWebRTCのAPI側で裏で実施されており、これに関するイベントは下記でハンドルされる。
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
...
remotePeerConnection.addEventListener('icecandidate', handleConnection);
remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
映像通信の確立
IceConnectionState
がConnected
に遷移すると、ローカルピアとリモートピア間での接続が確立されたことを意味する。
接続が完了すると、addStream
イベントが発生しリモートピアはローカルピアの映像を表示する。
remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
Section. 6
Skip
Section. 7 and Section. 8
シグナリングサーバを建てる
Discussion