WebRTCをさわってみる&手動シグナリングしてみる
今回は、WebRTCについて書きたいと思います。WebRTCって何ぞ?
WebRTCって何ぞ?ってとこから書きたいなと思ったのですが、他の方の記事見てるとよく書けてるな。。
改めて書く必要ないんじゃないかな。。
図ないと分かりづらいよな。。面倒だなこれ。。
って感じになりました。
ということで、WebRTCって何ぞ?とかは下記の記事を参考にしてください!
・WebRTCを仕組みから実装までやってみる
http://blog.wnotes.net/blog/article/webrtc-beginning
・WebRTCで変わるWebの未来
http://www.qcontokyo.com/data_2013/ToruYoshikawa_QConTokyo2013.pdf
んで、今回書くのは
P2Pで通信を開始するまでのシーケンスを1手順ずつ、自分でわかるように、自分のために書きます。
今回やろうとすること
端末A・端末BをPeer to Peerし、Macに内蔵しているカメラで撮影している内容をリアルタイムに共有するところまでやる。
必要っぽいもの
シグナリングサーバーICEサーバー(STUN / TURN)
と思ったけど、シグナリングは手動でできそうだし、ICEサーバーだけでいいんじゃないか感。
ということで、今回は↓でやります。
・シグナリングは手動
・Googleが提供しているSTUNサーバー
接続手順
事前準備(共通)No | 端末A | 端末B |
1 | video要素を作っておく | video要素を作っておく |
2 | Peerを生成 | Peerを生成 |
3 | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる |
この作業は接続する端末共通です。
ここまで実施することで、ブラウザで自身の姿が見れるようになるはずです。
接続対象へSDPを送受信してセッションを確立
Simple Traversal of UDP through NATs (STUN): NAT越えの方法としてRFC3489で定められた標準的な仕組み。
外部のSTUNサーバに対してクライアントが一度接続し、グローバルIPとマッピングされたポート番号を記憶しておくことで、そのデータを使ってPeerは相手のマシンを特定することができる。
No | 端末A | 端末B |
4 | 端末Bへ送信するOfferを生成 | |
5 | 端末Aで生成したOfferを受信 | |
6 | 端末Bへ送信するAnswerを生成 | |
7 | 端末Bで生成したAnswerを受信 |
リモート側のストリームを共有する
接続経由を共有することで、端末Aと端末BをPeer to Peer接続する。
No | 端末A | 端末B |
8 | 端末Bへ送信する経路情報を出力 | |
9 | 端末Aで生成した経路情報を受信 | |
10 | 端末Aへ送信する経路情報を出力 | |
11 | 端末Bで生成した経路情報を受信 |
ここまでくると、接続した端末のカメラの動画が表示されるようになっているはずです。
まとめて書くと、こんな手順です。
No | 端末A | 端末B |
1 | video要素を作っておく | video要素を作っておく |
2 | Peerを生成 | Peerを生成 |
3 | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる | メディアに接続(カメラの起動を許可すること) ・Peerにストリームを接続し、自身の端末の状態をvideo要素で閲覧できる状態になる |
4 | 端末Bへ送信するOfferを生成 | |
5 | 端末Aで生成したOfferを受信 | |
6 | 端末Aへ送信するAnswerを生成 | |
7 | 端末Bで生成したAnswerを受信 | |
8 | 端末Bへ送信する経路情報を出力 | |
9 | 端末Aで生成した経路情報を受信 | |
10 | 端末Aへ送信する経路情報を出力 | |
11 | 端末Bで生成した経路情報を受信 |
手動でやると面倒ですね。これ。
接続してみよう!
コードはこんな感じ(1〜11の上記手順通り番号をつけています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE HTML> | |
<html lang="ja"> | |
<head> | |
<meta charset="UTF-8"> | |
<title></title> | |
</head> | |
<body> | |
<style type="text/css"> | |
video { | |
width: 480px; | |
height: 360px; | |
margin: 5px; | |
padding: 0; | |
border: 10px solid #000; | |
} | |
textarea { | |
width: 480px; | |
height: 200px; | |
margin: 5px; | |
padding: 0; | |
border: 10px solid #0f0; | |
} | |
</style> | |
<script type="text/javascript"> | |
var webrtc = {}; | |
(function(webrtc) { | |
// 1. video要素が必要 | |
var body = document.getElementsByTagName('body')[0]; | |
var localVideo = document.createElement('video'); | |
localVideo.autoplay = true; | |
var remoteVideo = document.createElement('video'); | |
remoteVideo.autoplay = true; | |
var receiveSdpInput = document.createElement('textarea'); | |
receiveSdpInput.value = "ここにはSDPを貼り付けてね"; | |
var candidatesInput = document.createElement('textarea'); | |
candidatesInput.value = "ここにはCandidateを貼り付けてね"; | |
body.appendChild(localVideo); | |
body.appendChild(remoteVideo); | |
body.appendChild(receiveSdpInput); | |
body.appendChild(candidatesInput); | |
// 2. Peerを生成 | |
// Googleが提供しているSTUNサーバを使う | |
// Simple Traversal of UDP through NATs (STUN): NAT越えの方法としてRFC3489で定められた標準的な仕組み | |
// 外部のSTUNサーバに対してクライアントが一度接続し、グローバルIPとマッピングされたポート番号を記憶しておくことで、そのデータを使ってPeerは相手のマシンを特定することができる | |
var peer = new webkitRTCPeerConnection({ | |
"iceServers": [{"url": "stun:stun.l.google.com:19302"}] | |
}); | |
// 3. メディアに接続(カメラ | |
// メディアに接続しvideo要素に内容を表示する | |
navigator.webkitGetUserMedia({video: true, audio: false}, | |
function(stream) { | |
// blob URLを指定するとカメラの画像video要素で閲覧できる | |
// 自分の顔を見たくなかったらコメントアウトしてね | |
localVideo.src = window.URL.createObjectURL(stream); | |
// Peerにストリームを接続 | |
peer.addStream(stream); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
// --- 接続対象へSDPを送受信し、セッションを確立させる | |
// Session Description Protocol (SDP): 各ブラウザの情報(セッションが含むメディアの種類、IPアドレス、ポート番号などなど)を示し、文字列で表現される | |
// 本来ならばシグナリングサーバーを用意して接続対象を選びSDPを送信するという手順になるが、用意が面倒なので手動でやる | |
// 4. Offer生成: 端末A | |
// 接続対象へSDPを送信する(今回はconsole.logに出力するだけ | |
var createOffer = function() { | |
peer.createOffer(function(sdp) { | |
peer.setLocalDescription(sdp, | |
function() { | |
// 本来ならばこのタイミングでシグナリングサーバーへ自分のSDPを送信する | |
console.log(sdp); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
// コピーしやすいように文字列化 | |
console.log(JSON.stringify(sdp)); | |
}); | |
}; | |
webrtc.createOffer = createOffer; | |
// 5. Offerを受信: 端末B | |
// SDPを受信する(手順4. で生成したSDPをコピーしテキストエリアに入力する. その後、Developer Toolsのコンソールから下記関数を実行する | |
var receiveSdp = function() { | |
var sdp = receiveSdpInput.value;// コンソールから値を渡すようにしたかったが、自動文字変換が走ってうまくいかないので左側のテキストエリアに記載して値を取る | |
sdp = JSON.parse(sdp); | |
var remoteSdp = new RTCSessionDescription(sdp); | |
peer.setRemoteDescription(remoteSdp, | |
function() { | |
if (remoteSdp.type === "offer") { | |
console.log("receive offer"); | |
} | |
if (remoteSdp.type === "answer") { | |
console.log("receive answer"); | |
} | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
}; | |
webrtc.receiveSdp = receiveSdp; | |
// 6. Answer生成: 端末B | |
// 接続対象へSDPを送信する(今回はconsole.logに出力するだけ | |
var createAnswer = function() { | |
peer.createAnswer(function(sdp) { | |
peer.setLocalDescription(sdp, | |
function() { | |
console.log(sdp); | |
}, | |
function(err) { | |
console.log(err); | |
} | |
); | |
// コピーしやすいように文字列化 | |
console.log(JSON.stringify(sdp)); | |
}); | |
}; | |
webrtc.createAnswer = createAnswer; | |
// 7. Answerを受信: 端末A | |
// SDPを受信する(手順6. で生成したSDPをコピーしテキストエリアに入力する. その後、Developer Toolsのコンソールから5で定義した下記関数を実行する | |
// webrtc.receiveSdp() | |
// --- ストリームの共有 | |
peer.onaddstream = function(event) { | |
remoteVideo.src = window.webkitURL.createObjectURL(event.stream); | |
}; | |
// 通常は1経路ずつやりとりするが、手動では辛いので配列に入れていっぺんに共有する | |
var candidates = []; | |
peer.onicecandidate = function(event) { | |
candidates.push(event.candidate); | |
}; | |
// 8. candidateの出力: 端末A | |
var displayCandidates = function() { | |
// コピーしやすいように文字列化 | |
console.log(JSON.stringify(candidates)); | |
}; | |
webrtc.displayCandidates = displayCandidates; | |
// 9. candidateの受信: 端末B | |
var receiveCandidates = function() { | |
var candidates = candidatesInput.value;// コンソールから値を渡すようにしたかったが、自動文字変換が走ってうまくいかないので右側のテキストエリアに記載して値を取る | |
candidates = JSON.parse(candidates); | |
for (var i = 0, l = candidates.length; i < l; i++) { | |
if (candidates[i]) { | |
var candidate = new RTCIceCandidate(candidates[i]); | |
peer.addIceCandidate(candidate); | |
} else { | |
console.log("no candidate"); | |
} | |
} | |
}; | |
webrtc.receiveCandidates = receiveCandidates; | |
// 10. candidateの出力: 端末B | |
// 8.と同様の手順 | |
// 11. candidateの受信: 端末A | |
// 9.と同様の手順 | |
})(webrtc); | |
</script> | |
</body> | |
</html> |
![]() |
図. webrtcテストサンプル |
(カメラを許可すると、左上に端末A(自分の顔)が表示されます。
では、ブラウザを2つ立ち上げ(別のPCでも可)、端末Aと端末Bとして作業してみましょう。
4〜11は下記のような作業が必要なので実行してみてください。
![]() |
図. 手順説明用 |
No | 端末A | 端末B |
4 | 端末Bへ送信するOfferを生成 ・Developer ToolsのConsoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.createOffer() | |
5 | 端末Aで生成したOfferを受信 ・4でコピーした文字列をブラウザに表示されてるテキストエリア左側(図の①)に貼り付け、Consoleから下記を実行する webrtc.receiveSdp() | |
6 | 端末Aへ送信するAnswerを生成 ・Consoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.createAnswer() | |
7 | 端末Bで生成したAnswerを受信 ・6でコピーした文字列をブラウザに表示されてるテキストエリア左側(図の①)に貼り付け、Consoleから下記を実行する webrtc.receiveSdp() | |
8 | 端末Bへ送信する経路情報を出力 ・Consoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.displayCandidates() | |
9 | 端末Aで生成した経路情報を受信 ・8でコピーした文字列をブラウザに表示されてるテキストエリア右側(図の②)に貼り付け、Consoleから下記を実行する webrtc.receiveCandidates() | |
10 | 端末Aへ送信する経路情報を出力 ・Consoleで下記を実行し、表示される文字列をコピーする(文字列に変換して表示されてるやつ webrtc.displayCandidates() | |
11 | 端末Bで生成した経路情報を受信 ・10でコピーした文字列をブラウザに表示されてるテキストエリア右側(図の②)に貼り付け、Consoleから下記を実行する webrtc.receiveCandidates() |
11を実行することで、右上に端末B(相手の顔)が表示されます。
おわりに
手動でやるとことで、流れがだいぶ理解できました。
今度は自動でシグナリングするようにして、DataChannelでもいじってみたいと思います。
以上!
登録:
コメントの投稿
(
Atom
)
0 件のコメント :
コメントを投稿