From ed78f22d0e171310d972f41a60cf432da3fce9bd Mon Sep 17 00:00:00 2001 From: Daniel Berteaud Date: Tue, 20 May 2014 22:38:37 +0200 Subject: [PATCH] Update to SimpleWebRTC 1.8.0 --- public/js/simplewebrtc.bundle.js | 3040 +++++++++++++++++++++----------------- 1 file changed, 1708 insertions(+), 1332 deletions(-) diff --git a/public/js/simplewebrtc.bundle.js b/public/js/simplewebrtc.bundle.js index 78f79fb..327011c 100644 --- a/public/js/simplewebrtc.bundle.js +++ b/public/js/simplewebrtc.bundle.js @@ -4,7 +4,6 @@ var WebRTC = require('webrtc'); var WildEmitter = require('wildemitter'); var webrtcSupport = require('webrtcsupport'); var attachMediaStream = require('attachmediastream'); -var getScreenMedia = require('getscreenmedia'); var mockconsole = require('mockconsole'); var io = require('socket.io-client'); @@ -160,6 +159,55 @@ function SimpleWebRTC(opts) { self.webrtc.sendToAll('mute', {name: 'video'}); }); + this.webrtc.on('localScreen', function (stream) { + var item, + el = document.createElement('video'), + container = self.getRemoteVideoContainer(); + + el.oncontextmenu = function () { return false; }; + el.id = 'localScreen'; + attachMediaStream(stream, el); + if (container) { + container.appendChild(el); + } + + self.emit('localScreenAdded', el); + self.connection.emit('shareScreen'); + + self.webrtc.peers.forEach(function (existingPeer) { + var peer; + if (existingPeer.type === 'video') { + peer = self.webrtc.createPeer({ + id: existingPeer.id, + type: 'screen', + sharemyscreen: true, + enableDataChannels: false, + receiveMedia: { + mandatory: { + OfferToReceiveAudio: false, + OfferToReceiveVideo: false + } + }, + broadcaster: self.connection.socket.sessionid, + }); + peer.start(); + } + }); + }); + this.webrtc.on('localScreenStopped', function (stream) { + console.log('local screen stopped'); + self.stopScreenShare(); + /* + self.connection.emit('unshareScreen'); + self.webrtc.peers.forEach(function (peer) { + console.log('peer', peer); + if (peer.sharemyscreen) { + peer.end(); + } + }); + */ + }); + if (this.config.autoRequestMedia) this.startLocalVideo(); } @@ -202,19 +250,10 @@ SimpleWebRTC.prototype.handlePeerStreamAdded = function (peer) { // the video element is created otherwise (which happens after // the async setRemoteDescription-createAnswer) window.setTimeout(function () { - var muted = false; - self.webrtc.localStream.getAudioTracks().forEach(function (track) { - muted = !track.enabled; - }); - if (muted) { + if (!self.webrtc.isAudioEnabled()) { peer.send('mute', {name: 'audio'}); } - - muted = false; - self.webrtc.localStream.getVideoTracks().forEach(function (track) { - muted = !track.enabled; - }); - if (muted) { + if (!self.webrtc.isVideoEnabled()) { peer.send('mute', {name: 'video'}); } }, 250); @@ -323,57 +362,7 @@ SimpleWebRTC.prototype.getRemoteVideoContainer = function () { }; SimpleWebRTC.prototype.shareScreen = function (cb) { - var self = this, - peer; - getScreenMedia(function (err, stream) { - var item, - el = document.createElement('video'), - container = self.getRemoteVideoContainer(); - - el.oncontextmenu = function () { return false; }; - if (!err) { - self.webrtc.localScreen = stream; - el.id = 'localScreen'; - attachMediaStream(stream, el); - if (container) { - container.appendChild(el); - } - - // TODO: might need to migrate to the video tracks onended - stream.onended = function () { - self.emit('localScreenRemoved', el); - self.stopScreenShare(); - }; - - self.emit('localScreenAdded', el); - self.connection.emit('shareScreen'); - self.webrtc.peers.forEach(function (existingPeer) { - var peer; - if (existingPeer.type === 'video') { - peer = self.webrtc.createPeer({ - id: existingPeer.id, - type: 'screen', - sharemyscreen: true, - enableDataChannels: false, - receiveMedia: { - mandatory: { - OfferToReceiveAudio: false, - OfferToReceiveVideo: false - } - }, - broadcaster: self.connection.socket.sessionid, - }); - peer.start(); - } - }); - } else { - err.media = 'screen'; - self.emit('localMediaError', err); - } - - // enable the callback - if (cb) cb(err, stream); - }); + this.webrtc.startScreenShare(cb); }; SimpleWebRTC.prototype.getLocalScreen = function () { @@ -399,7 +388,7 @@ SimpleWebRTC.prototype.stopScreenShare = function () { peer.end(); } }); - delete this.webrtc.localScreen; + //delete this.webrtc.localScreen; }; SimpleWebRTC.prototype.testReadiness = function () { @@ -426,45 +415,7 @@ SimpleWebRTC.prototype.sendFile = function () { module.exports = SimpleWebRTC; -},{"attachmediastream":5,"getscreenmedia":6,"mockconsole":7,"socket.io-client":8,"webrtc":2,"webrtcsupport":4,"wildemitter":3}],4:[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 -}; - -},{}],3:[function(require,module,exports){ +},{"attachmediastream":5,"mockconsole":4,"socket.io-client":7,"webrtc":6,"webrtcsupport":3,"wildemitter":2}],2:[function(require,module,exports){ /* WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based on @visionmedia's Emitter from UI Kit. @@ -601,6 +552,44 @@ WildEmitter.prototype.getWildcardCallbacks = function (eventName) { 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; @@ -642,7 +631,7 @@ module.exports = function (stream, el, options) { return element; }; -},{}],7:[function(require,module,exports){ +},{}],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 () {}; @@ -654,7 +643,7 @@ while (l--) { module.exports = mockconsole; -},{}],8:[function(require,module,exports){ +},{}],7:[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); @@ -4528,149 +4517,354 @@ if (typeof define === "function" && define.amd) { define([], function () { return io; }); } })(); -},{}],6:[function(require,module,exports){ -// getScreenMedia helper by @HenrikJoreteg -var getUserMedia = require('getusermedia'); +},{}],8:[function(require,module,exports){ +var events = require('events'); -// cache for constraints and callback -var cache = {}; +exports.isArray = isArray; +exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'}; +exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'}; -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); - } +exports.print = function () {}; +exports.puts = function () {}; +exports.debug = function() {}; - 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 }, '*'); - } - } -}; +exports.inspect = function(obj, showHidden, depth, colors) { + var seen = []; -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]; + var stylize = function(str, styleType) { + // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics + var styles = + { 'bold' : [1, 22], + 'italic' : [3, 23], + 'underline' : [4, 24], + 'inverse' : [7, 27], + 'white' : [37, 39], + 'grey' : [90, 39], + 'black' : [30, 39], + 'blue' : [34, 39], + 'cyan' : [36, 39], + 'green' : [32, 39], + 'magenta' : [35, 39], + 'red' : [31, 39], + 'yellow' : [33, 39] }; - 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); + var style = + { 'special': 'cyan', + 'number': 'blue', + 'boolean': 'yellow', + 'undefined': 'grey', + 'null': 'bold', + 'string': 'green', + 'date': 'magenta', + // "name": intentionally not styling + 'regexp': 'red' }[styleType]; + + if (style) { + return '\u001b[' + styles[style][0] + 'm' + str + + '\u001b[' + styles[style][1] + 'm'; + } else { + return str; } -}); + }; + if (! colors) { + stylize = function(str, styleType) { return str; }; + } -},{"getusermedia":9}],10:[function(require,module,exports){ -// getUserMedia helper by @HenrikJoreteg -var func = (navigator.getUserMedia || - navigator.webkitGetUserMedia || - navigator.mozGetUserMedia || - navigator.msGetUserMedia); + function format(value, recurseTimes) { + // Provide a hook for user-specified inspect functions. + // Check that value is an object with an inspect function on it + if (value && typeof value.inspect === 'function' && + // Filter out the util module, it's inspect function is special + value !== exports && + // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + return value.inspect(recurseTimes); + } + // Primitive types cannot have properties + switch (typeof value) { + case 'undefined': + return stylize('undefined', 'undefined'); -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'; + case 'string': + var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') + .replace(/'/g, "\\'") + .replace(/\\"/g, '"') + '\''; + return stylize(simple, 'string'); - // make constraints optional - if (!haveOpts) { - cb = constraints; - constraints = defaultOpts; + case 'number': + return stylize('' + value, 'number'); + + case 'boolean': + return stylize('' + value, 'boolean'); + } + // For some reason typeof null is "object", so special case here. + if (value === null) { + return stylize('null', 'null'); } - // 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); + // Look up the keys of the object. + var visible_keys = Object_keys(value); + var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys; + + // Functions without properties can be shortcutted. + if (typeof value === 'function' && keys.length === 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + var name = value.name ? ': ' + value.name : ''; + return stylize('[Function' + name + ']', 'special'); + } } - func.call(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; + // Dates without properties can be shortcutted + if (isDate(value) && keys.length === 0) { + return stylize(value.toUTCString(), 'date'); + } + + var base, type, braces; + // Determine the object type + if (isArray(value)) { + type = 'Array'; + braces = ['[', ']']; + } else { + type = 'Object'; + braces = ['{', '}']; + } + + // Make functions say that they are functions + if (typeof value === 'function') { + var n = value.name ? ': ' + value.name : ''; + base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; + } else { + base = ''; + } + + // Make dates with properties first say the date + if (isDate(value)) { + base = ' ' + value.toUTCString(); + } + + if (keys.length === 0) { + return braces[0] + base + braces[1]; + } + + if (recurseTimes < 0) { + if (isRegExp(value)) { + return stylize('' + value, 'regexp'); + } else { + return stylize('[Object]', 'special'); + } + } + + seen.push(value); + + var output = keys.map(function(key) { + var name, str; + if (value.__lookupGetter__) { + if (value.__lookupGetter__(key)) { + if (value.__lookupSetter__(key)) { + str = stylize('[Getter/Setter]', 'special'); + } else { + str = stylize('[Getter]', 'special'); + } + } else { + if (value.__lookupSetter__(key)) { + str = stylize('[Setter]', 'special'); + } + } + } + if (visible_keys.indexOf(key) < 0) { + name = '[' + key + ']'; + } + if (!str) { + if (seen.indexOf(value[key]) < 0) { + if (recurseTimes === null) { + str = format(value[key]); + } else { + str = format(value[key], recurseTimes - 1); + } + if (str.indexOf('\n') > -1) { + if (isArray(value)) { + str = str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n').substr(2); } else { - error.name = notSatified; + str = '\n' + str.split('\n').map(function(line) { + return ' ' + line; + }).join('\n'); } + } } 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; - } - } + str = stylize('[Circular]', 'special'); } + } + if (typeof name === 'undefined') { + if (type === 'Array' && key.match(/^\d+$/)) { + return str; + } + name = JSON.stringify('' + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.substr(1, name.length - 2); + name = stylize(name, 'name'); + } else { + name = name.replace(/'/g, "\\'") + .replace(/\\"/g, '"') + .replace(/(^"|"$)/g, "'"); + name = stylize(name, 'string'); + } + } - cb(error); + return name + ': ' + str; }); + + seen.pop(); + + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf('\n') >= 0) numLinesEst++; + return prev + cur.length + 1; + }, 0); + + if (length > 50) { + output = braces[0] + + (base === '' ? '' : base + '\n ') + + ' ' + + output.join(',\n ') + + ' ' + + braces[1]; + + } else { + output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + } + + return output; + } + return format(obj, (typeof depth === 'undefined' ? 2 : depth)); }; -},{}],11:[function(require,module,exports){ + +function isArray(ar) { + return Array.isArray(ar) || + (typeof ar === 'object' && Object.prototype.toString.call(ar) === '[object Array]'); +} + + +function isRegExp(re) { + typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]'; +} + + +function isDate(d) { + return typeof d === 'object' && Object.prototype.toString.call(d) === '[object Date]'; +} + +function pad(n) { + return n < 10 ? '0' + n.toString(10) : n.toString(10); +} + +var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', + 'Oct', 'Nov', 'Dec']; + +// 26 Feb 16:19:34 +function timestamp() { + var d = new Date(); + var time = [pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds())].join(':'); + return [d.getDate(), months[d.getMonth()], time].join(' '); +} + +exports.log = function (msg) {}; + +exports.pump = null; + +var Object_keys = Object.keys || function (obj) { + var res = []; + for (var key in obj) res.push(key); + return res; +}; + +var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) { + var res = []; + for (var key in obj) { + if (Object.hasOwnProperty.call(obj, key)) res.push(key); + } + return res; +}; + +var Object_create = Object.create || function (prototype, properties) { + // from es5-shim + var object; + if (prototype === null) { + object = { '__proto__' : null }; + } + else { + if (typeof prototype !== 'object') { + throw new TypeError( + 'typeof prototype[' + (typeof prototype) + '] != \'object\'' + ); + } + var Type = function () {}; + Type.prototype = prototype; + object = new Type(); + object.__proto__ = prototype; + } + if (typeof properties !== 'undefined' && Object.defineProperties) { + Object.defineProperties(object, properties); + } + return object; +}; + +exports.inherits = function(ctor, superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object_create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); +}; + +var formatRegExp = /%[sdj%]/g; +exports.format = function(f) { + if (typeof f !== 'string') { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(exports.inspect(arguments[i])); + } + return objects.join(' '); + } + + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x) { + if (x === '%%') return '%'; + if (i >= len) return x; + switch (x) { + case '%s': return String(args[i++]); + case '%d': return Number(args[i++]); + case '%j': return JSON.stringify(args[i++]); + default: + return x; + } + }); + for(var x = args[i]; i < len; x = args[++i]){ + if (x === null || typeof x !== 'object') { + str += ' ' + x; + } else { + str += ' ' + exports.inspect(x); + } + } + return str; +}; + +},{"events":9}],10:[function(require,module,exports){ // created by @HenrikJoreteg var prefix; var isChrome = false; @@ -4708,78 +4902,13 @@ module.exports = { IceCandidate: IceCandidate }; -},{}],9:[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); - }); -}; - -},{}],2:[function(require,module,exports){ +},{}],6:[function(require,module,exports){ +var util = require('util'); var webrtc = require('webrtcsupport'); -var getUserMedia = require('getusermedia'); var PeerConnection = require('rtcpeerconnection'); var WildEmitter = require('wildemitter'); -var hark = require('hark'); -var GainController = require('mediastream-gain'); var mockconsole = require('mockconsole'); +var localMedia = require('localmedia'); function WebRTC(opts) { @@ -4787,9 +4916,6 @@ function WebRTC(opts) { var options = opts || {}; var config = this.config = { debug: false, - localVideoEl: '', - remoteVideosEl: '', - autoRequestMedia: false, // makes the entire PC config overridable peerConnectionConfig: { iceServers: [{"url": "stun:stun.l.google.com:19302"}] @@ -4799,21 +4925,15 @@ function WebRTC(opts) { {DtlsSrtpKeyAgreement: true} ] }, - autoAdjustMic: false, - media: { - audio: true, - video: true - }, receiveMedia: { mandatory: { OfferToReceiveAudio: true, OfferToReceiveVideo: true } }, - detectSpeakingEvents: true, enableDataChannels: true }; - var item, connection; + var item; // expose screensharing check this.screenSharingSupport = webrtc.screenSharing; @@ -4845,112 +4965,24 @@ function WebRTC(opts) { this.logger.error('Your browser doesn\'t seem to support WebRTC'); } - // where we'll store our peer connections - this.peers = []; - - WildEmitter.call(this); - - // log events in debug mode - if (this.config.debug) { - this.on('*', function (event, val1, val2) { - var logger; - // if you didn't pass in a logger and you explicitly turning on debug - // we're just going to assume you're wanting log output with console - if (self.config.logger === mockconsole) { - logger = console; - } else { - logger = self.logger; - } - logger.log('event:', event, val1, val2); - }); - } -} - -WebRTC.prototype = Object.create(WildEmitter.prototype, { - constructor: { - value: WebRTC - } -}); - -WebRTC.prototype.createPeer = function (opts) { - var peer; - opts.parent = this; - peer = new Peer(opts); - this.peers.push(peer); - return peer; -}; - -WebRTC.prototype.startLocalMedia = function (mediaConstraints, cb) { - var self = this; - var constraints = mediaConstraints || {video: true, audio: true}; - - getUserMedia(constraints, function (err, stream) { - if (!err) { - if (constraints.audio && self.config.detectSpeakingEvents) { - self.setupAudioMonitor(stream); - } - self.localStream = stream; - - if (self.config.autoAdjustMic) { - self.gainController = new GainController(stream); - // start out somewhat muted if we can track audio - self.setMicIfEnabled(0.5); - } - - self.emit('localStream', stream); - } - if (cb) cb(err, stream); - }); -}; - -WebRTC.prototype.stopLocalMedia = function () { - if (this.localStream) { - this.localStream.stop(); - this.emit('localStreamStopped'); - } -}; - -// Audio controls -WebRTC.prototype.mute = function () { - this._audioEnabled(false); - this.hardMuted = true; - this.emit('audioOff'); -}; -WebRTC.prototype.unmute = function () { - this._audioEnabled(true); - this.hardMuted = false; - this.emit('audioOn'); -}; - -// Audio monitor -WebRTC.prototype.setupAudioMonitor = function (stream) { - this.logger.log('Setup audio'); - var audio = hark(stream); - var self = this; - var timeout; - - audio.on('speaking', function () { - self.emit('speaking'); - if (self.hardMuted) return; - self.setMicIfEnabled(1); - self.sendToAll('speaking', {}); - }); - - audio.on('stopped_speaking', function () { - if (timeout) clearTimeout(timeout); + // where we'll store our peer connections + this.peers = []; - timeout = setTimeout(function () { - self.emit('stoppedSpeaking'); - if (self.hardMuted) return; - self.setMicIfEnabled(0.5); - self.sendToAll('stopped_speaking', {}); - }, 1000); + // call localMedia constructor + localMedia.call(this, this.config); + + this.on('speaking', function () { + if (!self.hardMuted) { + self.sendToAll('speaking'); + } + }); + this.on('stoppedSpeaking', function () { + if (!self.hardMuted) { + self.sendToAll('stopped_speaking'); + } }); - if (this.config.enableDataChannels) { - // until https://code.google.com/p/chromium/issues/detail?id=121673 is fixed... - audio.on('volume_change', function (volume, treshold) { - self.emit('volumeChange', volume, treshold); - if (self.hardMuted) return; + this.on('volumeChange', function (volume, treshold) { + if (!self.hardMuted) { // FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload self.peers.forEach(function (peer) { if (peer.enableDataChannels) { @@ -4959,51 +4991,33 @@ WebRTC.prototype.setupAudioMonitor = function (stream) { dc.send(JSON.stringify({type: 'volume', volume: volume })); } }); + } + }); + + // log events in debug mode + if (this.config.debug) { + this.on('*', function (event, val1, val2) { + var logger; + // if you didn't pass in a logger and you explicitly turning on debug + // we're just going to assume you're wanting log output with console + if (self.config.logger === mockconsole) { + logger = console; + } else { + logger = self.logger; + } + logger.log('event:', event, val1, val2); }); } -}; - -// We do this as a seperate method in order to -// still leave the "setMicVolume" as a working -// method. -WebRTC.prototype.setMicIfEnabled = function (volume) { - if (!this.config.autoAdjustMic) return; - this.gainController.setGain(volume); -}; - -// Video controls -WebRTC.prototype.pauseVideo = function () { - this._videoEnabled(false); - this.emit('videoOff'); -}; -WebRTC.prototype.resumeVideo = function () { - this._videoEnabled(true); - this.emit('videoOn'); -}; +} -// Combined controls -WebRTC.prototype.pause = function () { - this._audioEnabled(false); - this.pauseVideo(); -}; -WebRTC.prototype.resume = function () { - this._audioEnabled(true); - this.resumeVideo(); -}; +util.inherits(WebRTC, localMedia); -// Internal methods for enabling/disabling audio/video -WebRTC.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.localStream.getAudioTracks().forEach(function (track) { - track.enabled = !!bool; - }); -}; -WebRTC.prototype._videoEnabled = function (bool) { - this.localStream.getVideoTracks().forEach(function (track) { - track.enabled = !!bool; - }); +WebRTC.prototype.createPeer = function (opts) { + var peer; + opts.parent = this; + peer = new Peer(opts); + this.peers.push(peer); + return peer; }; // removes peers @@ -5062,6 +5076,17 @@ function Peer(options) { // automatically. We'll just leave it be for now while this stabalizes. this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded')); this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange')); + this.pc.on('iceConnectionStateChange', function () { + switch (self.pc.iceConnectionState) { + 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}); + } + break; + } + }); this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange')); this.logger = this.parent.logger; @@ -5073,7 +5098,9 @@ function Peer(options) { this.broadcaster = options.broadcaster; } } else { - this.pc.addStream(this.parent.localStream); + this.parent.localStreams.forEach(function (stream) { + self.pc.addStream(stream); + }); } // call emitter constructor @@ -5213,6 +5240,8 @@ Peer.prototype.handleRemoteStreamAdded = function (event) { this.logger.warn('Already have a remote stream'); } else { this.stream = event.stream; + // FIXME: addEventListener('ended', ...) would be nicer + // but does not work in firefox this.stream.onended = function () { self.end(); }; @@ -5233,354 +5262,258 @@ Peer.prototype.handleDataChannelAdded = function (channel) { module.exports = WebRTC; -},{"getusermedia":10,"hark":13,"mediastream-gain":14,"mockconsole":7,"rtcpeerconnection":12,"webrtcsupport":11,"wildemitter":3}],15:[function(require,module,exports){ -var events = require('events'); - -exports.isArray = isArray; -exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'}; -exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'}; - - -exports.print = function () {}; -exports.puts = function () {}; -exports.debug = function() {}; - -exports.inspect = function(obj, showHidden, depth, colors) { - var seen = []; - - var stylize = function(str, styleType) { - // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics - var styles = - { 'bold' : [1, 22], - 'italic' : [3, 23], - 'underline' : [4, 24], - 'inverse' : [7, 27], - 'white' : [37, 39], - 'grey' : [90, 39], - 'black' : [30, 39], - 'blue' : [34, 39], - 'cyan' : [36, 39], - 'green' : [32, 39], - 'magenta' : [35, 39], - 'red' : [31, 39], - 'yellow' : [33, 39] }; +},{"localmedia":12,"mockconsole":4,"rtcpeerconnection":11,"util":8,"webrtcsupport":10,"wildemitter":2}],13:[function(require,module,exports){ +// shim for using process in browser - var style = - { 'special': 'cyan', - 'number': 'blue', - 'boolean': 'yellow', - 'undefined': 'grey', - 'null': 'bold', - 'string': 'green', - 'date': 'magenta', - // "name": intentionally not styling - 'regexp': 'red' }[styleType]; +var process = module.exports = {}; - if (style) { - return '\u001b[' + styles[style][0] + 'm' + str + - '\u001b[' + styles[style][1] + 'm'; - } else { - return str; - } - }; - if (! colors) { - stylize = function(str, styleType) { return str; }; - } +process.nextTick = (function () { + var canSetImmediate = typeof window !== 'undefined' + && window.setImmediate; + var canPost = typeof window !== 'undefined' + && window.postMessage && window.addEventListener + ; - function format(value, recurseTimes) { - // Provide a hook for user-specified inspect functions. - // Check that value is an object with an inspect function on it - if (value && typeof value.inspect === 'function' && - // Filter out the util module, it's inspect function is special - value !== exports && - // Also filter out any prototype objects using the circular check. - !(value.constructor && value.constructor.prototype === value)) { - return value.inspect(recurseTimes); + if (canSetImmediate) { + return function (f) { return window.setImmediate(f) }; } - // Primitive types cannot have properties - switch (typeof value) { - case 'undefined': - return stylize('undefined', 'undefined'); - - case 'string': - var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '') - .replace(/'/g, "\\'") - .replace(/\\"/g, '"') + '\''; - return stylize(simple, 'string'); - - case 'number': - return stylize('' + value, 'number'); + if (canPost) { + var queue = []; + window.addEventListener('message', function (ev) { + var source = ev.source; + if ((source === window || source === null) && ev.data === 'process-tick') { + ev.stopPropagation(); + if (queue.length > 0) { + var fn = queue.shift(); + fn(); + } + } + }, true); - case 'boolean': - return stylize('' + value, 'boolean'); - } - // For some reason typeof null is "object", so special case here. - if (value === null) { - return stylize('null', 'null'); + return function nextTick(fn) { + queue.push(fn); + window.postMessage('process-tick', '*'); + }; } - // Look up the keys of the object. - var visible_keys = Object_keys(value); - var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys; + return function nextTick(fn) { + setTimeout(fn, 0); + }; +})(); - // Functions without properties can be shortcutted. - if (typeof value === 'function' && keys.length === 0) { - if (isRegExp(value)) { - return stylize('' + value, 'regexp'); - } else { - var name = value.name ? ': ' + value.name : ''; - return stylize('[Function' + name + ']', 'special'); - } - } +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; - // Dates without properties can be shortcutted - if (isDate(value) && keys.length === 0) { - return stylize(value.toUTCString(), 'date'); - } +process.binding = function (name) { + throw new Error('process.binding is not supported'); +} - var base, type, braces; - // Determine the object type - if (isArray(value)) { - type = 'Array'; - braces = ['[', ']']; - } else { - type = 'Object'; - braces = ['{', '}']; - } +// TODO(shtylman) +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; - // Make functions say that they are functions - if (typeof value === 'function') { - var n = value.name ? ': ' + value.name : ''; - base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']'; - } else { - base = ''; - } +},{}],9:[function(require,module,exports){ +var process=require("__browserify_process");if (!process.EventEmitter) process.EventEmitter = function () {}; - // Make dates with properties first say the date - if (isDate(value)) { - base = ' ' + value.toUTCString(); +var EventEmitter = exports.EventEmitter = process.EventEmitter; +var isArray = typeof Array.isArray === 'function' + ? Array.isArray + : function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]' } - - if (keys.length === 0) { - return braces[0] + base + braces[1]; +; +function indexOf (xs, x) { + if (xs.indexOf) return xs.indexOf(x); + for (var i = 0; i < xs.length; i++) { + if (x === xs[i]) return i; } + return -1; +} - if (recurseTimes < 0) { - if (isRegExp(value)) { - return stylize('' + value, 'regexp'); - } else { - return stylize('[Object]', 'special'); - } - } +// By default EventEmitters will print a warning if more than +// 10 listeners are added to it. This is a useful default which +// helps finding memory leaks. +// +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +var defaultMaxListeners = 10; +EventEmitter.prototype.setMaxListeners = function(n) { + if (!this._events) this._events = {}; + this._events.maxListeners = n; +}; - seen.push(value); - var output = keys.map(function(key) { - var name, str; - if (value.__lookupGetter__) { - if (value.__lookupGetter__(key)) { - if (value.__lookupSetter__(key)) { - str = stylize('[Getter/Setter]', 'special'); - } else { - str = stylize('[Getter]', 'special'); - } - } else { - if (value.__lookupSetter__(key)) { - str = stylize('[Setter]', 'special'); - } - } - } - if (visible_keys.indexOf(key) < 0) { - name = '[' + key + ']'; - } - if (!str) { - if (seen.indexOf(value[key]) < 0) { - if (recurseTimes === null) { - str = format(value[key]); - } else { - str = format(value[key], recurseTimes - 1); - } - if (str.indexOf('\n') > -1) { - if (isArray(value)) { - str = str.split('\n').map(function(line) { - return ' ' + line; - }).join('\n').substr(2); - } else { - str = '\n' + str.split('\n').map(function(line) { - return ' ' + line; - }).join('\n'); - } - } - } else { - str = stylize('[Circular]', 'special'); - } - } - if (typeof name === 'undefined') { - if (type === 'Array' && key.match(/^\d+$/)) { - return str; - } - name = JSON.stringify('' + key); - if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { - name = name.substr(1, name.length - 2); - name = stylize(name, 'name'); - } else { - name = name.replace(/'/g, "\\'") - .replace(/\\"/g, '"') - .replace(/(^"|"$)/g, "'"); - name = stylize(name, 'string'); - } +EventEmitter.prototype.emit = function(type) { + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events || !this._events.error || + (isArray(this._events.error) && !this._events.error.length)) + { + if (arguments[1] instanceof Error) { + throw arguments[1]; // Unhandled 'error' event + } else { + throw new Error("Uncaught, unspecified 'error' event."); } + return false; + } + } - return name + ': ' + str; - }); - - seen.pop(); + if (!this._events) return false; + var handler = this._events[type]; + if (!handler) return false; - var numLinesEst = 0; - var length = output.reduce(function(prev, cur) { - numLinesEst++; - if (cur.indexOf('\n') >= 0) numLinesEst++; - return prev + cur.length + 1; - }, 0); + if (typeof handler == 'function') { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + var args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + return true; - if (length > 50) { - output = braces[0] + - (base === '' ? '' : base + '\n ') + - ' ' + - output.join(',\n ') + - ' ' + - braces[1]; + } else if (isArray(handler)) { + var args = Array.prototype.slice.call(arguments, 1); - } else { - output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1]; + var listeners = handler.slice(); + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); } + return true; - return output; + } else { + return false; } - return format(obj, (typeof depth === 'undefined' ? 2 : depth)); }; +// EventEmitter is defined in src/node_events.cc +// EventEmitter.prototype.emit() is also defined there. +EventEmitter.prototype.addListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('addListener only takes instances of Function'); + } -function isArray(ar) { - return Array.isArray(ar) || - (typeof ar === 'object' && Object.prototype.toString.call(ar) === '[object Array]'); -} - + if (!this._events) this._events = {}; -function isRegExp(re) { - typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]'; -} + // To avoid recursion in the case that type == "newListeners"! Before + // adding it to the listeners, first emit "newListeners". + this.emit('newListener', type, listener); + if (!this._events[type]) { + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + } else if (isArray(this._events[type])) { -function isDate(d) { - return typeof d === 'object' && Object.prototype.toString.call(d) === '[object Date]'; -} + // Check for listener leak + if (!this._events[type].warned) { + var m; + if (this._events.maxListeners !== undefined) { + m = this._events.maxListeners; + } else { + m = defaultMaxListeners; + } -function pad(n) { - return n < 10 ? '0' + n.toString(10) : n.toString(10); -} + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + console.trace(); + } + } -var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', - 'Oct', 'Nov', 'Dec']; + // If we've already got an array, just append. + this._events[type].push(listener); + } else { + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + } -// 26 Feb 16:19:34 -function timestamp() { - var d = new Date(); - var time = [pad(d.getHours()), - pad(d.getMinutes()), - pad(d.getSeconds())].join(':'); - return [d.getDate(), months[d.getMonth()], time].join(' '); -} + return this; +}; -exports.log = function (msg) {}; +EventEmitter.prototype.on = EventEmitter.prototype.addListener; -exports.pump = null; +EventEmitter.prototype.once = function(type, listener) { + var self = this; + self.on(type, function g() { + self.removeListener(type, g); + listener.apply(this, arguments); + }); -var Object_keys = Object.keys || function (obj) { - var res = []; - for (var key in obj) res.push(key); - return res; + return this; }; -var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) { - var res = []; - for (var key in obj) { - if (Object.hasOwnProperty.call(obj, key)) res.push(key); - } - return res; -}; +EventEmitter.prototype.removeListener = function(type, listener) { + if ('function' !== typeof listener) { + throw new Error('removeListener only takes instances of Function'); + } -var Object_create = Object.create || function (prototype, properties) { - // from es5-shim - var object; - if (prototype === null) { - object = { '__proto__' : null }; - } - else { - if (typeof prototype !== 'object') { - throw new TypeError( - 'typeof prototype[' + (typeof prototype) + '] != \'object\'' - ); - } - var Type = function () {}; - Type.prototype = prototype; - object = new Type(); - object.__proto__ = prototype; - } - if (typeof properties !== 'undefined' && Object.defineProperties) { - Object.defineProperties(object, properties); - } - return object; + // does not use listeners(), so no side effect of creating _events[type] + if (!this._events || !this._events[type]) return this; + + var list = this._events[type]; + + if (isArray(list)) { + var i = indexOf(list, listener); + if (i < 0) return this; + list.splice(i, 1); + if (list.length == 0) + delete this._events[type]; + } else if (this._events[type] === listener) { + delete this._events[type]; + } + + return this; }; -exports.inherits = function(ctor, superCtor) { - ctor.super_ = superCtor; - ctor.prototype = Object_create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); +EventEmitter.prototype.removeAllListeners = function(type) { + if (arguments.length === 0) { + this._events = {}; + return this; + } + + // does not use listeners(), so no side effect of creating _events[type] + if (type && this._events && this._events[type]) this._events[type] = null; + return this; }; -var formatRegExp = /%[sdj%]/g; -exports.format = function(f) { - if (typeof f !== 'string') { - var objects = []; - for (var i = 0; i < arguments.length; i++) { - objects.push(exports.inspect(arguments[i])); - } - return objects.join(' '); +EventEmitter.prototype.listeners = function(type) { + if (!this._events) this._events = {}; + if (!this._events[type]) this._events[type] = []; + if (!isArray(this._events[type])) { + this._events[type] = [this._events[type]]; } + return this._events[type]; +}; - var i = 1; - var args = arguments; - var len = args.length; - var str = String(f).replace(formatRegExp, function(x) { - if (x === '%%') return '%'; - if (i >= len) return x; - switch (x) { - case '%s': return String(args[i++]); - case '%d': return Number(args[i++]); - case '%j': return JSON.stringify(args[i++]); - default: - return x; - } - }); - for(var x = args[i]; i < len; x = args[++i]){ - if (x === null || typeof x !== 'object') { - str += ' ' + x; - } else { - str += ' ' + exports.inspect(x); - } - } - return str; +EventEmitter.listenerCount = function(emitter, type) { + var ret; + if (!emitter._events || !emitter._events[type]) + ret = 0; + else if (typeof emitter._events[type] === 'function') + ret = 1; + else + ret = emitter._events[type].length; + return ret; }; -},{"events":16}],17:[function(require,module,exports){ +},{"__browserify_process":13}],14:[function(require,module,exports){ // Underscore.js 1.6.0 // http://underscorejs.org // (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors @@ -6925,7 +6858,212 @@ exports.format = function(f) { } }).call(this); -},{}],18:[function(require,module,exports){ +},{}],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) { + +}); + +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--; + } + } + } + return this; +}; + +// 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; +}; + +/// 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; +}; + +// 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; +}; + +},{}],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; + } + + // 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); + }); +}; + +},{}],17:[function(require,module,exports){ /* WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based on @visionmedia's Emitter from UI Kit. @@ -7066,7 +7204,7 @@ WildEmitter.prototype.getWildcardCallbacks = function (eventName) { return result; }; -},{}],12:[function(require,module,exports){ +},{}],11:[function(require,module,exports){ var _ = require('underscore'); var util = require('util'); var webrtc = require('webrtcsupport'); @@ -7402,460 +7540,321 @@ PeerConnection.prototype._onIce = function (event) { }; } - this.emit('ice', expandedCandidate); - } else { - this.emit('endOfCandidates'); - } -}; - -// 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 = PeerConnection; - -},{"sdp-jingle-json":20,"traceablepeerconnection":19,"underscore":17,"util":15,"webrtcsupport":11,"wildemitter":18}],14:[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":11}],21:[function(require,module,exports){ -// shim for using process in browser - -var process = module.exports = {}; - -process.nextTick = (function () { - var canSetImmediate = typeof window !== 'undefined' - && window.setImmediate; - var canPost = typeof window !== 'undefined' - && window.postMessage && window.addEventListener - ; - - if (canSetImmediate) { - return function (f) { return window.setImmediate(f) }; - } - - if (canPost) { - var queue = []; - window.addEventListener('message', function (ev) { - var source = ev.source; - if ((source === window || source === null) && ev.data === 'process-tick') { - ev.stopPropagation(); - if (queue.length > 0) { - var fn = queue.shift(); - fn(); - } - } - }, true); - - return function nextTick(fn) { - queue.push(fn); - window.postMessage('process-tick', '*'); - }; - } - - return function nextTick(fn) { - setTimeout(fn, 0); - }; -})(); - -process.title = 'browser'; -process.browser = true; -process.env = {}; -process.argv = []; - -process.binding = function (name) { - throw new Error('process.binding is not supported'); -} - -// TODO(shtylman) -process.cwd = function () { return '/' }; -process.chdir = function (dir) { - throw new Error('process.chdir is not supported'); -}; - -},{}],16:[function(require,module,exports){ -var process=require("__browserify_process");if (!process.EventEmitter) process.EventEmitter = function () {}; - -var EventEmitter = exports.EventEmitter = process.EventEmitter; -var isArray = typeof Array.isArray === 'function' - ? Array.isArray - : function (xs) { - return Object.prototype.toString.call(xs) === '[object Array]' - } -; -function indexOf (xs, x) { - if (xs.indexOf) return xs.indexOf(x); - for (var i = 0; i < xs.length; i++) { - if (x === xs[i]) return i; - } - return -1; -} - -// By default EventEmitters will print a warning if more than -// 10 listeners are added to it. This is a useful default which -// helps finding memory leaks. -// -// Obviously not all Emitters should be limited to 10. This function allows -// that to be increased. Set to zero for unlimited. -var defaultMaxListeners = 10; -EventEmitter.prototype.setMaxListeners = function(n) { - if (!this._events) this._events = {}; - this._events.maxListeners = n; -}; - - -EventEmitter.prototype.emit = function(type) { - // If there is no 'error' event listener then throw. - if (type === 'error') { - if (!this._events || !this._events.error || - (isArray(this._events.error) && !this._events.error.length)) - { - if (arguments[1] instanceof Error) { - throw arguments[1]; // Unhandled 'error' event - } else { - throw new Error("Uncaught, unspecified 'error' event."); - } - return false; - } - } - - if (!this._events) return false; - var handler = this._events[type]; - if (!handler) return false; - - if (typeof handler == 'function') { - switch (arguments.length) { - // fast cases - case 1: - handler.call(this); - break; - case 2: - handler.call(this, arguments[1]); - break; - case 3: - handler.call(this, arguments[1], arguments[2]); - break; - // slower - default: - var args = Array.prototype.slice.call(arguments, 1); - handler.apply(this, args); - } - return true; - - } else if (isArray(handler)) { - var args = Array.prototype.slice.call(arguments, 1); - - var listeners = handler.slice(); - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].apply(this, args); - } - return true; - - } else { - return false; - } -}; - -// EventEmitter is defined in src/node_events.cc -// EventEmitter.prototype.emit() is also defined there. -EventEmitter.prototype.addListener = function(type, listener) { - if ('function' !== typeof listener) { - throw new Error('addListener only takes instances of Function'); - } - - if (!this._events) this._events = {}; - - // To avoid recursion in the case that type == "newListeners"! Before - // adding it to the listeners, first emit "newListeners". - this.emit('newListener', type, listener); - - if (!this._events[type]) { - // Optimize the case of one listener. Don't need the extra array object. - this._events[type] = listener; - } else if (isArray(this._events[type])) { - - // Check for listener leak - if (!this._events[type].warned) { - var m; - if (this._events.maxListeners !== undefined) { - m = this._events.maxListeners; - } else { - m = defaultMaxListeners; - } - - if (m && m > 0 && this._events[type].length > m) { - this._events[type].warned = true; - console.error('(node) warning: possible EventEmitter memory ' + - 'leak detected. %d listeners added. ' + - 'Use emitter.setMaxListeners() to increase limit.', - this._events[type].length); - console.trace(); - } - } - - // If we've already got an array, just append. - this._events[type].push(listener); - } else { - // Adding the second element, need to change to array. - this._events[type] = [this._events[type], listener]; - } - - return this; + this.emit('ice', expandedCandidate); + } else { + this.emit('endOfCandidates'); + } }; -EventEmitter.prototype.on = EventEmitter.prototype.addListener; +// Internal method for processing a new data channel being added by the +// other peer. +PeerConnection.prototype._onDataChannel = function (event) { + this.emit('addChannel', event.channel); +}; -EventEmitter.prototype.once = function(type, listener) { - var self = this; - self.on(type, function g() { - self.removeListener(type, g); - listener.apply(this, arguments); - }); +// Internal handling of adding stream +PeerConnection.prototype._onAddStream = function (event) { + this.remoteStreams.push(event.stream); + this.emit('addStream', event); +}; - return this; +// 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; }; -EventEmitter.prototype.removeListener = function(type, listener) { - if ('function' !== typeof listener) { - throw new Error('removeListener only takes instances of Function'); - } +module.exports = PeerConnection; - // does not use listeners(), so no side effect of creating _events[type] - if (!this._events || !this._events[type]) return this; +},{"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'); - var list = this._events[type]; - if (isArray(list)) { - var i = indexOf(list, listener); - if (i < 0) return this; - list.splice(i, 1); - if (list.length == 0) - delete this._events[type]; - } else if (this._events[type] === listener) { - delete this._events[type]; - } +exports.toSessionSDP = tosdp.toSessionSDP; +exports.toMediaSDP = tosdp.toMediaSDP; +exports.toCandidateSDP = tosdp.toCandidateSDP; - return this; -}; +exports.toSessionJSON = tojson.toSessionJSON; +exports.toMediaJSON = tojson.toMediaJSON; +exports.toCandidateJSON = tojson.toCandidateJSON; -EventEmitter.prototype.removeAllListeners = function(type) { - if (arguments.length === 0) { - this._events = {}; - return this; - } +},{"./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'); - // does not use listeners(), so no side effect of creating _events[type] - if (type && this._events && this._events[type]) this._events[type] = null; - return this; -}; -EventEmitter.prototype.listeners = function(type) { - if (!this._events) this._events = {}; - if (!this._events[type]) this._events[type] = []; - if (!isArray(this._events[type])) { - this._events[type] = [this._events[type]]; - } - return this._events[type]; -}; +function LocalMedia(opts) { + WildEmitter.call(this); -EventEmitter.listenerCount = function(emitter, type) { - var ret; - if (!emitter._events || !emitter._events[type]) - ret = 0; - else if (typeof emitter._events[type] === 'function') - ret = 1; - else - ret = emitter._events[type].length; - return ret; -}; + var config = this.config = { + autoAdjustMic: false, + detectSpeakingEvents: true, + media: { + audio: true, + video: true + }, + logger: mockconsole + }; -},{"__browserify_process":21}],13:[function(require,module,exports){ -var WildEmitter = require('wildemitter'); + var item; + for (item in opts) { + this.config[item] = opts[item]; + } -function getMaxVolume (analyser, fftBins) { - var maxVolume = -Infinity; - analyser.getFloatFrequencyData(fftBins); + this.logger = config.logger; + this._log = this.logger.log.bind(this.logger, 'LocalMedia:'); + this._logerror = this.logger.error.bind(this.logger, 'LocalMedia:'); - for(var i=0, ii=fftBins.length; i < ii; i++) { - if (fftBins[i] > maxVolume && fftBins[i] < 0) { - maxVolume = fftBins[i]; - } - }; + this.screenSharingSupport = webrtc.screenSharing; - return maxVolume; + this.localStreams = []; + this.localScreens = []; + + if (!webrtc.support) { + this._logerror('Your browser does not support local media capture.'); + } } +util.inherits(LocalMedia, WildEmitter); -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(); +LocalMedia.prototype.start = function (mediaConstraints, cb) { + var self = this; + var constraints = mediaConstraints || this.config.media; - // make it not break in non-supported browsers - if (!audioContextType) return harker; + getUserMedia(constraints, function (err, stream) { + if (!err) { + if (constraints.audio && self.config.detectSpeakingEvents) { + self.setupAudioMonitor(stream); + } + self.localStreams.push(stream); - //Config - var options = options || {}, - smoothing = (options.smoothing || 0.5), - interval = (options.interval || 100), - threshold = options.threshold, - play = options.play, - running = true; + if (self.config.autoAdjustMic) { + self.gainController = new GainController(stream); + // start out somewhat muted if we can track audio + self.setMicIfEnabled(0.5); + } - //Setup Audio Context - if (!audioContext) { - audioContext = new audioContextType(); - } - var sourceNode, fftBins, analyser; + // 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); + */ + }; - analyser = audioContext.createAnalyser(); - analyser.fftSize = 512; - analyser.smoothingTimeConstant = smoothing; - fftBins = new Float32Array(analyser.fftSize); + self.emit('localStream', stream); + } + if (cb) { + return cb(err, stream); + } + }); +}; - 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; - } +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 = []; + } +}; - sourceNode.connect(analyser); - if (play) analyser.connect(audioContext.destination); +LocalMedia.prototype.startScreenShare = function (cb) { + var self = this; + getScreenMedia(function (err, stream) { + if (!err) { + self.localScreens.push(stream); - harker.speaking = false; + // 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); + } - harker.setThreshold = function(t) { - threshold = t; - }; + // enable the callback + if (cb) { + return cb(err, stream); + } + }); +}; - 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'); +LocalMedia.prototype.stopScreenShare = function (stream) { + if (stream) { + stream.stop(); + } else { + this.localScreens.forEach(function (stream) { + stream.stop(); + }); + this.localScreens = []; } - }; +}; - // 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); +// Audio controls +LocalMedia.prototype.mute = function () { + this._audioEnabled(false); + this.hardMuted = true; + this.emit('audioOff'); +}; - harker.emit('volume_change', currentVolume, threshold); +LocalMedia.prototype.unmute = function () { + this._audioEnabled(true); + this.hardMuted = false; + this.emit('audioOn'); +}; + +LocalMedia.prototype.setupAudioMonitor = function (stream) { + this._log('Setup audio'); + var audio = hark(stream); + var self = this; + var timeout; + + audio.on('speaking', function () { + self.emit('speaking'); + if (self.hardMuted) { + return; + } + self.setMicIfEnabled(1); + }); + + audio.on('stopped_speaking', function () { + if (timeout) { + clearTimeout(timeout); + } + + 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); + }); +}; + +// 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.gainController.setGain(volume); +}; - if (currentVolume > threshold) { - if (!harker.speaking) { - harker.speaking = true; - harker.emit('speaking'); - } - } else { - if (harker.speaking) { - harker.speaking = false; - harker.emit('stopped_speaking'); - } - } +// Video controls +LocalMedia.prototype.pauseVideo = function () { + this._videoEnabled(false); + this.emit('videoOff'); +}; +LocalMedia.prototype.resumeVideo = function () { + this._videoEnabled(true); + this.emit('videoOn'); +}; - looper(); - }, interval); - }; - looper(); +// Combined controls +LocalMedia.prototype.pause = function () { + this._audioEnabled(false); + this.pauseVideo(); +}; +LocalMedia.prototype.resume = function () { + this._audioEnabled(true); + this.resumeVideo(); +}; +// 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; + }); + }); +}; - return harker; -} +// 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; +}; -},{"wildemitter":3}],20:[function(require,module,exports){ -var tosdp = require('./lib/tosdp'); -var tojson = require('./lib/tojson'); +// 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; +}; +// Backwards Compat +LocalMedia.prototype.startLocalMedia = LocalMedia.prototype.start; +LocalMedia.prototype.stopLocalMedia = LocalMedia.prototype.stop; -exports.toSessionSDP = tosdp.toSessionSDP; -exports.toMediaSDP = tosdp.toMediaSDP; -exports.toCandidateSDP = tosdp.toCandidateSDP; +// 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; + } +}); -exports.toSessionJSON = tojson.toSessionJSON; -exports.toMediaJSON = tojson.toMediaJSON; -exports.toCandidateJSON = tojson.toCandidateJSON; +module.exports = LocalMedia; -},{"./lib/tojson":23,"./lib/tosdp":22}],22:[function(require,module,exports){ +},{"getscreenmedia":23,"getusermedia":16,"hark":22,"mediastream-gain":24,"mockconsole":4,"util":8,"webrtcsupport":10,"wildemitter":17}],20:[function(require,module,exports){ var senders = { 'initiator': 'sendonly', 'responder': 'recvonly', @@ -8031,208 +8030,259 @@ exports.toCandidateSDP = function (candidate) { return 'a=candidate:' + sdp.join(' '); }; -},{}],19:[function(require,module,exports){ -// based on https://github.com/ESTOS/strophe.jingle/ -// adds wildemitter support -var util = require('util'); -var webrtc = require('webrtcsupport'); +},{}],22:[function(require,module,exports){ var WildEmitter = require('wildemitter'); -function dumpSDP(description) { - return 'type: ' + description.type + '\r\n' + description.sdp; +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; } -function TraceablePeerConnection(config, constraints) { - var self = this; - WildEmitter.call(this); - this.peerconnection = new webrtc.PeerConnection(config, constraints); +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(); - this.trace = function (what, info) { - self.emit('PeerConnectionTrace', { - 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); + // 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'); } - }; - self.ondatachannel = null; - this.peerconnection.ondatachannel = function (event) { - self.trace('ondatachannel', event); - if (self.ondatachannel !== null) { - self.ondatachannel(event); + } else { + if (harker.speaking) { + harker.speaking = false; + harker.emit('stopped_speaking'); } - }; -} + } -util.inherits(TraceablePeerConnection, WildEmitter); + looper(); + }, interval); + }; + looper(); -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; }); + + return harker; } -TraceablePeerConnection.prototype.addStream = function (stream) { - this.trace('addStream', stream.id); - this.peerconnection.addStream(stream); -}; +},{"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; -TraceablePeerConnection.prototype.removeStream = function (stream) { - this.trace('removeStream', stream.id); - this.peerconnection.removeStream(stream); -}; +function WildEmitter() { + this.callbacks = {}; +} -TraceablePeerConnection.prototype.createDataChannel = function (label, opts) { - this.trace('createDataChannel', label, opts); - return this.peerconnection.createDataChannel(label, opts); +// 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; }; -TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) { - var self = this; - this.trace('setLocalDescription', dumpSDP(description)); - this.peerconnection.setLocalDescription(description, - function () { - self.trace('setLocalDescriptionOnSuccess'); - successCallback(); - }, - function (err) { - self.trace('setLocalDescriptionOnFailure', err); - failureCallback(err); - } - ); +// 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; }; -TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) { - var self = this; - this.trace('setRemoteDescription', dumpSDP(description)); - this.peerconnection.setRemoteDescription(description, - function () { - self.trace('setRemoteDescriptionOnSuccess'); - successCallback(); - }, - function (err) { - self.trace('setRemoteDescriptionOnFailure', err); - failureCallback(err); +// 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; }; -TraceablePeerConnection.prototype.close = function () { - this.trace('stop'); - if (this.statsinterval !== null) { - window.clearInterval(this.statsinterval); - this.statsinterval = null; - } - if (this.peerconnection.signalingState != 'closed') { - this.peerconnection.close(); +// 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; } -}; -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 - ); + // remove specific handler + i = callbacks.indexOf(fn); + callbacks.splice(i, 1); + return this; }; -TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) { - var self = this; - this.trace('createAnswer', JSON.stringify(constraints, null, ' ')); - this.peerconnection.createAnswer( - function (answer) { - self.trace('createAnswerOnSuccess', dumpSDP(answer)); - successCallback(answer); - }, - function (err) { - self.trace('createAnswerOnFailure', err); - failureCallback(err); - }, - constraints - ); -}; +// 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; -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); + if (callbacks) { + for (i = 0, len = callbacks.length; i < len; ++i) { + if (callbacks[i]) { + callbacks[i].apply(this, args); + } else { + break; + } } - ); - */ -}; + } -TraceablePeerConnection.prototype.getStats = function (callback, errback) { - if (navigator.mozGetUserMedia) { - // ignore for now... - } else { - this.peerconnection.getStats(callback); + if (specialCallbacks) { + for (i = 0, len = specialCallbacks.length; i < len; ++i) { + if (specialCallbacks[i]) { + specialCallbacks[i].apply(this, [event].concat(args)); + } else { + break; + } + } } + + return this; }; -module.exports = TraceablePeerConnection; +// 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[1].length) === split[1])) { + result = result.concat(this.callbacks[item]); + } + } + return result; +}; -},{"util":15,"webrtcsupport":11,"wildemitter":18}],23:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ var parsers = require('./parsers'); var idCounter = Math.random(); @@ -8398,7 +8448,85 @@ exports.toCandidateJSON = function (line) { return candidate; }; -},{"./parsers":24}],24:[function(require,module,exports){ +},{"./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); + } + } else if (event.data.type == 'getScreenPending') { + window.clearTimeout(event.data.id); + } +}); + +},{"getusermedia":16}],26:[function(require,module,exports){ exports.lines = function (sdp) { return sdp.split('\r\n').filter(function (line) { return line.length > 0; @@ -8632,6 +8760,254 @@ exports.groups = function (lines) { return parsed; }; -},{}]},{},[1])(1) +},{}],19:[function(require,module,exports){ +// based on https://github.com/ESTOS/strophe.jingle/ +// adds wildemitter support +var util = require('util'); +var webrtc = require('webrtcsupport'); +var WildEmitter = require('wildemitter'); + +function dumpSDP(description) { + return 'type: ' + description.type + '\r\n' + description.sdp; +} + +function TraceablePeerConnection(config, constraints) { + var self = this; + WildEmitter.call(this); + + this.peerconnection = new webrtc.PeerConnection(config, constraints); + + this.trace = function (what, info) { + self.emit('PeerConnectionTrace', { + 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); + } + }; +} + +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; }); +} + +TraceablePeerConnection.prototype.addStream = function (stream) { + this.trace('addStream', stream.id); + this.peerconnection.addStream(stream); +}; + +TraceablePeerConnection.prototype.removeStream = function (stream) { + this.trace('removeStream', stream.id); + 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; + this.trace('setLocalDescription', dumpSDP(description)); + this.peerconnection.setLocalDescription(description, + function () { + self.trace('setLocalDescriptionOnSuccess'); + successCallback(); + }, + function (err) { + self.trace('setLocalDescriptionOnFailure', err); + failureCallback(err); + } + ); +}; + +TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) { + var self = this; + this.trace('setRemoteDescription', dumpSDP(description)); + this.peerconnection.setRemoteDescription(description, + function () { + self.trace('setRemoteDescriptionOnSuccess'); + successCallback(); + }, + function (err) { + self.trace('setRemoteDescriptionOnFailure', err); + failureCallback(err); + } + ); +}; + +TraceablePeerConnection.prototype.close = function () { + this.trace('stop'); + if (this.statsinterval !== null) { + window.clearInterval(this.statsinterval); + this.statsinterval = null; + } + if (this.peerconnection.signalingState != 'closed') { + 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) { + 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); + } +}; + +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) }); ;