diff --git a/public/js/simplewebrtc.bundle.js b/public/js/simplewebrtc.bundle.js index c101f56..f214b41 100644 --- a/public/js/simplewebrtc.bundle.js +++ b/public/js/simplewebrtc.bundle.js @@ -145,6 +145,14 @@ function SimpleWebRTC(opts) { self.emit('turnservers', args); }); + this.webrtc.on('iceFailed', function (peer) { + // local ice failure + }); + this.webrtc.on('connectivityError', function (peer) { + // remote ice failure + }); + + // sending mute/unmute to all peers this.webrtc.on('audioOn', function () { self.webrtc.sendToAll('unmute', {name: 'audio'}); @@ -415,7 +423,98 @@ SimpleWebRTC.prototype.sendFile = function () { module.exports = SimpleWebRTC; -},{"attachmediastream":5,"mockconsole":4,"socket.io-client":7,"webrtc":6,"webrtcsupport":3,"wildemitter":2}],2:[function(require,module,exports){ +},{"attachmediastream":5,"mockconsole":4,"socket.io-client":6,"webrtc":2,"webrtcsupport":3,"wildemitter":7}],3:[function(require,module,exports){ +// created by @HenrikJoreteg +var prefix; +var isChrome = false; +var isFirefox = false; +var ua = window.navigator.userAgent.toLowerCase(); + +// basic sniffing +if (ua.indexOf('firefox') !== -1) { + prefix = 'moz'; + isFirefox = true; +} else if (ua.indexOf('chrome') !== -1) { + prefix = 'webkit'; + isChrome = true; +} + +var PC = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; +var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; +var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; +var MediaStream = window.webkitMediaStream || window.MediaStream; +var screenSharing = window.location.protocol === 'https:' && window.navigator.userAgent.match('Chrome') && parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 26; +var AudioContext = window.webkitAudioContext || window.AudioContext; + + +// export support flags and constructors.prototype && PC +module.exports = { + support: !!PC, + dataChannel: isChrome || isFirefox || (PC && PC.prototype && PC.prototype.createDataChannel), + prefix: prefix, + webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + screenSharing: !!screenSharing, + AudioContext: AudioContext, + PeerConnection: PC, + SessionDescription: SessionDescription, + IceCandidate: IceCandidate +}; + +},{}],4:[function(require,module,exports){ +var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); +var l = methods.length; +var fn = function () {}; +var mockconsole = {}; + +while (l--) { + mockconsole[methods[l]] = fn; +} + +module.exports = mockconsole; + +},{}],5:[function(require,module,exports){ +module.exports = function (stream, el, options) { + var URL = window.URL; + var opts = { + autoplay: true, + mirror: false, + muted: false + }; + var element = el || document.createElement('video'); + var item; + + if (options) { + for (item in options) { + opts[item] = options[item]; + } + } + + if (opts.autoplay) element.autoplay = 'autoplay'; + if (opts.muted) element.muted = true; + if (opts.mirror) { + ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { + var styleName = prefix ? prefix + 'Transform' : 'transform'; + element.style[styleName] = 'scaleX(-1)'; + }); + } + + // this first one should work most everywhere now + // but we have a few fallbacks just in case. + if (URL && URL.createObjectURL) { + element.src = URL.createObjectURL(stream); + } else if (element.srcObject) { + element.srcObject = stream; + } else if (element.mozSrcObject) { + element.mozSrcObject = stream; + } else { + return false; + } + + return element; +}; + +},{}],7:[function(require,module,exports){ /* WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based on @visionmedia's Emitter from UI Kit. @@ -444,7 +543,7 @@ function WildEmitter() { // Listen on the given `event` with `fn`. Store a group name if present. WildEmitter.prototype.on = function (event, groupName, fn) { var hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, + group = hasGroup ? arguments[1] : undefined, func = hasGroup ? arguments[2] : arguments[1]; func._groupName = group; (this.callbacks[event] = this.callbacks[event] || []).push(func); @@ -456,7 +555,7 @@ WildEmitter.prototype.on = function (event, groupName, fn) { WildEmitter.prototype.once = function (event, groupName, fn) { var self = this, hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, + group = hasGroup ? arguments[1] : undefined, func = hasGroup ? arguments[2] : arguments[1]; function on() { self.off(event, on); @@ -489,7 +588,7 @@ WildEmitter.prototype.releaseGroup = function (groupName) { WildEmitter.prototype.off = function (event, fn) { var callbacks = this.callbacks[event], i; - + if (!callbacks) return this; // remove all handlers @@ -504,7 +603,7 @@ WildEmitter.prototype.off = function (event, fn) { return this; }; -// Emit `event` with the given args. +/// Emit `event` with the given args. // also calls any `*` handlers WildEmitter.prototype.emit = function (event) { var args = [].slice.call(arguments, 1), @@ -512,12 +611,14 @@ WildEmitter.prototype.emit = function (event) { specialCallbacks = this.getWildcardCallbacks(event), i, len, - item; + item, + listeners; if (callbacks) { - for (i = 0, len = callbacks.length; i < len; ++i) { - if (callbacks[i]) { - callbacks[i].apply(this, args); + listeners = callbacks.slice(); + for (i = 0, len = listeners.length; i < len; ++i) { + if (listeners[i]) { + listeners[i].apply(this, args); } else { break; } @@ -525,9 +626,11 @@ WildEmitter.prototype.emit = function (event) { } if (specialCallbacks) { - for (i = 0, len = specialCallbacks.length; i < len; ++i) { - if (specialCallbacks[i]) { - specialCallbacks[i].apply(this, [event].concat(args)); + len = specialCallbacks.length; + listeners = specialCallbacks.slice(); + for (i = 0, len = listeners.length; i < len; ++i) { + if (listeners[i]) { + listeners[i].apply(this, [event].concat(args)); } else { break; } @@ -545,105 +648,14 @@ WildEmitter.prototype.getWildcardCallbacks = function (eventName) { for (item in this.callbacks) { split = item.split('*'); - if (item === '*' || (split.length === 2 && eventName.slice(0, split[1].length) === split[1])) { + if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) { result = result.concat(this.callbacks[item]); } } return result; }; -},{}],3:[function(require,module,exports){ -// created by @HenrikJoreteg -var prefix; -var isChrome = false; -var isFirefox = false; -var ua = window.navigator.userAgent.toLowerCase(); - -// basic sniffing -if (ua.indexOf('firefox') !== -1) { - prefix = 'moz'; - isFirefox = true; -} else if (ua.indexOf('chrome') !== -1) { - prefix = 'webkit'; - isChrome = true; -} - -var PC = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; -var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; -var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; -var MediaStream = window.webkitMediaStream || window.MediaStream; -var screenSharing = window.location.protocol === 'https:' && window.navigator.userAgent.match('Chrome') && parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 26; -var AudioContext = window.webkitAudioContext || window.AudioContext; - - -// export support flags and constructors.prototype && PC -module.exports = { - support: !!PC, - dataChannel: isChrome || isFirefox || (PC && PC.prototype && PC.prototype.createDataChannel), - prefix: prefix, - webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - screenSharing: !!screenSharing, - AudioContext: AudioContext, - PeerConnection: PC, - SessionDescription: SessionDescription, - IceCandidate: IceCandidate -}; - -},{}],5:[function(require,module,exports){ -module.exports = function (stream, el, options) { - var URL = window.URL; - var opts = { - autoplay: true, - mirror: false, - muted: false - }; - var element = el || document.createElement('video'); - var item; - - if (options) { - for (item in options) { - opts[item] = options[item]; - } - } - - if (opts.autoplay) element.autoplay = 'autoplay'; - if (opts.muted) element.muted = true; - if (opts.mirror) { - ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { - var styleName = prefix ? prefix + 'Transform' : 'transform'; - element.style[styleName] = 'scaleX(-1)'; - }); - } - - // this first one should work most everywhere now - // but we have a few fallbacks just in case. - if (URL && URL.createObjectURL) { - element.src = URL.createObjectURL(stream); - } else if (element.srcObject) { - element.srcObject = stream; - } else if (element.mozSrcObject) { - element.mozSrcObject = stream; - } else { - return false; - } - - return element; -}; - -},{}],4:[function(require,module,exports){ -var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); -var l = methods.length; -var fn = function () {}; -var mockconsole = {}; - -while (l--) { - mockconsole[methods[l]] = fn; -} - -module.exports = mockconsole; - -},{}],7:[function(require,module,exports){ +},{}],6:[function(require,module,exports){ /*! Socket.IO.js build:0.9.16, development. Copyright(c) 2011 LearnBoost MIT Licensed */ var io = ('undefined' === typeof module ? {} : module.exports); @@ -4518,6 +4530,44 @@ if (typeof define === "function" && define.amd) { } })(); },{}],8:[function(require,module,exports){ +// created by @HenrikJoreteg +var prefix; +var isChrome = false; +var isFirefox = false; +var ua = navigator.userAgent.toLowerCase(); + +// basic sniffing +if (ua.indexOf('firefox') !== -1) { + prefix = 'moz'; + isFirefox = true; +} else if (ua.indexOf('chrome') !== -1) { + prefix = 'webkit'; + isChrome = true; +} + +var PC = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; +var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; +var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; +var MediaStream = window.webkitMediaStream || window.MediaStream; +var screenSharing = navigator.userAgent.match('Chrome') && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 26; +var AudioContext = window.webkitAudioContext || window.AudioContext; + + +// export support flags and constructors.prototype && PC +module.exports = { + support: !!PC, + dataChannel: isChrome || isFirefox || (PC && PC.prototype && PC.prototype.createDataChannel), + prefix: prefix, + webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + screenSharing: !!screenSharing, + AudioContext: AudioContext, + PeerConnection: PC, + SessionDescription: SessionDescription, + IceCandidate: IceCandidate +}; + +},{}],9:[function(require,module,exports){ var events = require('events'); exports.isArray = isArray; @@ -4864,51 +4914,13 @@ exports.format = function(f) { return str; }; -},{"events":9}],10:[function(require,module,exports){ -// created by @HenrikJoreteg -var prefix; -var isChrome = false; -var isFirefox = false; -var ua = navigator.userAgent.toLowerCase(); - -// basic sniffing -if (ua.indexOf('firefox') !== -1) { - prefix = 'moz'; - isFirefox = true; -} else if (ua.indexOf('chrome') !== -1) { - prefix = 'webkit'; - isChrome = true; -} - -var PC = window.mozRTCPeerConnection || window.webkitRTCPeerConnection; -var IceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate; -var SessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription; -var MediaStream = window.webkitMediaStream || window.MediaStream; -var screenSharing = navigator.userAgent.match('Chrome') && parseInt(navigator.userAgent.match(/Chrome\/(.*) /)[1], 10) >= 26; -var AudioContext = window.webkitAudioContext || window.AudioContext; - - -// export support flags and constructors.prototype && PC -module.exports = { - support: !!PC, - dataChannel: isChrome || isFirefox || (PC && PC.prototype && PC.prototype.createDataChannel), - prefix: prefix, - webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - screenSharing: !!screenSharing, - AudioContext: AudioContext, - PeerConnection: PC, - SessionDescription: SessionDescription, - IceCandidate: IceCandidate -}; - -},{}],6:[function(require,module,exports){ -var util = require('util'); -var webrtc = require('webrtcsupport'); -var PeerConnection = require('rtcpeerconnection'); -var WildEmitter = require('wildemitter'); -var mockconsole = require('mockconsole'); -var localMedia = require('localmedia'); +},{"events":10}],2:[function(require,module,exports){ +var util = require('util'); +var webrtc = require('webrtcsupport'); +var PeerConnection = require('rtcpeerconnection'); +var WildEmitter = require('wildemitter'); +var mockconsole = require('mockconsole'); +var localMedia = require('localmedia'); function WebRTC(opts) { @@ -5081,8 +5093,9 @@ function Peer(options) { case 'failed': // currently, in chrome only the initiator goes to failed // so we need to signal this to the peer - if (self.pc.config.isInitiator) { - self.parent.emit('iceFailed', {id: self.id}); + if (self.pc.pc.peerconnection.localDescription.type === 'offer') { + self.parent.emit('iceFailed', self); + self.send('connectivityError'); } break; } @@ -5139,6 +5152,8 @@ Peer.prototype.handleMessage = function (message) { this.pc.handleAnswer(message.payload); } else if (message.type === 'candidate') { this.pc.processIce(message.payload); + } else if (message.type === 'connectivityError') { + this.parent.emit('connectivityError', self); } else if (message.type === 'speaking') { this.parent.emit('speaking', {id: message.from}); } else if (message.type === 'stopped_speaking') { @@ -5262,7 +5277,7 @@ Peer.prototype.handleDataChannelAdded = function (channel) { module.exports = WebRTC; -},{"localmedia":12,"mockconsole":4,"rtcpeerconnection":11,"util":8,"webrtcsupport":10,"wildemitter":2}],13:[function(require,module,exports){ +},{"localmedia":11,"mockconsole":4,"rtcpeerconnection":12,"util":9,"webrtcsupport":8,"wildemitter":7}],13:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; @@ -5317,7 +5332,7 @@ process.chdir = function (dir) { throw new Error('process.chdir is not supported'); }; -},{}],9:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ var process=require("__browserify_process");if (!process.EventEmitter) process.EventEmitter = function () {}; var EventEmitter = exports.EventEmitter = process.EventEmitter; @@ -5514,6 +5529,70 @@ EventEmitter.listenerCount = function(emitter, type) { }; },{"__browserify_process":13}],14:[function(require,module,exports){ +// getUserMedia helper by @HenrikJoreteg +var func = (window.navigator.getUserMedia || + window.navigator.webkitGetUserMedia || + window.navigator.mozGetUserMedia || + window.navigator.msGetUserMedia); + + +module.exports = function (constraints, cb) { + var options; + var haveOpts = arguments.length === 2; + var defaultOpts = {video: true, audio: true}; + var error; + var denied = 'PERMISSION_DENIED'; + var notSatified = 'CONSTRAINT_NOT_SATISFIED'; + + // make constraints optional + if (!haveOpts) { + cb = constraints; + constraints = defaultOpts; + } + + // treat lack of browser support like an error + if (!func) { + // throw proper error per spec + error = new Error('NavigatorUserMediaError'); + error.name = 'NOT_SUPPORTED_ERROR'; + return cb(error); + } + + func.call(window.navigator, constraints, function (stream) { + cb(null, stream); + }, function (err) { + var error; + // coerce into an error object since FF gives us a string + // there are only two valid names according to the spec + // we coerce all non-denied to "constraint not satisfied". + if (typeof err === 'string') { + error = new Error('NavigatorUserMediaError'); + if (err === denied) { + error.name = denied; + } else { + error.name = notSatified; + } + } else { + // if we get an error object make sure '.name' property is set + // according to spec: http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermediaerror-and-navigatorusermediaerrorcallback + error = err; + if (!error.name) { + // this is likely chrome which + // sets a property called "ERROR_DENIED" on the error object + // if so we make sure to set a name + if (error[denied]) { + err.name = denied; + } else { + err.name = notSatified; + } + } + } + + cb(error); + }); +}; + +},{}],15:[function(require,module,exports){ // Underscore.js 1.6.0 // http://underscorejs.org // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors @@ -6858,1003 +6937,712 @@ EventEmitter.listenerCount = function(emitter, type) { } }).call(this); -},{}],15:[function(require,module,exports){ -/* -WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based -on @visionmedia's Emitter from UI Kit. - -Why? I wanted it standalone. - -I also wanted support for wildcard emitters like this: - -emitter.on('*', function (eventName, other, event, payloads) { - -}); - -emitter.on('somenamespace*', function (eventName, payloads) { - -}); +},{}],11:[function(require,module,exports){ +var util = require('util'); +var hark = require('hark'); +var webrtc = require('webrtcsupport'); +var getUserMedia = require('getusermedia'); +var getScreenMedia = require('getscreenmedia'); +var WildEmitter = require('wildemitter'); +var GainController = require('mediastream-gain'); +var mockconsole = require('mockconsole'); -Please note that callbacks triggered by wildcard registered events also get -the event name as the first argument. -*/ -module.exports = WildEmitter; -function WildEmitter() { - this.callbacks = {}; -} +function LocalMedia(opts) { + WildEmitter.call(this); -// Listen on the given `event` with `fn`. Store a group name if present. -WildEmitter.prototype.on = function (event, groupName, fn) { - var hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, - func = hasGroup ? arguments[2] : arguments[1]; - func._groupName = group; - (this.callbacks[event] = this.callbacks[event] || []).push(func); - return this; -}; + var config = this.config = { + autoAdjustMic: false, + detectSpeakingEvents: true, + media: { + audio: true, + video: true + }, + logger: mockconsole + }; -// Adds an `event` listener that will be invoked a single -// time then automatically removed. -WildEmitter.prototype.once = function (event, groupName, fn) { - var self = this, - hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, - func = hasGroup ? arguments[2] : arguments[1]; - function on() { - self.off(event, on); - func.apply(this, arguments); + var item; + for (item in opts) { + this.config[item] = opts[item]; } - this.on(event, group, on); - return this; -}; -// Unbinds an entire group -WildEmitter.prototype.releaseGroup = function (groupName) { - var item, i, len, handlers; - for (item in this.callbacks) { - handlers = this.callbacks[item]; - for (i = 0, len = handlers.length; i < len; i++) { - if (handlers[i]._groupName === groupName) { - //console.log('removing'); - // remove it and shorten the array we're looping through - handlers.splice(i, 1); - i--; - len--; - } - } - } - return this; -}; + this.logger = config.logger; + this._log = this.logger.log.bind(this.logger, 'LocalMedia:'); + this._logerror = this.logger.error.bind(this.logger, 'LocalMedia:'); -// Remove the given callback for `event` or all -// registered callbacks. -WildEmitter.prototype.off = function (event, fn) { - var callbacks = this.callbacks[event], - i; + this.screenSharingSupport = webrtc.screenSharing; - if (!callbacks) return this; + this.localStreams = []; + this.localScreens = []; - // remove all handlers - if (arguments.length === 1) { - delete this.callbacks[event]; - return this; + if (!webrtc.support) { + this._logerror('Your browser does not support local media capture.'); } +} - // remove specific handler - i = callbacks.indexOf(fn); - callbacks.splice(i, 1); - return this; -}; +util.inherits(LocalMedia, WildEmitter); -/// Emit `event` with the given args. -// also calls any `*` handlers -WildEmitter.prototype.emit = function (event) { - var args = [].slice.call(arguments, 1), - callbacks = this.callbacks[event], - specialCallbacks = this.getWildcardCallbacks(event), - i, - len, - item, - listeners; - if (callbacks) { - listeners = callbacks.slice(); - for (i = 0, len = listeners.length; i < len; ++i) { - if (listeners[i]) { - listeners[i].apply(this, args); - } else { - break; - } - } - } +LocalMedia.prototype.start = function (mediaConstraints, cb) { + var self = this; + var constraints = mediaConstraints || this.config.media; - if (specialCallbacks) { - len = specialCallbacks.length; - listeners = specialCallbacks.slice(); - for (i = 0, len = listeners.length; i < len; ++i) { - if (listeners[i]) { - listeners[i].apply(this, [event].concat(args)); - } else { - break; + getUserMedia(constraints, function (err, stream) { + if (!err) { + if (constraints.audio && self.config.detectSpeakingEvents) { + self.setupAudioMonitor(stream, self.config.harkOptions); } - } - } + self.localStreams.push(stream); - return this; -}; + if (self.config.autoAdjustMic) { + self.gainController = new GainController(stream); + // start out somewhat muted if we can track audio + self.setMicIfEnabled(0.5); + } -// Helper for for finding special wildcard event handlers that match the event -WildEmitter.prototype.getWildcardCallbacks = function (eventName) { - var item, - split, - result = []; + // TODO: might need to migrate to the video tracks onended + // FIXME: firefox does not seem to trigger this... + stream.onended = function () { + /* + var idx = self.localStreams.indexOf(stream); + if (idx > -1) { + self.localScreens.splice(idx, 1); + } + self.emit('localStreamStopped', stream); + */ + }; - for (item in this.callbacks) { - split = item.split('*'); - if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) { - result = result.concat(this.callbacks[item]); + self.emit('localStream', stream); } - } - return result; + if (cb) { + return cb(err, stream); + } + }); }; -},{}],16:[function(require,module,exports){ -// getUserMedia helper by @HenrikJoreteg -var func = (window.navigator.getUserMedia || - window.navigator.webkitGetUserMedia || - window.navigator.mozGetUserMedia || - window.navigator.msGetUserMedia); - - -module.exports = function (constraints, cb) { - var options; - var haveOpts = arguments.length === 2; - var defaultOpts = {video: true, audio: true}; - var error; - var denied = 'PERMISSION_DENIED'; - var notSatified = 'CONSTRAINT_NOT_SATISFIED'; - - // make constraints optional - if (!haveOpts) { - cb = constraints; - constraints = defaultOpts; +LocalMedia.prototype.stop = function (stream) { + var self = this; + // FIXME: duplicates cleanup code until fixed in FF + if (stream) { + stream.stop(); + self.emit('localStreamStopped', stream); + var idx = self.localStreams.indexOf(stream); + if (idx > -1) { + self.localStreams = self.localStreams.splice(idx, 1); + } + } else { + this.localStreams.forEach(function (stream) { + stream.stop(); + self.emit('localStreamStopped', stream); + }); + this.localStreams = []; } +}; - // treat lack of browser support like an error - if (!func) { - // throw proper error per spec - error = new Error('NavigatorUserMediaError'); - error.name = 'NOT_SUPPORTED_ERROR'; - return cb(error); - } +LocalMedia.prototype.startScreenShare = function (cb) { + var self = this; + getScreenMedia(function (err, stream) { + if (!err) { + self.localScreens.push(stream); - func.call(window.navigator, constraints, function (stream) { - cb(null, stream); - }, function (err) { - var error; - // coerce into an error object since FF gives us a string - // there are only two valid names according to the spec - // we coerce all non-denied to "constraint not satisfied". - if (typeof err === 'string') { - error = new Error('NavigatorUserMediaError'); - if (err === denied) { - error.name = denied; - } else { - error.name = notSatified; - } - } else { - // if we get an error object make sure '.name' property is set - // according to spec: http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermediaerror-and-navigatorusermediaerrorcallback - error = err; - if (!error.name) { - // this is likely chrome which - // sets a property called "ERROR_DENIED" on the error object - // if so we make sure to set a name - if (error[denied]) { - err.name = denied; - } else { - err.name = notSatified; + // TODO: might need to migrate to the video tracks onended + // Firefox does not support .onended but it does not support + // screensharing either + stream.onended = function () { + var idx = self.localScreens.indexOf(stream); + if (idx > -1) { + self.localScreens.splice(idx, 1); } - } + self.emit('localScreenStopped', stream); + }; + self.emit('localScreen', stream); } - cb(error); + // enable the callback + if (cb) { + return cb(err, stream); + } }); }; -},{}],17:[function(require,module,exports){ -/* -WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based -on @visionmedia's Emitter from UI Kit. - -Why? I wanted it standalone. +LocalMedia.prototype.stopScreenShare = function (stream) { + if (stream) { + stream.stop(); + } else { + this.localScreens.forEach(function (stream) { + stream.stop(); + }); + this.localScreens = []; + } +}; -I also wanted support for wildcard emitters like this: +// Audio controls +LocalMedia.prototype.mute = function () { + this._audioEnabled(false); + this.hardMuted = true; + this.emit('audioOff'); +}; -emitter.on('*', function (eventName, other, event, payloads) { - -}); +LocalMedia.prototype.unmute = function () { + this._audioEnabled(true); + this.hardMuted = false; + this.emit('audioOn'); +}; -emitter.on('somenamespace*', function (eventName, payloads) { - -}); +LocalMedia.prototype.setupAudioMonitor = function (stream, harkOptions) { + this._log('Setup audio'); + var audio = hark(stream, harkOptions); + var self = this; + var timeout; -Please note that callbacks triggered by wildcard registered events also get -the event name as the first argument. -*/ -module.exports = WildEmitter; + audio.on('speaking', function () { + self.emit('speaking'); + if (self.hardMuted) { + return; + } + self.setMicIfEnabled(1); + }); -function WildEmitter() { - this.callbacks = {}; -} + audio.on('stopped_speaking', function () { + if (timeout) { + clearTimeout(timeout); + } -// Listen on the given `event` with `fn`. Store a group name if present. -WildEmitter.prototype.on = function (event, groupName, fn) { - var hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, - func = hasGroup ? arguments[2] : arguments[1]; - func._groupName = group; - (this.callbacks[event] = this.callbacks[event] || []).push(func); - return this; + timeout = setTimeout(function () { + self.emit('stoppedSpeaking'); + if (self.hardMuted) { + return; + } + self.setMicIfEnabled(0.5); + }, 1000); + }); + audio.on('volume_change', function (volume, treshold) { + self.emit('volumeChange', volume, treshold); + }); }; -// Adds an `event` listener that will be invoked a single -// time then automatically removed. -WildEmitter.prototype.once = function (event, groupName, fn) { - var self = this, - hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, - func = hasGroup ? arguments[2] : arguments[1]; - function on() { - self.off(event, on); - func.apply(this, arguments); +// We do this as a seperate method in order to +// still leave the "setMicVolume" as a working +// method. +LocalMedia.prototype.setMicIfEnabled = function (volume) { + if (!this.config.autoAdjustMic) { + return; } - this.on(event, group, on); - return this; + this.gainController.setGain(volume); }; -// Unbinds an entire group -WildEmitter.prototype.releaseGroup = function (groupName) { - var item, i, len, handlers; - for (item in this.callbacks) { - handlers = this.callbacks[item]; - for (i = 0, len = handlers.length; i < len; i++) { - if (handlers[i]._groupName === groupName) { - //console.log('removing'); - // remove it and shorten the array we're looping through - handlers.splice(i, 1); - i--; - len--; - } - } - } - return this; +// Video controls +LocalMedia.prototype.pauseVideo = function () { + this._videoEnabled(false); + this.emit('videoOff'); }; - -// Remove the given callback for `event` or all -// registered callbacks. -WildEmitter.prototype.off = function (event, fn) { - var callbacks = this.callbacks[event], - i; - - if (!callbacks) return this; - - // remove all handlers - if (arguments.length === 1) { - delete this.callbacks[event]; - return this; - } - - // remove specific handler - i = callbacks.indexOf(fn); - callbacks.splice(i, 1); - return this; +LocalMedia.prototype.resumeVideo = function () { + this._videoEnabled(true); + this.emit('videoOn'); }; -/// Emit `event` with the given args. -// also calls any `*` handlers -WildEmitter.prototype.emit = function (event) { - var args = [].slice.call(arguments, 1), - callbacks = this.callbacks[event], - specialCallbacks = this.getWildcardCallbacks(event), - i, - len, - item, - listeners; - - if (callbacks) { - listeners = callbacks.slice(); - for (i = 0, len = listeners.length; i < len; ++i) { - if (listeners[i]) { - listeners[i].apply(this, args); - } else { - break; - } - } - } - - if (specialCallbacks) { - len = specialCallbacks.length; - listeners = specialCallbacks.slice(); - for (i = 0, len = listeners.length; i < len; ++i) { - if (listeners[i]) { - listeners[i].apply(this, [event].concat(args)); - } else { - break; - } - } - } - - return this; +// Combined controls +LocalMedia.prototype.pause = function () { + this._audioEnabled(false); + this.pauseVideo(); }; - -// Helper for for finding special wildcard event handlers that match the event -WildEmitter.prototype.getWildcardCallbacks = function (eventName) { - var item, - split, - result = []; - - for (item in this.callbacks) { - split = item.split('*'); - if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) { - result = result.concat(this.callbacks[item]); - } - } - return result; +LocalMedia.prototype.resume = function () { + this._audioEnabled(true); + this.resumeVideo(); }; -},{}],11:[function(require,module,exports){ -var _ = require('underscore'); -var util = require('util'); -var webrtc = require('webrtcsupport'); -var SJJ = require('sdp-jingle-json'); -var WildEmitter = require('wildemitter'); -var peerconn = require('traceablepeerconnection'); - -function PeerConnection(config, constraints) { - var self = this; - var item; - WildEmitter.call(this); - - config = config || {}; - config.iceServers = config.iceServers || []; - - this.pc = new peerconn(config, constraints); - // proxy events - this.pc.on('*', function () { - self.emit.apply(self, arguments); - }); - - // proxy some events directly - this.pc.onremovestream = this.emit.bind(this, 'removeStream'); - this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded'); - this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange'); - this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange'); - - // handle incoming ice and data channel events - this.pc.onaddstream = this._onAddStream.bind(this); - this.pc.onicecandidate = this._onIce.bind(this); - this.pc.ondatachannel = this._onDataChannel.bind(this); - - this.localDescription = { - contents: [] - }; - this.remoteDescription = { - contents: [] - }; - - this.localStream = null; - this.remoteStreams = []; - - this.config = { - debug: false, - ice: {}, - sid: '', - isInitiator: true, - sdpSessionID: Date.now(), - useJingle: false - }; - - // apply our config - for (item in config) { - this.config[item] = config[item]; - } - - if (this.config.debug) { - this.on('*', function (eventName, event) { - var logger = config.logger || console; - logger.log('PeerConnection event:', arguments); +// Internal methods for enabling/disabling audio/video +LocalMedia.prototype._audioEnabled = function (bool) { + // work around for chrome 27 bug where disabling tracks + // doesn't seem to work (works in canary, remove when working) + this.setMicIfEnabled(bool ? 1 : 0); + this.localStreams.forEach(function (stream) { + stream.getAudioTracks().forEach(function (track) { + track.enabled = !!bool; }); - } -} - -util.inherits(PeerConnection, WildEmitter); - -if (PeerConnection.prototype.__defineGetter__) { - PeerConnection.prototype.__defineGetter__('signalingState', function () { - return this.pc.signalingState; - }); - PeerConnection.prototype.__defineGetter__('iceConnectionState', function () { - return this.pc.iceConnectionState; }); -} - -// Add a stream to the peer connection object -PeerConnection.prototype.addStream = function (stream) { - this.localStream = stream; - this.pc.addStream(stream); }; - - -// Init and add ice candidate object with correct constructor -PeerConnection.prototype.processIce = function (update, cb) { - cb = cb || function () {}; - var self = this; - - if (update.contents) { - var contentNames = _.pluck(this.remoteDescription.contents, 'name'); - var contents = update.contents; - - contents.forEach(function (content) { - var transport = content.transport || {}; - var candidates = transport.candidates || []; - var mline = contentNames.indexOf(content.name); - var mid = content.name; - - candidates.forEach(function (candidate) { - console.log('addicecandidate'); - var iceCandidate = SJJ.toCandidateSDP(candidate) + '\r\n'; - self.pc.addIceCandidate(new webrtc.IceCandidate({ - candidate: iceCandidate, - sdpMLineIndex: mline, - sdpMid: mid - }) - /* not yet, breaks Chrome M32 */ - /* - , function () { - // well, this success callback is pretty meaningless - }, - function (err) { - self.emit('error', err); - } - */ - ); - }); +LocalMedia.prototype._videoEnabled = function (bool) { + this.localStreams.forEach(function (stream) { + stream.getVideoTracks().forEach(function (track) { + track.enabled = !!bool; }); - } else { - self.pc.addIceCandidate(new webrtc.IceCandidate(update.candidate)); - } - cb(); -}; - -// Generate and emit an offer with the given constraints -PeerConnection.prototype.offer = function (constraints, cb) { - var self = this; - var hasConstraints = arguments.length === 2; - var mediaConstraints = hasConstraints ? constraints : { - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - } - }; - cb = hasConstraints ? cb : constraints; - cb = cb || function () {}; - - // Actually generate the offer - this.pc.createOffer( - function (offer) { - self.pc.setLocalDescription(offer, - function () { - var jingle; - var expandedOffer = { - type: 'offer', - sdp: offer.sdp - }; - if (self.config.useJingle) { - jingle = SJJ.toSessionJSON(offer.sdp, self.config.isInitiator ? 'initiator' : 'responder'); - jingle.sid = self.config.sid; - self.localDescription = jingle; - - // Save ICE credentials - _.each(jingle.contents, function (content) { - var transport = content.transport || {}; - if (transport.ufrag) { - self.config.ice[content.name] = { - ufrag: transport.ufrag, - pwd: transport.pwd - }; - } - }); - - expandedOffer.jingle = jingle; - } - - self.emit('offer', expandedOffer); - cb(null, expandedOffer); - }, - function (err) { - self.emit('error', err); - cb(err); - } - ); - }, - function (err) { - self.emit('error', err); - cb(err); - }, - mediaConstraints - ); -}; - - -// Process an incoming offer so that ICE may proceed before deciding -// to answer the request. -PeerConnection.prototype.handleOffer = function (offer, cb) { - cb = cb || function () {}; - var self = this; - offer.type = 'offer'; - if (offer.jingle) { - offer.sdp = SJJ.toSessionSDP(offer.jingle, self.config.sdpSessionID); - } - self.pc.setRemoteDescription(new webrtc.SessionDescription(offer), function () { - cb(); - }, cb); -}; - -// Answer an offer with audio only -PeerConnection.prototype.answerAudioOnly = function (cb) { - var mediaConstraints = { - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: false - } - }; - this._answer(mediaConstraints, cb); -}; - -// Answer an offer without offering to recieve -PeerConnection.prototype.answerBroadcastOnly = function (cb) { - var mediaConstraints = { - mandatory: { - OfferToReceiveAudio: false, - OfferToReceiveVideo: false - } - }; - this._answer(mediaConstraints, cb); + }); }; -// Answer an offer with given constraints default is audio/video -PeerConnection.prototype.answer = function (constraints, cb) { - var self = this; - var hasConstraints = arguments.length === 2; - var callback = hasConstraints ? cb : constraints; - var mediaConstraints = hasConstraints ? constraints : { - mandatory: { - OfferToReceiveAudio: true, - OfferToReceiveVideo: true - } - }; - - this._answer(mediaConstraints, callback); +// check if all audio streams are enabled +LocalMedia.prototype.isAudioEnabled = function () { + var enabled = true; + this.localStreams.forEach(function (stream) { + stream.getAudioTracks().forEach(function (track) { + enabled = enabled && track.enabled; + }); + }); + return enabled; }; -// Process an answer -PeerConnection.prototype.handleAnswer = function (answer, cb) { - cb = cb || function () {}; - var self = this; - if (answer.jingle) { - answer.sdp = SJJ.toSessionSDP(answer.jingle, self.config.sdpSessionID); - self.remoteDescription = answer.jingle; - } - self.pc.setRemoteDescription( - new webrtc.SessionDescription(answer), - function () { - cb(null); - }, - cb - ); +// check if all video streams are enabled +LocalMedia.prototype.isVideoEnabled = function () { + var enabled = true; + this.localStreams.forEach(function (stream) { + stream.getVideoTracks().forEach(function (track) { + enabled = enabled && track.enabled; + }); + }); + return enabled; }; -// Close the peer connection -PeerConnection.prototype.close = function () { - this.pc.close(); - this.emit('close'); -}; +// Backwards Compat +LocalMedia.prototype.startLocalMedia = LocalMedia.prototype.start; +LocalMedia.prototype.stopLocalMedia = LocalMedia.prototype.stop; -// Internal code sharing for various types of answer methods -PeerConnection.prototype._answer = function (constraints, cb) { - cb = cb || function () {}; - var self = this; - if (!this.pc.remoteDescription) { - // the old API is used, call handleOffer - throw new Error('remoteDescription not set'); +// fallback for old .localStream behaviour +Object.defineProperty(LocalMedia.prototype, 'localStream', { + get: function () { + return this.localStreams.length > 0 ? this.localStreams[0] : null; } - self.pc.createAnswer( - function (answer) { - self.pc.setLocalDescription(answer, - function () { - var expandedAnswer = { - type: 'answer', - sdp: answer.sdp - }; - if (self.config.useJingle) { - var jingle = SJJ.toSessionJSON(answer.sdp); - jingle.sid = self.config.sid; - self.localDescription = jingle; - expandedAnswer.jingle = jingle; - } - self.emit('answer', expandedAnswer); - cb(null, expandedAnswer); - }, - function (err) { - self.emit('error', err); - cb(err); - } - ); - }, - function (err) { - self.emit('error', err); - cb(err); - }, - constraints - ); -}; - -// Internal method for emitting ice candidates on our peer object -PeerConnection.prototype._onIce = function (event) { - var self = this; - if (event.candidate) { - var ice = event.candidate; - - var expandedCandidate = { - candidate: event.candidate - }; - - if (self.config.useJingle) { - if (!self.config.ice[ice.sdpMid]) { - var jingle = SJJ.toSessionJSON(self.pc.localDescription.sdp, self.config.isInitiator ? 'initiator' : 'responder'); - _.each(jingle.contents, function (content) { - var transport = content.transport || {}; - if (transport.ufrag) { - self.config.ice[content.name] = { - ufrag: transport.ufrag, - pwd: transport.pwd - }; - } - }); - } - expandedCandidate.jingle = { - contents: [{ - name: ice.sdpMid, - creator: self.config.isInitiator ? 'initiator' : 'responder', - transport: { - transType: 'iceUdp', - ufrag: self.config.ice[ice.sdpMid].ufrag, - pwd: self.config.ice[ice.sdpMid].pwd, - candidates: [ - SJJ.toCandidateJSON(ice.candidate) - ] - } - }] - }; - } - - this.emit('ice', expandedCandidate); - } else { - this.emit('endOfCandidates'); +}); +// fallback for old .localScreen behaviour +Object.defineProperty(LocalMedia.prototype, 'localScreen', { + get: function () { + return this.localScreens.length > 0 ? this.localScreens[0] : null; } -}; - -// Internal method for processing a new data channel being added by the -// other peer. -PeerConnection.prototype._onDataChannel = function (event) { - this.emit('addChannel', event.channel); -}; - -// Internal handling of adding stream -PeerConnection.prototype._onAddStream = function (event) { - this.remoteStreams.push(event.stream); - this.emit('addStream', event); -}; +}); -// Create a data channel spec reference: -// http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCDataChannelInit -PeerConnection.prototype.createDataChannel = function (name, opts) { - var channel = this.pc.createDataChannel(name, opts); - return channel; -}; +module.exports = LocalMedia; -module.exports = PeerConnection; +},{"getscreenmedia":17,"getusermedia":14,"hark":16,"mediastream-gain":18,"mockconsole":4,"util":9,"webrtcsupport":8,"wildemitter":7}],12:[function(require,module,exports){ +var _ = require('underscore'); +var util = require('util'); +var webrtc = require('webrtcsupport'); +var SJJ = require('sdp-jingle-json'); +var WildEmitter = require('wildemitter'); +var peerconn = require('traceablepeerconnection'); -},{"sdp-jingle-json":18,"traceablepeerconnection":19,"underscore":14,"util":8,"webrtcsupport":10,"wildemitter":15}],18:[function(require,module,exports){ -var tosdp = require('./lib/tosdp'); -var tojson = require('./lib/tojson'); +function PeerConnection(config, constraints) { + var self = this; + var item; + WildEmitter.call(this); + config = config || {}; + config.iceServers = config.iceServers || []; -exports.toSessionSDP = tosdp.toSessionSDP; -exports.toMediaSDP = tosdp.toMediaSDP; -exports.toCandidateSDP = tosdp.toCandidateSDP; + this.pc = new peerconn(config, constraints); + // proxy events + this.pc.on('*', function () { + self.emit.apply(self, arguments); + }); -exports.toSessionJSON = tojson.toSessionJSON; -exports.toMediaJSON = tojson.toMediaJSON; -exports.toCandidateJSON = tojson.toCandidateJSON; + // proxy some events directly + this.pc.onremovestream = this.emit.bind(this, 'removeStream'); + this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded'); + this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange'); + this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange'); -},{"./lib/tojson":21,"./lib/tosdp":20}],12:[function(require,module,exports){ -var util = require('util'); -var hark = require('hark'); -var webrtc = require('webrtcsupport'); -var getUserMedia = require('getusermedia'); -var getScreenMedia = require('getscreenmedia'); -var WildEmitter = require('wildemitter'); -var GainController = require('mediastream-gain'); -var mockconsole = require('mockconsole'); + // handle incoming ice and data channel events + this.pc.onaddstream = this._onAddStream.bind(this); + this.pc.onicecandidate = this._onIce.bind(this); + this.pc.ondatachannel = this._onDataChannel.bind(this); + this.localDescription = { + contents: [] + }; + this.remoteDescription = { + contents: [] + }; -function LocalMedia(opts) { - WildEmitter.call(this); + this.localStream = null; + this.remoteStreams = []; - var config = this.config = { - autoAdjustMic: false, - detectSpeakingEvents: true, - media: { - audio: true, - video: true - }, - logger: mockconsole + this.config = { + debug: false, + ice: {}, + sid: '', + isInitiator: true, + sdpSessionID: Date.now(), + useJingle: false }; - var item; - for (item in opts) { - this.config[item] = opts[item]; + // apply our config + for (item in config) { + this.config[item] = config[item]; } - this.logger = config.logger; - this._log = this.logger.log.bind(this.logger, 'LocalMedia:'); - this._logerror = this.logger.error.bind(this.logger, 'LocalMedia:'); - - this.screenSharingSupport = webrtc.screenSharing; + if (this.config.debug) { + this.on('*', function (eventName, event) { + var logger = config.logger || console; + logger.log('PeerConnection event:', arguments); + }); + } + this.hadLocalStunCandidate = false; + this.hadRemoteStunCandidate = false; + this.hadLocalRelayCandidate = false; + this.hadRemoteRelayCandidate = false; +} - this.localStreams = []; - this.localScreens = []; +util.inherits(PeerConnection, WildEmitter); - if (!webrtc.support) { - this._logerror('Your browser does not support local media capture.'); +Object.defineProperty(PeerConnection.prototype, 'signalingState', { + get: function () { + return this.pc.signalingState; } -} +}); +Object.defineProperty(PeerConnection.prototype, 'iceConnectionState', { + get: function () { + return this.pc.iceConnectionState; + } +}); -util.inherits(LocalMedia, WildEmitter); +// Add a stream to the peer connection object +PeerConnection.prototype.addStream = function (stream) { + this.localStream = stream; + this.pc.addStream(stream); +}; -LocalMedia.prototype.start = function (mediaConstraints, cb) { +// Init and add ice candidate object with correct constructor +PeerConnection.prototype.processIce = function (update, cb) { + cb = cb || function () {}; var self = this; - var constraints = mediaConstraints || this.config.media; - getUserMedia(constraints, function (err, stream) { - if (!err) { - if (constraints.audio && self.config.detectSpeakingEvents) { - self.setupAudioMonitor(stream); - } - self.localStreams.push(stream); + if (update.contents) { + var contentNames = _.pluck(this.remoteDescription.contents, 'name'); + var contents = update.contents; - if (self.config.autoAdjustMic) { - self.gainController = new GainController(stream); - // start out somewhat muted if we can track audio - self.setMicIfEnabled(0.5); - } + contents.forEach(function (content) { + var transport = content.transport || {}; + var candidates = transport.candidates || []; + var mline = contentNames.indexOf(content.name); + var mid = content.name; - // TODO: might need to migrate to the video tracks onended - // FIXME: firefox does not seem to trigger this... - stream.onended = function () { + candidates.forEach(function (candidate) { + var iceCandidate = SJJ.toCandidateSDP(candidate) + '\r\n'; + self.pc.addIceCandidate(new webrtc.IceCandidate({ + candidate: iceCandidate, + sdpMLineIndex: mline, + sdpMid: mid + }) + /* not yet, breaks Chrome M32 */ /* - var idx = self.localStreams.indexOf(stream); - if (idx > -1) { - self.localScreens.splice(idx, 1); + , function () { + // well, this success callback is pretty meaningless + }, + function (err) { + self.emit('error', err); } - self.emit('localStreamStopped', stream); */ - }; - - self.emit('localStream', stream); + ); + if (candidate.type === 'srflx') { + self.hadRemoteStunCandidate = true; + } + else if (candidate.type === 'relay') { + self.hadRemoteRelayCandidate = true; + } + }); + }); + } else { + self.pc.addIceCandidate(new webrtc.IceCandidate(update.candidate)); + if (update.candidate.candidate.indexOf('typ srflx') !== -1) { + self.hadRemoteStunCandidate = true; } - if (cb) { - return cb(err, stream); + else if (update.candidate.candidate.indexOf('typ relay') !== -1) { + self.hadRemoteRelayCandidate = true; } - }); + } + cb(); }; -LocalMedia.prototype.stop = function (stream) { +// Generate and emit an offer with the given constraints +PeerConnection.prototype.offer = function (constraints, cb) { var self = this; - // FIXME: duplicates cleanup code until fixed in FF - if (stream) { - stream.stop(); - self.emit('localStreamStopped', stream); - var idx = self.localStreams.indexOf(stream); - if (idx > -1) { - self.localStreams = self.localStreams.splice(idx, 1); - } - } else { - this.localStreams.forEach(function (stream) { - stream.stop(); - self.emit('localStreamStopped', stream); - }); - this.localStreams = []; + var hasConstraints = arguments.length === 2; + var mediaConstraints = hasConstraints ? constraints : { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true + } + }; + cb = hasConstraints ? cb : constraints; + cb = cb || function () {}; + + // Actually generate the offer + this.pc.createOffer( + function (offer) { + self.pc.setLocalDescription(offer, + function () { + var jingle; + var expandedOffer = { + type: 'offer', + sdp: offer.sdp + }; + if (self.config.useJingle) { + jingle = SJJ.toSessionJSON(offer.sdp, self.config.isInitiator ? 'initiator' : 'responder'); + jingle.sid = self.config.sid; + self.localDescription = jingle; + + // Save ICE credentials + _.each(jingle.contents, function (content) { + var transport = content.transport || {}; + if (transport.ufrag) { + self.config.ice[content.name] = { + ufrag: transport.ufrag, + pwd: transport.pwd + }; + } + }); + + expandedOffer.jingle = jingle; + } + + self.emit('offer', expandedOffer); + cb(null, expandedOffer); + }, + function (err) { + self.emit('error', err); + cb(err); + } + ); + }, + function (err) { + self.emit('error', err); + cb(err); + }, + mediaConstraints + ); +}; + + +// Process an incoming offer so that ICE may proceed before deciding +// to answer the request. +PeerConnection.prototype.handleOffer = function (offer, cb) { + cb = cb || function () {}; + var self = this; + offer.type = 'offer'; + if (offer.jingle) { + offer.sdp = SJJ.toSessionSDP(offer.jingle, self.config.sdpSessionID); + self.remoteDescription = offer.jingle; } + self.pc.setRemoteDescription(new webrtc.SessionDescription(offer), function () { + cb(); + }, cb); +}; + +// Answer an offer with audio only +PeerConnection.prototype.answerAudioOnly = function (cb) { + var mediaConstraints = { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: false + } + }; + this._answer(mediaConstraints, cb); }; -LocalMedia.prototype.startScreenShare = function (cb) { - var self = this; - getScreenMedia(function (err, stream) { - if (!err) { - self.localScreens.push(stream); +// Answer an offer without offering to recieve +PeerConnection.prototype.answerBroadcastOnly = function (cb) { + var mediaConstraints = { + mandatory: { + OfferToReceiveAudio: false, + OfferToReceiveVideo: false + } + }; + this._answer(mediaConstraints, cb); +}; - // TODO: might need to migrate to the video tracks onended - // Firefox does not support .onended but it does not support - // screensharing either - stream.onended = function () { - var idx = self.localScreens.indexOf(stream); - if (idx > -1) { - self.localScreens.splice(idx, 1); - } - self.emit('localScreenStopped', stream); - }; - self.emit('localScreen', stream); - } +// Answer an offer with given constraints default is audio/video +PeerConnection.prototype.answer = function (constraints, cb) { + var self = this; + var hasConstraints = arguments.length === 2; + var callback = hasConstraints ? cb : constraints; + var mediaConstraints = hasConstraints ? constraints : { + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true + } + }; - // enable the callback - if (cb) { - return cb(err, stream); - } - }); + this._answer(mediaConstraints, callback); }; -LocalMedia.prototype.stopScreenShare = function (stream) { - if (stream) { - stream.stop(); - } else { - this.localScreens.forEach(function (stream) { - stream.stop(); - }); - this.localScreens = []; +// Process an answer +PeerConnection.prototype.handleAnswer = function (answer, cb) { + cb = cb || function () {}; + var self = this; + if (answer.jingle) { + answer.sdp = SJJ.toSessionSDP(answer.jingle, self.config.sdpSessionID); + self.remoteDescription = answer.jingle; } + self.pc.setRemoteDescription( + new webrtc.SessionDescription(answer), + function () { + cb(null); + }, + cb + ); }; -// Audio controls -LocalMedia.prototype.mute = function () { - this._audioEnabled(false); - this.hardMuted = true; - this.emit('audioOff'); +// Close the peer connection +PeerConnection.prototype.close = function () { + this.pc.close(); + this.emit('close'); }; -LocalMedia.prototype.unmute = function () { - this._audioEnabled(true); - this.hardMuted = false; - this.emit('audioOn'); +// Internal code sharing for various types of answer methods +PeerConnection.prototype._answer = function (constraints, cb) { + cb = cb || function () {}; + var self = this; + if (!this.pc.remoteDescription) { + // the old API is used, call handleOffer + throw new Error('remoteDescription not set'); + } + self.pc.createAnswer( + function (answer) { + self.pc.setLocalDescription(answer, + function () { + var expandedAnswer = { + type: 'answer', + sdp: answer.sdp + }; + if (self.config.useJingle) { + var jingle = SJJ.toSessionJSON(answer.sdp); + jingle.sid = self.config.sid; + self.localDescription = jingle; + expandedAnswer.jingle = jingle; + } + self.emit('answer', expandedAnswer); + cb(null, expandedAnswer); + }, + function (err) { + self.emit('error', err); + cb(err); + } + ); + }, + function (err) { + self.emit('error', err); + cb(err); + }, + constraints + ); }; -LocalMedia.prototype.setupAudioMonitor = function (stream) { - this._log('Setup audio'); - var audio = hark(stream); +// Internal method for emitting ice candidates on our peer object +PeerConnection.prototype._onIce = function (event) { var self = this; - var timeout; - - audio.on('speaking', function () { - self.emit('speaking'); - if (self.hardMuted) { - return; - } - self.setMicIfEnabled(1); - }); + if (event.candidate) { + var ice = event.candidate; - audio.on('stopped_speaking', function () { - if (timeout) { - clearTimeout(timeout); - } + var expandedCandidate = { + candidate: event.candidate + }; - timeout = setTimeout(function () { - self.emit('stoppedSpeaking'); - if (self.hardMuted) { - return; + if (self.config.useJingle) { + if (!self.config.ice[ice.sdpMid]) { + var jingle = SJJ.toSessionJSON(self.pc.localDescription.sdp, self.config.isInitiator ? 'initiator' : 'responder'); + _.each(jingle.contents, function (content) { + var transport = content.transport || {}; + if (transport.ufrag) { + self.config.ice[content.name] = { + ufrag: transport.ufrag, + pwd: transport.pwd + }; + } + }); } - self.setMicIfEnabled(0.5); - }, 1000); - }); - audio.on('volume_change', function (volume, treshold) { - self.emit('volumeChange', volume, treshold); - }); -}; + expandedCandidate.jingle = { + contents: [{ + name: ice.sdpMid, + creator: self.config.isInitiator ? 'initiator' : 'responder', + transport: { + transType: 'iceUdp', + ufrag: self.config.ice[ice.sdpMid].ufrag, + pwd: self.config.ice[ice.sdpMid].pwd, + candidates: [ + SJJ.toCandidateJSON(ice.candidate) + ] + } + }] + }; + } + if (ice.candidate.indexOf('typ srflx') !== -1) { + this.hadLocalStunCandidate = true; + } + else if (ice.candidate.indexOf('typ relay') !== -1) { + this.hadLocalRelayCandidate = true; + } -// We do this as a seperate method in order to -// still leave the "setMicVolume" as a working -// method. -LocalMedia.prototype.setMicIfEnabled = function (volume) { - if (!this.config.autoAdjustMic) { - return; + this.emit('ice', expandedCandidate); + } else { + this.emit('endOfCandidates'); } - this.gainController.setGain(volume); -}; - -// Video controls -LocalMedia.prototype.pauseVideo = function () { - this._videoEnabled(false); - this.emit('videoOff'); -}; -LocalMedia.prototype.resumeVideo = function () { - this._videoEnabled(true); - this.emit('videoOn'); }; -// Combined controls -LocalMedia.prototype.pause = function () { - this._audioEnabled(false); - this.pauseVideo(); -}; -LocalMedia.prototype.resume = function () { - this._audioEnabled(true); - this.resumeVideo(); +// Internal method for processing a new data channel being added by the +// other peer. +PeerConnection.prototype._onDataChannel = function (event) { + this.emit('addChannel', event.channel); }; -// Internal methods for enabling/disabling audio/video -LocalMedia.prototype._audioEnabled = function (bool) { - // work around for chrome 27 bug where disabling tracks - // doesn't seem to work (works in canary, remove when working) - this.setMicIfEnabled(bool ? 1 : 0); - this.localStreams.forEach(function (stream) { - stream.getAudioTracks().forEach(function (track) { - track.enabled = !!bool; - }); - }); -}; -LocalMedia.prototype._videoEnabled = function (bool) { - this.localStreams.forEach(function (stream) { - stream.getVideoTracks().forEach(function (track) { - track.enabled = !!bool; - }); - }); +// Internal handling of adding stream +PeerConnection.prototype._onAddStream = function (event) { + this.remoteStreams.push(event.stream); + this.emit('addStream', event); }; -// check if all audio streams are enabled -LocalMedia.prototype.isAudioEnabled = function () { - var enabled = true; - this.localStreams.forEach(function (stream) { - stream.getAudioTracks().forEach(function (track) { - enabled = enabled && track.enabled; - }); - }); - return enabled; +// Create a data channel spec reference: +// http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCDataChannelInit +PeerConnection.prototype.createDataChannel = function (name, opts) { + var channel = this.pc.createDataChannel(name, opts); + return channel; }; -// check if all video streams are enabled -LocalMedia.prototype.isVideoEnabled = function () { - var enabled = true; - this.localStreams.forEach(function (stream) { - stream.getVideoTracks().forEach(function (track) { - enabled = enabled && track.enabled; +// a wrapper around getStats which hides the differences (where possible) +PeerConnection.prototype.getStats = function (cb) { + if (webrtc.prefix === 'moz') { + this.pc.getStats( + function (res) { + var items = []; + res.forEach(function (result) { + items.push(result); + }); + cb(null, items); + }, + cb + ); + } else { + this.pc.getStats(function (res) { + var items = []; + res.result().forEach(function (result) { + var item = {}; + result.names().forEach(function (name) { + item[name] = result.stat(name); + }); + item.id = result.id; + item.type = result.type; + item.timestamp = result.timestamp; + items.push(item); + }); + cb(null, items); }); - }); - return enabled; + } }; -// Backwards Compat -LocalMedia.prototype.startLocalMedia = LocalMedia.prototype.start; -LocalMedia.prototype.stopLocalMedia = LocalMedia.prototype.stop; +module.exports = PeerConnection; -// fallback for old .localStream behaviour -Object.defineProperty(LocalMedia.prototype, 'localStream', { - get: function () { - return this.localStreams.length > 0 ? this.localStreams[0] : null; - } -}); -// fallback for old .localScreen behaviour -Object.defineProperty(LocalMedia.prototype, 'localScreen', { - get: function () { - return this.localScreens.length > 0 ? this.localScreens[0] : null; - } -}); +},{"sdp-jingle-json":19,"traceablepeerconnection":20,"underscore":15,"util":9,"webrtcsupport":8,"wildemitter":7}],19:[function(require,module,exports){ +var tosdp = require('./lib/tosdp'); +var tojson = require('./lib/tojson'); -module.exports = LocalMedia; -},{"getscreenmedia":23,"getusermedia":16,"hark":22,"mediastream-gain":24,"mockconsole":4,"util":8,"webrtcsupport":10,"wildemitter":17}],20:[function(require,module,exports){ +exports.toSessionSDP = tosdp.toSessionSDP; +exports.toMediaSDP = tosdp.toMediaSDP; +exports.toCandidateSDP = tosdp.toCandidateSDP; + +exports.toSessionJSON = tojson.toSessionJSON; +exports.toMediaJSON = tojson.toMediaJSON; +exports.toCandidateJSON = tojson.toCandidateJSON; + +},{"./lib/tojson":22,"./lib/tosdp":21}],21:[function(require,module,exports){ var senders = { 'initiator': 'sendonly', 'responder': 'recvonly', @@ -7983,306 +7771,132 @@ exports.toMediaSDP = function (content) { var ssrcGroups = desc.sourceGroups || []; ssrcGroups.forEach(function (ssrcGroup) { - sdp.push('a=ssrc-group:' + ssrcGroup.semantics + ' ' + ssrcGroup.sources.join(' ')); - }); - - var ssrcs = desc.sources || []; - ssrcs.forEach(function (ssrc) { - for (var i = 0; i < ssrc.parameters.length; i++) { - var param = ssrc.parameters[i]; - sdp.push('a=ssrc:' + (ssrc.ssrc || desc.ssrc) + ' ' + param.key + (param.value ? (':' + param.value) : '')); - } - }); - - var candidates = transport.candidates || []; - candidates.forEach(function (candidate) { - sdp.push(exports.toCandidateSDP(candidate)); - }); - - return sdp.join('\r\n'); -}; - -exports.toCandidateSDP = function (candidate) { - var sdp = []; - - sdp.push(candidate.foundation); - sdp.push(candidate.component); - sdp.push(candidate.protocol); - sdp.push(candidate.priority); - sdp.push(candidate.ip); - sdp.push(candidate.port); - - var type = candidate.type; - sdp.push('typ'); - sdp.push(type); - if (type === 'srflx' || type === 'prflx' || type === 'relay') { - if (candidate.relAddr && candidate.relPort) { - sdp.push('raddr'); - sdp.push(candidate.relAddr); - sdp.push('rport'); - sdp.push(candidate.relPort); - } - } - - sdp.push('generation'); - sdp.push(candidate.generation || '0'); - - return 'a=candidate:' + sdp.join(' '); -}; - -},{}],22:[function(require,module,exports){ -var WildEmitter = require('wildemitter'); - -function getMaxVolume (analyser, fftBins) { - var maxVolume = -Infinity; - analyser.getFloatFrequencyData(fftBins); - - for(var i=0, ii=fftBins.length; i < ii; i++) { - if (fftBins[i] > maxVolume && fftBins[i] < 0) { - maxVolume = fftBins[i]; - } - }; - - return maxVolume; -} - - -var audioContextType = window.webkitAudioContext || window.AudioContext; -// use a single audio context due to hardware limits -var audioContext = null; -module.exports = function(stream, options) { - var harker = new WildEmitter(); - - - // make it not break in non-supported browsers - if (!audioContextType) return harker; - - //Config - var options = options || {}, - smoothing = (options.smoothing || 0.5), - interval = (options.interval || 100), - threshold = options.threshold, - play = options.play, - running = true; - - //Setup Audio Context - if (!audioContext) { - audioContext = new audioContextType(); - } - var sourceNode, fftBins, analyser; - - analyser = audioContext.createAnalyser(); - analyser.fftSize = 512; - analyser.smoothingTimeConstant = smoothing; - fftBins = new Float32Array(analyser.fftSize); - - if (stream.jquery) stream = stream[0]; - if (stream instanceof HTMLAudioElement) { - //Audio Tag - sourceNode = audioContext.createMediaElementSource(stream); - if (typeof play === 'undefined') play = true; - threshold = threshold || -65; - } else { - //WebRTC Stream - sourceNode = audioContext.createMediaStreamSource(stream); - threshold = threshold || -45; - } - - sourceNode.connect(analyser); - if (play) analyser.connect(audioContext.destination); - - harker.speaking = false; - - harker.setThreshold = function(t) { - threshold = t; - }; - - harker.setInterval = function(i) { - interval = i; - }; - - harker.stop = function() { - running = false; - harker.emit('volume_change', -100, threshold); - if (harker.speaking) { - harker.speaking = false; - harker.emit('stopped_speaking'); - } - }; - - // Poll the analyser node to determine if speaking - // and emit events if changed - var looper = function() { - setTimeout(function() { - - //check if stop has been called - if(!running) { - return; - } - - var currentVolume = getMaxVolume(analyser, fftBins); - - harker.emit('volume_change', currentVolume, threshold); - - if (currentVolume > threshold) { - if (!harker.speaking) { - harker.speaking = true; - harker.emit('speaking'); - } - } else { - if (harker.speaking) { - harker.speaking = false; - harker.emit('stopped_speaking'); - } - } - - looper(); - }, interval); - }; - looper(); - - - return harker; -} - -},{"wildemitter":25}],25:[function(require,module,exports){ -/* -WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based -on @visionmedia's Emitter from UI Kit. - -Why? I wanted it standalone. - -I also wanted support for wildcard emitters like this: - -emitter.on('*', function (eventName, other, event, payloads) { - -}); - -emitter.on('somenamespace*', function (eventName, payloads) { - -}); - -Please note that callbacks triggered by wildcard registered events also get -the event name as the first argument. -*/ -module.exports = WildEmitter; - -function WildEmitter() { - this.callbacks = {}; -} - -// Listen on the given `event` with `fn`. Store a group name if present. -WildEmitter.prototype.on = function (event, groupName, fn) { - var hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, - func = hasGroup ? arguments[2] : arguments[1]; - func._groupName = group; - (this.callbacks[event] = this.callbacks[event] || []).push(func); - return this; -}; - -// Adds an `event` listener that will be invoked a single -// time then automatically removed. -WildEmitter.prototype.once = function (event, groupName, fn) { - var self = this, - hasGroup = (arguments.length === 3), - group = hasGroup ? arguments[1] : undefined, - func = hasGroup ? arguments[2] : arguments[1]; - function on() { - self.off(event, on); - func.apply(this, arguments); - } - this.on(event, group, on); - return this; -}; - -// Unbinds an entire group -WildEmitter.prototype.releaseGroup = function (groupName) { - var item, i, len, handlers; - for (item in this.callbacks) { - handlers = this.callbacks[item]; - for (i = 0, len = handlers.length; i < len; i++) { - if (handlers[i]._groupName === groupName) { - //console.log('removing'); - // remove it and shorten the array we're looping through - handlers.splice(i, 1); - i--; - len--; - } + sdp.push('a=ssrc-group:' + ssrcGroup.semantics + ' ' + ssrcGroup.sources.join(' ')); + }); + + var ssrcs = desc.sources || []; + ssrcs.forEach(function (ssrc) { + for (var i = 0; i < ssrc.parameters.length; i++) { + var param = ssrc.parameters[i]; + sdp.push('a=ssrc:' + (ssrc.ssrc || desc.ssrc) + ' ' + param.key + (param.value ? (':' + param.value) : '')); } - } - return this; + }); + + var candidates = transport.candidates || []; + candidates.forEach(function (candidate) { + sdp.push(exports.toCandidateSDP(candidate)); + }); + + return sdp.join('\r\n'); }; -// Remove the given callback for `event` or all -// registered callbacks. -WildEmitter.prototype.off = function (event, fn) { - var callbacks = this.callbacks[event], - i; - - if (!callbacks) return this; +exports.toCandidateSDP = function (candidate) { + var sdp = []; - // remove all handlers - if (arguments.length === 1) { - delete this.callbacks[event]; - return this; + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type === 'srflx' || type === 'prflx' || type === 'relay') { + if (candidate.relAddr && candidate.relPort) { + sdp.push('raddr'); + sdp.push(candidate.relAddr); + sdp.push('rport'); + sdp.push(candidate.relPort); + } } - // remove specific handler - i = callbacks.indexOf(fn); - callbacks.splice(i, 1); - return this; + sdp.push('generation'); + sdp.push(candidate.generation || '0'); + + return 'a=candidate:' + sdp.join(' '); }; -// Emit `event` with the given args. -// also calls any `*` handlers -WildEmitter.prototype.emit = function (event) { - var args = [].slice.call(arguments, 1), - callbacks = this.callbacks[event], - specialCallbacks = this.getWildcardCallbacks(event), - i, - len, - item; +},{}],17:[function(require,module,exports){ +// getScreenMedia helper by @HenrikJoreteg +var getUserMedia = require('getusermedia'); - if (callbacks) { - for (i = 0, len = callbacks.length; i < len; ++i) { - if (callbacks[i]) { - callbacks[i].apply(this, args); - } else { - break; - } - } +// cache for constraints and callback +var cache = {}; + +module.exports = function (constraints, cb) { + var hasConstraints = arguments.length === 2; + var callback = hasConstraints ? cb : constraints; + var error; + + if (typeof window === 'undefined' || window.location.protocol === 'http:') { + error = new Error('NavigatorUserMediaError'); + error.name = 'HTTPS_REQUIRED'; + return callback(error); } - if (specialCallbacks) { - for (i = 0, len = specialCallbacks.length; i < len; ++i) { - if (specialCallbacks[i]) { - specialCallbacks[i].apply(this, [event].concat(args)); - } else { - break; - } + if (window.navigator.userAgent.match('Chrome')) { + var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); + var maxver = 33; + // "known‶ bug in chrome 34 on linux + if (window.navigator.userAgent.match('Linux')) maxver = 34; + if (chromever >= 26 && chromever <= maxver) { + // chrome 26 - chrome 33 way to do it -- requires bad chrome://flags + constraints = (hasConstraints && constraints) || { + video: { + mandatory: { + maxWidth: window.screen.width, + maxHeight: window.screen.height, + maxFrameRate: 3, + chromeMediaSource: 'screen' + } + } + }; + getUserMedia(constraints, callback); + } else { + // chrome 34+ way requiring an extension + var pending = window.setTimeout(function () { + error = new Error('NavigatorUserMediaError'); + error.name = 'EXTENSION_UNAVAILABLE'; + return callback(error); + }, 1000); + cache[pending] = [callback, hasConstraints ? constraint : null]; + window.postMessage({ type: 'getScreen', id: pending }, '*'); } } - - return this; }; -// Helper for for finding special wildcard event handlers that match the event -WildEmitter.prototype.getWildcardCallbacks = function (eventName) { - var item, - split, - result = []; +window.addEventListener('message', function (event) { + if (event.origin != window.location.origin) { + return; + } + if (event.data.type == 'gotScreen' && cache[event.data.id]) { + var data = cache[event.data.id]; + var constraints = data[1]; + var callback = data[0]; + delete cache[event.data.id]; - for (item in this.callbacks) { - split = item.split('*'); - if (item === '*' || (split.length === 2 && eventName.slice(0, split[1].length) === split[1])) { - result = result.concat(this.callbacks[item]); + if (event.data.sourceId === '') { // user canceled + var error = error = new Error('NavigatorUserMediaError'); + error.name = 'PERMISSION_DENIED'; + callback(error); + } else { + constraints = constraints || {audio: false, video: {mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: event.data.sourceId, + maxWidth: window.screen.width, + maxHeight: window.screen.height, + maxFrameRate: 3, + }}}; + getUserMedia(constraints, callback); } + } else if (event.data.type == 'getScreenPending') { + window.clearTimeout(event.data.id); } - return result; -}; +}); -},{}],21:[function(require,module,exports){ +},{"getusermedia":14}],22:[function(require,module,exports){ var parsers = require('./parsers'); var idCounter = Math.random(); @@ -8414,119 +8028,41 @@ exports.toMediaJSON = function (media, session, creator) { desc.sourceGroups = parsers.sourceGroups(ssrcGroupLines || []); var ssrcLines = parsers.findLines('a=ssrc:', lines); - desc.sources = parsers.sources(ssrcLines || []); - - var fingerprintLines = parsers.findLines('a=fingerprint:', lines, sessionLines); - fingerprintLines.forEach(function (line) { - var fp = parsers.fingerprint(line); - var setup = parsers.findLine('a=setup:', lines, sessionLines); - if (setup) { - fp.setup = setup.substr(8); - } - trans.fingerprints.push(fp); - }); - - var ufragLine = parsers.findLine('a=ice-ufrag:', lines, sessionLines); - var pwdLine = parsers.findLine('a=ice-pwd:', lines, sessionLines); - if (ufragLine && pwdLine) { - trans.ufrag = ufragLine.substr(12); - trans.pwd = pwdLine.substr(10); - trans.candidates = []; - - var candidateLines = parsers.findLines('a=candidate:', lines, sessionLines); - candidateLines.forEach(function (line) { - trans.candidates.push(exports.toCandidateJSON(line)); - }); - } - - return content; -}; - -exports.toCandidateJSON = function (line) { - var candidate = parsers.candidate(line.split('\r\n')[0]); - candidate.id = (idCounter++).toString(36).substr(0, 12); - return candidate; -}; - -},{"./parsers":26}],23:[function(require,module,exports){ -// getScreenMedia helper by @HenrikJoreteg -var getUserMedia = require('getusermedia'); - -// cache for constraints and callback -var cache = {}; - -module.exports = function (constraints, cb) { - var hasConstraints = arguments.length === 2; - var callback = hasConstraints ? cb : constraints; - var error; - - if (typeof window === 'undefined' || window.location.protocol === 'http:') { - error = new Error('NavigatorUserMediaError'); - error.name = 'HTTPS_REQUIRED'; - return callback(error); - } - - if (window.navigator.userAgent.match('Chrome')) { - var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); - var maxver = 33; - // "known‶ bug in chrome 34 on linux - if (window.navigator.userAgent.match('Linux')) maxver = 34; - if (chromever >= 26 && chromever <= maxver) { - // chrome 26 - chrome 33 way to do it -- requires bad chrome://flags - constraints = (hasConstraints && constraints) || { - video: { - mandatory: { - maxWidth: window.screen.width, - maxHeight: window.screen.height, - maxFrameRate: 3, - chromeMediaSource: 'screen' - } - } - }; - getUserMedia(constraints, callback); - } else { - // chrome 34+ way requiring an extension - var pending = window.setTimeout(function () { - error = new Error('NavigatorUserMediaError'); - error.name = 'EXTENSION_UNAVAILABLE'; - return callback(error); - }, 1000); - cache[pending] = [callback, hasConstraints ? constraint : null]; - window.postMessage({ type: 'getScreen', id: pending }, '*'); - } - } -}; - -window.addEventListener('message', function (event) { - if (event.origin != window.location.origin) { - return; - } - if (event.data.type == 'gotScreen' && cache[event.data.id]) { - var data = cache[event.data.id]; - var constraints = data[1]; - var callback = data[0]; - delete cache[event.data.id]; - - if (event.data.sourceId === '') { // user canceled - var error = error = new Error('NavigatorUserMediaError'); - error.name = 'PERMISSION_DENIED'; - callback(error); - } else { - constraints = constraints || {audio: false, video: {mandatory: { - chromeMediaSource: 'desktop', - chromeMediaSourceId: event.data.sourceId, - maxWidth: window.screen.width, - maxHeight: window.screen.height, - maxFrameRate: 3, - }}}; - getUserMedia(constraints, callback); + desc.sources = parsers.sources(ssrcLines || []); + + var fingerprintLines = parsers.findLines('a=fingerprint:', lines, sessionLines); + fingerprintLines.forEach(function (line) { + var fp = parsers.fingerprint(line); + var setup = parsers.findLine('a=setup:', lines, sessionLines); + if (setup) { + fp.setup = setup.substr(8); } - } else if (event.data.type == 'getScreenPending') { - window.clearTimeout(event.data.id); + trans.fingerprints.push(fp); + }); + + var ufragLine = parsers.findLine('a=ice-ufrag:', lines, sessionLines); + var pwdLine = parsers.findLine('a=ice-pwd:', lines, sessionLines); + if (ufragLine && pwdLine) { + trans.ufrag = ufragLine.substr(12); + trans.pwd = pwdLine.substr(10); + trans.candidates = []; + + var candidateLines = parsers.findLines('a=candidate:', lines, sessionLines); + candidateLines.forEach(function (line) { + trans.candidates.push(exports.toCandidateJSON(line)); + }); } -}); -},{"getusermedia":16}],26:[function(require,module,exports){ + return content; +}; + +exports.toCandidateJSON = function (line) { + var candidate = parsers.candidate(line.split('\r\n')[0]); + candidate.id = (idCounter++).toString(36).substr(0, 12); + return candidate; +}; + +},{"./parsers":23}],23:[function(require,module,exports){ exports.lines = function (sdp) { return sdp.split('\r\n').filter(function (line) { return line.length > 0; @@ -8760,7 +8296,169 @@ exports.groups = function (lines) { return parsed; }; -},{}],19:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ +var support = require('webrtcsupport'); + + +function GainController(stream) { + this.support = support.webAudio && support.mediaStream; + + // set our starting value + this.gain = 1; + + if (this.support) { + var context = this.context = new support.AudioContext(); + this.microphone = context.createMediaStreamSource(stream); + this.gainFilter = context.createGain(); + this.destination = context.createMediaStreamDestination(); + this.outputStream = this.destination.stream; + this.microphone.connect(this.gainFilter); + this.gainFilter.connect(this.destination); + stream.removeTrack(stream.getAudioTracks()[0]); + stream.addTrack(this.outputStream.getAudioTracks()[0]); + } + this.stream = stream; +} + +// setting +GainController.prototype.setGain = function (val) { + // check for support + if (!this.support) return; + this.gainFilter.gain.value = val; + this.gain = val; +}; + +GainController.prototype.getGain = function () { + return this.gain; +}; + +GainController.prototype.off = function () { + return this.setGain(0); +}; + +GainController.prototype.on = function () { + this.setGain(1); +}; + + +module.exports = GainController; + +},{"webrtcsupport":8}],16:[function(require,module,exports){ +var WildEmitter = require('wildemitter'); + +function getMaxVolume (analyser, fftBins) { + var maxVolume = -Infinity; + analyser.getFloatFrequencyData(fftBins); + + for(var i=0, ii=fftBins.length; i < ii; i++) { + if (fftBins[i] > maxVolume && fftBins[i] < 0) { + maxVolume = fftBins[i]; + } + }; + + return maxVolume; +} + + +var audioContextType = window.webkitAudioContext || window.AudioContext; +// use a single audio context due to hardware limits +var audioContext = null; +module.exports = function(stream, options) { + var harker = new WildEmitter(); + + + // make it not break in non-supported browsers + if (!audioContextType) return harker; + + //Config + var options = options || {}, + smoothing = (options.smoothing || 0.5), + interval = (options.interval || 100), + threshold = options.threshold, + play = options.play, + running = true; + + //Setup Audio Context + if (!audioContext) { + audioContext = new audioContextType(); + } + var sourceNode, fftBins, analyser; + + analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = smoothing; + fftBins = new Float32Array(analyser.fftSize); + + if (stream.jquery) stream = stream[0]; + if (stream instanceof HTMLAudioElement) { + //Audio Tag + sourceNode = audioContext.createMediaElementSource(stream); + if (typeof play === 'undefined') play = true; + threshold = threshold || -65; + } else { + //WebRTC Stream + sourceNode = audioContext.createMediaStreamSource(stream); + threshold = threshold || -45; + } + + sourceNode.connect(analyser); + if (play) analyser.connect(audioContext.destination); + + harker.speaking = false; + + harker.setThreshold = function(t) { + threshold = t; + }; + + harker.setInterval = function(i) { + interval = i; + }; + + harker.stop = function() { + running = false; + harker.emit('volume_change', -100, threshold); + if (harker.speaking) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + }; + + // Poll the analyser node to determine if speaking + // and emit events if changed + var looper = function() { + setTimeout(function() { + + //check if stop has been called + if(!running) { + return; + } + + var currentVolume = getMaxVolume(analyser, fftBins); + + harker.emit('volume_change', currentVolume, threshold); + + if (currentVolume > threshold) { + if (!harker.speaking) { + harker.speaking = true; + harker.emit('speaking'); + } + } else { + if (harker.speaking) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + } + + looper(); + }, interval); + }; + looper(); + + + return harker; +} + +},{"wildemitter":7}],20:[function(require,module,exports){ // based on https://github.com/ESTOS/strophe.jingle/ // adds wildemitter support var util = require('util'); @@ -8838,12 +8536,29 @@ function TraceablePeerConnection(config, constraints) { util.inherits(TraceablePeerConnection, WildEmitter); -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 () { return this.peerconnection.localDescription; }); - TraceablePeerConnection.prototype.__defineGetter__('remoteDescription', function () { return this.peerconnection.remoteDescription; }); -} +Object.defineProperty(TraceablePeerConnection.prototype, 'signalingState', { + get: function () { + return this.peerconnection.signalingState; + } +}); + +Object.defineProperty(TraceablePeerConnection.prototype, 'iceConnectionState', { + get: function () { + return this.peerconnection.iceConnectionState; + } +}); + +Object.defineProperty(TraceablePeerConnection.prototype, 'localDescription', { + get: function () { + return this.peerconnection.localDescription; + } +}); + +Object.defineProperty(TraceablePeerConnection.prototype, 'remoteDescription', { + get: function () { + return this.peerconnection.remoteDescription; + } +}); TraceablePeerConnection.prototype.addStream = function (stream) { this.trace('addStream', stream.id); @@ -8953,7 +8668,7 @@ TraceablePeerConnection.prototype.addIceCandidate = function (candidate, success TraceablePeerConnection.prototype.getStats = function (callback, errback) { if (navigator.mozGetUserMedia) { - // ignore for now... + this.peerconnection.getStats(null, callback, errback); } else { this.peerconnection.getStats(callback); } @@ -8961,53 +8676,6 @@ TraceablePeerConnection.prototype.getStats = function (callback, errback) { module.exports = TraceablePeerConnection; -},{"util":8,"webrtcsupport":10,"wildemitter":15}],24:[function(require,module,exports){ -var support = require('webrtcsupport'); - - -function GainController(stream) { - this.support = support.webAudio && support.mediaStream; - - // set our starting value - this.gain = 1; - - if (this.support) { - var context = this.context = new support.AudioContext(); - this.microphone = context.createMediaStreamSource(stream); - this.gainFilter = context.createGain(); - this.destination = context.createMediaStreamDestination(); - this.outputStream = this.destination.stream; - this.microphone.connect(this.gainFilter); - this.gainFilter.connect(this.destination); - stream.removeTrack(stream.getAudioTracks()[0]); - stream.addTrack(this.outputStream.getAudioTracks()[0]); - } - this.stream = stream; -} - -// setting -GainController.prototype.setGain = function (val) { - // check for support - if (!this.support) return; - this.gainFilter.gain.value = val; - this.gain = val; -}; - -GainController.prototype.getGain = function () { - return this.gain; -}; - -GainController.prototype.off = function () { - return this.setGain(0); -}; - -GainController.prototype.on = function () { - this.setGain(1); -}; - - -module.exports = GainController; - -},{"webrtcsupport":10}]},{},[1])(1) +},{"util":9,"webrtcsupport":8,"wildemitter":7}]},{},[1])(1) }); ;