diff --git a/static/js/controllers/statusmessagecontroller.js b/static/js/controllers/statusmessagecontroller.js index 5313fd0a..a9af9717 100644 --- a/static/js/controllers/statusmessagecontroller.js +++ b/static/js/controllers/statusmessagecontroller.js @@ -25,8 +25,8 @@ define([], function() { // StatusmessageController return ["$scope", "mediaStream", function($scope, mediaStream) { - $scope.doHangup = function() { - mediaStream.webrtc.doHangup(); + $scope.doHangup = function(reason, id) { + mediaStream.webrtc.doHangup(reason, id); } $scope.doAbort = function() { mediaStream.webrtc.doHangup("abort", $scope.dialing); @@ -35,10 +35,10 @@ define([], function() { mediaStream.connector.reconnect(); } $scope.doAccept = function() { - mediaStream.webrtc.doAccept(); + mediaStream.webrtc.doAccept($scope.incoming); } - $scope.doReject = function() { - mediaStream.webrtc.doHangup('reject'); + $scope.doReject = function(id) { + mediaStream.webrtc.doHangup('reject', id, $scope.incoming); } }]; diff --git a/static/js/controllers/uicontroller.js b/static/js/controllers/uicontroller.js index 500db72b..40e70774 100644 --- a/static/js/controllers/uicontroller.js +++ b/static/js/controllers/uicontroller.js @@ -206,10 +206,11 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web $scope.updatePeerFromConference = function() { if (!$scope.conferenceObject) { + $scope.conferencePeers.length = 0; return; } - var peerIds = $scope.conferenceObject.peerIds(); + var peerIds = $scope.conferenceObject.getCallIds(); if ($scope.peer && peerIds.indexOf($scope.peer) === -1) { $scope.peer = null; } @@ -229,7 +230,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web return; } - if ($scope.conference) { + if ($scope.conference || $scope.isConferenceRoom()) { $scope.setStatus("conference"); } else { $scope.setStatus("connected"); @@ -473,7 +474,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web $timeout.cancel(pickupTimeout); pickupTimeout = null; // Kill ringer. - if (peercall && peercall.from === null) { + if (peercall && peercall.isOutgoing()) { dialerEnabled = true; } else { dialerEnabled = false; @@ -485,7 +486,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web } // Apply peer call to scope. safeApply($scope, function(scope) { - // NOTE: the internal call will have a "id" of "null". scope.peer = peercall ? peercall.id : null; scope.setConnectedStatus(); }); @@ -497,20 +497,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web scope.conferenceObject = peerconference ? peerconference : null; scope.updatePeerFromConference(); scope.setConnectedStatus(); - if (!peerconference) { - scope.peer = null; - if (scope.usermedia) { - $timeout(function() { - scope.usermedia = null; - mediaStream.webrtc.stop(); - if (mediaStream.webrtc.isConferenceRoom()) { - mediaStream.webrtc.doUserMediaWithInternalCall(); - } - $scope.layout.buddylist = true; - $scope.layout.buddylistAutoHide = false; - }, 0); - } - } }); }); @@ -520,7 +506,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web }); if ($scope.updateAutoAccept(null, from)) { // Auto accept support. - mediaStream.webrtc.doAccept(); + mediaStream.webrtc.doAccept(from); return; } // Start to ring. @@ -533,7 +519,7 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web // Start accept timeout. pickupTimeout = $timeout(function() { console.log("Pickup timeout reached."); - mediaStream.webrtc.doHangup("pickuptimeout"); + mediaStream.webrtc.doHangup("pickuptimeout", from); $scope.$emit("notification", "incomingpickuptimeout", { reason: 'pickuptimeout', from: from @@ -631,10 +617,6 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web mediaStream.webrtc.e.on("waitforusermedia connecting", function(event, currentcall) { var t = event.type; - if (currentcall && currentcall.isinternal && t === "connecting") { - // Don't show "Calling Someone" for the internal call. - return; - } safeApply($scope, function(scope) { scope.dialing = currentcall ? currentcall.id : null; scope.setStatus(t); diff --git a/static/js/directives/buddylist.js b/static/js/directives/buddylist.js index ed4d000b..30e3eda4 100644 --- a/static/js/directives/buddylist.js +++ b/static/js/directives/buddylist.js @@ -56,6 +56,13 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { $scope.$apply(updateBuddyListVisibility); }); + $scope.$watch("peer", function() { + if ($scope.peer) { + // Also reset the buddylist if the peer is cleared after the "done" event. + updateBuddyListVisibility(); + } + }); + $scope.$on("room.joined", function(ev) { inRoom = true; updateBuddyListVisibility(); diff --git a/static/js/mediastream/peercall.js b/static/js/mediastream/peercall.js index 224f10fc..69d10e6f 100644 --- a/static/js/mediastream/peercall.js +++ b/static/js/mediastream/peercall.js @@ -38,6 +38,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection this.offerOptions = $.extend(true, {}, this.webrtc.settings.offerOptions); this.peerconnection = null; + this.pendingCandidates = []; this.datachannels = {}; this.streams = {}; @@ -47,6 +48,10 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection }; + PeerCall.prototype.isOutgoing = function() { + return !!this.from; + }; + PeerCall.prototype.setInitiate = function(initiate) { this.initiate = !! initiate; //console.log("Set initiate", this.initiate, this); @@ -74,6 +79,10 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection // TODO(longsleep): Check if this can happen? error_cb(peerconnection); } + while (this.pendingCandidates.length > 0) { + var candidate = this.pendingCandidates.shift(); + this.addIceCandidate(candidate); + } return peerconnection; }; @@ -96,6 +105,12 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection PeerCall.prototype.onCreateAnswerOffer = function(cb, sessionDescription) { + if (sessionDescription.type === "answer") { + // We processed the incoming Offer by creating an answer, so it's safe + // to create own Offers to perform renegotiation. + this.peerconnection.setReadyForRenegotiation(true); + } + this.setLocalSdp(sessionDescription); // Convert to object to allow custom property injection. @@ -130,6 +145,10 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection console.error("Failed to create answer/offer", event); + // Even though the Offer/Answer could not be created, we now allow + // to create own Offers to perform renegotiation again. + this.peerconnection.setReadyForRenegotiation(true); + }; PeerCall.prototype.setRemoteDescription = function(sessionDescription, cb) { @@ -142,6 +161,12 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection this.setRemoteSdp(sessionDescription); + if (sessionDescription.type === "offer") { + // Prevent creation of Offer messages to renegotiate streams while the + // remote Offer is being processed. + peerconnection.setReadyForRenegotiation(false); + } + peerconnection.setRemoteDescription(sessionDescription, _.bind(function() { console.log("Set remote session description.", sessionDescription, this); if (cb) { @@ -255,6 +280,10 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection }; PeerCall.prototype.onNegotiationNeeded = function() { + if (!this.peerconnection.readyForRenegotiation) { + console.log("PeerConnection is not ready for renegotiation yet", this); + return; + } if (!this.negotiationNeeded) { this.negotiationNeeded = true; @@ -270,6 +299,10 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection // Avoid errors when still receiving candidates but closed. return; } + if (!this.peerconnection) { + this.pendingCandidates.push(candidate); + return; + } this.peerconnection.addIceCandidate(candidate, function() { //console.log("Remote candidate added successfully.", candidate); }, function(error) { diff --git a/static/js/mediastream/peerconference.js b/static/js/mediastream/peerconference.js index c51f6ecd..8f96065c 100644 --- a/static/js/mediastream/peerconference.js +++ b/static/js/mediastream/peerconference.js @@ -22,180 +22,111 @@ "use strict"; define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall) { - //NOTE(longsleep): This id should be changed to something undeterministic. var conferences = 0; - var PeerConference = function(webrtc, currentcall, id) { + var STATE_ACTIVE = "active"; + var STATE_INCOMING = "incoming"; + var STATE_OUTGOING = "outgoing"; + + var PeerConference = function(webrtc) { this.webrtc = webrtc; - this.currentcall = currentcall; + this.calls = {}; - this.callsIn = {}; + this.callsCount = 0; + this.callStates = {}; + this.connectedCalls = {}; + this.conferenceMode = false; this.e = $({}); + this.id = null; - if (!id) { - this.id = webrtc.api.id + "_" + (++conferences); - } else { - this.id = id; - } - - if (!webrtc.usermedia) { - // Conference was started without getUM being called before. This - // happens for server-manager conference rooms. Create internal - // dummy call to trigger getUM, so actual conference calls can - // be established. - webrtc.doUserMediaWithInternalCall(); - } - - this.usermedia = webrtc.usermedia; - webrtc.e.on("usermedia", _.bind(function(event, um) { - console.log("Conference user media changed", um); - this.usermedia = um; + // Send conference updates to the other peers once we get a new connection. + webrtc.e.on("statechange", _.bind(function(event, iceConnectionState, currentcall) { + this.onConnectionStateChange(iceConnectionState, currentcall); }, this)); + }; - console.log("Created conference", this.id); - + // Creates a new unique random id to be used as conference id. + PeerConference.prototype._createConferenceId = function() { + return this.webrtc.api.id + "_" + (++conferences) + "_" + Math.round(Math.random() * 1e16); }; - PeerConference.prototype.checkEmpty = function() { - if (!_.isEmpty(this.calls) || (this.currentcall && this.currentcall.id)) { - return false; + PeerConference.prototype.getOrCreateId = function() { + if (!this.id) { + this.id = this._createConferenceId(); + console.log("Created new conference id", this.id); } - - console.log("Conference is now empty -> cleaning up."); - this.e.triggerHandler("finished"); - return true; + return this.id; }; - PeerConference.prototype.createCall = function(id, from, to) { - - var currentcall = new PeerCall(this.webrtc, id, from, to); - currentcall.e.on("closed", _.bind(function() { - delete this.calls[id]; - if (this.callsIn.hasOwnProperty(id)) { - delete this.callsIn[id]; - } - console.log("Cleaned up conference call", id); - this.checkEmpty(); - }, this)); - currentcall.e.on("connectionStateChange", _.bind(function(event, iceConnectionState, currentcall) { - this.onConnectionStateChange(iceConnectionState, currentcall); - }, this)); - currentcall.e.on("remoteStreamAdded", _.bind(function(event, stream, currentcall) { - this.webrtc.onRemoteStreamAdded(stream, currentcall); - }, this)); - currentcall.e.on("remoteStreamRemoved", _.bind(function(event, stream, currentcall) { - this.webrtc.onRemoteStreamRemoved(stream, currentcall); - }, this)); - - return currentcall; - + PeerConference.prototype.hasCalls = function() { + return this.callsCount > 0; }; - PeerConference.prototype.doCall = function(id, autocall) { - - if ((this.currentcall && id === this.currentcall.id) || this.calls.hasOwnProperty(id)) { - // Ignore calls which we already have. - //console.debug("Already got a call to this id (doCall)", id, this.calls, this.currentcall); - return; - } - - var call = this.calls[id] = this.createCall(id, null, id); - call.setInitiate(true); - call.e.on("sessiondescription", _.bind(function(event, sessionDescription) { - console.log("Injected conference id into sessionDescription", this.id); - sessionDescription._conference = this.id; - }, this)); + // Return number of currently active and pending calls. + PeerConference.prototype.getCallsCount = function() { + return this.callsCount; + }; - if (!autocall) { - this.webrtc.e.triggerHandler("connecting", [call]); + PeerConference.prototype._addCallWithState = function(id, call, state) { + if (this.calls.hasOwnProperty(id)) { + console.warn("Already has a call for", id); + return false; } - console.log("Creating PeerConnection", call); - call.createPeerConnection(_.bind(function(peerconnection) { - // Success call. - if (this.usermedia) { - this.usermedia.addToPeerConnection(peerconnection); - } - call.e.on("negotiationNeeded", _.bind(function(event, extracall) { - this.webrtc.sendOfferWhenNegotiationNeeded(extracall); - }, this)); - }, this), _.bind(function() { - // Error call. - console.error("Failed to create peer connection for conference call."); - }, this)); + this.calls[id] = call; + this.callStates[id] = state; + this.callsCount += 1; + return true; + }; + PeerConference.prototype.addIncoming = function(from, call) { + return this._addCallWithState(from, call, STATE_INCOMING); }; - PeerConference.prototype.callClosed = function(call) { - if (_.isEmpty(this.callsIn)) { - // No more calls in the conference - return null; - } + PeerConference.prototype.addOutgoing = function(to, call) { + return this._addCallWithState(to, call, STATE_OUTGOING); + }; - if (call !== this.currentcall) { - // An arbitrary call of the conference hung up. - delete this.calls[call.id]; - delete this.callsIn[call.id]; - console.log("Conference call closed", call); - } else { - // The "initiator" call of the conference hung up, promote another - // call to "initator" and return it. - var calls = _.keys(this.callsIn); - var id = calls[0]; - this.currentcall = this.calls[id]; - delete this.calls[id]; - delete this.callsIn[id]; - console.log("Handed over conference to", id, this.currentcall); + PeerConference.prototype._setCallState = function(id, state) { + if (this.callStates.hasOwnProperty(id)) { + this.callStates[id] = state; + console.log("Call state changed", id, state); } - return this.currentcall; }; - PeerConference.prototype.autoAnswer = function(from, rtcsdp) { - - if ((this.currentcall && from === this.currentcall.id) || this.calls.hasOwnProperty(from)) { - console.warn("Already got a call to this id (autoAnswer)", from, this.calls); - return; - } + PeerConference.prototype.setCallActive = function(id) { + this._setCallState(id, STATE_ACTIVE); + }; - var call = this.calls[from] = this.createCall(from, this.webrtc.api.id, from); - console.log("Creating PeerConnection", call); - call.createPeerConnection(_.bind(function(peerconnection) { - // Success call. - call.setRemoteDescription(rtcsdp, _.bind(function() { - if (this.usermedia) { - this.usermedia.addToPeerConnection(peerconnection); - } - call.e.on("negotiationNeeded", _.bind(function(event, extracall) { - this.webrtc.sendOfferWhenNegotiationNeeded(extracall); - }, this)); - call.createAnswer(_.bind(function(sessionDescription, extracall) { - console.log("Sending answer", sessionDescription, extracall.id); - this.webrtc.api.sendAnswer(extracall.id, sessionDescription); - }, this)); - }, this)); - }, this), _.bind(function() { - // Error call. - console.error("Failed to create peer connection for auto answer."); - }, this)); + PeerConference.prototype.getCall = function(id) { + return this.calls[id] || null; + }; + PeerConference.prototype.getCalls = function() { + return _.values(this.calls); }; - PeerConference.prototype.getCall = function(id) { + PeerConference.prototype.getCallIds = function() { + return _.keys(this.calls); + }; - var call = this.calls[id]; - if (!call) { - call = null; + PeerConference.prototype.removeCall = function(id) { + if (!this.calls.hasOwnProperty(id)) { + return null; } + var call = this.calls[id]; + delete this.calls[id]; + delete this.callStates[id]; + delete this.connectedCalls[id]; + this.callsCount -= 1; return call; - }; PeerConference.prototype.close = function() { - this.currentcall = null; var api = this.webrtc.api; _.each(this.calls, function(c) { c.close(); @@ -205,6 +136,10 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall } }); this.calls = {}; + this.callStates = {}; + this.connectedCalls = {}; + this.callsCount = 0; + this.id = null; }; @@ -214,8 +149,8 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall switch (iceConnectionState) { case "completed": case "connected": - if (!this.callsIn.hasOwnProperty(currentcall.id)) { - this.callsIn[currentcall.id] = true; + if (!this.connectedCalls.hasOwnProperty(currentcall.id)) { + this.connectedCalls[currentcall.id] = true; this.pushUpdate(); } break; @@ -223,7 +158,6 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall console.warn("Conference peer connection state failed", currentcall); break; } - this.webrtc.onConnectionStateChange(iceConnectionState, currentcall); }; @@ -233,44 +167,16 @@ define(['jquery', 'underscore', 'mediastream/peercall'], function($, _, PeerCall return; } - var calls = _.keys(this.callsIn); - if (calls) { - if (this.currentcall) { - calls.push(this.currentcall.id); - calls.push(this.webrtc.api.id); - } + var ids = _.keys(this.connectedCalls); + if (ids.length > 1) { + ids.push(this.webrtc.api.id); + console.log("Calls in conference:", ids); + this.webrtc.api.sendConference(this.getOrCreateId(), ids); } - console.log("Calls in conference: ", calls); - this.webrtc.api.sendConference(this.id, calls); - - }; - - PeerConference.prototype.applyUpdate = function(ids) { - - console.log("Applying conference update", this.id, ids); - var myid = this.webrtc.api.id; - _.each(ids, _.bind(function(id) { - var res = myid < id ? -1 : myid > id ? 1 : 0; - console.log("Considering conference peers to call", res, id); - if (res === -1) { - this.doCall(id, true); - } - }, this)); - }; PeerConference.prototype.peerIds = function() { - - var result = _.keys(this.calls); - // "peerIds" returns the session ids of all participants in the - // conference, so we need to add the id of the peer the user called - // manually before migrating to a conference (but only if it has an id, - // i.e. is not an internal call object). - if (this.currentcall && this.currentcall.id && result.indexOf(this.currentcall.id) === -1) { - result.push(this.currentcall.id); - } - return result; - + return this.getCallIds(); }; return PeerConference; diff --git a/static/js/mediastream/peerconnection.js b/static/js/mediastream/peerconnection.js index 5c575c83..9bdc014d 100644 --- a/static/js/mediastream/peerconnection.js +++ b/static/js/mediastream/peerconnection.js @@ -33,6 +33,7 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { this.pc = null; this.datachannel = null; this.datachannelReady = false; + this.readyForRenegotiation = true; if (currentcall) { this.createPeerConnection(currentcall); @@ -40,6 +41,10 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { }; + PeerConnection.prototype.setReadyForRenegotiation = function(ready) { + this.readyForRenegotiation = !!ready; + }; + PeerConnection.prototype.createPeerConnection = function(currentcall) { // XXX(longsleep): This function is a mess. @@ -318,7 +323,6 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { }; PeerConnection.prototype.createAnswer = function() { - return this.pc.createAnswer.apply(this.pc, arguments); }; diff --git a/static/js/mediastream/webrtc.js b/static/js/mediastream/webrtc.js index 0a8505e9..93bd0896 100644 --- a/static/js/mediastream/webrtc.js +++ b/static/js/mediastream/webrtc.js @@ -56,6 +56,9 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u InternalPC.prototype.addStream = function() { }; + InternalPC.prototype.removeStream = function() { + }; + InternalPC.prototype.negotiationNeeded = function() { }; @@ -87,13 +90,11 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u this.e = $({}); - this.currentcall = null; - this.currentconference = null; + this.conference = new PeerConference(this); this.currentroom = null; - this.msgQueue = []; - - this.started = false; - this.initiator = null; + this.msgQueues = {}; + this.usermediaReady = false; + this.pendingMediaCalls = []; this.usermedia = null; this.audioMute = false; @@ -160,22 +161,39 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u }; WebRTC.prototype.receivedRoom = function(event, room) { - this.currentroom = room; if (this.isConferenceRoom()) { - if (!this.usermedia) { - this.doUserMediaWithInternalCall(); - } - } else { - if (this.currentcall && this.currentcall.isinternal) { - this.stop(); - } + // Switching from a conference room closes all current connections. + _.defer(_.bind(function() { + this.doHangup(); + }, this)); } + console.log("Joined room", room, this.api.id); + this.currentroom = room; + _.defer(_.bind(function() { + this.maybeStartLocalVideo(); + }, this), 100); }; WebRTC.prototype.isConferenceRoom = function() { return this.currentroom && this.currentroom.Type === roomTypeConference; }; + WebRTC.prototype.maybeStartLocalVideo = function() { + if (!this.isConferenceRoom()) { + return; + } + + console.log("Start local video"); + var call = new InternalCall(this); + this._doCallUserMedia(call); + }; + + WebRTC.prototype.stopLocalVideo = function() { + if (this.usermedia) { + this.usermedia.stop(); + } + }; + WebRTC.prototype.processReceived = function(event, to, data, type, to2, from) { //console.log(">>>>>>>>>>>>", type, from, data, to, to2); @@ -191,227 +209,210 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u return; } - if (!this.initiator && !this.started) { - switch (type) { - case "Offer": - if (this.currentcall && !this.currentcall.isinternal) { - console.warn("Received Offer while not started and with current call -> busy.", from); - this.api.sendBye(from, "busy"); - this.e.triggerHandler("busy", [from, to2, to]); - return; - } - this.msgQueue.unshift([to, data, type, to2, from]); - if (this.currentcall && this.currentcall.isinternal) { - // Internal getUM is currently in progress, defer - // evaluation of "Offer" until that is completed. - return; - } - // Create call. - this.currentcall = this.createCall(from, from, from); - // Delegate next steps to UI. - this.e.triggerHandler("offer", [from, to2, to]); - break; - case "Bye": - if (!this.currentcall) { - console.warn("Received Bye while without currentcall -> ignore.", from); - return; - } - if (this.currentcall.from !== from) { - console.warn("Received Bye from another id -> ignore.", from); - return; - } - console.log("Bye process (started false)"); - this.doHangup("receivedbye", from); - // Delegate bye to UI. - this.e.triggerHandler("bye", [data.Reason, from, to, to2]); - break; - case "Conference": - // No existing call yet, only supported for server-managed - // conference. - if (!this.isConferenceRoom()) { - console.warn("Received Conference outside call for invalid room type."); - return; - } - this.processReceivedMessage(to, data, type, to2, from); - break; - default: - this.msgQueue.push([to, data, type, to2, from]); - break; - } - } else { - this.processReceivedMessage(to, data, type, to2, from); - } - + this.processReceivedMessage(to, data, type, to2, from); }; WebRTC.prototype.findTargetCall = function(id) { + return this.conference.getCall(id); + }; - var targetcall = null; - if (this.currentcall) { - do { - if (this.initiator && this.currentcall.to === id) { - targetcall = this.currentcall; - break; - } - if (!this.initiator && this.currentcall.from === id) { - targetcall = this.currentcall; - break; - } - if (this.currentcall.id === id) { - targetcall = this.currentcall; - break; - } - if (this.currentconference) { - targetcall = this.currentconference.getCall(id) - } - } while (false); + WebRTC.prototype.callForEachCall = function(fn) { + var calls = this.conference.getCalls(); + if (!calls.length) { + return 0; } - return targetcall; + _.map(calls, fn); + return calls.length; + }; + WebRTC.prototype._getMessageQueue = function(id, create) { + var queue = this.msgQueues[id] || null; + if (queue === null && create) { + queue = this.msgQueues[id] = []; + } + return queue; }; - WebRTC.prototype.callForEachCall = function(fn) { + WebRTC.prototype.pushBackMessage = function(id, message) { + this._getMessageQueue(id, true).push(message); + }; - var count = 0; - if (this.currentcall) { - fn(this.currentcall, count); - count++; - if (this.currentconference) { - _.each(this.currentconference.calls, function(v, count) { - fn(v); - count++; - }); + WebRTC.prototype.pushFrontMessage = function(id, message) { + this._getMessageQueue(id, true).unshift(message); + }; + + WebRTC.prototype.popFrontMessage = function(id) { + var queue = this._getMessageQueue(id); + if (!queue) { + return null; + } + var message = queue.shift(); + if (!queue.length) { + delete this.msgQueues[id]; + } + return message; + }; + + WebRTC.prototype._processOffer = function(to, data, type, to2, from) { + console.log("Offer process."); + var call = this.conference.getCall(from); + if (call) { + // Remote peer is trying to renegotiate media. + if (!this.settings.renegotiation && call.peerconnection && call.peerconnection.hasRemoteDescription()) { + // Call replace support without renegotiation. + this.doHangup("unsupported", from); + console.error("Processing new offers is not implemented without renegotiation."); + return; + } + + call.setRemoteDescription(new window.RTCSessionDescription(data), _.bind(function(sessionDescription, currentcall) { + this.e.triggerHandler("peercall", [currentcall]); + currentcall.createAnswer(_.bind(function(sessionDescription, currentcall) { + console.log("Sending answer", sessionDescription, currentcall.id); + this.api.sendAnswer(currentcall.id, sessionDescription); + }, this)); + }, this)); + return; + } + + var autoaccept = false; + if (data._conference) { + if (this.conference.id !== data._conference) { + console.warn("Received Offer for unknown conference -> busy.", from); + this.api.sendBye(from, "busy"); + this.e.triggerHandler("busy", [from, to2, to]); + return; + } + + console.log("Received conference Offer -> auto.", from, data._conference); + // Clean own internal data before feeding into browser. + delete data._conference; + autoaccept = true; + } else if (this.conference.hasCalls()) { + // TODO(fancycode): support joining callers to currently active conference. + console.warn("Received Offer while already in a call -> busy.", from); + this.api.sendBye(from, "busy"); + this.e.triggerHandler("busy", [from, to2, to]); + return; + } + + call = this.createCall(from, this.api.id, from); + if (!this.conference.addIncoming(from, call)) { + console.warn("Already got a call, not processing Offer", from, autoaccept); + return; + } + + this.pushFrontMessage(from, [to, data, type, to2, from]); + if (autoaccept) { + if (!this.doAccept(call, true)) { + this.popFrontMessage(from); } + return; } - return count; + // Delegate next steps to UI. + this.e.triggerHandler("offer", [from, to2, to]); }; - WebRTC.prototype.processReceivedMessage = function(to, data, type, to2, from) { + WebRTC.prototype._processCandidate = function(to, data, type, to2, from) { + var call = this.conference.getCall(from); + if (!call) { + console.warn("Received Candidate for unknown id -> ignore.", from); + return; + } + + var candidate = new window.RTCIceCandidate({ + sdpMLineIndex: data.sdpMLineIndex, + sdpMid: data.sdpMid, + candidate: data.candidate + }); + call.addIceCandidate(candidate); + //console.log("Got candidate", data.sdpMid, data.sdpMLineIndex, data.candidate); + }; - if (!this.started && type !== "Conference") { - console.log('PeerConnection has not been created yet!'); + WebRTC.prototype._processAnswer = function(to, data, type, to2, from) { + var call = this.conference.getCall(from); + if (!call) { + console.warn("Received Answer from unknown id -> ignore", from); return; } - var targetcall; + console.log("Answer process."); + this.conference.setCallActive(call.id); + // TODO(longsleep): In case of negotiation this could switch offer and answer + // and result in a offer sdp sent as answer data. We need to handle this. + call.setRemoteDescription(new window.RTCSessionDescription(data), function() { + // Received remote description as answer. + console.log("Received answer after we sent offer", data); + }); + }; + + WebRTC.prototype._processBye = function(to, data, type, to2, from) { + console.log("Bye process."); + this.doHangup("receivedbye", from); + // Delegate bye to UI. + this.e.triggerHandler("bye", [data.Reason, from, to, to2]); + }; + + WebRTC.prototype._processConference = function(to, data, type, to2, from) { + var ids = this.conference.getCallIds(); + if (!ids.length && !this.isConferenceRoom()) { + console.warn("Received Conference for unknown call -> ignore.", to, data); + return; + } else if (ids.length == 1) { + // Peer-to-peer call will be upgraded to conference. + if (data.indexOf(ids[0]) === -1) { + console.warn("Received Conference for unknown call -> ignore.", to, data); + return; + } + this.conference.id = to; + } else if (this.conference.id && this.conference.id !== to) { + console.warn("Received Conference for wrong id -> ignore.", to, this.conference); + return; + } + + if (!this.conference.id) { + if (!this.isConferenceRoom()) { + console.warn("Received initial Conference for non-conference room -> ignore.", to, this.conference); + return; + } + this.conference.id = to; + console.log("Setting received conference id", to); + } + + console.log("Applying conference update", data); + var myid = this.api.id; + _.each(data, _.bind(function(id) { + var res = myid < id ? -1 : myid > id ? 1 : 0; + console.log("Considering conference peers to call", res, id); + if (res === -1) { + this.doCall(id, true); + } + }, this)); + this.e.triggerHandler("peerconference", [this.conference]); + }; + WebRTC.prototype.processReceivedMessage = function(to, data, type, to2, from) { switch (type) { case "Offer": - console.log("Offer process."); - targetcall = this.findTargetCall(from); - if (targetcall) { - if (!this.settings.renegotiation && targetcall.peerconnection && targetcall.peerconnection.hasRemoteDescription()) { - // Call replace support without renegotiation. - this.doHangup("unsupported", from); - console.error("Processing new offers is not implemented without renegotiation."); - return; - } - // Hey we know this call. - targetcall.setRemoteDescription(new window.RTCSessionDescription(data), _.bind(function(sessionDescription, currentcall) { - if (currentcall === this.currentcall) { - // Main call. - this.e.triggerHandler("peercall", [this.currentcall]); - } - currentcall.createAnswer(_.bind(function(sessionDescription, currentcall) { - console.log("Sending answer", sessionDescription, currentcall.id); - this.api.sendAnswer(currentcall.id, sessionDescription); - }, this)); - }, this)); - } else { - // No target call. Check conference auto answer support. - if (this.currentconference && this.currentconference.id === data._conference) { - console.log("Received conference Offer -> auto.", from, data._conference); - // Clean own internal data before feeding into browser. - delete data._conference; - this.currentconference.autoAnswer(from, new window.RTCSessionDescription(data)); - break; - } - // Cannot do anything with this offer, reply with busy. - console.log("Received Offer from unknown id -> busy.", from); - this.api.sendBye(from, "busy"); - this.e.triggerHandler("busy", [from, to2, to]); - } + this._processOffer(to, data, type, to2, from); break; case "Candidate": - targetcall = this.findTargetCall(from); - if (!targetcall) { - console.warn("Received Candidate for unknown id -> ignore.", from); - return; - } - var candidate = new window.RTCIceCandidate({ - sdpMLineIndex: data.sdpMLineIndex, - sdpMid: data.sdpMid, - candidate: data.candidate - }); - targetcall.addIceCandidate(candidate); - //console.log("Got candidate", data.sdpMid, data.sdpMLineIndex, data.candidate); + this._processCandidate(to, data, type, to2, from); break; case "Answer": - targetcall = this.findTargetCall(from); - if (!targetcall) { - console.warn("Received Answer from unknown id -> ignore", from); - return; - } - console.log("Answer process."); - // TODO(longsleep): In case of negotiation this could switch offer and answer - // and result in a offer sdp sent as answer data. We need to handle this. - targetcall.setRemoteDescription(new window.RTCSessionDescription(data), function() { - // Received remote description as answer. - console.log("Received answer after we sent offer", data); - }); + this._processAnswer(to, data, type, to2, from); break; case "Bye": - targetcall = this.findTargetCall(from); - if (!targetcall) { - console.warn("Received Bye from unknown id -> ignore.", from); - return; - } - console.log("Bye process."); - if (this.currentconference) { - // Hand over current call to next conference call. - var newcurrentcall = this.currentconference.callClosed(targetcall); - targetcall.close() - if (newcurrentcall && newcurrentcall != this.currentcall) { - this.currentcall = newcurrentcall; - this.e.triggerHandler("peercall", [newcurrentcall]); - } else if (!newcurrentcall) { - this.doHangup("receivedbye", targetcall.id); - } - if (this.currentconference && !this.currentconference.checkEmpty()) { - this.e.triggerHandler("peerconference", [this.currentconference]); - } - } else { - this.doHangup("receivedbye", targetcall.id); - } - this.e.triggerHandler("bye", [data.Reason, from, to, to2]); + this._processBye(to, data, type, to2, from); break; case "Conference": - if ((!this.currentcall || data.indexOf(this.currentcall.id) === -1) && !this.isConferenceRoom()) { - console.warn("Received Conference for unknown call -> ignore.", to, data); - return; - } else { - var currentconference = this.currentconference; - if (!currentconference) { - currentconference = this.currentconference = new PeerConference(this, this.currentcall, to); - currentconference.e.one("finished", _.bind(function() { - this.currentconference = null; - this.e.triggerHandler("peerconference", [null]); - }, this)); - } else { - if (currentconference.id !== to) { - console.warn("Received Conference for wrong id -> ignore.", to, currentconference); - return; - } - } - currentconference.applyUpdate(data); - this.e.triggerHandler("peerconference", [currentconference]); - } + this._processConference(to, data, type, to2, from); break; default: console.log("Unhandled message type", type, data); + break; } - }; WebRTC.prototype.testMediaAccess = function(cb) { @@ -419,50 +420,46 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u var success = function(stream) { console.info("testMediaAccess success"); cb(true); - } + }; var failed = function() { console.info("testMediaAccess failed"); cb(false); - } + }; UserMedia.testGetUserMedia(success, failed); }; WebRTC.prototype.createCall = function(id, from, to) { - - var currentcall = new PeerCall(this, id, from, to); - currentcall.e.on("connectionStateChange", _.bind(function(event, iceConnectionState, currentcall) { + var call = new PeerCall(this, id, from, to); + call.e.on("connectionStateChange", _.bind(function(event, iceConnectionState, currentcall) { this.onConnectionStateChange(iceConnectionState, currentcall); }, this)); - currentcall.e.on("remoteStreamAdded", _.bind(function(event, stream, currentcall) { + call.e.on("remoteStreamAdded", _.bind(function(event, stream, currentcall) { this.onRemoteStreamAdded(stream, currentcall); }, this)); - currentcall.e.on("remoteStreamRemoved", _.bind(function(event, stream, currentcall) { + call.e.on("remoteStreamRemoved", _.bind(function(event, stream, currentcall) { this.onRemoteStreamRemoved(stream, currentcall); }, this)); - currentcall.e.on("error", _.bind(function(event, id, message) { - if (!id) { - id = "failed_peerconnection"; + call.e.on("error", _.bind(function(event, error_id, message) { + if (!error_id) { + error_id = "failed_peerconnection"; } - this.e.triggerHandler("error", [message, id]); - _.defer(_.bind(this.doHangup, this), "error", currentcall.id); // Hangup on error is good yes?? + this.e.triggerHandler("error", [message, error_id]); + _.defer(_.bind(this.doHangup, this), "error", id); // Hangup on error is good yes?? }, this)); - - return currentcall; - + call.e.on("closed", _.bind(function() { + this.conference.removeCall(id); + }, this)); + return call; }; - WebRTC.prototype.doUserMediaWithInternalCall = function() { - if (this.currentcall && !this.currentcall.isinternal) { - console.warn("Already have a current call, not doing internal getUM", this.currentcall); - return; - } - var currentcall = this.currentcall = new InternalCall(this); - this.e.triggerHandler("peercall", [currentcall]); - this.doUserMedia(currentcall); - }; + WebRTC.prototype.doUserMedia = function(call) { - WebRTC.prototype.doUserMedia = function(currentcall) { + if (this.usermedia) { + // We should not create a new UserMedia object while the current one + // is still being used. + console.error("UserMedia already created, check caller"); + } // Create default media (audio/video). var usermedia = new UserMedia({ @@ -472,8 +469,12 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u }); usermedia.e.on("mediasuccess mediaerror", _.bind(function(event, um) { this.e.triggerHandler("usermedia", [um]); + this.usermediaReady = true; // Start always, no matter what. - this.maybeStart(um); + while (this.pendingMediaCalls.length > 0) { + var c = this.pendingMediaCalls.shift(); + this.maybeStart(um, c); + } }, this)); usermedia.e.on("mediachanged", _.bind(function(event, um) { // Propagate media change events. @@ -482,7 +483,9 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u usermedia.e.on("stopped", _.bind(function(event, um) { if (um === this.usermedia) { this.e.triggerHandler("usermedia", [null]); + this.usermediaReady = false; this.usermedia = null; + this.maybeStartLocalVideo(); } }, this)); this.e.one("stop", function() { @@ -490,55 +493,99 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u }); this.usermedia = usermedia; this.e.triggerHandler("usermedia", [usermedia]); + this.pendingMediaCalls.push(call); - return usermedia.doGetUserMedia(currentcall); + return usermedia.doGetUserMedia(call); }; - WebRTC.prototype.doCall = function(id) { - - if (this.currentcall) { - // Conference mode. - var currentconference = this.currentconference; - if (!currentconference) { - currentconference = this.currentconference = new PeerConference(this, this.currentcall); - currentconference.e.one("finished", _.bind(function() { - this.currentconference = null; - this.e.triggerHandler("peerconference", [null]); - }, this)); - } - currentconference.doCall(id); - this.e.triggerHandler("peerconference", [currentconference]); - } else { - var currentcall = this.currentcall = this.createCall(id, null, id); - this.e.triggerHandler("peercall", [currentcall]); - var ok = this.doUserMedia(currentcall); - if (ok) { - this.e.triggerHandler("waitforusermedia", [currentcall]); + WebRTC.prototype._doCallUserMedia = function(call) { + if (this.usermedia) { + if (!this.usermediaReady) { + this.pendingMediaCalls.push(call); } else { - this.e.triggerHandler("error", ["Failed to access camera/microphone.", "failed_getusermedia"]); - return this.doHangup(); + this.maybeStart(this.usermedia, call); } - this.initiator = true; + return true; + } + + var ok = this.doUserMedia(call); + if (ok) { + this.e.triggerHandler("waitforusermedia", [call]); + return true; } + + this.e.triggerHandler("error", ["Failed to access camera/microphone.", "failed_getusermedia"]); + if (call.id) { + this.doHangup("usermedia", call.id); + } + return false; }; - WebRTC.prototype.doAccept = function() { + WebRTC.prototype._doAutoStartCall = function(call) { + if (!this.usermedia) { + return false; + } - //NOTE(longsleep): currentcall was created as early as possible to be able to process incoming candidates. - var currentcall = this.currentcall; - if (!currentcall) { - console.warn("Trying to accept without a call.", currentcall); + if (!this.usermediaReady) { + // getUserMedia is still pending, defer starting of call. + this.pendingMediaCalls.push(call); + } else { + this.maybeStart(this.usermedia, call, true); + } + return true; + }; + + WebRTC.prototype.doCall = function(id, autocall) { + var call = this.createCall(id, null, id); + call.setInitiate(true); + var count = this.conference.getCallsCount(); + if (!this.conference.addOutgoing(id, call)) { + console.log("Already has a call with", id); return; } - var ok = this.doUserMedia(currentcall); - if (ok) { - this.e.triggerHandler("waitforusermedia", [currentcall]); - } else { - this.e.triggerHandler("error", ["Failed to access camera/microphone.", "failed_getusermedia"]); - return this.doHangup(); + this.e.triggerHandler("peercall", [call]); + if (!autocall) { + this.e.triggerHandler("connecting", [call]); + } + if ((autocall && count > 0) || this.isConferenceRoom()) { + call.e.on("sessiondescription", _.bind(function(event, sessionDescription) { + var cid = this.conference.getOrCreateId(); + console.log("Injected conference id into sessionDescription", cid); + sessionDescription._conference = cid; + }, this)); + } + if (count > 0) { + if (count === 1) { + // Notify UI that it's a conference now. + this.e.triggerHandler("peerconference", [this.conference]); + } + if (this._doAutoStartCall(call)) { + return; + } } + if (!this._doCallUserMedia(call)) { + return; + } + }; + + WebRTC.prototype.doAccept = function(call, autoanswer) { + if (typeof call === "string") { + var id = call; + call = this.conference.getCall(id); + if (!call) { + console.warn("Trying to accept unknown call.", id); + return false; + } + } + + this.conference.setCallActive(call.id); + if (autoanswer && this._doAutoStartCall(call)) { + return true; + } + + return this._doCallUserMedia(call); }; WebRTC.prototype.doXfer = function(id, token, options) { @@ -670,99 +717,94 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u WebRTC.prototype.stop = function() { - if (this.currentconference) { - this.currentconference.close(); - this.currentconference = null; - } - if (this.currentcall) { - this.currentcall.close(); - this.currentcall = null; - } + this.conference.close(); this.e.triggerHandler("peerconference", [null]); this.e.triggerHandler("peercall", [null]); this.e.triggerHandler("stop"); - this.msgQueue.length = 0; - this.initiator = null; - this.started = false; + this.msgQueues = {}; } WebRTC.prototype.doHangup = function(reason, id) { + if (!id) { + console.log("Closing all calls") + _.each(this.conference.getCallIds(), _.bind(function(callid) { + this.doHangup(reason, callid); + }, this)); + this.stop(); + return true; + } + console.log("Hanging up.", id); - if (id) { - var currentcall = this.findTargetCall(id); - if (!currentcall) { - console.warn("Tried to hangup unknown call.", reason, id); - return; - } - if (currentcall !== this.currentcall) { - currentcall.close(); - if (reason !== "receivedbye") { - this.api.sendBye(id, reason); - } - _.defer(_.bind(function() { - if (this.currentcall && currentcall) { - this.e.triggerHandler("statechange", ["connected", this.currentcall]); - } else { - this.e.triggerHandler("done", [reason]); - } - }, this)); - return; - } + var call = this.conference.removeCall(id); + if (!call) { + console.warn("Tried to hangup unknown call.", reason, id); + return false; + } + call.close(); + if (reason !== "receivedbye") { + this.api.sendBye(id, reason); } - if (this.currentcall) { - id = this.currentcall.id; + var calls = this.conference.getCalls(); + if (!calls.length) { + // Last peer disconnected, perform cleanup. + this.e.triggerHandler("peercall", [null]); _.defer(_.bind(function() { this.e.triggerHandler("done", [reason]); }, this)); + this.stop(); + } else if (calls.length === 1) { + this.e.triggerHandler("peerconference", [null]); + this.e.triggerHandler("peercall", [calls[0]]); } - this.stop(); - if (id) { - if (reason !== "receivedbye") { - this.api.sendBye(id, reason); - } - } - + return true; } - WebRTC.prototype.maybeStart = function(usermedia) { - - //console.log("maybeStart", this.started); - if (!this.started) { - - var currentcall = this.currentcall; - currentcall.setInitiate(this.initiator); - this.e.triggerHandler("connecting", [currentcall]); - console.log('Creating PeerConnection.', currentcall); - currentcall.createPeerConnection(_.bind(function(peerconnection) { - // Success call. - usermedia.addToPeerConnection(peerconnection); - this.started = true; - if (!this.initiator) { - this.calleeStart(); - } - currentcall.e.on("negotiationNeeded", _.bind(function(event, currentcall) { - this.sendOfferWhenNegotiationNeeded(currentcall); - }, this)); - }, this), _.bind(function() { - // Error call. - this.e.triggerHandler("error", ["Failed to create peer connection. See log for details."]); - this.doHangup(); - }, this)); + WebRTC.prototype.maybeStart = function(usermedia, call, autocall) { + //console.log("maybeStart", call); + if (call.peerconnection) { + console.log("Already started", call); + return; } - }; - - WebRTC.prototype.calleeStart = function() { - - var args; - while (this.msgQueue.length > 0) { - args = this.msgQueue.shift(); - this.processReceivedMessage.apply(this, args); + if (!autocall) { + if (!call.isinternal) { + this.e.triggerHandler("connecting", [call]); + } else if (!this.conference.hasCalls()) { + // Signal UI that media access has been granted. + this.e.triggerHandler("done"); + } } + console.log('Creating PeerConnection.', call); + call.createPeerConnection(_.bind(function(peerconnection) { + // Success call. + usermedia.addToPeerConnection(peerconnection); + if (!call.initiate) { + this.processPendingMessages(call.id); + } + call.e.on("negotiationNeeded", _.bind(function(event, call) { + this.sendOfferWhenNegotiationNeeded(call); + }, this)); + }, this), _.bind(function() { + // Error call. + this.e.triggerHandler("error", ["Failed to create peer connection. See log for details."]); + if (call.id) { + this.doHangup("failed", call.id); + } + }, this)); + + }; + WebRTC.prototype.processPendingMessages = function(id) { + do { + var message = this.popFrontMessage(id); + if (!message) { + break; + } + this.processReceivedMessage.apply(this, message); + } while (true); }; WebRTC.prototype.sendOfferWhenNegotiationNeeded = function(currentcall, to) {