Browse Source

Major refactoring of call / conference handling.

Removed difference between single peer-to-peer calls and conferences
with multiple peers. There is only a single code path now that creates
calls and stores them in a conference (which holds all active calls).
With this also fixed some timing issues that could cause conference
peers to not send or receive media streams.

Should help with some of the issues reported in #276.
pull/293/head
Joachim Bauch 9 years ago
parent
commit
d4f936d57b
  1. 10
      static/js/controllers/statusmessagecontroller.js
  2. 30
      static/js/controllers/uicontroller.js
  3. 7
      static/js/directives/buddylist.js
  4. 33
      static/js/mediastream/peercall.js
  5. 250
      static/js/mediastream/peerconference.js
  6. 6
      static/js/mediastream/peerconnection.js
  7. 622
      static/js/mediastream/webrtc.js

10
static/js/controllers/statusmessagecontroller.js

@ -25,8 +25,8 @@ define([], function() { @@ -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() { @@ -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);
}
}];

30
static/js/controllers/uicontroller.js

@ -206,10 +206,11 @@ define(['jquery', 'underscore', 'bigscreen', 'moment', 'sjcl', 'modernizr', 'web @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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);

7
static/js/directives/buddylist.js

@ -56,6 +56,13 @@ define(['underscore', 'text!partials/buddylist.html'], function(_, template) { @@ -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();

33
static/js/mediastream/peercall.js

@ -38,6 +38,7 @@ define(['jquery', 'underscore', 'mediastream/utils', 'mediastream/peerconnection @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) {

250
static/js/mediastream/peerconference.js

@ -22,180 +22,111 @@ @@ -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);
};
PeerConference.prototype.checkEmpty = function() {
if (!_.isEmpty(this.calls) || (this.currentcall && this.currentcall.id)) {
return false;
}
console.log("Conference is now empty -> cleaning up.");
this.e.triggerHandler("finished");
return true;
// 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.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];
PeerConference.prototype.getOrCreateId = function() {
if (!this.id) {
this.id = this._createConferenceId();
console.log("Created new conference id", this.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;
return this.id;
};
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;
}
PeerConference.prototype.hasCalls = function() {
return this.callsCount > 0;
};
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 @@ -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 @@ -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 @@ -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 @@ -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);
}
}
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);
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);
}
}, 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;

6
static/js/mediastream/peerconnection.js

@ -33,6 +33,7 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) { @@ -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($, _) { @@ -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($, _) { @@ -318,7 +323,6 @@ define(['jquery', 'underscore', 'webrtc.adapter'], function($, _) {
};
PeerConnection.prototype.createAnswer = function() {
return this.pc.createAnswer.apply(this.pc, arguments);
};

622
static/js/mediastream/webrtc.js

@ -56,6 +56,9 @@ function($, _, PeerCall, PeerConference, PeerXfer, PeerScreenshare, UserMedia, u @@ -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 @@ -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 @@ -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 @@ -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);
}
};
WebRTC.prototype.findTargetCall = function(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);
}
return targetcall;
return this.conference.getCall(id);
};
WebRTC.prototype.callForEachCall = function(fn) {
var count = 0;
if (this.currentcall) {
fn(this.currentcall, count);
count++;
if (this.currentconference) {
_.each(this.currentconference.calls, function(v, count) {
fn(v);
count++;
});
var calls = this.conference.getCalls();
if (!calls.length) {
return 0;
}
_.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 count;
return queue;
};
WebRTC.prototype.pushBackMessage = function(id, message) {
this._getMessageQueue(id, true).push(message);
};
WebRTC.prototype.processReceivedMessage = function(to, data, type, to2, from) {
WebRTC.prototype.pushFrontMessage = function(id, message) {
this._getMessageQueue(id, true).unshift(message);
};
if (!this.started && type !== "Conference") {
console.log('PeerConnection has not been created yet!');
return;
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;
};
var targetcall;
switch (type) {
case "Offer":
WebRTC.prototype._processOffer = function(to, data, type, to2, from) {
console.log("Offer process.");
targetcall = this.findTargetCall(from);
if (targetcall) {
if (!this.settings.renegotiation && targetcall.peerconnection && targetcall.peerconnection.hasRemoteDescription()) {
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;
}
// 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]);
}
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));
} else {
// No target call. Check conference auto answer support.
if (this.currentconference && this.currentconference.id === data._conference) {
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;
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);
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;
}
break;
case "Candidate":
targetcall = this.findTargetCall(from);
if (!targetcall) {
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;
}
// Delegate next steps to UI.
this.e.triggerHandler("offer", [from, to2, to]);
};
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
});
targetcall.addIceCandidate(candidate);
call.addIceCandidate(candidate);
//console.log("Got candidate", data.sdpMid, data.sdpMLineIndex, data.candidate);
break;
case "Answer":
targetcall = this.findTargetCall(from);
if (!targetcall) {
};
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;
}
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.
targetcall.setRemoteDescription(new window.RTCSessionDescription(data), function() {
call.setRemoteDescription(new window.RTCSessionDescription(data), function() {
// Received remote description as answer.
console.log("Received answer after we sent offer", data);
});
break;
case "Bye":
targetcall = this.findTargetCall(from);
if (!targetcall) {
console.warn("Received Bye from unknown id -> ignore.", from);
return;
}
};
WebRTC.prototype._processBye = function(to, data, type, to2, from) {
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.doHangup("receivedbye", from);
// Delegate bye to UI.
this.e.triggerHandler("bye", [data.Reason, from, to, to2]);
break;
case "Conference":
if ((!this.currentcall || data.indexOf(this.currentcall.id) === -1) && !this.isConferenceRoom()) {
};
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 {
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);
} 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;
}
currentconference.applyUpdate(data);
this.e.triggerHandler("peerconference", [currentconference]);
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":
this._processOffer(to, data, type, to2, from);
break;
case "Candidate":
this._processCandidate(to, data, type, to2, from);
break;
case "Answer":
this._processAnswer(to, data, type, to2, from);
break;
case "Bye":
this._processBye(to, data, type, to2, from);
break;
case "Conference":
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 @@ -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 @@ -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 @@ -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 @@ -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]);
WebRTC.prototype._doCallUserMedia = function(call) {
if (this.usermedia) {
if (!this.usermediaReady) {
this.pendingMediaCalls.push(call);
} else {
var currentcall = this.currentcall = this.createCall(id, null, id);
this.e.triggerHandler("peercall", [currentcall]);
var ok = this.doUserMedia(currentcall);
this.maybeStart(this.usermedia, call);
}
return true;
}
var ok = this.doUserMedia(call);
if (ok) {
this.e.triggerHandler("waitforusermedia", [currentcall]);
} else {
this.e.triggerHandler("waitforusermedia", [call]);
return true;
}
this.e.triggerHandler("error", ["Failed to access camera/microphone.", "failed_getusermedia"]);
return this.doHangup();
if (call.id) {
this.doHangup("usermedia", call.id);
}
return false;
};
WebRTC.prototype._doAutoStartCall = function(call) {
if (!this.usermedia) {
return false;
}
this.initiator = true;
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.doAccept = function() {
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;
}
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;
}
}
//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._doCallUserMedia(call)) {
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();
};
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 @@ -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) {
var call = this.conference.removeCall(id);
if (!call) {
console.warn("Tried to hangup unknown call.", reason, id);
return;
return false;
}
if (currentcall !== this.currentcall) {
currentcall.close();
call.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;
}
}
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();
if (id) {
if (reason !== "receivedbye") {
this.api.sendBye(id, reason);
}
} else if (calls.length === 1) {
this.e.triggerHandler("peerconference", [null]);
this.e.triggerHandler("peercall", [calls[0]]);
}
return true;
}
WebRTC.prototype.maybeStart = function(usermedia) {
WebRTC.prototype.maybeStart = function(usermedia, call, autocall) {
//console.log("maybeStart", this.started);
if (!this.started) {
//console.log("maybeStart", call);
if (call.peerconnection) {
console.log("Already started", call);
return;
}
var currentcall = this.currentcall;
currentcall.setInitiate(this.initiator);
this.e.triggerHandler("connecting", [currentcall]);
console.log('Creating PeerConnection.', currentcall);
currentcall.createPeerConnection(_.bind(function(peerconnection) {
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);
this.started = true;
if (!this.initiator) {
this.calleeStart();
if (!call.initiate) {
this.processPendingMessages(call.id);
}
currentcall.e.on("negotiationNeeded", _.bind(function(event, currentcall) {
this.sendOfferWhenNegotiationNeeded(currentcall);
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."]);
this.doHangup();
}, this));
if (call.id) {
this.doHangup("failed", call.id);
}
}, this));
};
WebRTC.prototype.calleeStart = function() {
var args;
while (this.msgQueue.length > 0) {
args = this.msgQueue.shift();
this.processReceivedMessage.apply(this, args);
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) {

Loading…
Cancel
Save