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 /** * Array of ssrcs that will be added on next modifySources call. * @type {Array} */ this.addssrc = []; /** * Array of ssrcs that will be added on next modifySources call. * @type {Array} */ this.removessrc = []; /** * Pending operation that will be done during modifySources call. * Currently 'mute'/'unmute' operations are supported. * * @type {String} */ this.pendingop = null; /** * Flag indicates that peer connection stream have changed and modifySources should proceed. * @type {boolean} */ this.switchstreams = 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) { 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 publicLocalDescription = simulcast.reverseTransformLocalDescription(this.peerconnection.localDescription); return publicLocalDescription; }); TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function() { var publicRemoteDescription = simulcast.reverseTransformRemoteDescription(this.peerconnection.remoteDescription); return publicRemoteDescription; }); } TraceablePeerConnection.prototype.addStream = function (stream) { this.trace('addStream', stream.id); simulcast.resetSender(); this.peerconnection.addStream(stream); }; TraceablePeerConnection.prototype.removeStream = function (stream) { this.trace('removeStream', stream.id); simulcast.resetSender(); this.peerconnection.removeStream(stream); }; TraceablePeerConnection.prototype.createDataChannel = function (label, opts) { this.trace('createDataChannel', label, opts); return this.peerconnection.createDataChannel(label, opts); }; TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) { var self = this; description = simulcast.transformLocalDescription(description); this.trace('setLocalDescription', dumpSDP(description)); 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) { var self = this; description = simulcast.transformRemoteDescription(description); this.trace('setRemoteDescription', dumpSDP(description)); 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.hardMuteVideo = function (muted) { this.pendingop = muted ? 'mute' : 'unmute'; }; TraceablePeerConnection.prototype.enqueueAddSsrc = function(channel, ssrcLines) { if (!this.addssrc[channel]) { this.addssrc[channel] = ''; } this.addssrc[channel] += ssrcLines; }; TraceablePeerConnection.prototype.addSource = function (elem) { console.log('addssrc', new Date().getTime()); console.log('ice', this.iceConnectionState); var sdp = new SDP(this.remoteDescription.sdp); var mySdp = new SDP(this.peerconnection.localDescription.sdp); var self = this; $(elem).each(function (idx, content) { var name = $(content).attr('name'); var lines = ''; tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() { var semantics = this.getAttribute('semantics'); var ssrcs = $(this).find('>source').map(function () { return this.getAttribute('ssrc'); }).get(); if (ssrcs.length != 0) { lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n'; } }); tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source tmp.each(function () { var ssrc = $(this).attr('ssrc'); if(mySdp.containsSSRC(ssrc)){ /** * This happens when multiple participants change their streams at the same time and * ColibriFocus.modifySources have to wait for stable state. In the meantime multiple * addssrc are scheduled for update IQ. See */ console.warn("Got add stream request for my own ssrc: "+ssrc); return; } $(this).find('>parameter').each(function () { lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); if ($(this).attr('value') && $(this).attr('value').length) lines += ':' + $(this).attr('value'); lines += '\r\n'; }); }); sdp.media.forEach(function(media, idx) { if (!SDPUtil.find_line(media, 'a=mid:' + name)) return; sdp.media[idx] += lines; self.enqueueAddSsrc(idx, lines); }); sdp.raw = sdp.session + sdp.media.join(''); }); }; TraceablePeerConnection.prototype.enqueueRemoveSsrc = function(channel, ssrcLines) { if (!this.removessrc[channel]){ this.removessrc[channel] = ''; } this.removessrc[channel] += ssrcLines; }; TraceablePeerConnection.prototype.removeSource = function (elem) { console.log('removessrc', new Date().getTime()); console.log('ice', this.iceConnectionState); var sdp = new SDP(this.remoteDescription.sdp); var mySdp = new SDP(this.peerconnection.localDescription.sdp); var self = this; $(elem).each(function (idx, content) { var name = $(content).attr('name'); var lines = ''; tmp = $(content).find('ssrc-group[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]').each(function() { var semantics = this.getAttribute('semantics'); var ssrcs = $(this).find('>source').map(function () { return this.getAttribute('ssrc'); }).get(); if (ssrcs.length != 0) { lines += 'a=ssrc-group:' + semantics + ' ' + ssrcs.join(' ') + '\r\n'; } }); tmp = $(content).find('source[xmlns="urn:xmpp:jingle:apps:rtp:ssma:0"]'); // can handle both >source and >description>source tmp.each(function () { var ssrc = $(this).attr('ssrc'); // This should never happen, but can be useful for bug detection if(mySdp.containsSSRC(ssrc)){ console.error("Got remove stream request for my own ssrc: "+ssrc); return; } $(this).find('>parameter').each(function () { lines += 'a=ssrc:' + ssrc + ' ' + $(this).attr('name'); if ($(this).attr('value') && $(this).attr('value').length) lines += ':' + $(this).attr('value'); lines += '\r\n'; }); }); sdp.media.forEach(function(media, idx) { if (!SDPUtil.find_line(media, 'a=mid:' + name)) return; sdp.media[idx] += lines; self.enqueueRemoveSsrc(idx, lines); }); sdp.raw = sdp.session + sdp.media.join(''); }); }; TraceablePeerConnection.prototype.modifySources = function(successCallback) { var self = this; if (this.signalingState == 'closed') return; if (!(this.addssrc.length || this.removessrc.length || this.pendingop !== null || this.switchstreams)){ // There is nothing to do since scheduled job might have been executed by another succeeding call if(successCallback){ successCallback(); } return; } // FIXME: this is a big hack // https://code.google.com/p/webrtc/issues/detail?id=2688 // ^ has been fixed. if (!(this.signalingState == 'stable' && this.iceConnectionState == 'connected')) { console.warn('modifySources not yet', this.signalingState, this.iceConnectionState); this.wait = true; window.setTimeout(function() { self.modifySources(successCallback); }, 250); return; } if (this.wait) { window.setTimeout(function() { self.modifySources(successCallback); }, 2500); this.wait = false; return; } // Reset switch streams flag this.switchstreams = false; var sdp = new SDP(this.remoteDescription.sdp); // add sources this.addssrc.forEach(function(lines, idx) { sdp.media[idx] += lines; }); this.addssrc = []; // remove sources this.removessrc.forEach(function(lines, idx) { lines = lines.split('\r\n'); lines.pop(); // remove empty last element; lines.forEach(function(line) { sdp.media[idx] = sdp.media[idx].replace(line + '\r\n', ''); }); }); this.removessrc = []; // FIXME: // this was a hack for the situation when only one peer exists // in the conference. // check if still required and remove if (sdp.media[0]) sdp.media[0] = sdp.media[0].replace('a=recvonly', 'a=sendrecv'); if (sdp.media[1]) sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); sdp.raw = sdp.session + sdp.media.join(''); this.setRemoteDescription(new RTCSessionDescription({type: 'offer', sdp: sdp.raw}), function() { if(self.signalingState == 'closed') { console.error("createAnswer attempt on closed state"); return; } self.createAnswer( function(modifiedAnswer) { // change video direction, see https://github.com/jitsi/jitmeet/issues/41 if (self.pendingop !== null) { var sdp = new SDP(modifiedAnswer.sdp); if (sdp.media.length > 1) { switch(self.pendingop) { case 'mute': sdp.media[1] = sdp.media[1].replace('a=sendrecv', 'a=recvonly'); break; case 'unmute': sdp.media[1] = sdp.media[1].replace('a=recvonly', 'a=sendrecv'); break; } sdp.raw = sdp.session + sdp.media.join(''); modifiedAnswer.sdp = sdp.raw; } self.pendingop = null; } // FIXME: pushing down an answer while ice connection state // is still checking is bad... //console.log(self.peerconnection.iceConnectionState); // trying to work around another chrome bug //modifiedAnswer.sdp = modifiedAnswer.sdp.replace(/a=setup:active/g, 'a=setup:actpass'); self.setLocalDescription(modifiedAnswer, function() { //console.log('modified setLocalDescription ok'); if(successCallback){ successCallback(); } }, function(error) { console.error('modified setLocalDescription failed', error); } ); }, function(error) { console.error('modified answer failed', error); } ); }, function(error) { console.error('modify failed', error); } ); }; 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', 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) { answer = simulcast.transformAnswer(answer); self.trace('createAnswerOnSuccess', 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... } else { this.peerconnection.getStats(callback); } }; // mozilla chrome compat layer -- very similar to adapter.js function setupRTC() { var RTC = null; if (navigator.mozGetUserMedia) { console.log('This appears to be Firefox'); var version = parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10); if (version >= 22) { RTC = { peerconnection: mozRTCPeerConnection, browser: 'firefox', getUserMedia: navigator.mozGetUserMedia.bind(navigator), attachMediaStream: function (element, stream) { element[0].mozSrcObject = stream; element[0].play(); }, pc_constraints: {} }; if (!MediaStream.prototype.getVideoTracks) MediaStream.prototype.getVideoTracks = function () { return []; }; if (!MediaStream.prototype.getAudioTracks) MediaStream.prototype.getAudioTracks = function () { return []; }; RTCSessionDescription = mozRTCSessionDescription; RTCIceCandidate = mozRTCIceCandidate; } } else if (navigator.webkitGetUserMedia) { console.log('This appears to be Chrome'); RTC = { peerconnection: webkitRTCPeerConnection, browser: 'chrome', getUserMedia: navigator.webkitGetUserMedia.bind(navigator), attachMediaStream: function (element, stream) { element.attr('src', webkitURL.createObjectURL(stream)); }, // DTLS should now be enabled by default but.. pc_constraints: {'optional': [{'DtlsSrtpKeyAgreement': 'true'}]} }; if (navigator.userAgent.indexOf('Android') != -1) { RTC.pc_constraints = {}; // disable DTLS on Android } if (!webkitMediaStream.prototype.getVideoTracks) { webkitMediaStream.prototype.getVideoTracks = function () { return this.videoTracks; }; } if (!webkitMediaStream.prototype.getAudioTracks) { webkitMediaStream.prototype.getAudioTracks = function () { return this.audioTracks; }; } } if (RTC === null) { try { console.log('Browser does not appear to be WebRTC-capable'); } catch (e) { } } return RTC; } function getUserMediaWithConstraints(um, success_callback, failure_callback, resolution, bandwidth, fps, desktopStream) { var constraints = {audio: false, video: false}; if (um.indexOf('video') >= 0) { constraints.video = { mandatory: {}, optional: [] };// same behaviour as true } if (um.indexOf('audio') >= 0) { constraints.audio = { mandatory: {}, optional: []};// same behaviour as true } if (um.indexOf('screen') >= 0) { constraints.video = { mandatory: { chromeMediaSource: "screen", googLeakyBucket: true, maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 }, optional: [] }; } if (um.indexOf('desktop') >= 0) { constraints.video = { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: desktopStream, googLeakyBucket: true, maxWidth: window.screen.width, maxHeight: window.screen.height, maxFrameRate: 3 }, optional: [] } } if (constraints.audio) { // if it is good enough for hangouts... constraints.audio.optional.push( {googEchoCancellation: true}, {googAutoGainControl: true}, {googNoiseSupression: true}, {googHighpassFilter: true}, {googNoisesuppression2: true}, {googEchoCancellation2: true}, {googAutoGainControl2: true} ); } if (constraints.video) { constraints.video.optional.push( {googNoiseReduction: false} // chrome 37 workaround for issue 3807, reenable in M38 ); if (um.indexOf('video') >= 0) { constraints.video.optional.push( {googLeakyBucket: true} ); } } // Check if we are running on Android device var isAndroid = navigator.userAgent.indexOf('Android') != -1; if (resolution && !constraints.video || isAndroid) { constraints.video = { mandatory: {}, optional: [] };// same behaviour as true } // see https://code.google.com/p/chromium/issues/detail?id=143631#c9 for list of supported resolutions switch (resolution) { // 16:9 first case '1080': case 'fullhd': constraints.video.mandatory.minWidth = 1920; constraints.video.mandatory.minHeight = 1080; break; case '720': case 'hd': constraints.video.mandatory.minWidth = 1280; constraints.video.mandatory.minHeight = 720; break; case '360': constraints.video.mandatory.minWidth = 640; constraints.video.mandatory.minHeight = 360; break; case '180': constraints.video.mandatory.minWidth = 320; constraints.video.mandatory.minHeight = 180; break; // 4:3 case '960': constraints.video.mandatory.minWidth = 960; constraints.video.mandatory.minHeight = 720; break; case '640': case 'vga': constraints.video.mandatory.minWidth = 640; constraints.video.mandatory.minHeight = 480; break; case '320': constraints.video.mandatory.minWidth = 320; constraints.video.mandatory.minHeight = 240; break; default: if (isAndroid) { constraints.video.mandatory.minWidth = 320; constraints.video.mandatory.minHeight = 240; constraints.video.mandatory.maxFrameRate = 15; } break; } if (constraints.video.mandatory.minWidth) constraints.video.mandatory.maxWidth = constraints.video.mandatory.minWidth; if (constraints.video.mandatory.minHeight) constraints.video.mandatory.maxHeight = constraints.video.mandatory.minHeight; if (bandwidth) { // doesn't work currently, see webrtc issue 1846 if (!constraints.video) constraints.video = {mandatory: {}, optional: []};//same behaviour as true constraints.video.optional.push({bandwidth: bandwidth}); } if (fps) { // for some cameras it might be necessary to request 30fps // so they choose 30fps mjpg over 10fps yuy2 if (!constraints.video) constraints.video = {mandatory: {}, optional: []};// same behaviour as true; constraints.video.mandatory.minFrameRate = fps; } var isFF = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; try { if (config.enableSimulcast && constraints.video && constraints.video.chromeMediaSource !== 'screen' && constraints.video.chromeMediaSource !== 'desktop' && !isAndroid // We currently do not support FF, as it doesn't have multistream support. && !isFF) { simulcast.getUserMedia(constraints, function (stream) { console.log('onUserMediaSuccess'); success_callback(stream); }, function (error) { console.warn('Failed to get access to local media. Error ', error); if (failure_callback) { failure_callback(error); } }); } else { RTC.getUserMedia(constraints, function (stream) { console.log('onUserMediaSuccess'); success_callback(stream); }, function (error) { console.warn('Failed to get access to local media. Error ', error); if (failure_callback) { failure_callback(error); } }); } } catch (e) { console.error('GUM failed: ', e); if(failure_callback) { failure_callback(e); } } }