|
@@ -0,0 +1,319 @@
|
|
|
+/**
|
|
|
+ * Manages all negotiations between Peers.
|
|
|
+ */
|
|
|
+// TODO: LOCKS.
|
|
|
+// TODO: FIREFOX new PC after offer made for DC.
|
|
|
+var Negotiator = {
|
|
|
+ pcs: {}, // pc id => pc.
|
|
|
+ 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(type, peer, connection, provider, options) {
|
|
|
+ Negotiator._addProvider(peer, provider);
|
|
|
+
|
|
|
+ var pc;
|
|
|
+ // options.pc is the PC's ID.
|
|
|
+ pc = Negotiator.pcs[options.pc]
|
|
|
+ if (!pc || pc.signalingState !== 'stable') {
|
|
|
+ pc = Negotiator._startPeerConnection(peer, provider);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (type === 'media' && options._stream) {
|
|
|
+ // Add the stream.
|
|
|
+ pc.addStream(options._stream);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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 {
|
|
|
+ Negotiator._handleSDP(peer, connection, options);
|
|
|
+ }
|
|
|
+
|
|
|
+ return pc;
|
|
|
+}
|
|
|
+
|
|
|
+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(peer, provider) {
|
|
|
+ util.log('Creating RTCPeerConnection.');
|
|
|
+
|
|
|
+ var id = Negotiator._idPrefix + util.randomToken();
|
|
|
+ pc = new RTCPeerConnection(provider.options.config, {optional: [{RtpDataChannels: true}]});
|
|
|
+ Negotiator.pcs[id] = pc;
|
|
|
+
|
|
|
+ Negotiator._startListeners(peer, provider, pc, id);
|
|
|
+
|
|
|
+ return pc;
|
|
|
+}
|
|
|
+
|
|
|
+/** Set up various WebRTC listeners. */
|
|
|
+Negotiator._setupListeners = function(peer, provider, pc, id) {
|
|
|
+ // ICE CANDIDATES.
|
|
|
+ util.log('Listening for ICE candidates.');
|
|
|
+ pc.onicecandidate = function(evt) {
|
|
|
+ if (evt.candidate) {
|
|
|
+ util.log('Received ICE candidates.');
|
|
|
+ provider.socket.send({
|
|
|
+ type: 'CANDIDATE',
|
|
|
+ payload: {
|
|
|
+ candidate: evt.candidate,
|
|
|
+ pc: id // Send along this PC's ID.
|
|
|
+ },
|
|
|
+ dst: peer,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ 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.
|
|
|
+ pc.onicechange = pc.oniceconnectionstatechange;
|
|
|
+
|
|
|
+ // ONNEGOTIATIONNEEDED (Chrome)
|
|
|
+ util.log('Listening for `negotiationneeded`');
|
|
|
+ pc.onnegotiationneeded = function() {
|
|
|
+ util.log('`negotiationneeded` triggered');
|
|
|
+ Negotiator._makeOffer();
|
|
|
+ };
|
|
|
+
|
|
|
+ // DATACONNECTION.
|
|
|
+ util.log('Listening for data channel');
|
|
|
+ // 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;
|
|
|
+ connection = provider.getConnection(peer, connection);
|
|
|
+ connection.initialize(dc);
|
|
|
+ Negotiator._attachConnectionListeners(dc);
|
|
|
+ };
|
|
|
+
|
|
|
+ // MEDIACONNECTION.
|
|
|
+ util.log('Listening for remote stream');
|
|
|
+ pc.onaddstream = function(evt) {
|
|
|
+ util.log('Received remote stream');
|
|
|
+ var stream = evt.stream;
|
|
|
+ provider.getConnection(peer, id).receiveStream(stream);
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+Negotiator._cleanup = function() {
|
|
|
+ // TODO
|
|
|
+}
|
|
|
+
|
|
|
+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;
|
|
|
+
|
|
|
+ if (util.browserisms === 'Webkit') {
|
|
|
+ //offer.sdp = Reliable.higherBandwidthSDP(offer.sdp);
|
|
|
+ }
|
|
|
+
|
|
|
+ self.pc.setLocalDescription(offer, function() {
|
|
|
+ util.log('Set localDescription to offer');
|
|
|
+ self._socket.send({
|
|
|
+ type: 'OFFER',
|
|
|
+ payload: {
|
|
|
+ sdp: offer,
|
|
|
+ config: self._options.config,
|
|
|
+ labels: self.labels,
|
|
|
+ call: !!self._call
|
|
|
+ },
|
|
|
+ dst: self.peer,
|
|
|
+ manager: self._managerId
|
|
|
+ });
|
|
|
+ // We can now reset labels because all info has been communicated.
|
|
|
+ self.labels = {};
|
|
|
+ }, function(err) {
|
|
|
+ self.emit('error', err);
|
|
|
+ util.log('Failed to setLocalDescription, ', err);
|
|
|
+ });
|
|
|
+ }, function(err) {
|
|
|
+ self.emit('error', err);
|
|
|
+ util.log('Failed to createOffer, ', err);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+Negotiator._makeAnswer = function() {
|
|
|
+ // TODO
|
|
|
+}
|
|
|
+
|
|
|
+/** Create an answer for PC. */
|
|
|
+ConnectionManager.prototype._makeAnswer = function() {
|
|
|
+ var self = this;
|
|
|
+ this.pc.createAnswer(function(answer) {
|
|
|
+ util.log('Created answer.');
|
|
|
+
|
|
|
+ if (util.browserisms === 'Webkit') {
|
|
|
+ //answer.sdp = Reliable.higherBandwidthSDP(answer.sdp);
|
|
|
+ }
|
|
|
+
|
|
|
+ self.pc.setLocalDescription(answer, function() {
|
|
|
+ util.log('Set localDescription to answer.');
|
|
|
+ self._socket.send({
|
|
|
+ type: 'ANSWER',
|
|
|
+ payload: {
|
|
|
+ sdp: answer
|
|
|
+ },
|
|
|
+ dst: self.peer,
|
|
|
+ manager: self._managerId
|
|
|
+ });
|
|
|
+ }, function(err) {
|
|
|
+ self.emit('error', err);
|
|
|
+ util.log('Failed to setLocalDescription, ', err);
|
|
|
+ });
|
|
|
+ }, function(err) {
|
|
|
+ self.emit('error', err);
|
|
|
+ util.log('Failed to create answer, ', err);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** Clean up PC, close related DCs. */
|
|
|
+ConnectionManager.prototype._cleanup = function() {
|
|
|
+ util.log('Cleanup ConnectionManager for ' + this.peer);
|
|
|
+ if (!!this.pc && (this.pc.readyState !== 'closed' || this.pc.signalingState !== 'closed')) {
|
|
|
+ this.pc.close();
|
|
|
+ this.pc = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ var self = this;
|
|
|
+ this._socket.send({
|
|
|
+ type: 'LEAVE',
|
|
|
+ dst: self.peer
|
|
|
+ });
|
|
|
+
|
|
|
+ this.destroyed = true;
|
|
|
+ this.emit('close');
|
|
|
+}
|
|
|
+
|
|
|
+/** Attach connection listeners. */
|
|
|
+ConnectionManager.prototype._attachConnectionListeners = function(connection) {
|
|
|
+ var self = this;
|
|
|
+ connection.on('close', function() {
|
|
|
+ if (!!self.connections[connection.label]) {
|
|
|
+ delete self.connections[connection.label];
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!Object.keys(self.connections).length) {
|
|
|
+ self._cleanup();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ connection.on('open', function() {
|
|
|
+ self._lock = false;
|
|
|
+ self._processQueue();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/** Handle an SDP. */
|
|
|
+ConnectionManager.prototype.handleSDP = function(sdp, type, call) {
|
|
|
+ sdp = new RTCSessionDescription(sdp);
|
|
|
+
|
|
|
+ var self = this;
|
|
|
+ this.pc.setRemoteDescription(sdp, function() {
|
|
|
+ util.log('Set remoteDescription: ' + type);
|
|
|
+ if (type === 'OFFER') {
|
|
|
+ if (call && !self._call) {
|
|
|
+ self._call = new MediaConnection(self.peer);
|
|
|
+ self._call.on('answer', function(stream){
|
|
|
+ if (stream) {
|
|
|
+ self.pc.addStream(stream);
|
|
|
+ }
|
|
|
+ self._makeAnswer();
|
|
|
+ util.setZeroTimeout(function(){
|
|
|
+ // Add remote streams
|
|
|
+ self._call.receiveStream(self.pc.getRemoteStreams()[0]);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ self.emit('call', self._call);
|
|
|
+ } else {
|
|
|
+ self._makeAnswer();
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Got answer from remote
|
|
|
+ self._lock = false;
|
|
|
+ }
|
|
|
+ }, function(err) {
|
|
|
+ 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) {
|
|
|
+ var labels = Object.keys(updates);
|
|
|
+ for (var i = 0, ii = labels.length; i < ii; i += 1) {
|
|
|
+ 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() {
|
|
|
+ if (this.destroyed) {
|
|
|
+ this.emit('error', new Error('Connections to ' + this.peer + 'are already closed.'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var labels = Object.keys(this.connections);
|
|
|
+ for (var i = 0, ii = labels.length; i < ii; i += 1) {
|
|
|
+ var label = labels[i];
|
|
|
+ var connection = this.connections[label];
|
|
|
+ connection.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ // TODO: close the call.
|
|
|
+ this.connections = null;
|
|
|
+ this._cleanup();
|
|
|
+}
|