第 2 章メディアストリームを取得する・・・ Media Capture API 20 めるダイアログが表示されます。プラウザーによってダイアログのデザインは異なります デモアプリにアクセスして「取得開始」ボタンを押すと、人力デバイスの使用許可を求 event. preventDefau1t ( ) ; alert(' デバイスを利用できませんでした , ) ; console . log(error) ; / / ※ブラウザーによってエラーオブジェクトの型が異なる / / メディアストリーム取得失敗時 }catch(error){ document . b0dy. removeChi1d(captureButton) ; / / 取得開始ボタンを削除 document. body. appendChi1d (mediaE1ement) ; / / ⅵ de 。タグを貼り付ける mediaEIement . setAttribute( 'playsinline' ' true ' ) ; / / インライン再生を許可する playsinline 属性 (iPhone で必要 ) mediaE1ement . autoplay = true ; / / 自動再生を設定 ( これを忘れると再生されない ) mediaEIement . srcObject = mediaStream; / / 取得したメディアストリームを src0bject 属性に設定する 1et mediaE1ement = document . createE1ement( 'video' ) ; / / メディアストリームを再生する video タグを生成 ( 音声だけなら audio タグでも可 ) {video : true , audio: true} / / 取得したいストリームの条件を指定する 1et mediaStream = await navigator. mediaDevices. getUserMedia( try{ captureButton. addEventListener( , async (event) = > { let captureButton = document . getElementById( ' capture ' ) ; マ get-video ・ js く /html> く /body> く script src="get—video. js"> く /script> く button id=" capture" > 取得開始く / button > く body> く /head> く tit1e>Media Capture API く /title> く meta charset="UTF—8'i> く he ad> く html> く ! DOCTYPE html> V get—video. html マリスト 2.1 メディアストリームを画面に表示するデモアプリ ま再生するだけの単純なデモアプリです。 ト 2.1 は、人力デバイスから取得したメディアストリームをプラウザーの画面内でそのま 難しいことは抜きにして、とりあえずメディアストリームを取得してみましよう。リス 2.1 とりあえずメディアストリームを取得してみる
第 3 章通信相手を見つけて、つなげる・・・シグナリング ますが、このときだけは candidate プロバティの値は空になっています。 マリスト 3.6 通信経路候補の交換 V 通信経路候補を見つけた / / 通信経路候補が見つかったとき (event) = > { pc ・ onicecandidate if (event . candidate) { / / 通信経路候補を通話相手に教えるため送信 ws . send({ tO : remoteUser, ) icecandidate' event : data : event . candidate }else{ / / 通信経路候補を見つけ終えたら event . candidate は空 マ相手から通信経路候補が送られてきた / / WebSocket メッセージを受信 ws ・ onmessage = async (e) = > { ) icecandidate' ) { if (e . data. event / / 相手から送られてきた通信経路候補を登録 await pc. addIceCandidate(new RTClceCandidate(). data. data) ) ; 接続に必要な手順は以上で終わりです。 通信経路候補の交換が終わると、その情報をもとに両者のプラウザーがお互いに接続を 式行します。見事接続に成功すれば、お互いのメディアストリームが送信されてビデオ チャット通話がはじまります。 一三ロ 46
第 3 章通信相手を見つけて、つなげる・・・シグナリング アストリーム単位で登録していました。 40 うな操作をするためには、トラック単位で扱えるほうが都合がよいのです。 替えたり、複数の映像トラックを同時に送信したりすることができます。そのよ 本書では説明していませんが、 WebRTC では送信する映像トラックを動的に切り からです。 なぜこのような仕様変更があったかというと、そのほうが都合がよい場合がある 比べると少し冗長なように思えます。 新旧 2 つの仕様を見比べると、旧仕様のほうがシンプルで、現在の仕様はそれに トラックとメディアストリーム event . stream ; remoteVideoEIem. srcObj ect pc. onaddstream = (event) = > { / / 旧仕様 }else{ remoteVideoEIem. srcObject = event. streams [ 0 ] ; if (event . track . kind 'video' ) { / / ontrack イベントは video と audio で 2 回発火するが、 video のときのみ処理を行う pc. ontrack = (event) = > { / / 新仕様 if (typeof pc . ontrack ! = 'undefined' ) { / / ※ remoteVideoE1em は映像の再生を行う video 要素 / / 相手のメディアストリームを表示する マリスト 3.3 ontrack(onaddstream) どうかで処理を分けるとよいでしよう。 Edge は旧仕様のみの対応となっているため、。 ntrack イベントハンドラーが存在するか きます。こちらもやはり Firefox と Safari は新仕様に対応しているのに対し、 Chrome と で、これを video 要素の srcObject として設定すれば相手のメディアストリームを再生で イベントが発火します。これらのイベントの引数にメディアストリームが含まれているの 一方、相手のメディアストリームが利用可能になると、 ontrack または onaddstream ソッドが存在するかどうかを判定して、処理を分ける必要があります。 に対応していますが、 Chrome と Edge は旧仕様にしか対応していません。 addTrack メ プラウザーによって対応状況が分かれており、執筆時現在、 Firefox と Safari は新仕様
3.3 シグナリングのシーケンス / / ※ remoteVideoE1em は映像の再生を行う video 要素 'undefined ' ) { if (typeof pc . ontrack ! = / / 新仕様 pc . ontrack = (event) = > { if (event . track . kind = = 'video'){ remoteVideoE1em. srcObj ect = event. streams [ 0 ] ; }else{ / / 旧仕様 (event) = > { pc . onaddstream remoteVideoE1em. srcObject = event . stream; 先述のとおり、シグナリングサーバーの仕様は開発者が自由に実装できます。 リスト リスト 3.5 SDP の例 その理由を説明するため、とりあえず SDP の中身を見てみましよう。 このような一見冗長にみえる手順を踏む必要があるのでしようか。 点で LocaIDescription の設定まで自動的に済ませてくれてもいいように思えます。なぜ 定しています。わざわざこのような手順を踏まなくても、最初のメソッドを呼び出した時 生成した SDP を、再び setLoca1Description メソッドに渡して LocalDescription に設 ところでセッション情報の交換の手順では、 createOffer/createAnswer メソッドで SDP についてちょっと詳しく WebSocket を使うかも含め、このとおりに実装する必要はありません。 3.4 では WebSocket を利用していますが、これはあくまでもひとつの例であり、そもそも v=0 0 = mozilla . t=0 0 . THIS_IS_SDPARTA-52.0.1 6105063152592429256 0 IN IP4 0 . 0 . 0 . 0 a=fingerprint : sha-256 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 : 00 43 a=ice—pwd:4f996da2dd42eeb2e2b6370cba016f34 a=fmtp: 101 0 ー 15 a=fmtp : 109 maxp1aybackrate=48000 ; stereo=l ; useinbandfec=l a=extmap: l/sendonly urn: ietf :params:rtp-hdrext : ssrc-audio—level a=sendrecv c=IN IP4 0 . 0 . 0 . 0 m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8 101 a=msid—semantic : WMS * a=ice—options :trickle a=group : BUNDLE sdparta_O sdparta-l
第 6 章映像・音声以外をやり取りする・・・アータチャネル if(e . data. event = = 'offer'){ オプジェクトの onopen イベントが発火します。データの送信は、このイベントが発火し データチャネルの接続確立後、データの送受信ができるようになると RTCDataChannel 6.2 データの送受信 sdp : answerSdp event : ' answer ) to: 'A1ice' ws . send({ await pc . setLoca1Description(answerSdp) ; 1et answerSdp = await pc. createAnswer() ; await pc. setRemoteDescription(new RTCSessionDescription(). data. sdp) ) ; / / 以降の手順はメディアストリームの場合と同じ dc = event . channel ; / / RTCDataChanne1 オブジェクトは引数の channel プロバティに格納されている pc ・ ondatachannel = (event) = > { / / 接続を要求された側は ondatachannel イベントの引数で RTCDataChanne1 を取得する iceTransportPOIicy: ) a11 ' iceservers : [{urls : 'stun: stun ・ 1 ・ google.com : 19302 ' } ] , pc = new RTCPeerConnection ( { / / RTCPeerConnection の生成は接続を要求する側と同じ マリスト 6.3 データの送信 ドを呼び出すだけです。 データの送信 てから行うようにしてください。 データの送信自体はいたって簡単で、 RTCDataChannel オプジェクトの send メソッ dc. onopen = ( ) = > { / / データチャネルが利用可能になると。Ⅱ。 pe 取イベントが発火する 68 "text/plain"})) ; dc . send(new B10b( ['blob'] , {type : dc. send(new ArrayBuffer(128) ) ; dc . send(new Uint8Array( [ 0 , 1 , 2 , 253 , 254 , 255 ] ) ) ; / / バイナリデータも送信できる dc . send(' 文字列 ,); / / 文字列も送信できる
第 3 章通信相手を見つけて、つなげる・・・シグナリング 42 pc. addStream(10ca1Stream) ; / / 旧仕様 }else{ pc. addTrack(track, 10ca1Stream) ; 10ca1Stream. getTracks ( ) . forEach( (track) = > { / / 新仕様 if ( 'addTrack' in pc){ / / ※メディアストリームは事前に取得できている想定 / / ※ IocaIStre は web カメラ等から取得したメディアストリーム iceTransportP01icy: ' a11 ) iceservers : [{urIS : ' stu Ⅱ : stu Ⅱ・ 1 ・ google. com : 19302 ' } ] , let pc = new RTCPeerConnection({ function createRTCPeerConnection(){ / / 各種コールバック関数の設定は発信側・着信側の両方で行うので、関数化しておくと便利 / / RTCPeerConnection オブジェクトの生成や、メディアストリームの登録、 V 発信側・着信側共通 await pc . setRemoteDescription(new RTCSessionDescription(). data. sdp) ) ; / / 相手から送られてきたオファー SDP を RemoteDescription に設定 if (e . data. event = = 'answer' ) { WS. onmessage = async (e) = > { / / WebSocket メッセージを受信 マ再びアリス sdp : answerSdp event : answer ) to: 'A1ice' ws . send({ / / アリスにアンサー SDP を送信 await pc . setLoca1Description(answerSdp) ; / / 生成したアンサー SDP を LocaIDescription に設定 1et answerSdp = await pc. createAnswer() ; / / オファーに対するアンサー SDP を生成 await pc. setRemoteDescription(new RTCSessionDescription(). data. sdp) ) ; / / 相手から送られてきたオファー SDP を RemoteDescription に設定 1et pc createRTCPeerConnection ( ) ; if (e . data. event ws . onmessage = async (e) = > { / / WebSocket メッセージを受信 マボブ ( 着信側 ) sdp: 0fferSdp event : ) offer ) t 0 : ' Bob ' ws . send({ / / ※この部分は仕様で定められていないので自由に実装できる / / ※ ws は WebSocket 接続オブジェクト / / ボブにオファー SDP を送信し、通話開始を要求 await pc . setLoca1Description(offerSdp) ;
6.3 WebSocket とデータチャネル データの受信 うものなので、そもそもそうせざるを得ないという理由はありますが、他のユーザーとの ザーとの情報交換を行っていました。シグナリングの場合は P2P 接続を確立する前に行 WebSocket を使用し、メッセージに宛先を指定することで、サーバーを経由して他のユー 第 3 章で紹介したシグナリングの手順では、シグナリングサーバーとのやりとりに れを模倣して作られています。 われた方も多いかもしれません。そのとおり、データチャネルの API は WebSocket のそ こまでのコードを見て「 WebSocket に似ている・・・というか一緒じゃないか ! 」と思 6.3 WebSocket とデータチャネル co 取 sole . log(event . data) ; / / 送信されたデータは data プロバティに格納されている dC . onmessage (event) = > { リスト 6.4 データの受信 トが発火します。送信されたデータは、引数の data プロバティに格納されています。 一方、データを受信したときは、 RTCDataChannel オプジェクトの onmessage イベン 間でのデータの送受信は WebSocket でも可能です。 WebSocket 図 6.2 TCP TC P U DP WebSocket とデータチャネルの接続イメー 69 シ
第 6 章映像・音声以外をやり取りする・・・アータチャネル V リスト 6.6 マ送信側 大きなデータをチャンクに分割して送受信 / / Fi1eReader を使い、 Fi1e を ArrayBuffer として読み込む new Fi1eReader() ; 1et fi1eReader fi1eReader . onload = (e) = > { / / 指定サイズでの切り出しができるように ArrayBufferView(Uint8Array) に変換 1et data = new Uint8Array (e . target . result) ; / / 16KiB のチャンクに分割しながら送信する / / 先頭 1 バイトは終了フラグとして扱うため、実際のデータは 16KiB-I バイトごと 1024 * 16 let chunkSize = 1et start = 0 ; while(start く data. byteLength){ 1et end = start 十 chunkSize ; 1et chunkData = data. slice(start , end) ; let chunk = new Uint8Array (chunkData. byteLength + 1 ) ; / / 先頭バイトに終了フラグをセット / / 続きがある場合・・・ 0 最後のチャンクの場合・・・ 1 chunk [ 0 ] = end > = data. byteLength? 1 : 0 ; / / 2 バイト目以降に実データをセット chunk. set (new Uint8Array (chunkData) , 1 ) ; / / 送信 dataChanne1. send(chunk) ; end ; start fi1eReader. readAsArrayBuffer(fi1e) ; マ受信側 / / 受信したチャンクを一時的に溜めておく配列 let chunks = ロ ; dataChanne1. onmessage = (event) = > { / / バイナリデータは ArrayBuffer 形式で送られてくるので / / Uint8Array に変換して配列に溜めておく Iet receivedData = new Uint8Array (event . data) ; chunks . push(receivedData) ; / / 先頭 1 バイトの終了フラグが立っているか確認 if (receivedDataCO] return ; / / ここから先は最後のチャンクを受け取った場合の処理 / / 受信したチャンクの合計データサイズを計算 ( 終了フラグ各 1 バイトは除外 ) let length = chunks. reduce ( (length , chunk) = > { return length + chunk. byteLength ー 1 ; / / データサイズぶんの Uint8Array を生成し、 / / 各チャンクの実データ部分をはめ込んでいく let data = new Uint8Array (length) ; let pos = chunks . forEach( (chunk) = > { data. set(chunk. slice(l) , pos) ; pos + = chunk. length ー 1 ; / / 完了 console. log(data) ;
第 8 章開発に便利な Tips & Tricks RTCConfiguration RTCPeerConnection の初期化設定 Time/Event WebRTC に関する JavaScript API のイベント・メソッドが呼び出された時間と、 そのときのメッセージの内容。 Stats TabIes 通信経路やメディアストリームなど諸々の情報。 GetUserMedia Requests Media Capture API の GetUserMedia で取得されたメディアストリームの情報。 Create Dump ←、 C { ・ Chrome chrome://webrtc-internals ・ 0 ・新 webRTC 断 n ーⅱ ; ヨー 1 li : は : なリ・トい′第 0 00 ( リ ( M ia Requests h p : / ハ 0 ( a 旧 05t : 9000 / , { se Ⅳ e 「 5 : [ stun : stun 」 . g009 厄 . ( om : 1930 幻 , iceTransportType: all, ; bundlePoIicy: balanced, rtcpMuxP01icy: require } , 第 m ・ 2017 / 4 / 12 3 : : 02 2017 / 4 / 12 3 : 00 : 02 2017 / 4 / 12 3 : 00 : 02 2017 / 4 / 12 3 : 00 : 02 2017 / 4 / 12 3 : 00 : 02 2017 / 4 / 12 3 : 00 : 02 2017 / 4 / 12 3 : 00 : 02 addStream 5 秋 mo 【 eths ( ⅱ p ⅱ on negotiationneeded signalingstatechange addtceCandidate (host) onAddStream setRemoteDescriptionOnSuccess △図 8.9 Chrome の WebRTC lnternals ペー シ Chrome の WebRTC lnternals はいろんな情報が "Stats TabIes" に押し込まれており、 Firefox に比べて情報が整理されていない印象があります。しかし、その内容は Firefox よりも詳しく表示できるものが多いです。 通信経路候補の一覧や試行結果は、 Stats TabIes の中に "googCandidatePair" という 項目名で表示されています。この項目は複数存在する場合がありますが、そのうち太字に なっているものが実際に採用された通信経路です。 項目を展開すると、双方の IP アドレスのほか、送受信されたバケット数やバイト数な どが表示されます。 Firefox と違って、こちらはリアルタイムで更新されます。 90
6.1 データチャネルの開始方法 第 2 引数にはデータチャネルのオプションを指定できます。特に必要がなければ、未指 着呼側 sdp: 0fferSdp event : 'offer' t 0 : ' Bob ' ws . send({ await pc . setLoca1Description(offerSdp) ; let offerSdp = await pc. createOffer() ; / / 以降の手順はメディアストリームの場合と同じ / / maxRetransmits : 0 / / maxPacketLifeTime: 100 , / / ordered: false, / / データチャネルのオプション ( 後述 ) 'label' / / ラベル ( 65 , 535 バイト以内の任意の文字列 ) Iet dc = pc . createDataChanneI ( / / 接続を要求する側は、 createDataChanneI メソッドを使って RTCDataChanne1 を生成する iceTransportP01icy: ' a11 ' 'stun: stun ・ 1 ・ google.com/ 19302 ' } ] , iceServers : [{urlS : Iet pc = new RTCPeerConnection({ / / RTCPeerConnection の生成はメディアストリームの場合と同じ マリスト 6.1 データチャネルの開始 ( 発呼側 ) 定でかまいません。これについては本章の後半で改めて説明します。 リスト 6.2 データチャネルの開始 ( 着呼側 ) オプジェクトを取得できるようにしておきます。 イベントが発火するのでこれにイベントハンドラーを設定し、引数から RTCDataChannel チャネルの接続が確立したときに RTCPeerConnection オプジェクトの ondatachannel また、着呼側では createDataChanne1 メソッドは呼び出しません。その代わり、データ を利用したいという情報は含まれているので、これを保存するだけでよいのです。 に何かする必要はありません。発呼側から送られてきたオファー SDP にデータチャネル 着呼側では、データチャネルの利用を RTCPeerConnection オプジェクトに伝えるため WS. onmessage = async (e) let dc; let pc; / / 接続を要求された側 67