|
@@ -1,172 +1,145 @@
|
|
|
/**
|
|
|
* Manages all negotiations between Peers.
|
|
|
*/
|
|
|
+// TODO: LOCKS.
|
|
|
+// TODO: FIREFOX new PC after offer made for DC.
|
|
|
var Negotiator = {
|
|
|
pcs: {}, // pc id => pc.
|
|
|
- providers: {} // peer id => provider.
|
|
|
-};
|
|
|
+ providers: {}, // provider's id => providers (there may be multiple providers/client.
|
|
|
+ options: {},
|
|
|
+ queue: [] // connections that are delayed due to a PC being in use.
|
|
|
+}
|
|
|
|
|
|
Negotiator._idPrefix = 'pc_'
|
|
|
|
|
|
-Negotiator.startConnection = function(peer, connection, provider, options) {
|
|
|
- // TODO
|
|
|
- if (!Negotiator.providers[peer]) {
|
|
|
- Negotiator.providers[peer] = provider;
|
|
|
- }
|
|
|
+Negotiator.startConnection = function(type, peer, connection, provider, options) {
|
|
|
+ Negotiator._addProvider(peer, provider);
|
|
|
|
|
|
var pc;
|
|
|
- if (!options || !options._pc) {
|
|
|
- Negotiator._startPeerConnection(provider);
|
|
|
+ // options.pc is the PC's ID.
|
|
|
+ pc = Negotiator.pcs[options.pc]
|
|
|
+ if (!pc || pc.signalingState !== 'stable') {
|
|
|
+ pc = Negotiator._startPeerConnection(peer, provider);
|
|
|
}
|
|
|
|
|
|
- if (options) {
|
|
|
- pc =
|
|
|
- if (options._stream) {
|
|
|
- if (options.sdp) { // Is a MC receiver.
|
|
|
- Negotiator.handleSDP(peer, connection, options);
|
|
|
- } else { // Is a MC originator.
|
|
|
+ if (type === 'media' && options._stream) {
|
|
|
+ // Add the stream.
|
|
|
+ pc.addStream(options._stream);
|
|
|
+ }
|
|
|
|
|
|
- }
|
|
|
- } else { // Is a DC receiver.
|
|
|
+ // What do we need to do now?
|
|
|
+ if (options.originator) {
|
|
|
+ if (type === 'data') {
|
|
|
+ // Create the datachannel.
|
|
|
+ dc = pc.createDataChannel(options.label, {reliable: reliable});
|
|
|
+ connection = provider.getConnection(peer, connection);
|
|
|
+ connection.initialize(dc);
|
|
|
+ Negotiator._attachConnectionListeners(dc);
|
|
|
+ }
|
|
|
|
|
|
+ if (!util.supports.onnegotiationneeded) {
|
|
|
+ Negotiator._makeOffer(peer, connection, options);
|
|
|
}
|
|
|
- } else { // Is a DC originator.
|
|
|
-
|
|
|
+ } else {
|
|
|
+ Negotiator._handleSDP(peer, connection, options);
|
|
|
}
|
|
|
|
|
|
+ return pc;
|
|
|
+}
|
|
|
|
|
|
- // Return the PeerConnection.
|
|
|
-
|
|
|
- // Set up PeerConnection.
|
|
|
- this._startPeerConnection();
|
|
|
-
|
|
|
- // Process queued DCs.
|
|
|
- this._processQueue();
|
|
|
-
|
|
|
- // Listen for ICE candidates.
|
|
|
- this._setupIce();
|
|
|
-
|
|
|
- // Listen for negotiation needed.
|
|
|
- // Chrome only **
|
|
|
- this._setupNegotiationHandler();
|
|
|
-
|
|
|
- // Listen for data channel.
|
|
|
- this._setupDataChannel();
|
|
|
-
|
|
|
- // Listen for remote streams.
|
|
|
- this._setupStreamListener();
|
|
|
-
|
|
|
+Negotiator._addProvider = function(peer, provider) {
|
|
|
+ if ((!provider.id && !provider.disconnected) || !provider.socket.open) {
|
|
|
+ // Wait for provider to obtain an ID.
|
|
|
+ provider.on('open', function(id) {
|
|
|
+ Negotiator._addProvider(peer, provider);
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ Negotiator.providers[provider.id] = provider;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
|
|
|
/** Start a PC. */
|
|
|
-Negotiator._startPeerConnection = function(provider) {
|
|
|
+Negotiator._startPeerConnection = function(peer, provider) {
|
|
|
util.log('Creating RTCPeerConnection.');
|
|
|
|
|
|
var id = Negotiator._idPrefix + util.randomToken();
|
|
|
- Negotiator.pcs[id] = new RTCPeerConnection(provider.options.config, { optional: [ { RtpDataChannels: true } ]});
|
|
|
-};
|
|
|
-
|
|
|
-/** Add DataChannels to all queued DataConnections. */
|
|
|
-ConnectionManager.prototype._processQueue = function() {
|
|
|
- for (var i = 0; i < this._queued.length; i++) {
|
|
|
- var conn = this._queued[i];
|
|
|
- if (conn.constructor == MediaConnection) {
|
|
|
- console.log('adding', conn.localStream);
|
|
|
- this.pc.addStream(conn.localStream);
|
|
|
- } else if (conn.constructor == DataConnection) {
|
|
|
- // reliable: true not yet implemented in Chrome
|
|
|
- var reliable = util.browserisms === 'Firefox' ? conn.reliable : false;
|
|
|
- conn.addDC(this.pc.createDataChannel(conn.label, { reliable: reliable }));
|
|
|
- }
|
|
|
- }
|
|
|
- // onnegotiationneeded not yet implemented in Firefox, must manually create offer
|
|
|
- if (util.browserisms === 'Firefox' && this._queued.length > 0) {
|
|
|
- this._makeOffer();
|
|
|
- }
|
|
|
- this._queued = [];
|
|
|
-};
|
|
|
+ pc = new RTCPeerConnection(provider.options.config, {optional: [{RtpDataChannels: true}]});
|
|
|
+ Negotiator.pcs[id] = pc;
|
|
|
+
|
|
|
+ Negotiator._startListeners(peer, provider, pc, id);
|
|
|
|
|
|
-/** Set up ICE candidate handlers. */
|
|
|
-ConnectionManager.prototype._setupIce = function() {
|
|
|
+ return pc;
|
|
|
+}
|
|
|
+
|
|
|
+/** Set up various WebRTC listeners. */
|
|
|
+Negotiator._setupListeners = function(peer, provider, pc, id) {
|
|
|
+ // ICE CANDIDATES.
|
|
|
util.log('Listening for ICE candidates.');
|
|
|
- var self = this;
|
|
|
- this.pc.onicecandidate = function(evt) {
|
|
|
+ pc.onicecandidate = function(evt) {
|
|
|
if (evt.candidate) {
|
|
|
util.log('Received ICE candidates.');
|
|
|
- self._socket.send({
|
|
|
+ provider.socket.send({
|
|
|
type: 'CANDIDATE',
|
|
|
payload: {
|
|
|
- candidate: evt.candidate
|
|
|
+ candidate: evt.candidate,
|
|
|
+ pc: id // Send along this PC's ID.
|
|
|
},
|
|
|
- dst: self.peer,
|
|
|
- manager: self._managerId
|
|
|
+ dst: peer,
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
- this.pc.oniceconnectionstatechange = function() {
|
|
|
- if (!!self.pc) {
|
|
|
- switch (self.pc.iceConnectionState) {
|
|
|
- case 'failed':
|
|
|
- util.log('iceConnectionState is disconnected, closing connections to ' + self.peer);
|
|
|
- self.close();
|
|
|
- break;
|
|
|
- case 'completed':
|
|
|
- self.pc.onicecandidate = null;
|
|
|
- break;
|
|
|
- }
|
|
|
+
|
|
|
+ pc.oniceconnectionstatechange = function() {
|
|
|
+ switch (pc.iceConnectionState) {
|
|
|
+ case 'failed':
|
|
|
+ util.log('iceConnectionState is disconnected, closing connections to ' + self.peer);
|
|
|
+ Negotiator._cleanup();
|
|
|
+ break;
|
|
|
+ case 'completed':
|
|
|
+ pc.onicecandidate = null;
|
|
|
+ break;
|
|
|
}
|
|
|
};
|
|
|
+
|
|
|
// Fallback for older Chrome impls.
|
|
|
- this.pc.onicechange = this.pc.oniceconnectionstatechange;
|
|
|
-};
|
|
|
+ pc.onicechange = pc.oniceconnectionstatechange;
|
|
|
|
|
|
-/** Set up onnegotiationneeded. */
|
|
|
-ConnectionManager.prototype._setupNegotiationHandler = function() {
|
|
|
- var self = this;
|
|
|
+ // ONNEGOTIATIONNEEDED (Chrome)
|
|
|
util.log('Listening for `negotiationneeded`');
|
|
|
- this.pc.onnegotiationneeded = function() {
|
|
|
+ pc.onnegotiationneeded = function() {
|
|
|
util.log('`negotiationneeded` triggered');
|
|
|
- self._makeOffer();
|
|
|
+ Negotiator._makeOffer();
|
|
|
};
|
|
|
-};
|
|
|
|
|
|
-/** Set up Data Channel listener. */
|
|
|
-ConnectionManager.prototype._setupDataChannel = function() {
|
|
|
- var self = this;
|
|
|
+ // DATACONNECTION.
|
|
|
util.log('Listening for data channel');
|
|
|
- this.pc.ondatachannel = function(evt) {
|
|
|
+ // Fired between offer and answer, so options should already be saved
|
|
|
+ // in the options hash.
|
|
|
+ pc.ondatachannel = function(evt) {
|
|
|
util.log('Received data channel');
|
|
|
var dc = evt.channel;
|
|
|
- var label = dc.label;
|
|
|
- // This should not be empty.
|
|
|
- var options = self.labels[label] || {};
|
|
|
- var connection = new DataConnection(self.peer, dc, options);
|
|
|
- self._attachConnectionListeners(connection);
|
|
|
- self.connections[label] = connection;
|
|
|
- self.emit('connection', connection);
|
|
|
+ connection = provider.getConnection(peer, connection);
|
|
|
+ connection.initialize(dc);
|
|
|
+ Negotiator._attachConnectionListeners(dc);
|
|
|
};
|
|
|
-};
|
|
|
|
|
|
-/** Set up remote stream listener. */
|
|
|
-ConnectionManager.prototype._setupStreamListener = function() {
|
|
|
- var self = this;
|
|
|
+ // MEDIACONNECTION.
|
|
|
util.log('Listening for remote stream');
|
|
|
- this.pc.onaddstream = function(evt) {
|
|
|
+ pc.onaddstream = function(evt) {
|
|
|
util.log('Received remote stream');
|
|
|
var stream = evt.stream;
|
|
|
- if (!!self._call) {
|
|
|
- self._call.receiveStream(stream);
|
|
|
-
|
|
|
- }
|
|
|
+ provider.getConnection(peer, id).receiveStream(stream);
|
|
|
};
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
+Negotiator._cleanup = function() {
|
|
|
+ // TODO
|
|
|
+}
|
|
|
|
|
|
-/** Send an offer. */
|
|
|
-ConnectionManager.prototype._makeOffer = function() {
|
|
|
- var self = this;
|
|
|
- this.pc.createOffer(function(offer) {
|
|
|
+Negotiator._makeOffer = function() {
|
|
|
+ // TODO
|
|
|
+ pc.createOffer(function(offer) {
|
|
|
util.log('Created offer.');
|
|
|
// Firefox currently does not support multiplexing once an offer is made.
|
|
|
self.firefoxSingular = true;
|
|
@@ -198,7 +171,11 @@ ConnectionManager.prototype._makeOffer = function() {
|
|
|
self.emit('error', err);
|
|
|
util.log('Failed to createOffer, ', err);
|
|
|
});
|
|
|
-};
|
|
|
+}
|
|
|
+
|
|
|
+Negotiator._makeAnswer = function() {
|
|
|
+ // TODO
|
|
|
+}
|
|
|
|
|
|
/** Create an answer for PC. */
|
|
|
ConnectionManager.prototype._makeAnswer = function() {
|
|
@@ -228,7 +205,7 @@ ConnectionManager.prototype._makeAnswer = function() {
|
|
|
self.emit('error', err);
|
|
|
util.log('Failed to create answer, ', err);
|
|
|
});
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Clean up PC, close related DCs. */
|
|
|
ConnectionManager.prototype._cleanup = function() {
|
|
@@ -246,7 +223,7 @@ ConnectionManager.prototype._cleanup = function() {
|
|
|
|
|
|
this.destroyed = true;
|
|
|
this.emit('close');
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Attach connection listeners. */
|
|
|
ConnectionManager.prototype._attachConnectionListeners = function(connection) {
|
|
@@ -264,7 +241,7 @@ ConnectionManager.prototype._attachConnectionListeners = function(connection) {
|
|
|
self._lock = false;
|
|
|
self._processQueue();
|
|
|
});
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Handle an SDP. */
|
|
|
ConnectionManager.prototype.handleSDP = function(sdp, type, call) {
|
|
@@ -298,14 +275,14 @@ ConnectionManager.prototype.handleSDP = function(sdp, type, call) {
|
|
|
self.emit('error', err);
|
|
|
util.log('Failed to setRemoteDescription, ', err);
|
|
|
});
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Handle a candidate. */
|
|
|
ConnectionManager.prototype.handleCandidate = function(message) {
|
|
|
var candidate = new RTCIceCandidate(message.candidate);
|
|
|
this.pc.addIceCandidate(candidate);
|
|
|
util.log('Added ICE candidate.');
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Updates label:[serialization, reliable, metadata] pairs from offer. */
|
|
|
ConnectionManager.prototype.handleUpdate = function(updates) {
|
|
@@ -314,13 +291,13 @@ ConnectionManager.prototype.handleUpdate = function(updates) {
|
|
|
var label = labels[i];
|
|
|
this.labels[label] = updates[label];
|
|
|
}
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Handle peer leaving. */
|
|
|
ConnectionManager.prototype.handleLeave = function() {
|
|
|
util.log('Peer ' + this.peer + ' disconnected.');
|
|
|
this.close();
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
/** Closes manager and all related connections. */
|
|
|
ConnectionManager.prototype.close = function() {
|
|
@@ -335,80 +312,8 @@ ConnectionManager.prototype.close = function() {
|
|
|
var connection = this.connections[label];
|
|
|
connection.close();
|
|
|
}
|
|
|
-
|
|
|
- // TODO: close the call
|
|
|
-
|
|
|
+
|
|
|
+ // TODO: close the call.
|
|
|
this.connections = null;
|
|
|
this._cleanup();
|
|
|
-};
|
|
|
-
|
|
|
-/** Create and returns a DataConnection with the peer with the given label. */
|
|
|
-ConnectionManager.prototype.connect = function(options) {
|
|
|
- if (this.destroyed) {
|
|
|
- return;
|
|
|
- }
|
|
|
-console.log('trying to connect');
|
|
|
- options = util.extend({
|
|
|
- label: 'peerjs',
|
|
|
- reliable: (util.browserisms === 'Firefox')
|
|
|
- }, options);
|
|
|
-
|
|
|
- // Check if label is taken...if so, generate a new label randomly.
|
|
|
- while (!!this.connections[options.label]) {
|
|
|
- options.label = 'peerjs' + this._default;
|
|
|
- this._default += 1;
|
|
|
- }
|
|
|
-
|
|
|
- this.labels[options.label] = options;
|
|
|
-
|
|
|
- var dc;
|
|
|
- if (!!this.pc && !this._lock) {
|
|
|
- var reliable = util.browserisms === 'Firefox' ? options.reliable : false;
|
|
|
- dc = this.pc.createDataChannel(options.label, { reliable: reliable });
|
|
|
- if (util.browserisms === 'Firefox') {
|
|
|
- this._makeOffer();
|
|
|
- }
|
|
|
- }
|
|
|
- var connection = new DataConnection(this.peer, dc, options);
|
|
|
- this._attachConnectionListeners(connection);
|
|
|
- this.connections[options.label] = connection;
|
|
|
-
|
|
|
- if (!this.pc || this._lock) {
|
|
|
- console.log('qing', this._lock);
|
|
|
- this._queued.push(connection);
|
|
|
- }
|
|
|
-
|
|
|
- this._lock = true
|
|
|
- return connection;
|
|
|
-};
|
|
|
-
|
|
|
-ConnectionManager.prototype.call = function(stream, options) {
|
|
|
- if (this.destroyed) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- options = util.extend({
|
|
|
-
|
|
|
- }, options);
|
|
|
-
|
|
|
- if (!!this.pc && !this._lock) {
|
|
|
- this.pc.addStream(stream);
|
|
|
- if (util.browserisms === 'Firefox') {
|
|
|
- this._makeOffer();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- var connection = new MediaConnection(this.peer, stream, options);
|
|
|
- this._call = connection;
|
|
|
-
|
|
|
- if (!this.pc || this._lock) {
|
|
|
- this._queued.push(connection);
|
|
|
- }
|
|
|
-
|
|
|
- this._lock = true;
|
|
|
-
|
|
|
-
|
|
|
- return connection;
|
|
|
-};
|
|
|
-
|
|
|
-
|
|
|
+}
|