こんにちは! 前回はシグナリングサーバーを動かして、WebRTCでPeer-to-Peer通信をつなぐ処理を作りました。最後に書いた通り、前回の実装ではサーバーあたり2人だけしか同時に通知できません。今回はこれをもっと実用的にしていきましょう。 ※今回もNode学園祭2013で発表した内容と共通の部分が多いです。その時の資料も併せてご参照ください。
※こちらの記事は2014年に書かれました。2016年8月のアップデート記事がありますので、そちらもご参照ください。
複数会議室を作ろう
前回作ったのは、いわばカップル1組限定サイトのシングルテナントアプリでした(左)。これを複数組が共存できる、マルチテナント(複数会議室)のアプリに改造します(右)。
複数組が共存できない理由は、シグナリングの通信が同じシグナリングサーバーに接続している全員に飛んでしまうからです。これを混線しないように分離してあげる必要があります。シグナリングサーバーで利用しているsocket.ioでは、これを簡単に実現できるroom機能があります。
- roomに入室する… socket.join()
- roomから退室する… socket.leave()
- room内だけにメッセージを送る… socket.broadcast.to.emit()
まずクライアント側を一部手直しします。
1 2 3 4 5 6 7 |
function onOpened(evt) { console.log('socket opened.'); socketReady = true; var roomname = getRoomName(); // 会議室名を取得する socket.emit('enter', roomname); } |
ソケット接続が確立したら、シグナリングサーバーに対して入室要求(enter)を送っています。ここでgetRoomName()はアプリケーション側で実装する部分で、何らかの方法で会議室名を取得して返します。 手抜きなサンプルとしてはこんな感じでしょうか。URLの?以降をそのまま切り出して返しています。
1 2 3 4 5 6 7 8 9 10 11 |
function getRoomName() { // たとえば、 URLに ?roomname とする var url = document.location.href; var args = url.split('?'); if (args.length > 1) { var room = args[1]; if (room != "") { return room; } } return "_defaultroom"; } |
ついでにもう少し直しましょう。実は前回までのサンプルではカメラしかアクセスしていません。マイクはアクセスしていないので、声が聞こえません。今回はマイクも取得するように一カ所だけ修正します。※webkitGetUserMedia()の引数を変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// start local video function startVideo() { navigator.webkitGetUserMedia({video: true, audio: true}, // <--- audio: true に変更 function (stream) { // success localStream = stream; localVideo.src = window.webkitURL.createObjectURL(stream); localVideo.play(); localVideo.volume = 0; }, function (error) { // error console.error('An error occurred: [CODE ' + error.code + ']'); return; } ); } |
※同一PC上で複数の2つのウィンドウ/タブを開いて通信する場合、ハウリングしやすいので音量を絞るか、ヘッドフォンを利用してください。
今度はシグナリングサーバー側も修正しましょう。クライアントからの入室要求(enter)に対応するのと、会議室内だけに通信する部分です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 入室 socket.on('enter', function(roomname) { socket.set('roomname', roomname); socket.join(roomname); }); socket.on('message', function(message) { emitMessage('message', message); }); socket.on('disconnect', function() { emitMessage('user disconnected'); }); // 会議室名が指定されていたら、室内だけに通知 function emitMessage(type, message) { var roomname; socket.get('roomname', function(err, _room) { roomname = _room; }); if (roomname) { socket.broadcast.to(roomname).emit(type, message); } else { socket.broadcast.emit(type, message); } } |
シグナリングサーバーを起動しなおして、HTMLをリロードすれば、複数会議室に対応したマルチテナントアプリの完成です。
URLの後ろに ?room1 や ?room2 などのように会議室名を指定すれば、 その部屋の人と通信できます。
複数人で通信してみたい
次は2人だけでなく、複数人で同時に話せるようにしてみたいと思います。こんな感じです。
複数のPeer-to-Peer通信を扱うには
複数人と通信するには、クライアント側(ブラウザ側)に相手の数だけPeerConnectionが必要です。それを管理するための便宜上のクラスを作ります。 通信状況や、相手のID(socket.ioが割り振る)を保持します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var MAX_CONNECTION_COUNT = 3; var connections = {}; // Connection hash function Connection() { // Connection Class var self = this; var id = ""; // socket.id of partner var peerconnection = null; // RTCPeerConnection instance var established = false; // is Already Established var iceReady = false; } function getConnection(id) { var con = null; con = connections[id]; return con; } function addConnection(id, connection) { connections[id] = connection; } |
ついでに、複数のConnectionを格納する配列と、それを管理する関数群も用意します。ここに挙げた2つ以外に、getConnectionCount(), isConnectPossible(), deleteConnection(id), などなど。(詳細は最後に全ソースを掲載します)
シグナリングを手直し
シグナリングの流れも手直しが必要です。
今までのシグナリングでは、最初にOffer SDPを送る際に同じ部屋の全員に送っていました(broadcast)。すると全員からAnswer SDPが返ってきてしまうので、情報が衝突してしまいます。
そこで、まず部屋に誰が居るかを確認し(call-response)、一人ずつ個別にOffer-Answerのやり取りをする必要があります。
では、クライアント側のソースを直していきましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function call() { if (! isLocalStreamStarted()) return; socket.json.send({type: "call"}); } function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); if (evt.type === 'call') { if (! isLocalStreamStarted()) return; if (conn) return; // already connected if (isConnectPossible()) { socket.json.send({type: "response", sendto: id }); } else { console.warn('max connections. so ignore call'); } } else if (evt.type === 'response') { sendOffer(id); return; } } |
call()で全員にbroadcastし、受け取った側はonMessage()の中でcallを受け取ると、responseを相手を特定して送り返します。発信側はreponseを受け取ると、その相手に対してoffer SDPを送っています。 sendOffer()の中身もちょっと変わります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function sendOffer(id) { var conn = getConnection(id); // <--- すでに作成済のコネクションを探す if (!conn) { conn = prepareNewConnection(id); } conn.peerconnection.createOffer(function (sessionDescription) { // in case of success conn.iceReady = true; conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = conn.id; // <--- 送る相手を指定する sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Offer failed"); }, mediaConstraints); conn.iceReady = true; } |
同様に、sendAnswer()もちょっと変えます。複数のコネクションに対応するのと、送る相手を指定するのが変更点です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function sendAnswer(evt) { console.log('sending Answer. Creating remote session description...' ); var id = evt.from; var conn = getConnection(id); // <--- すでに作成済のコネクションを探す if (! conn) { console.error('peerConnection not exist!'); return } conn.peerconnection.createAnswer(function (sessionDescription) { // in case of success conn.iceReady = true; conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; // <--- 送る相手を指定する sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Answer failed"); }, mediaConstraints); conn.iceReady = true; } |
さらに、SDPを覚える処理も複数セッションに対応させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function setOffer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { conn = prepareNewConnection(id); conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } else { console.error('peerConnection alreay exist!'); } } function setAnswer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return } conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } |
引き続きConnectionを生成する処理も修正します。今まではPeerConnectionを直接返していましたが、今回はConnectionのインスタンスを生成し、そこにPeerConnectionを保持させます。また、Candidateの送信時にも相手先を指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function prepareNewConnection(id) { var pc_config = {"iceServers":[]}; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config); } catch (e) { console.log("Failed to create PeerConnection, exception: " + e.message); } var conn = new Connection(); // <--- Connectionを作成し、PeerConnectionを保持させる conn.id = id; conn.peerconnection = peer; peer.id = id; addConnection(id, conn); // send any ice candidates to the other peer peer.onicecandidate = function (evt) { if (evt.candidate) { console.log(evt.candidate); sendCandidate({type: "candidate", sendto: conn.id, // <-- 送信先を指定 sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate}); } else { console.log("End of candidates. ------------------- phase=" + evt.eventPhase); conn.established = true; } }; // ... } |
Candidateの送信部分を変更したので、Candidateを受信した処理も変更しましょう。 onCandidate()も複数コネクションに対応させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function onCandidate(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } // --- check if ice ready --- if (! conn.iceReady) { console.warn("PeerConn is not ICE ready, so ignore"); return; } var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate}); console.log("Received Candidate...") console.log(candidate); conn.peerconnection.addIceCandidate(candidate); } |
さてさて、クライアント側の修正はいったん終わりにして、次はシグナリングサーバー側を修正します。前半では部屋の中だけに送信する機能を加えましたが、次は特定の相手にだけ送信できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
socket.on('message', function(message) { // 送信元のidをメッセージに追加(相手が分かるように) message.from = socket.id; // 送信先が指定されているか? var target = message.sendto; if (target) { // 送信先が指定されていた場合は、その相手のみに送信 io.sockets.socket(target).emit('message', message); return; } // 特に指定がなければ、ブロードキャスト emitMessage('message', message); }); |
ここまででいったん動かしてみましょう。まだ映像が2人までしか出ませんが、通信はできるはずです。
複数の映像を扱えるようにしよう
ここまでで複数人相手に通信をできるようにしました。でも通信できても映像は見えていません。ちゃんと見えるようにしましょう。 まずHTMLに複数のvideoタグを配置します。
1 2 3 4 5 6 7 |
<div style="position: relative;"> <video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> <!-- <video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> --> <video id="webrtc-remote-video-0" autoplay style="position: absolute; top: 250px; left: 0px; width: 320px; height: 240px; border: 1px solid black; "></video> <video id="webrtc-remote-video-1" autoplay style="position: absolute; top: 250px; left: 330px; width: 320px; height: 240px; border: 1px solid black; "></video> <video id="webrtc-remote-video-2" autoplay style="position: absolute; top: 0px; left: 330px; width: 320px; height: 240px; border: 1px solid black; " ></video> </div> |
その複数のvideoタグを扱えるような関数群を追加します。※本当は動的にタグを作成、削除するのがかっこいいのですが…。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
var localVideo = document.getElementById('local-video'); //var remoteVideo = document.getElementById('remote-video'); var localStream = null; var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }}; // ---- multi people video & audio ---- var videoElementsInUse = {}; var videoElementsStandBy = {}; pushVideoStandBy(getVideoForRemote(0)); pushVideoStandBy(getVideoForRemote(1)); pushVideoStandBy(getVideoForRemote(2)); function getVideoForRemote(index) { var elementID = 'webrtc-remote-video-' + index; var element = document.getElementById(elementID); return element; } function getAudioForRemote(index) { var elementID = 'webrtc-remote-audio-' + index; var element = document.getElementById(elementID); return element; } // ---- video element management --- function pushVideoStandBy(element) { videoElementsStandBy[element.id] = element; } function popVideoStandBy() { var element = null; for (var id in videoElementsStandBy) { element = videoElementsStandBy[id]; delete videoElementsStandBy[id]; return element; } return null; } function pushVideoInUse(id, element) { videoElementsInUse[id] = element; } function popVideoInUse(id) { element = videoElementsInUse[id]; delete videoElementsInUse[id]; return element; } function attachVideo(id, stream) { console.log('try to attach video. id=' + id); var videoElement = popVideoStandBy(); if (videoElement) { videoElement.src = window.URL.createObjectURL(stream); console.log("videoElement.src=" + videoElement.src); pushVideoInUse(id, videoElement); videoElement.style.display = 'block'; } else { console.error('--- no video element stand by.'); } } function detachVideo(id) { console.log('try to detach video. id=' + id); var videoElement = popVideoInUse(id); if (videoElement) { videoElement.pause(); videoElement.src = ""; console.log("videoElement.src=" + videoElement.src); pushVideoStandBy(videoElement); } else { console.warn('warning --- no video element using with id=' + id); } } // ... |
※ソース全体は最後に記載します。
これで準備が整いました。早速接続してみましょう。
[Start video]ボタンを押して、[Connect]を押す、という操作を一人ずつ行ってください。一人、また一人と接続され、最大4人まで通信できます。
補足 (2014/03/09追記)
Twitter経由でご指摘をいただきました。User B/Cからresponseではなく、Offerを送れば良いのでは?
アドバイスに従うと、次のように改善されます。
- 現状:User Aからcall, User B/Cからresponse, User AからOffer, User B/CからAnswer
- 改善:User Aからcallme, User B/CからOffer, User AからAnswer
確かにその通りです。メッセージのやり取りが片道分少なくなり、すっきりしますね。 ご指摘ありがとうございました。
次回は
次回は最終回の予定です。NATやFirewallを越えて通信するための、STUN/TURNについて説明したいと思います。
今回のソースコード
シグナリングサーバー (node.js)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
var port = 9001; var io = require('socket.io').listen(port); console.log((new Date()) + " Server is listening on port " + port); io.sockets.on('connection', function(socket) { // 入室 socket.on('enter', function(roomname) { socket.set('roomname', roomname); socket.join(roomname); }); socket.on('message', function(message) { // 送信元のidをメッセージに追加(相手が分かるように) message.from = socket.id; // 送信先が指定されているか? var target = message.sendto; if (target) { // 送信先が指定されていた場合は、その相手のみに送信 io.sockets.socket(target).emit('message', message); return; } // 特に指定がなければ、ブロードキャスト emitMessage('message', message); }); socket.on('disconnect', function() { emitMessage('user disconnected'); }); // 会議室名が指定されていたら、室内だけに通知 function emitMessage(type, message) { var roomname; socket.get('roomname', function(err, _room) { roomname = _room; }); if (roomname) { socket.broadcast.to(roomname).emit(type, message); } else { socket.broadcast.emit(type, message); } } }); |
クライアント側 (HTML, JavaScript)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 |
<!DOCTYPE html> <html> <head> <title>WebRTC 4</title> </head> <body> <button type="button" onclick="startVideo();">Start video</button> <button type="button" onclick="stopVideo();">Stop video</button> <!-- <button type="button" onclick="connect();">Connect</button> --> <button type="button" onclick="call();">Connect</button> <button type="button" onclick="hangUp();">Hang Up</button> <br /> <div style="position: relative;"> <video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> <!-- <video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video> --> <video id="webrtc-remote-video-0" autoplay style="position: absolute; top: 250px; left: 0px; width: 320px; height: 240px; border: 1px solid black; "></video> <video id="webrtc-remote-video-1" autoplay style="position: absolute; top: 250px; left: 330px; width: 320px; height: 240px; border: 1px solid black; "></video> <video id="webrtc-remote-video-2" autoplay style="position: absolute; top: 0px; left: 330px; width: 320px; height: 240px; border: 1px solid black; " ></video> </div> <!--- <p> SDP to send:<br /> <textarea id="text-for-send-sdp" rows="5" cols="100" disabled="1">SDP to send</textarea> </p> <p> SDP to receive:<br /> <textarea id="text-for-receive-sdp" rows="5" cols="100"></textarea><br /> <button type="button" onclick="onSDP();">Receive SDP</button> </p> <p> ICE Candidate to send:<br /> <textarea id="text-for-send-ice" rows="5" cols="100" disabled="1">ICE Candidate to send</textarea> </p> <p> ICE Candidates to receive:<br /> <textarea id="text-for-receive-ice" rows="5" cols="100"></textarea><br /> <button type="button" onclick="onICE();">Receive ICE Candidates</button> </p> ---> <!---- socket ------> <script src="http://localhost:9001/socket.io/socket.io.js"></script> <script> var localVideo = document.getElementById('local-video'); //var remoteVideo = document.getElementById('remote-video'); var localStream = null; var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }}; // ---- multi people video & audio ---- var videoElementsInUse = {}; var videoElementsStandBy = {}; pushVideoStandBy(getVideoForRemote(0)); pushVideoStandBy(getVideoForRemote(1)); pushVideoStandBy(getVideoForRemote(2)); function getVideoForRemote(index) { var elementID = 'webrtc-remote-video-' + index; var element = document.getElementById(elementID); return element; } // ---- video element management --- function pushVideoStandBy(element) { videoElementsStandBy[element.id] = element; } function popVideoStandBy() { var element = null; for (var id in videoElementsStandBy) { element = videoElementsStandBy[id]; delete videoElementsStandBy[id]; return element; } return null; } function pushVideoInUse(id, element) { videoElementsInUse[id] = element; } function popVideoInUse(id) { element = videoElementsInUse[id]; delete videoElementsInUse[id]; return element; } function attachVideo(id, stream) { console.log('try to attach video. id=' + id); var videoElement = popVideoStandBy(); if (videoElement) { videoElement.src = window.URL.createObjectURL(stream); console.log("videoElement.src=" + videoElement.src); pushVideoInUse(id, videoElement); videoElement.style.display = 'block'; } else { console.error('--- no video element stand by.'); } } function detachVideo(id) { console.log('try to detach video. id=' + id); var videoElement = popVideoInUse(id); if (videoElement) { videoElement.pause(); videoElement.src = ""; console.log("videoElement.src=" + videoElement.src); pushVideoStandBy(videoElement); } else { console.warn('warning --- no video element using with id=' + id); } } function detachAllVideo() { var element = null; for (var id in videoElementsInUse) { detachVideo(id); } } function getFirstVideoInUse() { var element = null; for (var id in videoElementsInUse) { element = videoElementsInUse[id]; return element; } return null; } function getVideoCountInUse() { var count = 0; for (var id in videoElementsInUse) { count++; } return count; } function isLocalStreamStarted() { if (localStream) { return true; } else { return false; } } // -------------- multi connections -------------------- var MAX_CONNECTION_COUNT = 3; var connections = {}; // Connection hash function Connection() { // Connection Class var self = this; var id = ""; // socket.id of partner var peerconnection = null; // RTCPeerConnection instance var established = false; // is Already Established var iceReady = false; } function getConnection(id) { var con = null; con = connections[id]; return con; } function addConnection(id, connection) { connections[id] = connection; } function getConnectionCount() { var count = 0; for (var id in connections) { count++; } console.log('getConnectionCount=' + count); return count; } function isConnectPossible() { if (getConnectionCount() < MAX_CONNECTION_COUNT) return true; else return false; } function getConnectionIndex(id_to_lookup) { var index = 0; for (var id in connections) { if (id == id_to_lookup) { return index; } index++; } // not found return -1; } function deleteConnection(id) { delete connections[id]; } function stopAllConnections() { for (var id in connections) { var conn = connections[id]; conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } } function stopConnection(id) { var conn = connections[id]; if(conn) { console.log('stop and delete connection with id=' + id); conn.peerconnection.close(); conn.peerconnection = null; delete connections[id]; } else { console.log('try to stop connection, but not found id=' + id); } } function isPeerStarted() { if (getConnectionCount() > 0) { return true; } else { return false; } } // ---- socket ------ // create socket var socketReady = false; var port = 9001; var socket = io.connect('http://localhost:' + port + '/'); // socket: channel connected socket.on('connect', onOpened) .on('message', onMessage); function onOpened(evt) { console.log('socket opened.'); socketReady = true; var roomname = getRoomName(); // 会議室名を取得する socket.emit('enter', roomname); console.log('enter to ' + roomname); } // socket: accept connection request function onMessage(evt) { var id = evt.from; var target = evt.sendto; var conn = getConnection(id); if (evt.type === 'call') { if (! isLocalStreamStarted()) { return; } if (conn) { return; // already connected } if (isConnectPossible()) { socket.json.send({type: "response", sendto: id }); } else { console.warn('max connections. so ignore call'); } return; } else if (evt.type === 'response') { sendOffer(id); return; } else if (evt.type === 'offer') { console.log("Received offer, set offer, sending answer....") onOffer(evt); } else if (evt.type === 'answer' && isPeerStarted()) { // ** console.log('Received answer, settinng answer SDP'); onAnswer(evt); } else if (evt.type === 'candidate' && isPeerStarted()) { // ** console.log('Received ICE candidate...'); onCandidate(evt); } else if (evt.type === 'user dissconnected' && isPeerStarted()) { // ** console.log("disconnected"); //stop(); detachVideo(id); // force detach video stopConnection(id); } } function getRoomName() { // たとえば、 URLに ?roomname とする var url = document.location.href; var args = url.split('?'); if (args.length > 1) { var room = args[1]; if (room != "") { return room; } } return "_defaultroom"; } // ----------------- handshake -------------- //var textForSendSDP = document.getElementById('text-for-send-sdp'); //var textForSendICE = document.getElementById('text-for-send-ice'); //var textToReceiveSDP = document.getElementById('text-for-receive-sdp'); //var textToReceiveICE = document.getElementById('text-for-receive-ice'); //var iceSeparator = '------ ICE Candidate -------'; //var CR = String.fromCharCode(13); /*-- function onSDP() { var text = textToReceiveSDP.value; var evt = JSON.parse(text); if (peerConnection) { onAnswer(evt); } else { onOffer(evt); } //textToReceiveSDP.value =""; } --*/ //--- multi ICE candidate --- /*-- function onICE() { var text = textToReceiveICE.value; var arr = text.split(iceSeparator); for (var i = 1, len = arr.length; i < len; i++) { var evt = JSON.parse(arr[i]); onCandidate(evt); } textToReceiveICE.value =""; } ---*/ function onOffer(evt) { console.log("Received offer...") console.log(evt); setOffer(evt); sendAnswer(evt); //peerStarted = true; -- } function onAnswer(evt) { console.log("Received Answer...") console.log(evt); setAnswer(evt); } function onCandidate(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return; } // --- check if ice ready --- if (! conn.iceReady) { console.warn("PeerConn is not ICE ready, so ignore"); return; } var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate}); console.log("Received Candidate...") console.log(candidate); conn.peerconnection.addIceCandidate(candidate); } function sendSDP(sdp) { var text = JSON.stringify(sdp); console.log("---sending sdp text ---"); console.log(text); //textForSendSDP.value = text; // send via socket socket.json.send(sdp); } function sendCandidate(candidate) { var text = JSON.stringify(candidate); console.log("---sending candidate text ---"); console.log(text); //textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR); //textForSendICE.scrollTop = textForSendICE.scrollHeight; // send via socket socket.json.send(candidate); } // ---------------------- video handling ----------------------- // start local video function startVideo() { navigator.webkitGetUserMedia({video: true, audio: true}, function (stream) { // success localStream = stream; localVideo.src = window.webkitURL.createObjectURL(stream); localVideo.play(); localVideo.volume = 0; }, function (error) { // error console.error('An error occurred:'); console.error(error); return; } ); } // stop local video function stopVideo() { localVideo.src = ""; localStream.stop(); } // ---------------------- connection handling ----------------------- function prepareNewConnection(id) { var pc_config = {"iceServers":[]}; var peer = null; try { peer = new webkitRTCPeerConnection(pc_config); } catch (e) { console.log("Failed to create PeerConnection, exception: " + e.message); } var conn = new Connection(); conn.id = id; conn.peerconnection = peer; peer.id = id; addConnection(id, conn); // send any ice candidates to the other peer peer.onicecandidate = function (evt) { if (evt.candidate) { console.log(evt.candidate); sendCandidate({type: "candidate", sendto: conn.id, sdpMLineIndex: evt.candidate.sdpMLineIndex, sdpMid: evt.candidate.sdpMid, candidate: evt.candidate.candidate}); } else { console.log("End of candidates. ------------------- phase=" + evt.eventPhase); conn.established = true; } }; console.log('Adding local stream...'); peer.addStream(localStream); peer.addEventListener("addstream", onRemoteStreamAdded, false); peer.addEventListener("removestream", onRemoteStreamRemoved, false) // when remote adds a stream, hand it on to the local video element function onRemoteStreamAdded(event) { console.log("Added remote stream"); attachVideo(this.id, event.stream); //remoteVideo.src = window.webkitURL.createObjectURL(event.stream); } // when remote removes a stream, remove it from the local video element function onRemoteStreamRemoved(event) { console.log("Remove remote stream"); detachVideo(this.id); //remoteVideo.pause(); //remoteVideo.src = ""; } return conn; } function sendOffer(id) { var conn = getConnection(id); if (!conn) { conn = prepareNewConnection(id); } conn.peerconnection.createOffer(function (sessionDescription) { // in case of success conn.iceReady = true; conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Offer failed"); }, mediaConstraints); conn.iceReady = true; } function setOffer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { conn = prepareNewConnection(id); conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } else { console.error('peerConnection alreay exist!'); } } function sendAnswer(evt) { console.log('sending Answer. Creating remote session description...' ); var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return } conn.peerconnection.createAnswer(function (sessionDescription) { // in case of success conn.iceReady = true; conn.peerconnection.setLocalDescription(sessionDescription); sessionDescription.sendto = id; sendSDP(sessionDescription); }, function () { // in case of error console.log("Create Answer failed"); }, mediaConstraints); conn.iceReady = true; } function setAnswer(evt) { var id = evt.from; var conn = getConnection(id); if (! conn) { console.error('peerConnection not exist!'); return } conn.peerconnection.setRemoteDescription(new RTCSessionDescription(evt)); } // -------- handling user UI event ----- /*----- // start the connection upon user request function connect() { if (!peerStarted && localStream && socketReady) { // ** //if (!peerStarted && localStream) { // -- sendOffer(); peerStarted = true; } else { alert("Local stream not running yet - try again."); } } ----------*/ // call others before connecting peer function call() { if (! isLocalStreamStarted()) { alert("Local stream not running yet. Please [Start Video] or [Start Screen]."); return; } if (! socketReady) { alert("Socket is not connected to server. Please reload and try again."); return; } // call others, in same room console.log("call others in same room, befeore offer"); socket.json.send({type: "call"}); } // stop the connection upon user request function hangUp() { console.log("Hang up."); socket.json.send({type: "bye"}); detachAllVideo(); stopAllConnections(); } /*-- function stop() { peerConnection.close(); peerConnection = null; //peerStarted = false; -- } --*/ </script> </body> </html> |