📝

WebRTC 勉強会資料

2022/02/07に公開

概要

https://codelabs.developers.google.com/codelabs/webrtc-web#3
上記のサイトを教材にし、
JavaScriptに触ったことがない人が読めるように解説しながら、
WebRTCのPeer to Peer通信の仕組みについて学ぶ。

環境構築

skip

Section. 4

ウェブカメラの映像をストリーミングする

このセクションでは、WebRTC APIでメディアストリームをつかむ
通信はしない

getUserMedia()の使い方

https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/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が持つことができるプロパティはこちら。

https://w3c.github.io/mediacapture-main/getusermedia.html#media-track-constraints

https://zenn.dev/yuki_uchida/books/c0946d19352af5/viewer/320c67#1.2.-getusermediaする際に設定を変える

  • MediaStreamは複数のMediaStreamTrackを持つ。MediaStreamTrackのメソッドを確認すれば、何が起こるかわかる。

https://developer.mozilla.org/ja/docs/Web/API/MediaStream
https://developer.mozilla.org/ja/docs/Web/API/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つのタスクを実行する。

  1. RTCPeerConnectionを作成し、getUserMedia()からローカルストリームを追加する。
  2. 自身のネットワーク情報を把握し、共有する。
  3. 自身のメディア情報を把握し、共有する。

コードを順に見ていく

リスナーの設定

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);

https://gist.github.com/think49/689d7d1e5c5fd0c5ca266e4c66b0b35e

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'イベントのリスナー関数を定義する。

次に、localStreamlocalPeerConnectionにセットしcreateOfferを開始する。

createOffer()について

https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/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として登録する。

https://zenn.dev/yuki_uchida/books/c0946d19352af5/viewer/e26b4f

また、登録したローカルピアの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);

映像通信の確立

IceConnectionStateConnectedに遷移すると、ローカルピアとリモートピア間での接続が確立されたことを意味する。

接続が完了すると、addStreamイベントが発生しリモートピアはローカルピアの映像を表示する。

remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);

Section. 6

Skip

Section. 7 and Section. 8

シグナリングサーバを建てる

GitHubで編集を提案

Discussion