/** * Manages all negotiations between Peers. */ // TODO: LOCKS. // TODO: FIREFOX new PC after offer made for DC. var Negotiator = { pcs: { data: {}, media: {} }, // type => {peerId: {pc_id: pc}}. //providers: {}, // provider's id => providers (there may be multiple providers/client. queue: [] // connections that are delayed due to a PC being in use. } Negotiator._idPrefix = 'pc_'; /** Returns a PeerConnection object set up correctly (for data, media). */ // Options preceeded with _ are ones we add artificially. Negotiator.startConnection = function(connection, options) { //Negotiator._addProvider(provider); var pc = Negotiator._getPeerConnection(connection, options); if (connection.type === 'media' && options._stream) { // Add the stream. pc.addStream(options._stream); } // Set the connection's PC. connection.pc = pc; // What do we need to do now? if (options.originator) { if (connection.type === 'data') { // Create the datachannel. var dc = pc.createDataChannel(connection.label, {reliable: options.reliable}); connection.initialize(dc); } if (!util.supports.onnegotiationneeded) { Negotiator._makeOffer(connection); } } else { Negotiator.handleSDP('OFFER', connection, options.sdp); } } Negotiator._getPeerConnection = function(connection, options) { if (!Negotiator.pcs[connection.type]) { util.error(connection.type + ' is not a valid connection type. Maybe you overrode the `type` property somewhere.'); } if (!Negotiator.pcs[connection.type][connection.peer]) { Negotiator.pcs[connection.type][connection.peer] = {}; } var peerConnections = Negotiator.pcs[connection.type][connection.peer]; var pc; if (options.multiplex) { // TODO: this doesn't work right now because we don't have PC ids. // Find an existing PC to use. ids = Object.keys(peerConnections); for (var i = 0, ii = ids.length; i < ii; i += 1) { pc = peerConnections[ids[i]]; if (pc.signalingState === 'stable') { break; // We can go ahead and use this PC. } } } else if (options.pc) { // Simplest case: PC id already provided for us. pc = Negotiator.pcs[connection.type][connection.peer][options.pc]; } if (!pc || pc.signalingState !== 'stable') { pc = Negotiator._startPeerConnection(connection); } return pc; } /* Negotiator._addProvider = function(provider) { if ((!provider.id && !provider.disconnected) || !provider.socket.open) { // Wait for provider to obtain an ID. provider.on('open', function(id) { Negotiator._addProvider(provider); }); } else { Negotiator.providers[provider.id] = provider; } }*/ /** Start a PC. */ Negotiator._startPeerConnection = function(connection) { util.log('Creating RTCPeerConnection.'); var id = Negotiator._idPrefix + util.randomToken(); pc = new RTCPeerConnection(connection.provider.options.config, {optional: [{RtpDataChannels: true}]}); Negotiator.pcs[connection.type][connection.peer][id] = pc; Negotiator._setupListeners(connection, pc, id); return pc; } /** Set up various WebRTC listeners. */ Negotiator._setupListeners = function(connection, pc, pc_id) { var peerId = connection.peer; var connectionId = connection.id; var provider = connection.provider; // ICE CANDIDATES. util.log('Listening for ICE candidates.'); pc.onicecandidate = function(evt) { if (evt.candidate) { util.log('Received ICE candidates for:', connection.peer); provider.socket.send({ type: 'CANDIDATE', payload: { candidate: evt.candidate, type: connection.type, connectionId: connection.id }, dst: peerId, }); } }; pc.oniceconnectionstatechange = function() { switch (pc.iceConnectionState) { case 'failed': util.log('iceConnectionState is disconnected, closing connections to ' + peerId); Negotiator.cleanup(connection); break; case 'completed': pc.onicecandidate = util.noop; break; } }; // Fallback for older Chrome impls. pc.onicechange = pc.oniceconnectionstatechange; // ONNEGOTIATIONNEEDED (Chrome) util.log('Listening for `negotiationneeded`'); pc.onnegotiationneeded = function() { util.log('`negotiationneeded` triggered'); if (pc.signalingState == 'stable') { Negotiator._makeOffer(connection); } else { util.log('onnegotiationneeded triggered when not stable. Is another connection being established?'); } }; // 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; var connection = provider.getConnection(peerId, connectionId); connection.initialize(dc); }; // MEDIACONNECTION. util.log('Listening for remote stream'); pc.onaddstream = function(evt) { util.log('Received remote stream'); var stream = evt.stream; provider.getConnection(peerId, connectionId).addStream(stream); }; } Negotiator.cleanup = function(connection) { connection.close(); // Will fail safely if connection is already closed. // TODO: close PeerConnection when all connections are closed. util.log('Cleanup PeerConnection for ' + connection.peer); /*if (!!this.pc && (this.pc.readyState !== 'closed' || this.pc.signalingState !== 'closed')) { this.pc.close(); this.pc = null; }*/ connection.provider.socket.send({ type: 'LEAVE', dst: connection.peer }); } Negotiator._makeOffer = function(connection) { var pc = connection.pc; pc.createOffer(function(offer) { util.log('Created offer.'); if (!util.supports.reliable) { //offer.sdp = Reliable.higherBandwidthSDP(offer.sdp); } pc.setLocalDescription(offer, function() { util.log('Set localDescription: offer', 'for:', connection.peer); connection.provider.socket.send({ type: 'OFFER', payload: { sdp: offer, type: connection.type, label: connection.label, reliable: connection.reliable, serialization: connection.serialization, metadata: connection.metadata, connectionId: connection.id }, dst: connection.peer, }); }, function(err) { connection.provider.emit('error', err); util.log('Failed to setLocalDescription, ', err); }); }, function(err) { connection.provider.emit('error', err); util.log('Failed to createOffer, ', err); }); } Negotiator._makeAnswer = function(connection) { var pc = connection.pc; pc.createAnswer(function(answer) { util.log('Created answer.'); if (!util.supports.reliable) { // TODO //answer.sdp = Reliable.higherBandwidthSDP(answer.sdp); } pc.setLocalDescription(answer, function() { util.log('Set localDescription: answer', 'for:', connection.peer); connection.provider.socket.send({ type: 'ANSWER', payload: { sdp: answer, type: connection.type, connectionId: connection.id }, dst: connection.peer }); }, function(err) { connection.provider.emit('error', err); util.log('Failed to setLocalDescription, ', err); }); }, function(err) { connection.provider.emit('error', err); util.log('Failed to create answer, ', err); }); } /** Handle an SDP. */ Negotiator.handleSDP = function(type, connection, sdp) { sdp = new RTCSessionDescription(sdp); var pc = connection.pc; util.log('Setting remote description', sdp); pc.setRemoteDescription(sdp, function() { util.log('Set remoteDescription:', type, 'for:', connection.peer); if (type === 'OFFER') { if (connection.type === 'media') { if (connection.localStream) { // Add local stream (from answer). pc.addStream(connection.localStream); } //util.setZeroTimeout(function(){ // // Add remote streams // connection.addStream(pc.getRemoteStreams()[0]); //}); } Negotiator._makeAnswer(connection); } }, function(err) { connection.provider.emit('error', err); util.log('Failed to setRemoteDescription, ', err); }); } /** Handle a candidate. */ Negotiator.handleCandidate = function(connection, candidate) { var candidate = new RTCIceCandidate(candidate); connection.pc.addIceCandidate(candidate); util.log('Added ICE candidate for:', connection.peer); }