HTML5Experts.jp

シグナリングサーバーを応用! 「WebRTCを使って複数人で話してみよう」

こんにちは! 前回はシグナリングサーバーを動かして、WebRTCでPeer-to-Peer通信をつなぐ処理を作りました。最後に書いた通り、前回の実装ではサーバーあたり2人だけしか同時に通知できません。今回はこれをもっと実用的にしていきましょう。 ※今回もNode学園祭2013で発表した内容と共通の部分が多いです。その時の資料も併せてご参照ください。

※こちらの記事は2014年に書かれました。2016年8月のアップデート記事がありますので、そちらもご参照ください。

複数会議室を作ろう

前回作ったのは、いわばカップル1組限定サイトのシングルテナントアプリでした(左)。これを複数組が共存できる、マルチテナント(複数会議室)のアプリに改造します(右)。

複数組が共存できない理由は、シグナリングの通信が同じシグナリングサーバーに接続している全員に飛んでしまうからです。これを混線しないように分離してあげる必要があります。シグナリングサーバーで利用しているsocket.ioでは、これを簡単に実現できるroom機能があります。

まずクライアント側を一部手直しします。

function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;

var roomname = getRoomName(); // 会議室名を取得する
socket.emit('enter', roomname);

}

ソケット接続が確立したら、シグナリングサーバーに対して入室要求(enter)を送っています。ここでgetRoomName()はアプリケーション側で実装する部分で、何らかの方法で会議室名を取得して返します。 手抜きなサンプルとしてはこんな感じでしょうか。URLの?以降をそのまま切り出して返しています。

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()の引数を変更

// 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)に対応するのと、会議室内だけに通信する部分です。

// 入室
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が割り振る)を保持します。

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のやり取りをする必要があります。

では、クライアント側のソースを直していきましょう。

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()の中身もちょっと変わります。
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()もちょっと変えます。複数のコネクションに対応するのと、送る相手を指定するのが変更点です。

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を覚える処理も複数セッションに対応させます。

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の送信時にも相手先を指定します。
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()も複数コネクションに対応させます。

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


さてさて、クライアント側の修正はいったん終わりにして、次はシグナリングサーバー側を修正します。前半では部屋の中だけに送信する機能を加えましたが、次は特定の相手にだけ送信できるようにします。
  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タグを配置します。

  <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タグを扱えるような関数群を追加します。※本当は動的にタグを作成、削除するのがかっこいいのですが…。

  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を送れば良いのでは?
アドバイスに従うと、次のように改善されます。

確かにその通りです。メッセージのやり取りが片道分少なくなり、すっきりしますね。 ご指摘ありがとうございました。

次回は

次回は最終回の予定です。NATやFirewallを越えて通信するための、STUN/TURNについて説明したいと思います。

今回のソースコード

シグナリングサーバー (node.js)

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)

<!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> &nbsp;&nbsp;&nbsp;&nbsp; <!-- <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' &amp;&amp; isPeerStarted()) {  // **
  console.log('Received answer, settinng answer SDP');
  onAnswer(evt);
} else if (evt.type === 'candidate' &amp;&amp; isPeerStarted()) { // **
  console.log('Received ICE candidate...');
  onCandidate(evt);
} else if (evt.type === 'user dissconnected' &amp;&amp; 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>