function TraceablePeerConnection(ice_config, constraints) { var self = this; var RTCPeerconnection = navigator.mozGetUserMedia ? mozRTCPeerConnection : webkitRTCPeerConnection; this.peerconnection = new RTCPeerconnection(ice_config, constraints); this.updateLog = []; this.stats = {}; this.statsinterval = null; this.maxstats = 0; // limit to 300 values, i.e. 5 minutes; set to 0 to disable var Interop = require('sdp-interop').Interop; this.interop = new Interop(); var Simulcast = require('sdp-simulcast'); this.simulcast = new Simulcast({numOfLayers: 3, explodeRemoteSimulcast: false}); // override as desired this.trace = function (what, info) { //console.warn('WTRACE', what, info); self.updateLog.push({ time: new Date(), type: what, value: info || "" }); }; this.onicecandidate = null; this.peerconnection.onicecandidate = function (event) { self.trace('onicecandidate', JSON.stringify(event.candidate, null, ' ')); if (self.onicecandidate !== null) { self.onicecandidate(event); } }; this.onaddstream = null; this.peerconnection.onaddstream = function (event) { self.trace('onaddstream', event.stream.id); if (self.onaddstream !== null) { self.onaddstream(event); } }; this.onremovestream = null; this.peerconnection.onremovestream = function (event) { self.trace('onremovestream', event.stream.id); if (self.onremovestream !== null) { self.onremovestream(event); } }; this.onsignalingstatechange = null; this.peerconnection.onsignalingstatechange = function (event) { self.trace('onsignalingstatechange', self.signalingState); if (self.onsignalingstatechange !== null) { self.onsignalingstatechange(event); } }; this.oniceconnectionstatechange = null; this.peerconnection.oniceconnectionstatechange = function (event) { self.trace('oniceconnectionstatechange', self.iceConnectionState); if (self.oniceconnectionstatechange !== null) { self.oniceconnectionstatechange(event); } }; this.onnegotiationneeded = null; this.peerconnection.onnegotiationneeded = function (event) { self.trace('onnegotiationneeded'); if (self.onnegotiationneeded !== null) { self.onnegotiationneeded(event); } }; self.ondatachannel = null; this.peerconnection.ondatachannel = function (event) { self.trace('ondatachannel', event); if (self.ondatachannel !== null) { self.ondatachannel(event); } }; if (!navigator.mozGetUserMedia && this.maxstats) { this.statsinterval = window.setInterval(function() { self.peerconnection.getStats(function(stats) { var results = stats.result(); for (var i = 0; i < results.length; ++i) { //console.log(results[i].type, results[i].id, results[i].names()) var now = new Date(); results[i].names().forEach(function (name) { var id = results[i].id + '-' + name; if (!self.stats[id]) { self.stats[id] = { startTime: now, endTime: now, values: [], times: [] }; } self.stats[id].values.push(results[i].stat(name)); self.stats[id].times.push(now.getTime()); if (self.stats[id].values.length > self.maxstats) { self.stats[id].values.shift(); self.stats[id].times.shift(); } self.stats[id].endTime = now; }); } }); }, 1000); } }; dumpSDP = function(description) { if (typeof description === 'undefined' || description == null) { return ''; } return 'type: ' + description.type + '\r\n' + description.sdp; }; if (TraceablePeerConnection.prototype.__defineGetter__ !== undefined) { TraceablePeerConnection.prototype.__defineGetter__('signalingState', function() { return this.peerconnection.signalingState; }); TraceablePeerConnection.prototype.__defineGetter__('iceConnectionState', function() { return this.peerconnection.iceConnectionState; }); TraceablePeerConnection.prototype.__defineGetter__('localDescription', function() { var desc = this.peerconnection.localDescription; this.trace('getLocalDescription::preTransform', dumpSDP(desc)); // if we're running on FF, transform to Plan B first. if (navigator.mozGetUserMedia) { desc = this.interop.toPlanB(desc); this.trace('getLocalDescription::postTransform (Plan B)', dumpSDP(desc)); } return desc; }); TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { var desc = this.peerconnection.remoteDescription; this.trace('getRemoteDescription::preTransform', dumpSDP(desc)); // if we're running on FF, transform to Plan B first. if (navigator.mozGetUserMedia) { desc = this.interop.toPlanB(desc); this.trace('getRemoteDescription::postTransform (Plan B)', dumpSDP(desc)); } return desc; }); } TraceablePeerConnection.prototype.addStream = function (stream) { this.trace('addStream', stream.id); try { this.peerconnection.addStream(stream); } catch (e) { console.error(e); return; } }; TraceablePeerConnection.prototype.removeStream = function (stream, stopStreams) { this.trace('removeStream', stream.id); if(stopStreams) { stream.getAudioTracks().forEach(function (track) { track.stop(); }); stream.getVideoTracks().forEach(function (track) { track.stop(); }); } try { // FF doesn't support this yet. this.peerconnection.removeStream(stream); } catch (e) { console.error(e); } }; TraceablePeerConnection.prototype.createDataChannel = function (label, opts) { this.trace('createDataChannel', label, opts); return this.peerconnection.createDataChannel(label, opts); }; TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) { this.trace('setLocalDescription::preTransform', dumpSDP(description)); // if we're running on FF, transform to Plan A first. if (navigator.mozGetUserMedia) { description = this.interop.toUnifiedPlan(description); this.trace('setLocalDescription::postTransform (Plan A)', dumpSDP(description)); } var self = this; this.peerconnection.setLocalDescription(description, function () { self.trace('setLocalDescriptionOnSuccess'); successCallback(); }, function (err) { self.trace('setLocalDescriptionOnFailure', err); failureCallback(err); } ); /* if (this.statsinterval === null && this.maxstats > 0) { // start gathering stats } */ }; TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) { this.trace('setRemoteDescription::preTransform', dumpSDP(description)); // TODO the focus should squeze or explode the remote simulcast description = this.simulcast.mungeRemoteDescription(description); this.trace('setRemoteDescription::postTransform (simulcast)', dumpSDP(description)); // if we're running on FF, transform to Plan A first. if (navigator.mozGetUserMedia) { description = this.interop.toUnifiedPlan(description); this.trace('setRemoteDescription::postTransform (Plan A)', dumpSDP(description)); } var self = this; this.peerconnection.setRemoteDescription(description, function () { self.trace('setRemoteDescriptionOnSuccess'); successCallback(); }, function (err) { self.trace('setRemoteDescriptionOnFailure', err); failureCallback(err); } ); /* if (this.statsinterval === null && this.maxstats > 0) { // start gathering stats } */ }; TraceablePeerConnection.prototype.close = function () { this.trace('stop'); if (this.statsinterval !== null) { window.clearInterval(this.statsinterval); this.statsinterval = null; } this.peerconnection.close(); }; TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) { var self = this; this.trace('createOffer', JSON.stringify(constraints, null, ' ')); this.peerconnection.createOffer( function (offer) { self.trace('createOfferOnSuccess::preTransform', dumpSDP(offer)); // if we're running on FF, transform to Plan B first. // NOTE this is not tested because in meet the focus generates the // offer. if (navigator.mozGetUserMedia) { offer = self.interop.toPlanB(offer); self.trace('createOfferOnSuccess::postTransform (Plan B)', dumpSDP(offer)); } if (config.enableSimulcast && self.simulcast.isSupported()) { offer = self.simulcast.mungeLocalDescription(offer); self.trace('createOfferOnSuccess::postTransform (simulcast)', dumpSDP(offer)); } successCallback(offer); }, function(err) { self.trace('createOfferOnFailure', err); failureCallback(err); }, constraints ); }; TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) { var self = this; this.trace('createAnswer', JSON.stringify(constraints, null, ' ')); this.peerconnection.createAnswer( function (answer) { self.trace('createAnswerOnSuccess::preTransfom', dumpSDP(answer)); // if we're running on FF, transform to Plan A first. if (navigator.mozGetUserMedia) { answer = self.interop.toPlanB(answer); self.trace('createAnswerOnSuccess::postTransfom (Plan B)', dumpSDP(answer)); } if (config.enableSimulcast && self.simulcast.isSupported()) { answer = self.simulcast.mungeLocalDescription(answer); self.trace('createAnswerOnSuccess::postTransfom (simulcast)', dumpSDP(answer)); } successCallback(answer); }, function(err) { self.trace('createAnswerOnFailure', err); failureCallback(err); }, constraints ); }; TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) { var self = this; this.trace('addIceCandidate', JSON.stringify(candidate, null, ' ')); this.peerconnection.addIceCandidate(candidate); /* maybe later this.peerconnection.addIceCandidate(candidate, function () { self.trace('addIceCandidateOnSuccess'); successCallback(); }, function (err) { self.trace('addIceCandidateOnFailure', err); failureCallback(err); } ); */ }; TraceablePeerConnection.prototype.getStats = function(callback, errback) { if (navigator.mozGetUserMedia) { // ignore for now... if(!errback) errback = function () { } this.peerconnection.getStats(null,callback,errback); } else { this.peerconnection.getStats(callback); } }; module.exports = TraceablePeerConnection;