Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

WebRTCの技術解説 公開版

28,321 views

Published on

第1回NTT-WEST学生向けアプリ開発コンテスト(WebRTC)の勉強会資料です。
※コンテスト情報はFacebookページよりご覧ください!
https://www.facebook.com/nttw.w.con

Published in: Technology
  • Be the first to comment

WebRTCの技術解説 公開版

  1. 1. WebRTCの技術解説 WebRTC 勉強会 @ 第1回 NTT-WEST学生向けアプリ開発コンテスト https://www.facebook.com/nttw.w.con 2014/08/05 NTT西日本 http://ntt-west.co.jp/ 公開版
  2. 2. 資料公開にあたり 当資料は学生向けWebRTCコンテストの 関連イベントであるWebRTC勉強会で 使用されたものです。 コンテストの詳細については、 下記URLをご参照下さい。 第1回 NTT-WEST学生向けアプリ開発コンテスト https://www.facebook.com/nttw.w.con
  3. 3. きんじょう ゆう 金城 雄 Twitter @youkinjoh GitHub @youkinjoh SlideShare @You_Kinjoh 講師紹介 gihyo.jp Jettyで始めるWebSocket超入門 http://gihyo.jp/dev/feature/01/websocket/0001
  4. 4. デモ参加のお願い IN FO R M A T IO N IN FO R M A T IO N
  5. 5. 参加型デモ カメラ付きの端末でお願いします。 できるだけ 新しい Chrome Android パソコン IN FO R M A T IO N IN FO R M A T IO N
  6. 6. WebRTCの概要 WebRTCの2つの仕様 PeerJS SkyWay ユーザメディアを操作する その他雑多な内容 今日お話しすること
  7. 7. 質疑応答について
  8. 8. WebRTC
  9. 9. Web Real-Time Communication リアルタイムコミュニケーションのAPI ボイスチャット・ビデオチャットが プラグインなしにブラウザでできる テキストデータ・バイナリデータも送信可 P2P WebRTC
  10. 10. これまでの リアルタイム コミュニケーション との違い
  11. 11. http://www.slideshare.net/mganeko/2013-web-rtctechcross/6 より引用
  12. 12. キャリア型通信 手段の例 市場 ユーザ メリット 事業者 メリット 利用方法 固定電話 携帯電話 (TV放送) インフラを持つキャリアが支配 世界中の人と会話できる × 単独で利用 http://www.slideshare.net/mganeko/2013-web-rtctechcross/6 より改変して引用
  13. 13. Over The Top 手段の例 市場 ユーザ メリット 事業者 メリット 利用方法 Skype, WebEx (YouTube, Ustream) キャリアに縛られない独自の仕組みを 提供する少数のベンダーが参加可能 世界中の人と無料/安価で会話できる 限定的なAPI提供 一部連携可能 ユーザが組み合わせて利用 http://www.slideshare.net/mganeko/2013-web-rtctechcross/6 より改変して引用
  14. 14. Webブラウザ型 手段の例 市場 ユーザ メリット 事業者 メリット 利用方法 WebRTC 特別な仕組みは不要 誰でも参加可能 専用アプリ無しで会話できる 完全にプログラマブル部品として 利用可能 製品/サービスに組み込んで利用 http://www.slideshare.net/mganeko/2013-web-rtctechcross/6 より改変して引用
  15. 15. キャリア型通信 Over The Top Webブラウザ型 手段の例 市場 ユーザ メリット 事業者 メリット 利用方法 固定電話 携帯電話 (TV放送) Skype, WebEx (Youtube, Ustream) WebRTC インフラを持つ キャリアが支配 キャリアに縛られない 独自の仕組みを提供 する少数のベンダー が参加可能 特別な仕組みは不要 誰でも参加可能 世界中の人と 会話できる 世界中の人と無料/ 安価で会話できる 専用アプリ無しで 会話できる × 限定的なAPI提供 一部連携可能 完全にプログラマブル 部品として利用可能 単独で利用 ユーザが組み合わせて 利用 製品/サービスに 組み込んで利用 http://www.slideshare.net/mganeko/2013-web-rtctechcross/6 より改変して引用
  16. 16. 対応ブラウザ ※ 但し、iOSのブラウザは全て未対応 (2014/07現在)
  17. 17. 今後もまだ仕様が変更になる可能性がある ベンダープレフィックスが必要 先行したバージョンの使用がお勧め Google Chrome Canary Firefox Bata Firefox Aurora まだ策定中
  18. 18. ベンダープレフィックス 先行実装であることを示す慣習 ブラウザベンダーによって違う メソッドの挙動が同じであれば、 代入してメソッド名の違いを吸収できる navigator.getUserMedia = navigator.getUserMedia || //Specification navigator.webkitGetUserMedia || //for Chrome navigator.mozGetUserMedia ; //for Firefox ベンダープレフィックスの現状は以下を参照のこと。 https://plus.google.com/app/basic/stream/z121hnjxtqq2svgni23mznm4cxnctznc5
  19. 19. 主な2つの仕様 Media Capture and Streams ブラウザからカメラやマイクの メディアストリームを取得するための仕様 WebRTC 1.0: Real-time Communication Between Browsers ブラウザとブラウザをP2Pで接続し 通信を行なうための仕様
  20. 20. Media Capture and Streams (getUserMedia) ブラウザからマイクやカメラにアクセス 利用範囲はWebRTC以外とも 音声処理(with Web Audio API) ボイスチェンジャー etc. 画像処理(with Canvas) 顔検出 etc. 顔認識ができるようになるのも時間の問題。
  21. 21. DEMO
  22. 22. SAMPLE 音声と映像を取得 Media Capture and Streams
  23. 23. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="script.js"></script> <title>getUserMedia Sample</title> </head> <body> <video id="video"></video> </body> </html>
  24. 24. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="script.js"></script> <title>getUserMedia Sample</title> </head> <body> <video id="video"></video> </body> </html> HTML ベンダープレフィックスの処理 ユーザメディアの取得
  25. 25. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="script.js"></script> <title>getUserMedia Sample</title> </head> <body> <video id="video"></video> </body> </html>
  26. 26. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="script.js"></script> <title>getUserMedia Sample</title> </head> <body> <video id="video"></video> </body> </html> Scriptの読み込みと ビデオ要素の表示。
  27. 27. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; ベンダープレフィックス
  28. 28. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); });
  29. 29. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); ページ読み込み時の処理を指定。
  30. 30. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); ユーザメディアの取得開始。
  31. 31. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); 取得するユーザメディアは カメラとマイク。
  32. 32. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); ユーザメディア取得時の動作を指定。 streamはMediaStreamオブジェクト。
  33. 33. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); ビデオ要素の取得。 ユーザメディアの指定。 MediaStreamをURLにし、src属性に指定。
  34. 34. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); 再生開始。
  35. 35. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); ユーザメディア取得失敗時の処理を指定。 以前はエラー用のcallbackなしでも動きましたが、 最近のブラウザでは指定が必須のようです。
  36. 36. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); ユーザメディア取得失敗時の処理。 ここではエラーログを出力しているだけ。
  37. 37. window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); });
  38. 38. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; window.addEventListener('load', function() { navigator.getUserMedia( {video: true, audio: true}, function(stream) { var video = document.getElementById('video'); video.src = window.URL.createObjectURL(stream); video.play(); }, function(error) { console.error(error); } ); }); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="script.js"></script> <title>getUserMedia Sample</title> </head> <body> <video id="video"></video> </body> </html>
  39. 39. WebRTC 1.0: Real-time Communication Between Browsers ブラウザとブラウザをP2Pで接続 通信は全て暗号化される P2Pの前に要シグナリング シグナリングサーバが必要 WebSocketが良く使われている Node.jsならSocket.IO
  40. 40. WebRTC 1.0: Real-time Communication Between Browsers APIが複雑でわかりにくい 抽象化した仕様の多いHTML5の 他のAPIと比べると非常に複雑 それでも、従来のリアルタイム通信の 処理よりは断然楽 ジッタやパケットロス等の対策は、 ブラウザが全て対応してくれる
  41. 41. DEMO
  42. 42. WebRTC 1.0: Real-time Communication Between Browsers ICE (STUN + TURN + α) NAT通過・ネゴシエーション STUN P2P・UDPホールパンチング TURN P2Pが不可能ならサーバ経由で通信
  43. 43. WebRTC 1.0: Real-time Communication Between Browsers SDP セッションプロトコルを記述 セッション開始に必要な情報 DTLS (UDP等のデータグラム向けのTLS) データ通信は全て暗号化される
  44. 44. WebRTC 1.0: Real-time Communication Between Browsers MediaStream (SRTP・SRTCP) 音声データ・映像データ SRTP (RTPのセキュア版) リアルタイムデータ配信の仕様 SRTCP (RTCPのセキュア版) 配信用制御プロトコル
  45. 45. WebRTC 1.0: Real-time Communication Between Browsers DataChannel (SCTP) テキストデータ・バイナリデータ SCTP TCPとUDPの良いところ取りをしたプロトコル 標準ではTCPに似た動作をする 設定で信頼性と引き換えにUDPに似た動作にでき、 リアルタイム性の向上が可能
  46. 46. どの処理をブラウザが 勝手にやってくれるのか、 どの処理を自分で 実装する必要があるのかを 把握しないと 実装時に大変混乱する。
  47. 47. どうやって ブラウザで P2Pを 実現しているのか
  48. 48. ?????????? ?????????? 画面上部にメソッド名等の キーワードが表示されます。 実装時にご活用下さい。
  49. 49. Browser Browser NAT NAT NATが邪魔して直接通信ができない。
  50. 50. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT HTML+JS+CSS Global IP/Port signaling HTML+JS+CSS Global IP/Port signaling data
  51. 51. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ブラウザでWebRTCを使った ページにアクセス。
  52. 52. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT HTML+JS+CSS HTTP Request HTTP Response
  53. 53. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Signaling Serverとして利用する WebSocket Serverに接続。
  54. 54. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT signaling WebSocket open
  55. 55. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT signaling 対向のブラウザもWebRTCを使った ページにアクセス。
  56. 56. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT HTML+JS+CSS HTTP Request HTTP Response signaling
  57. 57. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT signaling 対向のブラウザも WebSocket Serverに接続。
  58. 58. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT signaling signaling WebSocket open
  59. 59. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT signaling signaling 以後、WebSocketは接続を継続する。 この先、説明の都合上、接続中でもグレーアウトします。 データが流れる時だけ色がつきます。
  60. 60. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT 接続を開始する側でoffer開始。 SDPを生成・自身に登録後送信。 対向のブラウザは受け取ったSDPを登録。
  61. 61. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Offer SDP createOffer SDPは以下の 情報を含む。 メディアタイプ コーデック 帯域幅 etc.
  62. 62. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Offer SDP setLocalDescription
  63. 63. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT WebSocket send Offer SDP signaling
  64. 64. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Offer SDP signaling
  65. 65. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT setRemoteDescription Offer SDP
  66. 66. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT 受け取ったofferに対してanswer開始。 SDPを生成・自身に登録後送信。 offerした側でも対向のSDPを登録。
  67. 67. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT createAnswer SDPは以下の 情報を含む。 メディアタイプ コーデック 帯域幅 etc. Answer SDP
  68. 68. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT setLocalDescription Answer SDP
  69. 69. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT WebSocket send Answer SDP signaling
  70. 70. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Answer SDP signaling
  71. 71. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT setRemoteDescription Answer SDP
  72. 72. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT P2Pの為に相手にIPを伝える必要がある。 その為にSTUNを使い自身のIPを調べる。 ブラウザが裏で処理してくれる。
  73. 73. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ここの Global IP/Port を知りたい
  74. 74. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Global IP/Port Global IP/Port
  75. 75. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Global IP/Port Global IP/Port
  76. 76. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ここの Global IP/Port を知りたい
  77. 77. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Global IP/Port Global IP/Port
  78. 78. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT Global IP/Port Global IP/Port
  79. 79. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT 自身のIP/Portは把握。 自身に接続できそうな経路の候補を 相手側に伝える必要がある。
  80. 80. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ICE candidate ブラウザは これまでの 情報を元に 裏で経路候補 (candidate)を 生成。
  81. 81. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ICE candidate icecandidate Event イベントに 登録した ハンドラ経由で 経路候補を取得。
  82. 82. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT WebSocket send ICE candidatesignaling
  83. 83. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ICE candidate signaling
  84. 84. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT addIceCandidate ICE candidate
  85. 85. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ブラウザは STUNからの 情報を元に 裏で経路候補 (candidate)を 生成。 ICE candidate
  86. 86. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT icecandidate Event ICE candidate イベントに 登録した ハンドラ経由で 経路候補を取得。
  87. 87. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT WebSocket send ICE candidate signaling
  88. 88. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT ICE candidate signaling
  89. 89. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT addIceCandidate ICE candidate
  90. 90. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT P2Pで通信する為の情報が (やっと)整った。 通信開始。
  91. 91. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT data
  92. 92. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT それでもP2Pが無理だった場合、 TURNサーバが中継を行なう。 TURN経由でもデータの暗号化は解かれずセキュア。
  93. 93. Web Server WebSocket Server ICE Server (STUN + TURN) Browser Browser NAT NAT data data
  94. 94. RTCPeerConnection ストリーミングを扱うための WebRTCの中心となるオブジェクト RTCSessionDescription SDPを扱うオブジェクト RTCIceCandidate 経路情報を扱うオブジェクト RTCDataChannel テキスト・バイナリ用のデータチャネル
  95. 95. SAMPLE このサンプルでは細かい制御は行なっていないので注意してください。 ビデオチャット (二者間通信) WebRTC 1.0: Real-time Communication Between Browsers
  96. 96. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection ; window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription ; window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate ; var ws = null; var peer = null; function initialize() { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); peer = new RTCPeerConnection({ iceServers: [ {url: 'stun:stun.l.google.com:19302'}, {url: 'stun:23.21.150.121'} ] }); navigator.getUserMedia( {audio: true, video: true}, function(stream) { var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); peer.addStream(stream); }, function(error) { console.error(error); } ); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); var offerbtn = document.getElementById('offer_button'); offerbtn.addEventListener('click', offer); } function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="scripts/script.js"></script> <title>WebRTC Sample</title> </head> <body> <input type="button" value="offer" id="offer_button" /> <video id="local" autoplay="autoplay"></video> <video id="remote" autoplay="autoplay"></video> </body> </html>
  97. 97. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection ; window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription ; window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate ; var ws = null; var peer = null; function initialize() { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); peer = new RTCPeerConnection({ iceServers: [ {url: 'stun:stun.l.google.com:19302'}, {url: 'stun:23.21.150.121'} ] }); navigator.getUserMedia( {audio: true, video: true}, function(stream) { var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); peer.addStream(stream); }, function(error) { console.error(error); } ); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); var offerbtn = document.getElementById('offer_button'); offerbtn.addEventListener('click', offer); } function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="scripts/script.js"></script> <title>WebRTC Sample</title> </head> <body> <input type="button" value="offer" id="offer_button" /> <video id="local" autoplay="autoplay"></video> <video id="remote" autoplay="autoplay"></video> </body> </html> HTML ベンダープレフィックスの 処理 WebSocketと PeerConnectionの 初期化 ユーザメディアの取得 WebSocketの イベント登録 PeerConnectionの イベント登録 変数宣言 ボタンのイベント登録 offer処理 answer処理 onload
  98. 98. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="scripts/script.js"></script> <title>WebRTC Sample</title> </head> <body> <input type="button" value="offer" id="offer_button" /> <video id="local" autoplay="autoplay"></video> <video id="remote" autoplay="autoplay"></video> </body> </html>
  99. 99. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <script src="scripts/script.js"></script> <title>WebRTC Sample</title> </head> <body> <input type="button" value="offer" id="offer_button" /> <video id="local" autoplay="autoplay"></video> <video id="remote" autoplay="autoplay"></video> </body> </html> Scriptの読み込みと offer開始用のボタンと ビデオ要素の表示。
  100. 100. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; ベンダープレフィックス
  101. 101. window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection ; window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription ; window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate ; ベンダープレフィックス
  102. 102. var ws = null; var peer = null; function initialize() { // other slides } window.addEventListener('load', initialize); WebSocketオブジェクトと RTCPeerConnectionオブジェクトの 変数宣言と、初期化関数の定義。
  103. 103. var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); peer = new RTCPeerConnection({ iceServers: [ {url: 'stun:stun.l.google.com:19302'}, {url: 'stun:23.21.150.121'} ] }); シグナリングに使う WebSocketの接続開始。 initialize内
  104. 104. var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); peer = new RTCPeerConnection({ iceServers: [ {url: 'stun:stun.l.google.com:19302'}, {url: 'stun:23.21.150.121'} ] }); RTCPeerConnection初期化。 STUNサーバ/TURNサーバを 引数に指定(複数可)。 initialize内
  105. 105. navigator.getUserMedia( {audio: true, video: true}, function(stream) { var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); peer.addStream(stream); }, function(error) { console.error(error); } ); ユーザメディアを取得し表示。 メディアストリームを RTCPeerConnectionに登録。 initialize内
  106. 106. navigator.getUserMedia( {audio: true, video: true}, function(stream) { var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); peer.addStream(stream); }, function(error) { console.error(error); } ); ユーザメディアの取得・表示。 メディアストリームを RTCPeerConnectionに登録。 initialize内
  107. 107. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); WebSocketに イベントハンドラを指定。 initialize内
  108. 108. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); WSのメッセージ受信時の処理を指定。 (SDP用) initialize内
  109. 109. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); SDPが含まれているか判定。 RTCSessionDescriptionを生成。 initialize内
  110. 110. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); RTCSessionDescriptionを PeerConnectionに登録。 合わせてcallbackを指定。 initialize内
  111. 111. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); 受け取ったSDPがofferだった場合、 自動的にanswer処理を実行。 answerの内容は後述。 initialize内
  112. 112. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); initialize内 WSのメッセージ受信時の処理を指定。 (経路情報用)
  113. 113. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); initialize内 経路情報が含まれているか判定。 RTCIceCandidateを生成。
  114. 114. ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); initialize内 RTCIceCandidateを PeerConnectionに登録。
  115. 115. peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); initialize内 RTCPeerConnectionに イベントハンドラを指定。
  116. 116. peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); RTCPeerConnectionの 経路候補取得時の処理を指定。 initialize内
  117. 117. peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); 経路候補が含まれているか判定。 経路候補を対向に送信。 initialize内
  118. 118. peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); RTCPeerConnectionの Stream(対向)取得時の処理を指定。 initialize内
  119. 119. peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); 対向のMediaStreamを表示。 initialize内
  120. 120. var offerbtn = document.getElementById('offer_button'); offerbtn.addEventListener('click', offer); offer開始用のボタンのクリック時に offer処理を実行。 offerの内容は後述。 initialize内
  121. 121. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer処理。
  122. 122. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer処理宣言。
  123. 123. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer生成。
  124. 124. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer生成成功時の動作を指定。
  125. 125. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } 取得したofferを PeerConnectionに登録。 合わせてcallbackを指定。
  126. 126. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer登録後、 offerを対向に送信。
  127. 127. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer取得失敗時の処理を指定。
  128. 128. function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } offer取得失敗時の処理。 ここではエラーログを出力しているだけ。
  129. 129. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer処理。
  130. 130. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer処理宣言。
  131. 131. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer生成。
  132. 132. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer生成成功時の動作を指定。
  133. 133. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } 取得したanswerを PeerConnectionに登録。 合わせてcallbackを指定。
  134. 134. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer登録後、 answerを対向に送信。
  135. 135. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer取得失敗時の処理を指定。
  136. 136. function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } answer取得失敗時の処理。 ここではエラーログを出力しているだけ。
  137. 137. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection ; window.RTCSessionDescription = window.RTCSessionDescription || window.webkitRTCSessionDescription || window.mozRTCSessionDescription ; window.RTCIceCandidate = window.RTCIceCandidate || window.webkitRTCIceCandidate || window.mozRTCIceCandidate ; var ws = null; var peer = null; function initialize() { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); peer = new RTCPeerConnection({ iceServers: [ {url: 'stun:stun.l.google.com:19302'}, {url: 'stun:23.21.150.121'} ] }); navigator.getUserMedia( {audio: true, video: true}, function(stream) { var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); peer.addStream(stream); }, function(error) { console.error(error); } ); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.sdp) {return;} var sdp = data.sdp; var description = new RTCSessionDescription(sdp); peer.setRemoteDescription(description, function() { if (description.type === 'offer') { answer(); } }); }); ws.addEventListener('message', function(evt) { var data = JSON.parse(evt.data); if (!data.candidate) {return;} var candidate = new RTCIceCandidate(data.candidate); peer.addIceCandidate(candidate); }); peer.addEventListener('icecandidate', function(evt) { if (!evt.candidate) {return;} var candidate = evt.candidate; ws.send(JSON.stringify({candidate: candidate})); }); peer.addEventListener('addstream', function(evt) { var video = document.getElementById('remote'); video.src = URL.createObjectURL(evt.stream); video.play(); }); var offerbtn = document.getElementById('offer_button'); offerbtn.addEventListener('click', offer); } function offer() { peer.createOffer( function(offer) { peer.setLocalDescription(offer, function() { ws.send(JSON.stringify({sdp: offer})); }); }, function(error) { console.error(error); } ); } function answer() { peer.createAnswer( function(answer) { peer.setLocalDescription(answer, function() { ws.send(JSON.stringify({sdp: answer})); }); }, function(error) { console.error(error); } ); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="scripts/script.js"></script> <title>WebRTC Sample</title> </head> <body> <input type="button" value="offer" id="offer_button" /> <video id="local" autoplay="autoplay"></video> <video id="remote" autoplay="autoplay"></video> </body> </html>
  138. 138. Library / PaaS APIのWrapperや、 Signaling Server機能を含むもの、 room機能の有無など様々。 ICE Serverを提供しているものも。 クライアント側の実装に 集中したい場合は、導入の価値あり。 サーバ側を細かく制御したい場合は 使わないという選択肢も。
  139. 139. Library / PaaS simpleRTC room機能あり PeerJS 日本のドキュメントあり(SkyWay内) SkyWay PeerJSを使ったPaaS 他にもたくさんあります。 コンテストで提供する実行環境には、PeerServerがインストールされています。
  140. 140. PeerJS Offer/Answerは PeerJSが裏でやってくれる。 接続先を制御するために、 新たにID(PeerID)の概念が増える。 (電話番号の概念に近い。) 通信開始には相手のIDが必要。 IDを意識させずに自動接続するには、 WebSocket等でID交換の必要がある。
  141. 141. Peer PeerJSの本体。 MediaConnection MediaStreamのWrapper。 DataConnection DataChannelのWrapper。 PeerJS
  142. 142. SAMPLE ビデオチャット (多者間通信) PeerJS版 with PeerServer Cloud
  143. 143. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var PEERJS_API_KEY = '[PEERJS_API_KEY]'; var ws = null; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="http://cdn.peerjs.com/0.3/peer.js"></script> <script src="scripts/script.js"></script> <title>PeerJS Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html>
  144. 144. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var PEERJS_API_KEY = '[PEERJS_API_KEY]'; var ws = null; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="http://cdn.peerjs.com/0.3/peer.js"></script> <script src="scripts/script.js"></script> <title>PeerJS Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html> HTML ベンダープレフィックスの 処理 API Keyと変数宣言 PeerJSの 初期化とイベント登録 ユーザメディアの取得 WebSocketの 初期化とイベント登録 MediaConnectionの 設定 (主にイベント登録) onloadと 初期化処理の呼び出しと シグナリング開始
  145. 145. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="http://cdn.peerjs.com/0.3/peer.js"></script> <script src="scripts/script.js"></script> <title>PeerJS Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html>
  146. 146. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="http://cdn.peerjs.com/0.3/peer.js"></script> <script src="scripts/script.js"></script> <title>PeerJS Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html> LibraryとScriptの読み込みと ビデオ要素の表示と 対向の表示領域の準備。
  147. 147. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var PEERJS_API_KEY = '[PEERJS_API_KEY]'; var ws = null; var peer = null; var selfid = null; var localStream = null; ベンダープレフィックスの処理。
  148. 148. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var PEERJS_API_KEY = '[PEERJS_API_KEY]'; var ws = null; var peer = null; var selfid = null; var localStream = null; API Key、WebSocketとPeerJS、 自分のidとStream用の変数宣言。 PeerServer Cloud service の API Key 取得は以下のURLから。 http://peerjs.com/peerserver
  149. 149. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); }PeerJSの各種設定。
  150. 150. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } PeerJSの初期化。 要API Key。
  151. 151. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } PeerServerと接続時に 自身のPeerIDを取得する。 ローカル変数にPeerIDを保存。
  152. 152. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } 初期化処理のためのcallback。 callbackの詳細は後述。
  153. 153. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } 相手から通話依頼に、自動応答させる。 MediaConnectionの設定。 MediaConnectionの設定の詳細は後述。
  154. 154. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } PeerServerとの通信が切断した時、 Peerオブジェクトを破棄。
  155. 155. function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } エラー処理。 ここではエラーログを出力しているだけ。
  156. 156. function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } UserMediaの各種設定。 同じ説明の繰り返しになるので、ここは簡単に解説。
  157. 157. function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } ユーザメディアを取得し表示。
  158. 158. function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } 初期化処理のためのcallback。 callbackの詳細は後述。
  159. 159. function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } WebSocketの各種設定。
  160. 160. function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } PeerIDを自動で交換して接続するための WebSocketの接続開始。
  161. 161. function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } WebSocket接続時の処理を指定。
  162. 162. function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } 初期化処理のためのcallback。 callbackの詳細は後述。
  163. 163. function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } WSのメッセージ受信時の処理を指定。 (PeerID用)
  164. 164. function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } 受け取ったPeerIDに、自動発信させる。 MediaConnectionの設定。 MediaConnectionの設定の詳細は後述。
  165. 165. function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } 発信/応答時に得た MediaConnectionに設定。 (主にイベント登録。)
  166. 166. function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } 発信/応答先のPeerIDを取得。 StreamとVideo要素の保存領域。 closure使用。
  167. 167. function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } Stream(対向)取得時の処理を指定。
  168. 168. function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } 対向のMediaStreamを表示。
  169. 169. function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } MediaConnectionが切断した時、 URLオブジェクトの削除と Video要素を削除。
  170. 170. function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } エラー処理。 ここではエラーログを出力しているだけ。
  171. 171. function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } window.addEventListener('load', initialize); 初期化処理。 callback地獄。
  172. 172. function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } window.addEventListener('load', initialize); Peerとメディアの初期化が終わらないと PeerIDの送信が行なえないため。 DefferedやPromiseを使えばもっと綺麗に書ける。
  173. 173. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var PEERJS_API_KEY = '[PEERJS_API_KEY]'; var ws = null; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: PEERJS_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="http://cdn.peerjs.com/0.3/peer.js"></script> <script src="scripts/script.js"></script> <title>PeerJS Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html>
  174. 174. SkyWay PeerJSを使ったPaaS。 PeerServer Cloudで できることに加え、 接続しているRemoteの一覧を RestAPIで取得可能。 IDを意識させずに自動接続する場合でも、 RestAPIでIDを取得できるため、 静的ファイルのみで構築可能。
  175. 175. RestAPI 接続しているPeerIDの一覧を取得 接続しているPeerの数を取得 SkyWay PeerServerでも、起動時に allowDiscovery オプションを指定すると、 同様のことが可能。
  176. 176. SAMPLE ビデオチャット (多者間通信) SkyWay版 (静的ファイルのみで動作可能)
  177. 177. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var SKYWAY_API_KEY = '[SKYWAY_API_KEY]'; var REST_API_LIST = 'https://skyway.io/v2/active/list/'; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: SKYWAY_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { callRemoteAll(); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="https://skyway.io/dist/v2/0.3/peer.js"></ script> <script src="scripts/script.js"></script> <title>SkyWay Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html>
  178. 178. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var SKYWAY_API_KEY = '[SKYWAY_API_KEY]'; var REST_API_LIST = 'https://skyway.io/v2/active/list/'; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: SKYWAY_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { callRemoteAll(); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="https://skyway.io/dist/v2/0.3/peer.js"></ script> <script src="scripts/script.js"></script> <title>SkyWay Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html> HTML PeerJSの 初期化とイベント登録 ユーザメディアの取得 MediaConnectionの 設定 (主にイベント登録) onloadと 初期化処理の呼び出しと 発信処理開始 接続先の一覧所得と 発信処理 ベンダープレフィックスの 処理 API Keyと変数宣言
  179. 179. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var SKYWAY_API_KEY = '[SKYWAY_API_KEY]'; var REST_API_LIST = 'https://skyway.io/v2/active/list/'; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: SKYWAY_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { callRemoteAll(); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="https://skyway.io/dist/v2/0.3/peer.js"></ script> <script src="scripts/script.js"></script> <title>SkyWay Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html> 主に PeerJS版との 差分のみを解説
  180. 180. ... <link rel="stylesheet" href="styles/style.css" /> <script src="http://cdn.peerjs.com/0.3/peer.js"></script> <script src="scripts/script.js"></script> ... ... <link rel="stylesheet" href="styles/style.css" /> <script src="https://skyway.io/dist/v2/0.3/peer.js"></script> <script src="scripts/script.js"></script> ... 読み込むLibraryの変更。
  181. 181. var PEERJS_API_KEY = '[PEERJS_API_KEY]'; var SKYWAY_API_KEY = '[SKYWAY_API_KEY]'; var REST_API_LIST = 'https://skyway.io/v2/active/list/'; API KeyをSkyWay用に変更。 接続しているPeerIDの 一覧を取得するRestAPIのURL。 SkyWay の API Key 取得は以下のURLから。 http://nttcom.github.io/skyway/registration.html
  182. 182. ... var ws = null; ... function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } ...
  183. 183. ... var ws = null; ... function initializeWebSocket(callback) { var secure = location.protocol === 'https:'; var protocol = secure ? 'wss' : 'ws'; var url = protocol + '://' + location.host + '/'; ws = new WebSocket(url); ws.addEventListener('open', function() { callback(); }); ws.addEventListener('message', function(evt) { var remoteid = evt.data; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); }); } ... WebSocketの処理は削除。
  184. 184. function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } RestAPIからPeerIDの一覧を取得し、 全てに発信。
  185. 185. function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); }RestAPIのURL + API Key。
  186. 186. function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } Ajax(XHR)お決まりの記述。
  187. 187. function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); }接続しているPeerIDの一覧を取得。
  188. 188. function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } RestAPIからPeerIDの一覧を取得し、 全てに自動発信。
  189. 189. function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } MediaConnectionの設定。
  190. 190. function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } function initialize() { initializePeer(function() { initializeMedia(function() { callRemoteAll(); }); }); }
  191. 191. function initialize() { initializePeer(function() { initializeMedia(function() { initializeWebSocket(function() { ws.send(selfid); }); }); }); } function initialize() { initializePeer(function() { initializeMedia(function() { callRemoteAll(); }); }); } WebSocketの初期化削除。 シグナリング開始処理削除。 発信処理追加。
  192. 192. navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia ; window.URL = window.URL || window.webkitURL ; var SKYWAY_API_KEY = '[SKYWAY_API_KEY]'; var REST_API_LIST = 'https://skyway.io/v2/active/list/'; var peer = null; var selfid = null; var localStream = null; function initializePeer(callback) { peer = new Peer({key: SKYWAY_API_KEY}); peer.on('open', function(id) { selfid = id; callback(); }); peer.on('call', function(mediaConnection) { mediaConnection.answer(localStream); settingMediaConnection(mediaConnection); }); peer.on('close', function() { peer.destroy(); }); peer.on('error', function(err) { console.error(err); }); } function initializeMedia(callback) { navigator.getUserMedia( {audio: true, video: true}, function(stream) { localStream = stream; var video = document.getElementById('local'); video.src = URL.createObjectURL(stream); video.play(); callback(); }, function(error) { console.error(error); } ); } function callRemoteAll() { var url = REST_API_LIST + SKYWAY_API_KEY; var xhr = new XMLHttpRequest(); xhr.addEventListener('readystatechange', function() { if (xhr.readyState != 4) {return;} if (xhr.status != 200) {return;} var remoteids = JSON.parse(xhr.responseText); for (var i = 0; i < remoteids.length; i++) { var remoteid = remoteids[i]; var mediaConnection = peer.call(remoteid, localStream); settingMediaConnection(mediaConnection); } }); xhr.open('GET', url); xhr.send(); } function settingMediaConnection(mediaConnection) { var remoteid = mediaConnection.peer; var remoteStream = null; var video = null; mediaConnection.on('stream', function(stream) { video = document.createElement('video'); video.src = URL.createObjectURL(stream); video.play(); var parent = document.getElementById('remotes'); parent.appendChild(video); }); mediaConnection.on('close', function() { URL.revokeObjectURL(video.src); video.parentNode.removeChild(video); }); mediaConnection.on('error', function() { console.error(err); }); } function initialize() { initializePeer(function() { initializeMedia(function() { callRemoteAll(); }); }); } window.addEventListener('load', initialize); <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="styles/style.css" /> <script src="https://skyway.io/dist/v2/0.3/peer.js"></ script> <script src="scripts/script.js"></script> <title>SkyWay Sample</title> </head> <body> <video id="local"></video> <div id="remotes"></div> </body> </html>
  193. 193. ユーザメディアを 操作する
  194. 194. Canvas Web Audio API Media Stream Media Stream 音声処理 画像処理 現在の実装状況 MediaStream Processing APIの中にCanvas Recordingという Canvas要素からstreamを取得する仕様案があったが進展なし?
  195. 195. Canvas Web Audio API Media Stream Media Stream 音声処理 画像処理 処理した音声は送信可能 処理した映像は送信不可 受信側ではどちらも処理可能 画像をバイナリデータとして DataChannelで送信・連続して表示することは可能ですが、 ジッタ処理等のWebRTCのメディア処理の恩恵を受けることができません。 必要であれば、ジッタ処理等を自分で実装することになります。 Canvas Web Audio API 再生 Media Stream 音声処理 画像処理 表示
  196. 196. MediaStream Web Audio API
  197. 197. SAMPLE 簡易 ボイスチェンジャー MediaStream <=> Web Audio API 当ボイスチェンジャーで変換した音声は、 音声解析によりほぼ元の音声を復元可能であり、 匿名性を担保するものではありません。
  198. 198. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); }
  199. 199. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } ユーザメディアを Web Audio APIで編集し、 RTCPeerConnectionに渡す。
  200. 200. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } getUserMediaで音声を取得。
  201. 201. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } Web Audio APIの AudioContextをインスタンス化。 ベンダープレフィックス(例:webkitAudioContext)
  202. 202. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } MediaStreamをWeb Audio APIの Source(入力側)にする。 WebRTCの世界からWeb Audio APIの世界へ。
  203. 203. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } 音声処理をJavaScriptで行なうための ScriptProcessorをインスタンス化。 旧名:JavaScriptNode。古い資料を見るときは注意。
  204. 204. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } Web Audio APIの Destination(出力側)をインスタンス化。
  205. 205. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } 音声処理に使うfunction(後述)を ScriptProcessorのイベントに登録。
  206. 206. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } source(入力側) scriptProcessor destination(出力側)の順に接続。
  207. 207. navigator.getUserMedia({audio: true}, function(inputStream) { var audioContext = new AudioContext(); // 要ベンダープレフィックス var mediastreamsource = audioContext.createMediaStreamSource(inputStream); var scriptProcessor = audioContext.createScriptProcessor(bufferSize, 1, 1); var mediastreamdestination = audioContext.createMediaStreamDestination(); scriptProcessor.addEventListener('audioprocess', onAudioProcess); mediastreamsource.connect(scriptProcessor); scriptProcessor.connect(mediastreamdestination); var outputStream = mediastreamdestination.stream; peer.addStream(outputStream); }, function(error) {} ); function onAudioProcess(evt) { var input = evt.inputBuffer.getChannelData(0); var output = evt.outputBuffer.getChannelData(0); var bufferData = new Float32Array(bufferSize); for (var i = 0; i < bufferSize; i++) { bufferData[i] = ( (input[(i * 2) % bufferSize] + input[(i * 2 + 1) % bufferSize]) / 2 + input[Math.floor(i / 2) % bufferSize] ) / 2; } output.set(bufferData); } Web Audio APIのSource(入力側)を MediaStreamにする。 Web Audio APIの世界からWebRTCの世界へ。

×