diff --git a/public/js/simplewebrtc.bundle.js b/public/js/simplewebrtc.bundle.js index 9613fae..9ec74fc 100644 --- a/public/js/simplewebrtc.bundle.js +++ b/public/js/simplewebrtc.bundle.js @@ -26,6 +26,12 @@ function SimpleWebRTC(opts) { video: true, audio: true }, + receiveMedia: { // FIXME: remove old chrome <= 37 constraints format + mandatory: { + OfferToReceiveAudio: true, + OfferToReceiveVideo: true + } + }, localVideo: { autoplay: true, mirror: true, @@ -80,7 +86,7 @@ function SimpleWebRTC(opts) { peers.forEach(function (p) { if (p.sid == message.sid) peer = p; }); - if (!peer) peer = peers[0]; // fallback for old protocol versions + //if (!peer) peer = peers[0]; // fallback for old protocol versions } if (!peer) { peer = self.webrtc.createPeer({ @@ -185,6 +191,7 @@ function SimpleWebRTC(opts) { self.webrtc.sendToAll('mute', {name: 'video'}); }); + // screensharing events this.webrtc.on('localScreen', function (stream) { var item, el = document.createElement('video'), @@ -233,6 +240,12 @@ function SimpleWebRTC(opts) { */ }); + this.webrtc.on('channelMessage', function (peer, label, data) { + if (data.type == 'volume') { + self.emit('remoteVolumeChange', peer, data.volume); + } + }); + if (this.config.autoRequestMedia) this.startLocalVideo(); } @@ -330,8 +343,8 @@ SimpleWebRTC.prototype.joinRoom = function (name, cb) { enableDataChannels: self.config.enableDataChannels && type !== 'screen', receiveMedia: { mandatory: { - OfferToReceiveAudio: type !== 'screen', - OfferToReceiveVideo: true + OfferToReceiveAudio: type !== 'screen' && self.config.receiveMedia.mandatory.OfferToReceiveAudio, + OfferToReceiveVideo: self.config.receiveMedia.mandatory.OfferToReceiveVideo } } }); @@ -446,47 +459,148 @@ SimpleWebRTC.prototype.sendFile = function () { module.exports = SimpleWebRTC; -},{"attachmediastream":5,"mockconsole":7,"socket.io-client":6,"webrtc":2,"webrtcsupport":4,"wildemitter":3}],4:[function(require,module,exports){ -// created by @HenrikJoreteg -var prefix; +},{"attachmediastream":3,"mockconsole":4,"socket.io-client":7,"webrtc":5,"webrtcsupport":6,"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. -if (window.mozRTCPeerConnection || navigator.mozGetUserMedia) { - prefix = 'moz'; -} else if (window.webkitRTCPeerConnection || navigator.webkitGetUserMedia) { - prefix = 'webkit'; +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 = {}; } -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) || - (window.navigator.userAgent.match('Firefox') && parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10) >= 33)); -var AudioContext = window.AudioContext || window.webkitAudioContext; -var supportVp8 = document.createElement('video').canPlayType('video/webm; codecs="vp8", vorbis') === "probably"; -var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia; +// 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; +}; -// export support flags and constructors.prototype && PC -module.exports = { - support: !!PC && supportVp8 && !!getUserMedia, - supportRTCPeerConnection: !!PC, - supportVp8: supportVp8, - supportGetUserMedia: !!getUserMedia, - supportDataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), - supportWebAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - supportMediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - supportScreenSharing: !!screenSharing, - prefix: prefix, - AudioContext: AudioContext, - PeerConnection: PC, - SessionDescription: SessionDescription, - IceCandidate: IceCandidate, - MediaStream: MediaStream, - getUserMedia: getUserMedia +// 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; }; -},{}],5:[function(require,module,exports){ +// 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; +}; + +},{}],3:[function(require,module,exports){ module.exports = function (stream, el, options) { var URL = window.URL; var opts = { @@ -527,7 +641,66 @@ module.exports = function (stream, el, options) { return element; }; +},{}],4:[function(require,module,exports){ +var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); +var l = methods.length; +var fn = function () {}; +var mockconsole = {}; + +while (l--) { + mockconsole[methods[l]] = fn; +} + +module.exports = mockconsole; + },{}],6:[function(require,module,exports){ +// created by @HenrikJoreteg +var prefix; + +if (window.mozRTCPeerConnection || navigator.mozGetUserMedia) { + prefix = 'moz'; +} else if (window.webkitRTCPeerConnection || navigator.webkitGetUserMedia) { + prefix = 'webkit'; +} + +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) || + (window.navigator.userAgent.match('Firefox') && parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10) >= 33)); +var AudioContext = window.AudioContext || window.webkitAudioContext; +var supportVp8 = document.createElement('video').canPlayType('video/webm; codecs="vp8", vorbis') === "probably"; +var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia; + +// export support flags and constructors.prototype && PC +module.exports = { + prefix: prefix, + support: !!PC && supportVp8 && !!getUserMedia, + // new support style + supportRTCPeerConnection: !!PC, + supportVp8: supportVp8, + supportGetUserMedia: !!getUserMedia, + supportDataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), + supportWebAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + supportMediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + supportScreenSharing: !!screenSharing, + // old deprecated style. Dont use this anymore + dataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), + webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + screenSharing: !!screenSharing, + // constructors + AudioContext: AudioContext, + PeerConnection: PC, + SessionDescription: SessionDescription, + IceCandidate: IceCandidate, + MediaStream: MediaStream, + getUserMedia: getUserMedia +}; + +},{}],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); @@ -4401,173 +4574,20 @@ if (typeof define === "function" && define.amd) { define([], function () { return io; }); } })(); -},{}],7:[function(require,module,exports){ -var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); -var l = methods.length; -var fn = function () {}; -var mockconsole = {}; - -while (l--) { - mockconsole[methods[l]] = fn; -} +},{}],8:[function(require,module,exports){ +var events = require('events'); -module.exports = mockconsole; +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]'}; -},{}],3:[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. +exports.print = function () {}; +exports.puts = function () {}; +exports.debug = function() {}; -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; -}; - -},{}],8:[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 = []; +exports.inspect = function(obj, showHidden, depth, colors) { + var seen = []; var stylize = function(str, styleType) { // http://en.wikipedia.org/wiki/ANSI_escape_code#graphics @@ -4901,7 +4921,7 @@ exports.format = function(f) { return str; }; -},{"events":9}],2:[function(require,module,exports){ +},{"events":9}],5:[function(require,module,exports){ var util = require('util'); var webrtc = require('webrtcsupport'); var WildEmitter = require('wildemitter'); @@ -5066,7 +5086,7 @@ WebRTC.prototype.sendDirectlyToAll = function (channel, message, payload) { module.exports = WebRTC; -},{"./peer":10,"localmedia":11,"mockconsole":7,"util":8,"webrtcsupport":4,"wildemitter":3}],12:[function(require,module,exports){ +},{"./peer":10,"localmedia":11,"mockconsole":4,"util":8,"webrtcsupport":6,"wildemitter":2}],12:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; @@ -5322,7 +5342,11 @@ var util = require('util'); var webrtc = require('webrtcsupport'); var PeerConnection = require('rtcpeerconnection'); var WildEmitter = require('wildemitter'); +var FileTransfer = require('filetransfer'); +// the inband-v1 protocol is sending metadata inband in a serialized JSON object +// followed by the actual data. Receiver closes the datachannel upon completion +var INBAND_FILETRANSFER_V1 = 'https://simplewebrtc.com/protocol/filetransfer#inband-v1'; function Peer(options) { var self = this; @@ -5387,6 +5411,20 @@ function Peer(options) { // call emitter constructor WildEmitter.call(this); + this.on('channelOpen', function (channel) { + if (channel.protocol === INBAND_FILETRANSFER_V1) { + channel.onmessage = function (event) { + var metadata = JSON.parse(event.data); + var receiver = new FileTransfer.Receiver(); + receiver.receive(metadata, channel); + self.emit('fileTransfer', metadata, receiver); + receiver.on('receivedFile', function (file, metadata) { + receiver.channel.close(); + }); + }; + } + }); + // proxy events to parent this.on('*', function () { self.parent.emit.apply(self.parent, arguments); @@ -5544,9 +5582,30 @@ Peer.prototype.handleDataChannelAdded = function (channel) { this._observeDataChannel(channel); }; +Peer.prototype.sendFile = function (file) { + var sender = new FileTransfer.Sender(); + var dc = this.getDataChannel('filetransfer' + (new Date()).getTime(), { + protocol: INBAND_FILETRANSFER_V1 + }); + // override onopen + dc.onopen = function () { + dc.send(JSON.stringify({ + size: file.size, + name: file.name + })); + sender.send(file, dc); + }; + // override onclose + dc.onclose = function () { + console.log('sender received transfer'); + sender.emit('complete'); + }; + return sender; +}; + module.exports = Peer; -},{"rtcpeerconnection":13,"util":8,"webrtcsupport":4,"wildemitter":3}],14:[function(require,module,exports){ +},{"filetransfer":14,"rtcpeerconnection":13,"util":8,"webrtcsupport":6,"wildemitter":2}],15:[function(require,module,exports){ // getUserMedia helper by @HenrikJoreteg var func = (window.navigator.getUserMedia || window.navigator.webkitGetUserMedia || @@ -5919,10 +5978,10 @@ Object.defineProperty(LocalMedia.prototype, 'localScreen', { module.exports = LocalMedia; -},{"getscreenmedia":16,"getusermedia":14,"hark":15,"mediastream-gain":17,"mockconsole":7,"util":8,"webrtcsupport":4,"wildemitter":3}],18:[function(require,module,exports){ -// Underscore.js 1.7.0 +},{"getscreenmedia":16,"getusermedia":15,"hark":17,"mediastream-gain":18,"mockconsole":4,"util":8,"webrtcsupport":6,"wildemitter":2}],19:[function(require,module,exports){ +// Underscore.js 1.8.2 // http://underscorejs.org -// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function() { @@ -5943,7 +6002,6 @@ module.exports = LocalMedia; var push = ArrayProto.push, slice = ArrayProto.slice, - concat = ArrayProto.concat, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; @@ -5952,7 +6010,11 @@ module.exports = LocalMedia; var nativeIsArray = Array.isArray, nativeKeys = Object.keys, - nativeBind = FuncProto.bind; + nativeBind = FuncProto.bind, + nativeCreate = Object.create; + + // Naked function reference for surrogate-prototype-swapping. + var Ctor = function(){}; // Create a safe reference to the Underscore object for use below. var _ = function(obj) { @@ -5974,12 +6036,12 @@ module.exports = LocalMedia; } // Current version. - _.VERSION = '1.7.0'; + _.VERSION = '1.8.2'; // Internal function that returns an efficient (for current engines) version // of the passed-in callback, to be repeatedly applied in other Underscore // functions. - var createCallback = function(func, context, argCount) { + var optimizeCb = function(func, context, argCount) { if (context === void 0) return func; switch (argCount == null ? 3 : argCount) { case 1: return function(value) { @@ -6003,12 +6065,52 @@ module.exports = LocalMedia; // A mostly-internal function to generate callbacks that can be applied // to each element in a collection, returning the desired result — either // identity, an arbitrary callback, a property matcher, or a property accessor. - _.iteratee = function(value, context, argCount) { + var cb = function(value, context, argCount) { if (value == null) return _.identity; - if (_.isFunction(value)) return createCallback(value, context, argCount); - if (_.isObject(value)) return _.matches(value); + if (_.isFunction(value)) return optimizeCb(value, context, argCount); + if (_.isObject(value)) return _.matcher(value); return _.property(value); }; + _.iteratee = function(value, context) { + return cb(value, context, Infinity); + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, undefinedOnly) { + return function(obj) { + var length = arguments.length; + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + }; + + // An internal function for creating a new object that inherits from another. + var baseCreate = function(prototype) { + if (!_.isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + }; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var isArrayLike = function(collection) { + var length = collection && collection.length; + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + }; // Collection Functions // -------------------- @@ -6017,11 +6119,10 @@ module.exports = LocalMedia; // Handles raw objects in addition to array-likes. Treats all // sparse array-likes as if they were dense. _.each = _.forEach = function(obj, iteratee, context) { - if (obj == null) return obj; - iteratee = createCallback(iteratee, context); - var i, length = obj.length; - if (length === +length) { - for (i = 0; i < length; i++) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj); } } else { @@ -6035,77 +6136,66 @@ module.exports = LocalMedia; // Return the results of applying the iteratee to each element. _.map = _.collect = function(obj, iteratee, context) { - if (obj == null) return []; - iteratee = _.iteratee(iteratee, context); - var keys = obj.length !== +obj.length && _.keys(obj), + iteratee = cb(iteratee, context); + var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, - results = Array(length), - currentKey; + results = Array(length); for (var index = 0; index < length; index++) { - currentKey = keys ? keys[index] : index; + var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; }; - var reduceError = 'Reduce of empty array with no initial value'; + // Create a reducing function iterating left or right. + function createReduce(dir) { + // Optimized iterator function as using arguments.length + // in the main function will deoptimize the, see #1991. + function iterator(obj, iteratee, memo, keys, index, length) { + for (; index >= 0 && index < length; index += dir) { + var currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + } + + return function(obj, iteratee, memo, context) { + iteratee = optimizeCb(iteratee, context, 4); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + index = dir > 0 ? 0 : length - 1; + // Determine the initial value if none is provided. + if (arguments.length < 3) { + memo = obj[keys ? keys[index] : index]; + index += dir; + } + return iterator(obj, iteratee, memo, keys, index, length); + }; + } // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. - _.reduce = _.foldl = _.inject = function(obj, iteratee, memo, context) { - if (obj == null) obj = []; - iteratee = createCallback(iteratee, context, 4); - var keys = obj.length !== +obj.length && _.keys(obj), - length = (keys || obj).length, - index = 0, currentKey; - if (arguments.length < 3) { - if (!length) throw new TypeError(reduceError); - memo = obj[keys ? keys[index++] : index++]; - } - for (; index < length; index++) { - currentKey = keys ? keys[index] : index; - memo = iteratee(memo, obj[currentKey], currentKey, obj); - } - return memo; - }; + _.reduce = _.foldl = _.inject = createReduce(1); // The right-associative version of reduce, also known as `foldr`. - _.reduceRight = _.foldr = function(obj, iteratee, memo, context) { - if (obj == null) obj = []; - iteratee = createCallback(iteratee, context, 4); - var keys = obj.length !== + obj.length && _.keys(obj), - index = (keys || obj).length, - currentKey; - if (arguments.length < 3) { - if (!index) throw new TypeError(reduceError); - memo = obj[keys ? keys[--index] : --index]; - } - while (index--) { - currentKey = keys ? keys[index] : index; - memo = iteratee(memo, obj[currentKey], currentKey, obj); - } - return memo; - }; + _.reduceRight = _.foldr = createReduce(-1); // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, predicate, context) { - var result; - predicate = _.iteratee(predicate, context); - _.some(obj, function(value, index, list) { - if (predicate(value, index, list)) { - result = value; - return true; - } - }); - return result; + var key; + if (isArrayLike(obj)) { + key = _.findIndex(obj, predicate, context); + } else { + key = _.findKey(obj, predicate, context); + } + if (key !== void 0 && key !== -1) return obj[key]; }; // Return all the elements that pass a truth test. // Aliased as `select`. _.filter = _.select = function(obj, predicate, context) { var results = []; - if (obj == null) return results; - predicate = _.iteratee(predicate, context); + predicate = cb(predicate, context); _.each(obj, function(value, index, list) { if (predicate(value, index, list)) results.push(value); }); @@ -6114,19 +6204,17 @@ module.exports = LocalMedia; // Return all the elements for which a truth test fails. _.reject = function(obj, predicate, context) { - return _.filter(obj, _.negate(_.iteratee(predicate)), context); + return _.filter(obj, _.negate(cb(predicate)), context); }; // Determine whether all of the elements match a truth test. // Aliased as `all`. _.every = _.all = function(obj, predicate, context) { - if (obj == null) return true; - predicate = _.iteratee(predicate, context); - var keys = obj.length !== +obj.length && _.keys(obj), - length = (keys || obj).length, - index, currentKey; - for (index = 0; index < length; index++) { - currentKey = keys ? keys[index] : index; + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; if (!predicate(obj[currentKey], currentKey, obj)) return false; } return true; @@ -6135,24 +6223,21 @@ module.exports = LocalMedia; // Determine if at least one element in the object matches a truth test. // Aliased as `any`. _.some = _.any = function(obj, predicate, context) { - if (obj == null) return false; - predicate = _.iteratee(predicate, context); - var keys = obj.length !== +obj.length && _.keys(obj), - length = (keys || obj).length, - index, currentKey; - for (index = 0; index < length; index++) { - currentKey = keys ? keys[index] : index; + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; if (predicate(obj[currentKey], currentKey, obj)) return true; } return false; }; // Determine if the array or object contains a given value (using `===`). - // Aliased as `include`. - _.contains = _.include = function(obj, target) { - if (obj == null) return false; - if (obj.length !== +obj.length) obj = _.values(obj); - return _.indexOf(obj, target) >= 0; + // Aliased as `includes` and `include`. + _.contains = _.includes = _.include = function(obj, target, fromIndex) { + if (!isArrayLike(obj)) obj = _.values(obj); + return _.indexOf(obj, target, typeof fromIndex == 'number' && fromIndex) >= 0; }; // Invoke a method (with arguments) on every item in a collection. @@ -6160,7 +6245,8 @@ module.exports = LocalMedia; var args = slice.call(arguments, 2); var isFunc = _.isFunction(method); return _.map(obj, function(value) { - return (isFunc ? method : value[method]).apply(value, args); + var func = isFunc ? method : value[method]; + return func == null ? func : func.apply(value, args); }); }; @@ -6172,13 +6258,13 @@ module.exports = LocalMedia; // Convenience version of a common use case of `filter`: selecting only objects // containing specific `key:value` pairs. _.where = function(obj, attrs) { - return _.filter(obj, _.matches(attrs)); + return _.filter(obj, _.matcher(attrs)); }; // Convenience version of a common use case of `find`: getting the first object // containing specific `key:value` pairs. _.findWhere = function(obj, attrs) { - return _.find(obj, _.matches(attrs)); + return _.find(obj, _.matcher(attrs)); }; // Return the maximum element (or element-based computation). @@ -6186,7 +6272,7 @@ module.exports = LocalMedia; var result = -Infinity, lastComputed = -Infinity, value, computed; if (iteratee == null && obj != null) { - obj = obj.length === +obj.length ? obj : _.values(obj); + obj = isArrayLike(obj) ? obj : _.values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value > result) { @@ -6194,7 +6280,7 @@ module.exports = LocalMedia; } } } else { - iteratee = _.iteratee(iteratee, context); + iteratee = cb(iteratee, context); _.each(obj, function(value, index, list) { computed = iteratee(value, index, list); if (computed > lastComputed || computed === -Infinity && result === -Infinity) { @@ -6211,7 +6297,7 @@ module.exports = LocalMedia; var result = Infinity, lastComputed = Infinity, value, computed; if (iteratee == null && obj != null) { - obj = obj.length === +obj.length ? obj : _.values(obj); + obj = isArrayLike(obj) ? obj : _.values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value < result) { @@ -6219,7 +6305,7 @@ module.exports = LocalMedia; } } } else { - iteratee = _.iteratee(iteratee, context); + iteratee = cb(iteratee, context); _.each(obj, function(value, index, list) { computed = iteratee(value, index, list); if (computed < lastComputed || computed === Infinity && result === Infinity) { @@ -6234,7 +6320,7 @@ module.exports = LocalMedia; // Shuffle a collection, using the modern version of the // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). _.shuffle = function(obj) { - var set = obj && obj.length === +obj.length ? obj : _.values(obj); + var set = isArrayLike(obj) ? obj : _.values(obj); var length = set.length; var shuffled = Array(length); for (var index = 0, rand; index < length; index++) { @@ -6250,7 +6336,7 @@ module.exports = LocalMedia; // The internal `guard` argument allows it to work with `map`. _.sample = function(obj, n, guard) { if (n == null || guard) { - if (obj.length !== +obj.length) obj = _.values(obj); + if (!isArrayLike(obj)) obj = _.values(obj); return obj[_.random(obj.length - 1)]; } return _.shuffle(obj).slice(0, Math.max(0, n)); @@ -6258,7 +6344,7 @@ module.exports = LocalMedia; // Sort the object's values by a criterion produced by an iteratee. _.sortBy = function(obj, iteratee, context) { - iteratee = _.iteratee(iteratee, context); + iteratee = cb(iteratee, context); return _.pluck(_.map(obj, function(value, index, list) { return { value: value, @@ -6280,7 +6366,7 @@ module.exports = LocalMedia; var group = function(behavior) { return function(obj, iteratee, context) { var result = {}; - iteratee = _.iteratee(iteratee, context); + iteratee = cb(iteratee, context); _.each(obj, function(value, index) { var key = iteratee(value, index, obj); behavior(result, value, key); @@ -6308,37 +6394,24 @@ module.exports = LocalMedia; if (_.has(result, key)) result[key]++; else result[key] = 1; }); - // Use a comparator function to figure out the smallest index at which - // an object should be inserted so as to maintain order. Uses binary search. - _.sortedIndex = function(array, obj, iteratee, context) { - iteratee = _.iteratee(iteratee, context, 1); - var value = iteratee(obj); - var low = 0, high = array.length; - while (low < high) { - var mid = low + high >>> 1; - if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; - } - return low; - }; - // Safely create a real, live array from anything iterable. _.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); - if (obj.length === +obj.length) return _.map(obj, _.identity); + if (isArrayLike(obj)) return _.map(obj, _.identity); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { if (obj == null) return 0; - return obj.length === +obj.length ? obj.length : _.keys(obj).length; + return isArrayLike(obj) ? obj.length : _.keys(obj).length; }; // Split a collection into two arrays: one whose elements all satisfy the given // predicate, and one whose elements all do not satisfy the predicate. _.partition = function(obj, predicate, context) { - predicate = _.iteratee(predicate, context); + predicate = cb(predicate, context); var pass = [], fail = []; _.each(obj, function(value, key, obj) { (predicate(value, key, obj) ? pass : fail).push(value); @@ -6355,30 +6428,27 @@ module.exports = LocalMedia; _.first = _.head = _.take = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[0]; - if (n < 0) return []; - return slice.call(array, 0, n); + return _.initial(array, array.length - n); }; // Returns everything but the last entry of the array. Especially useful on // the arguments object. Passing **n** will return all the values in - // the array, excluding the last N. The **guard** check allows it to work with - // `_.map`. + // the array, excluding the last N. _.initial = function(array, n, guard) { return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); }; // Get the last element of an array. Passing **n** will return the last N - // values in the array. The **guard** check allows it to work with `_.map`. + // values in the array. _.last = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[array.length - 1]; - return slice.call(array, Math.max(array.length - n, 0)); + return _.rest(array, Math.max(0, array.length - n)); }; // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. // Especially useful on the arguments object. Passing an **n** will return - // the rest N values in the array. The **guard** - // check allows it to work with `_.map`. + // the rest N values in the array. _.rest = _.tail = _.drop = function(array, n, guard) { return slice.call(array, n == null || guard ? 1 : n); }; @@ -6389,18 +6459,20 @@ module.exports = LocalMedia; }; // Internal implementation of a recursive `flatten` function. - var flatten = function(input, shallow, strict, output) { - if (shallow && _.every(input, _.isArray)) { - return concat.apply(output, input); - } - for (var i = 0, length = input.length; i < length; i++) { + var flatten = function(input, shallow, strict, startIndex) { + var output = [], idx = 0; + for (var i = startIndex || 0, length = input && input.length; i < length; i++) { var value = input[i]; - if (!_.isArray(value) && !_.isArguments(value)) { - if (!strict) output.push(value); - } else if (shallow) { - push.apply(output, value); - } else { - flatten(value, shallow, strict, output); + if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { + //flatten current level of array or arguments object + if (!shallow) value = flatten(value, shallow, strict); + var j = 0, len = value.length; + output.length += len; + while (j < len) { + output[idx++] = value[j++]; + } + } else if (!strict) { + output[idx++] = value; } } return output; @@ -6408,7 +6480,7 @@ module.exports = LocalMedia; // Flatten out an array, either recursively (by default), or just one level. _.flatten = function(array, shallow) { - return flatten(array, shallow, false, []); + return flatten(array, shallow, false); }; // Return a version of the array that does not contain the specified value(s). @@ -6426,21 +6498,21 @@ module.exports = LocalMedia; iteratee = isSorted; isSorted = false; } - if (iteratee != null) iteratee = _.iteratee(iteratee, context); + if (iteratee != null) iteratee = cb(iteratee, context); var result = []; var seen = []; for (var i = 0, length = array.length; i < length; i++) { - var value = array[i]; + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; if (isSorted) { - if (!i || seen !== value) result.push(value); - seen = value; + if (!i || seen !== computed) result.push(value); + seen = computed; } else if (iteratee) { - var computed = iteratee(value, i, array); - if (_.indexOf(seen, computed) < 0) { + if (!_.contains(seen, computed)) { seen.push(computed); result.push(value); } - } else if (_.indexOf(result, value) < 0) { + } else if (!_.contains(result, value)) { result.push(value); } } @@ -6450,7 +6522,7 @@ module.exports = LocalMedia; // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { - return _.uniq(flatten(arguments, true, true, [])); + return _.uniq(flatten(arguments, true, true)); }; // Produce an array that contains every item shared between all the @@ -6473,7 +6545,7 @@ module.exports = LocalMedia; // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. _.difference = function(array) { - var rest = flatten(slice.call(arguments, 1), true, true, []); + var rest = flatten(arguments, true, true, 1); return _.filter(array, function(value){ return !_.contains(rest, value); }); @@ -6481,23 +6553,28 @@ module.exports = LocalMedia; // Zip together multiple lists into a single array -- elements that share // an index go together. - _.zip = function(array) { - if (array == null) return []; - var length = _.max(arguments, 'length').length; - var results = Array(length); - for (var i = 0; i < length; i++) { - results[i] = _.pluck(arguments, i); + _.zip = function() { + return _.unzip(arguments); + }; + + // Complement of _.zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices + _.unzip = function(array) { + var length = array && _.max(array, 'length').length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = _.pluck(array, index); } - return results; + return result; }; // Converts lists into objects. Pass either a single array of `[key, value]` // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. _.object = function(list, values) { - if (list == null) return {}; var result = {}; - for (var i = 0, length = list.length; i < length; i++) { + for (var i = 0, length = list && list.length; i < length; i++) { if (values) { result[list[i]] = values[i]; } else { @@ -6512,30 +6589,63 @@ module.exports = LocalMedia; // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = function(array, item, isSorted) { - if (array == null) return -1; - var i = 0, length = array.length; - if (isSorted) { - if (typeof isSorted == 'number') { - i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; - } else { - i = _.sortedIndex(array, item); - return array[i] === item ? i : -1; - } + var i = 0, length = array && array.length; + if (typeof isSorted == 'number') { + i = isSorted < 0 ? Math.max(0, length + isSorted) : isSorted; + } else if (isSorted && length) { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + if (item !== item) { + return _.findIndex(slice.call(array, i), _.isNaN); } for (; i < length; i++) if (array[i] === item) return i; return -1; }; _.lastIndexOf = function(array, item, from) { - if (array == null) return -1; - var idx = array.length; + var idx = array ? array.length : 0; if (typeof from == 'number') { idx = from < 0 ? idx + from + 1 : Math.min(idx, from + 1); } + if (item !== item) { + return _.findLastIndex(slice.call(array, 0, idx), _.isNaN); + } while (--idx >= 0) if (array[idx] === item) return idx; return -1; }; + // Generator function to create the findIndex and findLastIndex functions + function createIndexFinder(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = array != null && array.length; + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + } + + // Returns the first index on an array-like that passes a predicate test + _.findIndex = createIndexFinder(1); + + _.findLastIndex = createIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = array.length; + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](http://docs.python.org/library/functions.html#range). @@ -6559,25 +6669,25 @@ module.exports = LocalMedia; // Function (ahem) Functions // ------------------ - // Reusable constructor function for prototype setting. - var Ctor = function(){}; + // Determines whether to execute a function as a constructor + // or a normal function with the provided arguments + var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (_.isObject(result)) return result; + return self; + }; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // available. _.bind = function(func, context) { - var args, bound; if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); - args = slice.call(arguments, 2); - bound = function() { - if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); - Ctor.prototype = func.prototype; - var self = new Ctor; - Ctor.prototype = null; - var result = func.apply(self, args.concat(slice.call(arguments))); - if (_.isObject(result)) return result; - return self; + var args = slice.call(arguments, 2); + var bound = function() { + return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); }; return bound; }; @@ -6587,15 +6697,16 @@ module.exports = LocalMedia; // as a placeholder, allowing any combination of arguments to be pre-filled. _.partial = function(func) { var boundArgs = slice.call(arguments, 1); - return function() { - var position = 0; - var args = boundArgs.slice(); - for (var i = 0, length = args.length; i < length; i++) { - if (args[i] === _) args[i] = arguments[position++]; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; } while (position < arguments.length) args.push(arguments[position++]); - return func.apply(this, args); + return executeBound(func, bound, this, this, args); }; + return bound; }; // Bind a number of an object's methods to that object. Remaining arguments @@ -6615,7 +6726,7 @@ module.exports = LocalMedia; _.memoize = function(func, hasher) { var memoize = function(key) { var cache = memoize.cache; - var address = hasher ? hasher.apply(this, arguments) : key; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); return cache[address]; }; @@ -6634,9 +6745,7 @@ module.exports = LocalMedia; // Defers a function, scheduling it to run after the current call stack has // cleared. - _.defer = function(func) { - return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); - }; + _.defer = _.partial(_.delay, _, 1); // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run @@ -6661,8 +6770,10 @@ module.exports = LocalMedia; context = this; args = arguments; if (remaining <= 0 || remaining > wait) { - clearTimeout(timeout); - timeout = null; + if (timeout) { + clearTimeout(timeout); + timeout = null; + } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; @@ -6683,7 +6794,7 @@ module.exports = LocalMedia; var later = function() { var last = _.now() - timestamp; - if (last < wait && last > 0) { + if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; @@ -6736,7 +6847,7 @@ module.exports = LocalMedia; }; }; - // Returns a function that will only be executed after being called N times. + // Returns a function that will only be executed on and after the Nth call. _.after = function(times, func) { return function() { if (--times < 1) { @@ -6745,15 +6856,14 @@ module.exports = LocalMedia; }; }; - // Returns a function that will only be executed before being called N times. + // Returns a function that will only be executed up to (but not including) the Nth call. _.before = function(times, func) { var memo; return function() { if (--times > 0) { memo = func.apply(this, arguments); - } else { - func = null; } + if (times <= 1) func = null; return memo; }; }; @@ -6765,13 +6875,47 @@ module.exports = LocalMedia; // Object Functions // ---------------- - // Retrieve the names of an object's properties. - // Delegates to **ECMAScript 5**'s native `Object.keys` - _.keys = function(obj) { - if (!_.isObject(obj)) return []; - if (nativeKeys) return nativeKeys(obj); - var keys = []; + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + function collectNonEnumProps(obj, keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { + keys.push(prop); + } + } + } + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; for (var key in obj) if (_.has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve all the property names of an object. + _.allKeys = function(obj) { + if (!_.isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; }; @@ -6786,6 +6930,21 @@ module.exports = LocalMedia; return values; }; + // Returns the results of applying the iteratee to each element of the object + // In contrast to _.map it returns an object + _.mapObject = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = _.keys(obj), + length = keys.length, + results = {}, + currentKey; + for (var index = 0; index < length; index++) { + currentKey = keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + // Convert an object into a list of `[key, value]` pairs. _.pairs = function(obj) { var keys = _.keys(obj); @@ -6818,37 +6977,38 @@ module.exports = LocalMedia; }; // Extend a given object with all the properties in passed-in object(s). - _.extend = function(obj) { - if (!_.isObject(obj)) return obj; - var source, prop; - for (var i = 1, length = arguments.length; i < length; i++) { - source = arguments[i]; - for (prop in source) { - if (hasOwnProperty.call(source, prop)) { - obj[prop] = source[prop]; - } - } + _.extend = createAssigner(_.allKeys); + + // Assigns a given object with all the own properties in the passed-in object(s) + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + _.extendOwn = _.assign = createAssigner(_.keys); + + // Returns the first key on an object that passes a predicate test + _.findKey = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = _.keys(obj), key; + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (predicate(obj[key], key, obj)) return key; } - return obj; }; // Return a copy of the object only containing the whitelisted properties. - _.pick = function(obj, iteratee, context) { - var result = {}, key; + _.pick = function(object, oiteratee, context) { + var result = {}, obj = object, iteratee, keys; if (obj == null) return result; - if (_.isFunction(iteratee)) { - iteratee = createCallback(iteratee, context); - for (key in obj) { - var value = obj[key]; - if (iteratee(value, key, obj)) result[key] = value; - } + if (_.isFunction(oiteratee)) { + keys = _.allKeys(obj); + iteratee = optimizeCb(oiteratee, context); } else { - var keys = concat.apply([], slice.call(arguments, 1)); - obj = new Object(obj); - for (var i = 0, length = keys.length; i < length; i++) { - key = keys[i]; - if (key in obj) result[key] = obj[key]; - } + keys = flatten(arguments, false, false, 1); + iteratee = function(value, key, obj) { return key in obj; }; + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; } return result; }; @@ -6858,7 +7018,7 @@ module.exports = LocalMedia; if (_.isFunction(iteratee)) { iteratee = _.negate(iteratee); } else { - var keys = _.map(concat.apply([], slice.call(arguments, 1)), String); + var keys = _.map(flatten(arguments, false, false, 1), String); iteratee = function(value, key) { return !_.contains(keys, key); }; @@ -6867,16 +7027,7 @@ module.exports = LocalMedia; }; // Fill in a given object with default properties. - _.defaults = function(obj) { - if (!_.isObject(obj)) return obj; - for (var i = 1, length = arguments.length; i < length; i++) { - var source = arguments[i]; - for (var prop in source) { - if (obj[prop] === void 0) obj[prop] = source[prop]; - } - } - return obj; - }; + _.defaults = createAssigner(_.allKeys, true); // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { @@ -6892,6 +7043,19 @@ module.exports = LocalMedia; return obj; }; + // Returns whether an object has a given set of `key:value` pairs. + _.isMatch = function(object, attrs) { + var keys = _.keys(attrs), length = keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + + // Internal recursive comparison function for `isEqual`. var eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. @@ -6926,74 +7090,76 @@ module.exports = LocalMedia; // of `NaN` are not equivalent. return +a === +b; } - if (typeof a != 'object' || typeof b != 'object') return false; + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; } - // Objects with different constructors are not equivalent, but `Object`s - // from different frames are. - var aCtor = a.constructor, bCtor = b.constructor; - if ( - aCtor !== bCtor && - // Handle Object.create(x) cases - 'constructor' in a && 'constructor' in b && - !(_.isFunction(aCtor) && aCtor instanceof aCtor && - _.isFunction(bCtor) && bCtor instanceof bCtor) - ) { - return false; - } + // Add the first object to the stack of traversed objects. aStack.push(a); bStack.push(b); - var size, result; + // Recursively compare objects and arrays. - if (className === '[object Array]') { + if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. - size = a.length; - result = size === b.length; - if (result) { - // Deep compare the contents, ignoring non-numeric properties. - while (size--) { - if (!(result = eq(a[size], b[size], aStack, bStack))) break; - } + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // Deep compare objects. var keys = _.keys(a), key; - size = keys.length; + length = keys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. - result = _.keys(b).length === size; - if (result) { - while (size--) { - // Deep compare each member - key = keys[size]; - if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; - } + if (_.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } } // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); - return result; + return true; }; // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { - return eq(a, b, [], []); + return eq(a, b); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { if (obj == null) return true; - if (_.isArray(obj) || _.isString(obj) || _.isArguments(obj)) return obj.length === 0; - for (var key in obj) if (_.has(obj, key)) return false; - return true; + if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; + return _.keys(obj).length === 0; }; // Is a given value a DOM element? @@ -7013,14 +7179,14 @@ module.exports = LocalMedia; return type === 'function' || type === 'object' && !!obj; }; - // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. - _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { _['is' + name] = function(obj) { return toString.call(obj) === '[object ' + name + ']'; }; }); - // Define a fallback version of the method in browsers (ahem, IE), where + // Define a fallback version of the method in browsers (ahem, IE < 9), where // there isn't any inspectable "Arguments" type. if (!_.isArguments(arguments)) { _.isArguments = function(obj) { @@ -7028,8 +7194,9 @@ module.exports = LocalMedia; }; } - // Optimize `isFunction` if appropriate. Work around an IE 11 bug. - if (typeof /./ !== 'function') { + // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, + // IE 11 (#1621), and in Safari 8 (#1929). + if (typeof /./ != 'function' && typeof Int8Array != 'object') { _.isFunction = function(obj) { return typeof obj == 'function' || false; }; @@ -7081,6 +7248,7 @@ module.exports = LocalMedia; return value; }; + // Predicate-generating functions. Often useful outside of Underscore. _.constant = function(value) { return function() { return value; @@ -7091,28 +7259,30 @@ module.exports = LocalMedia; _.property = function(key) { return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + }; + + // Generates a function for a given object that returns a given property. + _.propertyOf = function(obj) { + return obj == null ? function(){} : function(key) { return obj[key]; }; }; - // Returns a predicate for checking whether an object has a given set of `key:value` pairs. - _.matches = function(attrs) { - var pairs = _.pairs(attrs), length = pairs.length; + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + _.matcher = _.matches = function(attrs) { + attrs = _.extendOwn({}, attrs); return function(obj) { - if (obj == null) return !length; - obj = new Object(obj); - for (var i = 0; i < length; i++) { - var pair = pairs[i], key = pair[0]; - if (pair[1] !== obj[key] || !(key in obj)) return false; - } - return true; + return _.isMatch(obj, attrs); }; }; // Run a function **n** times. _.times = function(n, iteratee, context) { var accum = Array(Math.max(0, n)); - iteratee = createCallback(iteratee, context, 1); + iteratee = optimizeCb(iteratee, context, 1); for (var i = 0; i < n; i++) accum[i] = iteratee(i); return accum; }; @@ -7161,10 +7331,12 @@ module.exports = LocalMedia; // If the value of the named `property` is a function then invoke it with the // `object` as context; otherwise, return it. - _.result = function(object, property) { - if (object == null) return void 0; - var value = object[property]; - return _.isFunction(value) ? object[property]() : value; + _.result = function(object, property, fallback) { + var value = object == null ? void 0 : object[property]; + if (value === void 0) { + value = fallback; + } + return _.isFunction(value) ? value.call(object) : value; }; // Generate a unique integer id (unique within the entire client session). @@ -7279,8 +7451,8 @@ module.exports = LocalMedia; // underscore functions. Wrapped objects may be chained. // Helper function to continue chaining intermediate results. - var result = function(obj) { - return this._chain ? _(obj).chain() : obj; + var result = function(instance, obj) { + return instance._chain ? _(obj).chain() : obj; }; // Add your own custom functions to the Underscore object. @@ -7290,7 +7462,7 @@ module.exports = LocalMedia; _.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); - return result.call(this, func.apply(_, args)); + return result(this, func.apply(_, args)); }; }); }; @@ -7305,7 +7477,7 @@ module.exports = LocalMedia; var obj = this._wrapped; method.apply(obj, arguments); if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; - return result.call(this, obj); + return result(this, obj); }; }); @@ -7313,7 +7485,7 @@ module.exports = LocalMedia; _.each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { - return result.call(this, method.apply(this._wrapped, arguments)); + return result(this, method.apply(this._wrapped, arguments)); }; }); @@ -7322,6 +7494,14 @@ module.exports = LocalMedia; return this._wrapped; }; + // Provide unwrapping proxy for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return '' + this._wrapped; + }; + // AMD registration happens at the end for compatibility with AMD loaders // that may not enforce next-turn semantics on modules. Even though general // practice for AMD registration is to be anonymous, underscore registers @@ -7336,356 +7516,149 @@ module.exports = LocalMedia; } }.call(this)); -},{}],17:[function(require,module,exports){ -var support = require('webrtcsupport'); +},{}],13:[function(require,module,exports){ +var _ = require('underscore'); +var util = require('util'); +var webrtc = require('webrtcsupport'); +var SJJ = require('sdp-jingle-json'); +var WildEmitter = require('wildemitter'); +var peerconn = require('traceablepeerconnection'); +function PeerConnection(config, constraints) { + var self = this; + var item; + WildEmitter.call(this); -function GainController(stream) { - this.support = support.webAudio && support.mediaStream; + config = config || {}; + config.iceServers = config.iceServers || []; - // set our starting value - this.gain = 1; + // make sure this only gets enabled in Google Chrome + // EXPERIMENTAL FLAG, might get removed without notice + this.enableChromeNativeSimulcast = false; + if (constraints && constraints.optional && + webrtc.prefix === 'webkit' && + navigator.appVersion.match(/Chromium\//) === null) { + constraints.optional.forEach(function (constraint, idx) { + if (constraint.enableChromeNativeSimulcast) { + self.enableChromeNativeSimulcast = true; + } + }); + } - 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.addTrack(this.outputStream.getAudioTracks()[0]); - stream.removeTrack(stream.getAudioTracks()[0]); + // EXPERIMENTAL FLAG, might get removed without notice + this.enableMultiStreamHacks = false; + if (constraints && constraints.optional) { + constraints.optional.forEach(function (constraint, idx) { + if (constraint.enableMultiStreamHacks) { + self.enableMultiStreamHacks = true; + } + }); } - this.stream = stream; -} -// setting -GainController.prototype.setGain = function (val) { - // check for support - if (!this.support) return; - this.gainFilter.gain.value = val; - this.gain = val; -}; + this.pc = new peerconn(config, constraints); -GainController.prototype.getGain = function () { - return this.gain; -}; + this.getLocalStreams = this.pc.getLocalStreams.bind(this.pc); + this.getRemoteStreams = this.pc.getRemoteStreams.bind(this.pc); + this.addStream = this.pc.addStream.bind(this.pc); + this.removeStream = this.pc.removeStream.bind(this.pc); -GainController.prototype.off = function () { - return this.setGain(0); -}; + // proxy events + this.pc.on('*', function () { + self.emit.apply(self, arguments); + }); -GainController.prototype.on = function () { - this.setGain(1); -}; + // proxy some events directly + this.pc.onremovestream = this.emit.bind(this, 'removeStream'); + this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded'); + this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange'); + this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange'); + // handle incoming ice and data channel events + this.pc.onaddstream = this._onAddStream.bind(this); + this.pc.onicecandidate = this._onIce.bind(this); + this.pc.ondatachannel = this._onDataChannel.bind(this); -module.exports = GainController; + this.localDescription = { + contents: [] + }; + this.remoteDescription = { + contents: [] + }; -},{"webrtcsupport":19}],19:[function(require,module,exports){ -// created by @HenrikJoreteg -var prefix; -var isChrome = false; -var isFirefox = false; -var ua = window.navigator.userAgent.toLowerCase(); + this.localStream = null; + this.remoteStreams = []; -// basic sniffing -if (ua.indexOf('firefox') !== -1) { - prefix = 'moz'; - isFirefox = true; -} else if (ua.indexOf('chrome') !== -1) { - prefix = 'webkit'; - isChrome = true; -} + this.config = { + debug: false, + ice: {}, + sid: '', + isInitiator: true, + sdpSessionID: Date.now(), + useJingle: false + }; -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; + // apply our config + for (item in config) { + this.config[item] = config[item]; + } + this._role = this.isInitiator ? 'initiator' : 'responder'; -// 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 -}; + if (this.config.debug) { + this.on('*', function (eventName, event) { + var logger = config.logger || console; + logger.log('PeerConnection event:', arguments); + }); + } + this.hadLocalStunCandidate = false; + this.hadRemoteStunCandidate = false; + this.hadLocalRelayCandidate = false; + this.hadRemoteRelayCandidate = false; -},{}],20:[function(require,module,exports){ -var toSDP = require('./lib/tosdp'); -var toJSON = require('./lib/tojson'); + this.hadLocalIPv6Candidate = false; + this.hadRemoteIPv6Candidate = false; + // keeping references for all our data channels + // so they dont get garbage collected + // can be removed once the following bugs have been fixed + // https://crbug.com/405545 + // https://bugzilla.mozilla.org/show_bug.cgi?id=964092 + // to be filed for opera + this._remoteDataChannels = []; + this._localDataChannels = []; +} -// Converstion from JSON to SDP +util.inherits(PeerConnection, WildEmitter); -exports.toIncomingSDPOffer = function (session) { - return toSDP.toSessionSDP(session, { - role: 'responder', - direction: 'incoming' - }); +Object.defineProperty(PeerConnection.prototype, 'signalingState', { + get: function () { + return this.pc.signalingState; + } +}); +Object.defineProperty(PeerConnection.prototype, 'iceConnectionState', { + get: function () { + return this.pc.iceConnectionState; + } +}); + +// Add a stream to the peer connection object +PeerConnection.prototype.addStream = function (stream) { + this.localStream = stream; + this.pc.addStream(stream); }; -exports.toOutgoingSDPOffer = function (session) { - return toSDP.toSessionSDP(session, { - role: 'initiator', - direction: 'outgoing' - }); -}; -exports.toIncomingSDPAnswer = function (session) { - return toSDP.toSessionSDP(session, { - role: 'initiator', - direction: 'incoming' - }); -}; -exports.toOutgoingSDPAnswer = function (session) { - return toSDP.toSessionSDP(session, { - role: 'responder', - direction: 'outgoing' - }); -}; -exports.toIncomingMediaSDPOffer = function (media) { - return toSDP.toMediaSDP(media, { - role: 'responder', - direction: 'incoming' - }); -}; -exports.toOutgoingMediaSDPOffer = function (media) { - return toSDP.toMediaSDP(media, { - role: 'initiator', - direction: 'outgoing' - }); -}; -exports.toIncomingMediaSDPAnswer = function (media) { - return toSDP.toMediaSDP(media, { - role: 'initiator', - direction: 'incoming' - }); -}; -exports.toOutgoingMediaSDPAnswer = function (media) { - return toSDP.toMediaSDP(media, { - role: 'responder', - direction: 'outgoing' - }); -}; -exports.toCandidateSDP = toSDP.toCandidateSDP; -exports.toMediaSDP = toSDP.toMediaSDP; -exports.toSessionSDP = toSDP.toSessionSDP; - - -// Conversion from SDP to JSON - -exports.toIncomingJSONOffer = function (sdp, creators) { - return toJSON.toSessionJSON(sdp, { - role: 'responder', - direction: 'incoming', - creators: creators - }); -}; -exports.toOutgoingJSONOffer = function (sdp, creators) { - return toJSON.toSessionJSON(sdp, { - role: 'initiator', - direction: 'outgoing', - creators: creators - }); -}; -exports.toIncomingJSONAnswer = function (sdp, creators) { - return toJSON.toSessionJSON(sdp, { - role: 'initiator', - direction: 'incoming', - creators: creators - }); -}; -exports.toOutgoingJSONAnswer = function (sdp, creators) { - return toJSON.toSessionJSON(sdp, { - role: 'responder', - direction: 'outgoing', - creators: creators - }); -}; -exports.toIncomingMediaJSONOffer = function (sdp, creator) { - return toJSON.toMediaJSON(sdp, { - role: 'responder', - direction: 'incoming', - creator: creator - }); -}; -exports.toOutgoingMediaJSONOffer = function (sdp, creator) { - return toJSON.toMediaJSON(sdp, { - role: 'initiator', - direction: 'outgoing', - creator: creator - }); -}; -exports.toIncomingMediaJSONAnswer = function (sdp, creator) { - return toJSON.toMediaJSON(sdp, { - role: 'initiator', - direction: 'incoming', - creator: creator - }); -}; -exports.toOutgoingMediaJSONAnswer = function (sdp, creator) { - return toJSON.toMediaJSON(sdp, { - role: 'responder', - direction: 'outgoing', - creator: creator - }); -}; -exports.toCandidateJSON = toJSON.toCandidateJSON; -exports.toMediaJSON = toJSON.toMediaJSON; -exports.toSessionJSON = toJSON.toSessionJSON; - -},{"./lib/tojson":21,"./lib/tosdp":22}],13:[function(require,module,exports){ -var _ = require('underscore'); -var util = require('util'); -var webrtc = require('webrtcsupport'); -var SJJ = require('sdp-jingle-json'); -var WildEmitter = require('wildemitter'); -var peerconn = require('traceablepeerconnection'); - -function PeerConnection(config, constraints) { - var self = this; - var item; - WildEmitter.call(this); - - config = config || {}; - config.iceServers = config.iceServers || []; - - // make sure this only gets enabled in Google Chrome - // EXPERIMENTAL FLAG, might get removed without notice - this.enableChromeNativeSimulcast = false; - if (constraints && constraints.optional && - webrtc.prefix === 'webkit' && - navigator.appVersion.match(/Chromium\//) === null) { - constraints.optional.forEach(function (constraint, idx) { - if (constraint.enableChromeNativeSimulcast) { - self.enableChromeNativeSimulcast = true; - } - }); - } - - // EXPERIMENTAL FLAG, might get removed without notice - this.enableMultiStreamHacks = false; - if (constraints && constraints.optional) { - constraints.optional.forEach(function (constraint, idx) { - if (constraint.enableMultiStreamHacks) { - self.enableMultiStreamHacks = true; - } - }); - } - - this.pc = new peerconn(config, constraints); - - this.getLocalStreams = this.pc.getLocalStreams.bind(this.pc); - this.getRemoteStreams = this.pc.getRemoteStreams.bind(this.pc); - this.addStream = this.pc.addStream.bind(this.pc); - this.removeStream = this.pc.removeStream.bind(this.pc); - - // proxy events - this.pc.on('*', function () { - self.emit.apply(self, arguments); - }); - - // proxy some events directly - this.pc.onremovestream = this.emit.bind(this, 'removeStream'); - this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded'); - this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange'); - this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange'); - - // handle incoming ice and data channel events - this.pc.onaddstream = this._onAddStream.bind(this); - this.pc.onicecandidate = this._onIce.bind(this); - this.pc.ondatachannel = this._onDataChannel.bind(this); - - this.localDescription = { - contents: [] - }; - this.remoteDescription = { - contents: [] - }; - - this.localStream = null; - this.remoteStreams = []; - - this.config = { - debug: false, - ice: {}, - sid: '', - isInitiator: true, - sdpSessionID: Date.now(), - useJingle: false - }; - - // apply our config - for (item in config) { - this.config[item] = config[item]; - } - - this._role = this.isInitiator ? 'initiator' : 'responder'; - - if (this.config.debug) { - this.on('*', function (eventName, event) { - var logger = config.logger || console; - logger.log('PeerConnection event:', arguments); - }); - } - this.hadLocalStunCandidate = false; - this.hadRemoteStunCandidate = false; - this.hadLocalRelayCandidate = false; - this.hadRemoteRelayCandidate = false; - - this.hadLocalIPv6Candidate = false; - this.hadRemoteIPv6Candidate = false; - - // keeping references for all our data channels - // so they dont get garbage collected - // can be removed once the following bugs have been fixed - // https://crbug.com/405545 - // https://bugzilla.mozilla.org/show_bug.cgi?id=964092 - // to be filed for opera - this._remoteDataChannels = []; - this._localDataChannels = []; -} - -util.inherits(PeerConnection, WildEmitter); - -Object.defineProperty(PeerConnection.prototype, 'signalingState', { - get: function () { - return this.pc.signalingState; - } -}); -Object.defineProperty(PeerConnection.prototype, 'iceConnectionState', { - get: function () { - return this.pc.iceConnectionState; - } -}); - -// Add a stream to the peer connection object -PeerConnection.prototype.addStream = function (stream) { - this.localStream = stream; - this.pc.addStream(stream); -}; - -// helper function to check if a remote candidate is a stun/relay -// candidate or an ipv6 candidate -PeerConnection.prototype._checkLocalCandidate = function (candidate) { - var cand = SJJ.toCandidateJSON(candidate); - if (cand.type == 'srflx') { - this.hadLocalStunCandidate = true; - } else if (cand.type == 'relay') { - this.hadLocalRelayCandidate = true; - } - if (cand.ip.indexOf(':') != -1) { - this.hadLocalIPv6Candidate = true; - } + +// helper function to check if a remote candidate is a stun/relay +// candidate or an ipv6 candidate +PeerConnection.prototype._checkLocalCandidate = function (candidate) { + var cand = SJJ.toCandidateJSON(candidate); + if (cand.type == 'srflx') { + this.hadLocalStunCandidate = true; + } else if (cand.type == 'relay') { + this.hadLocalRelayCandidate = true; + } + if (cand.ip.indexOf(':') != -1) { + this.hadLocalIPv6Candidate = true; + } }; // helper function to check if a remote candidate is a stun/relay @@ -8189,17 +8162,227 @@ PeerConnection.prototype.getStats = function (cb) { module.exports = PeerConnection; -},{"sdp-jingle-json":20,"traceablepeerconnection":23,"underscore":18,"util":8,"webrtcsupport":4,"wildemitter":3}],16:[function(require,module,exports){ -// getScreenMedia helper by @HenrikJoreteg -var getUserMedia = require('getusermedia'); +},{"sdp-jingle-json":20,"traceablepeerconnection":21,"underscore":19,"util":8,"webrtcsupport":6,"wildemitter":2}],20:[function(require,module,exports){ +var toSDP = require('./lib/tosdp'); +var toJSON = require('./lib/tojson'); -// cache for constraints and callback -var cache = {}; -module.exports = function (constraints, cb) { - var hasConstraints = arguments.length === 2; - var callback = hasConstraints ? cb : constraints; - var error; +// Converstion from JSON to SDP + +exports.toIncomingSDPOffer = function (session) { + return toSDP.toSessionSDP(session, { + role: 'responder', + direction: 'incoming' + }); +}; +exports.toOutgoingSDPOffer = function (session) { + return toSDP.toSessionSDP(session, { + role: 'initiator', + direction: 'outgoing' + }); +}; +exports.toIncomingSDPAnswer = function (session) { + return toSDP.toSessionSDP(session, { + role: 'initiator', + direction: 'incoming' + }); +}; +exports.toOutgoingSDPAnswer = function (session) { + return toSDP.toSessionSDP(session, { + role: 'responder', + direction: 'outgoing' + }); +}; +exports.toIncomingMediaSDPOffer = function (media) { + return toSDP.toMediaSDP(media, { + role: 'responder', + direction: 'incoming' + }); +}; +exports.toOutgoingMediaSDPOffer = function (media) { + return toSDP.toMediaSDP(media, { + role: 'initiator', + direction: 'outgoing' + }); +}; +exports.toIncomingMediaSDPAnswer = function (media) { + return toSDP.toMediaSDP(media, { + role: 'initiator', + direction: 'incoming' + }); +}; +exports.toOutgoingMediaSDPAnswer = function (media) { + return toSDP.toMediaSDP(media, { + role: 'responder', + direction: 'outgoing' + }); +}; +exports.toCandidateSDP = toSDP.toCandidateSDP; +exports.toMediaSDP = toSDP.toMediaSDP; +exports.toSessionSDP = toSDP.toSessionSDP; + + +// Conversion from SDP to JSON + +exports.toIncomingJSONOffer = function (sdp, creators) { + return toJSON.toSessionJSON(sdp, { + role: 'responder', + direction: 'incoming', + creators: creators + }); +}; +exports.toOutgoingJSONOffer = function (sdp, creators) { + return toJSON.toSessionJSON(sdp, { + role: 'initiator', + direction: 'outgoing', + creators: creators + }); +}; +exports.toIncomingJSONAnswer = function (sdp, creators) { + return toJSON.toSessionJSON(sdp, { + role: 'initiator', + direction: 'incoming', + creators: creators + }); +}; +exports.toOutgoingJSONAnswer = function (sdp, creators) { + return toJSON.toSessionJSON(sdp, { + role: 'responder', + direction: 'outgoing', + creators: creators + }); +}; +exports.toIncomingMediaJSONOffer = function (sdp, creator) { + return toJSON.toMediaJSON(sdp, { + role: 'responder', + direction: 'incoming', + creator: creator + }); +}; +exports.toOutgoingMediaJSONOffer = function (sdp, creator) { + return toJSON.toMediaJSON(sdp, { + role: 'initiator', + direction: 'outgoing', + creator: creator + }); +}; +exports.toIncomingMediaJSONAnswer = function (sdp, creator) { + return toJSON.toMediaJSON(sdp, { + role: 'initiator', + direction: 'incoming', + creator: creator + }); +}; +exports.toOutgoingMediaJSONAnswer = function (sdp, creator) { + return toJSON.toMediaJSON(sdp, { + role: 'responder', + direction: 'outgoing', + creator: creator + }); +}; +exports.toCandidateJSON = toJSON.toCandidateJSON; +exports.toMediaJSON = toJSON.toMediaJSON; +exports.toSessionJSON = toJSON.toSessionJSON; + +},{"./lib/tojson":23,"./lib/tosdp":22}],14:[function(require,module,exports){ +var WildEmitter = require('wildemitter'); +var util = require('util'); + +function Sender(opts) { + WildEmitter.call(this); + var options = opts || {}; + this.config = { + chunksize: 16384, + pacing: 0 + }; + // set our config from options + var item; + for (item in options) { + this.config[item] = options[item]; + } + + this.file = null; + this.channel = null; +} +util.inherits(Sender, WildEmitter); + +Sender.prototype.send = function (file, channel) { + var self = this; + this.file = file; + this.channel = channel; + var sliceFile = function(offset) { + var reader = new window.FileReader(); + reader.onload = (function() { + return function(e) { + self.channel.send(e.target.result); + self.emit('progress', offset, file.size, e.target.result); + if (file.size > offset + e.target.result.byteLength) { + window.setTimeout(sliceFile, self.config.pacing, offset + self.config.chunksize); + } else { + self.emit('progress', file.size, file.size, null); + self.emit('sentFile'); + } + }; + })(file); + var slice = file.slice(offset, offset + self.config.chunksize); + reader.readAsArrayBuffer(slice); + }; + window.setTimeout(sliceFile, 0, 0); +}; + +function Receiver() { + WildEmitter.call(this); + + this.receiveBuffer = []; + this.received = 0; + this.metadata = {}; + this.channel = null; +} +util.inherits(Receiver, WildEmitter); + +Receiver.prototype.receive = function (metadata, channel) { + var self = this; + + if (metadata) { + this.metadata = metadata; + } + this.channel = channel; + // chrome only supports arraybuffers and those make it easier to calc the hash + channel.binaryType = 'arraybuffer'; + this.channel.onmessage = function (event) { + var len = event.data.byteLength; + self.received += len; + self.receiveBuffer.push(event.data); + + self.emit('progress', self.received, self.metadata.size, event.data); + if (self.received === self.metadata.size) { + self.emit('receivedFile', new window.Blob(self.receiveBuffer), self.metadata); + self.receiveBuffer = []; // discard receivebuffer + } else if (self.received > self.metadata.size) { + // FIXME + console.error('received more than expected, discarding...'); + self.receiveBuffer = []; // just discard... + + } + }; +}; + +module.exports = {}; +module.exports.support = window && window.File && window.FileReader && window.Blob; +module.exports.Sender = Sender; +module.exports.Receiver = Receiver; + +},{"util":8,"wildemitter":2}],16:[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'); @@ -8207,15 +8390,16 @@ module.exports = function (constraints, cb) { return callback(error); } - if (window.navigator.userAgent.match('Chrome')) { + if (window.navigator.userAgent.match('Chrome')) { var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(.*) /)[1], 10); var maxver = 33; + var isCef = !window.chrome.webstore; // "known" crash in chrome 34 and 35 on linux if (window.navigator.userAgent.match('Linux')) maxver = 35; - if (chromever >= 26 && chromever <= maxver) { + if (isCef || (chromever >= 26 && chromever <= maxver)) { // chrome 26 - chrome 33 way to do it -- requires bad chrome://flags // note: this is basically in maintenance mode and will go away soon - constraints = (hasConstraints && constraints) || { + constraints = (hasConstraints && constraints) || { video: { mandatory: { googLeakyBucket: true, @@ -8270,7 +8454,7 @@ module.exports = function (constraints, cb) { } }; -window.addEventListener('message', function (event) { +window.addEventListener('message', function (event) { if (event.origin != window.location.origin) { return; } @@ -8305,137 +8489,223 @@ window.addEventListener('message', function (event) { } }); -},{"getusermedia":14}],15:[function(require,module,exports){ -var WildEmitter = require('wildemitter'); +},{"getusermedia":15}],22:[function(require,module,exports){ +var SENDERS = require('./senders'); -function getMaxVolume (analyser, fftBins) { - var maxVolume = -Infinity; - analyser.getFloatFrequencyData(fftBins); - for(var i=4, ii=fftBins.length; i < ii; i++) { - if (fftBins[i] > maxVolume && fftBins[i] < 0) { - maxVolume = fftBins[i]; - } - }; +exports.toSessionSDP = function (session, opts) { + var role = opts.role || 'initiator'; + var direction = opts.direction || 'outgoing'; + var sid = opts.sid || session.sid || Date.now(); + var time = opts.time || Date.now(); - return maxVolume; -} + var sdp = [ + 'v=0', + 'o=- ' + sid + ' ' + time + ' IN IP4 0.0.0.0', + 's=-', + 't=0 0' + ]; + var groups = session.groups || []; + groups.forEach(function (group) { + sdp.push('a=group:' + group.semantics + ' ' + group.contents.join(' ')); + }); -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(); + var contents = session.contents || []; + contents.forEach(function (content) { + sdp.push(exports.toMediaSDP(content, opts)); + }); + return sdp.join('\r\n') + '\r\n'; +}; - // make it not break in non-supported browsers - if (!audioContextType) return harker; +exports.toMediaSDP = function (content, opts) { + var sdp = []; - //Config - var options = options || {}, - smoothing = (options.smoothing || 0.1), - interval = (options.interval || 50), - threshold = options.threshold, - play = options.play, - history = options.history || 10, - running = true; + var role = opts.role || 'initiator'; + var direction = opts.direction || 'outgoing'; - //Setup Audio Context - if (!audioContext) { - audioContext = new audioContextType(); - } - var sourceNode, fftBins, analyser; + var desc = content.description; + var transport = content.transport; + var payloads = desc.payloads || []; + var fingerprints = (transport && transport.fingerprints) || []; - analyser = audioContext.createAnalyser(); - analyser.fftSize = 512; - analyser.smoothingTimeConstant = smoothing; - fftBins = new Float32Array(analyser.fftSize); + var mline = []; + if (desc.descType == 'datachannel') { + mline.push('application'); + mline.push('1'); + mline.push('DTLS/SCTP'); + if (transport.sctp) { + transport.sctp.forEach(function (map) { + mline.push(map.number); + }); + } + } else { + mline.push(desc.media); + mline.push('1'); + if ((desc.encryption && desc.encryption.length > 0) || (fingerprints.length > 0)) { + mline.push('RTP/SAVPF'); + } else { + mline.push('RTP/AVPF'); + } + payloads.forEach(function (payload) { + mline.push(payload.id); + }); + } - if (stream.jquery) stream = stream[0]; - if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) { - //Audio Tag - sourceNode = audioContext.createMediaElementSource(stream); - if (typeof play === 'undefined') play = true; - threshold = threshold || -50; - } else { - //WebRTC Stream - sourceNode = audioContext.createMediaStreamSource(stream); - threshold = threshold || -50; - } - sourceNode.connect(analyser); - if (play) analyser.connect(audioContext.destination); + sdp.push('m=' + mline.join(' ')); - harker.speaking = false; + sdp.push('c=IN IP4 0.0.0.0'); + if (desc.bandwidth && desc.bandwidth.type && desc.bandwidth.bandwidth) { + sdp.push('b=' + desc.bandwidth.type + ':' + desc.bandwidth.bandwidth); + } + if (desc.descType == 'rtp') { + sdp.push('a=rtcp:1 IN IP4 0.0.0.0'); + } - harker.setThreshold = function(t) { - threshold = t; - }; + if (transport) { + if (transport.ufrag) { + sdp.push('a=ice-ufrag:' + transport.ufrag); + } + if (transport.pwd) { + sdp.push('a=ice-pwd:' + transport.pwd); + } - 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'); + var pushedSetup = false; + fingerprints.forEach(function (fingerprint) { + sdp.push('a=fingerprint:' + fingerprint.hash + ' ' + fingerprint.value); + if (fingerprint.setup && !pushedSetup) { + sdp.push('a=setup:' + fingerprint.setup); + } + }); + + if (transport.sctp) { + transport.sctp.forEach(function (map) { + sdp.push('a=sctpmap:' + map.number + ' ' + map.protocol + ' ' + map.streams); + }); + } } - }; - harker.speakingHistory = []; - for (var i = 0; i < history; i++) { - harker.speakingHistory.push(0); - } - // 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); + if (desc.descType == 'rtp') { + sdp.push('a=' + (SENDERS[role][direction][content.senders] || 'sendrecv')); + } + sdp.push('a=mid:' + content.name); - harker.emit('volume_change', currentVolume, threshold); + if (desc.mux) { + sdp.push('a=rtcp-mux'); + } - var history = 0; - if (currentVolume > threshold && !harker.speaking) { - // trigger quickly, short history - for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) { - history += harker.speakingHistory[i]; + var encryption = desc.encryption || []; + encryption.forEach(function (crypto) { + sdp.push('a=crypto:' + crypto.tag + ' ' + crypto.cipherSuite + ' ' + crypto.keyParams + (crypto.sessionParams ? ' ' + crypto.sessionParams : '')); + }); + if (desc.googConferenceFlag) { + sdp.push('a=x-google-flag:conference'); + } + + payloads.forEach(function (payload) { + var rtpmap = 'a=rtpmap:' + payload.id + ' ' + payload.name + '/' + payload.clockrate; + if (payload.channels && payload.channels != '1') { + rtpmap += '/' + payload.channels; } - if (history >= 2) { - harker.speaking = true; - harker.emit('speaking'); + sdp.push(rtpmap); + + if (payload.parameters && payload.parameters.length) { + var fmtp = ['a=fmtp:' + payload.id]; + var parameters = []; + payload.parameters.forEach(function (param) { + parameters.push((param.key ? param.key + '=' : '') + param.value); + }); + fmtp.push(parameters.join(';')); + sdp.push(fmtp.join(' ')); } - } else if (currentVolume < threshold && harker.speaking) { - for (var i = 0; i < harker.speakingHistory.length; i++) { - history += harker.speakingHistory[i]; + + if (payload.feedback) { + payload.feedback.forEach(function (fb) { + if (fb.type === 'trr-int') { + sdp.push('a=rtcp-fb:' + payload.id + ' trr-int ' + (fb.value ? fb.value : '0')); + } else { + sdp.push('a=rtcp-fb:' + payload.id + ' ' + fb.type + (fb.subtype ? ' ' + fb.subtype : '')); + } + }); } - if (history == 0) { - harker.speaking = false; - harker.emit('stopped_speaking'); + }); + + if (desc.feedback) { + desc.feedback.forEach(function (fb) { + if (fb.type === 'trr-int') { + sdp.push('a=rtcp-fb:* trr-int ' + (fb.value ? fb.value : '0')); + } else { + sdp.push('a=rtcp-fb:* ' + fb.type + (fb.subtype ? ' ' + fb.subtype : '')); + } + }); + } + + var hdrExts = desc.headerExtensions || []; + hdrExts.forEach(function (hdr) { + sdp.push('a=extmap:' + hdr.id + (hdr.senders ? '/' + SENDERS[role][direction][hdr.senders] : '') + ' ' + hdr.uri); + }); + + var ssrcGroups = desc.sourceGroups || []; + ssrcGroups.forEach(function (ssrcGroup) { + sdp.push('a=ssrc-group:' + ssrcGroup.semantics + ' ' + ssrcGroup.sources.join(' ')); + }); + + var ssrcs = desc.sources || []; + ssrcs.forEach(function (ssrc) { + for (var i = 0; i < ssrc.parameters.length; i++) { + var param = ssrc.parameters[i]; + sdp.push('a=ssrc:' + (ssrc.ssrc || desc.ssrc) + ' ' + param.key + (param.value ? (':' + param.value) : '')); } - } - harker.speakingHistory.shift(); - harker.speakingHistory.push(0 + (currentVolume > threshold)); + }); - looper(); - }, interval); - }; - looper(); + var candidates = transport.candidates || []; + candidates.forEach(function (candidate) { + sdp.push(exports.toCandidateSDP(candidate)); + }); + return sdp.join('\r\n'); +}; - return harker; -} +exports.toCandidateSDP = function (candidate) { + var sdp = []; + + sdp.push(candidate.foundation); + sdp.push(candidate.component); + sdp.push(candidate.protocol.toUpperCase()); + sdp.push(candidate.priority); + sdp.push(candidate.ip); + sdp.push(candidate.port); + + var type = candidate.type; + sdp.push('typ'); + sdp.push(type); + if (type === 'srflx' || type === 'prflx' || type === 'relay') { + if (candidate.relAddr && candidate.relPort) { + sdp.push('raddr'); + sdp.push(candidate.relAddr); + sdp.push('rport'); + sdp.push(candidate.relPort); + } + } + if (candidate.tcpType && candidate.protocol.toUpperCase() == 'TCP') { + sdp.push('tcptype'); + sdp.push(candidate.tcpType); + } + + sdp.push('generation'); + sdp.push(candidate.generation || '0'); -},{"wildemitter":3}],21:[function(require,module,exports){ + // FIXME: apparently this is wrong per spec + // but then, we need this when actually putting this into + // SDP so it's going to stay. + // decision needs to be revisited when browsers dont + // accept this any longer + return 'a=candidate:' + sdp.join(' '); +}; + +},{"./senders":24}],23:[function(require,module,exports){ var SENDERS = require('./senders'); var parsers = require('./parsers'); var idCounter = Math.random(); @@ -8505,7 +8775,7 @@ exports.toMediaJSON = function (media, session, opts) { transport: { transType: 'iceUdp', candidates: [], - fingerprints: [], + fingerprints: [] } }; if (mline.media == 'application') { @@ -8641,223 +8911,184 @@ exports.toCandidateJSON = function (line) { return candidate; }; -},{"./parsers":25,"./senders":24}],22:[function(require,module,exports){ -var SENDERS = require('./senders'); +},{"./parsers":25,"./senders":24}],18:[function(require,module,exports){ +var support = require('webrtcsupport'); -exports.toSessionSDP = function (session, opts) { - var role = opts.role || 'initiator'; - var direction = opts.direction || 'outgoing'; - var sid = opts.sid || session.sid || Date.now(); - var time = opts.time || Date.now(); +function GainController(stream) { + this.support = support.webAudio && support.mediaStream; - var sdp = [ - 'v=0', - 'o=- ' + sid + ' ' + time + ' IN IP4 0.0.0.0', - 's=-', - 't=0 0' - ]; + // set our starting value + this.gain = 1; - var groups = session.groups || []; - groups.forEach(function (group) { - sdp.push('a=group:' + group.semantics + ' ' + group.contents.join(' ')); - }); + 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.addTrack(this.outputStream.getAudioTracks()[0]); + stream.removeTrack(stream.getAudioTracks()[0]); + } + this.stream = stream; +} - var contents = session.contents || []; - contents.forEach(function (content) { - sdp.push(exports.toMediaSDP(content, opts)); - }); +// setting +GainController.prototype.setGain = function (val) { + // check for support + if (!this.support) return; + this.gainFilter.gain.value = val; + this.gain = val; +}; - return sdp.join('\r\n') + '\r\n'; +GainController.prototype.getGain = function () { + return this.gain; }; -exports.toMediaSDP = function (content, opts) { - var sdp = []; +GainController.prototype.off = function () { + return this.setGain(0); +}; - var role = opts.role || 'initiator'; - var direction = opts.direction || 'outgoing'; +GainController.prototype.on = function () { + this.setGain(1); +}; - var desc = content.description; - var transport = content.transport; - var payloads = desc.payloads || []; - var fingerprints = (transport && transport.fingerprints) || []; - var mline = []; - if (desc.descType == 'datachannel') { - mline.push('application'); - mline.push('1'); - mline.push('DTLS/SCTP'); - if (transport.sctp) { - transport.sctp.forEach(function (map) { - mline.push(map.number); - }); - } - } else { - mline.push(desc.media); - mline.push('1'); - if ((desc.encryption && desc.encryption.length > 0) || (fingerprints.length > 0)) { - mline.push('RTP/SAVPF'); - } else { - mline.push('RTP/AVPF'); - } - payloads.forEach(function (payload) { - mline.push(payload.id); - }); - } +module.exports = GainController; +},{"webrtcsupport":6}],17:[function(require,module,exports){ +var WildEmitter = require('wildemitter'); - sdp.push('m=' + mline.join(' ')); +function getMaxVolume (analyser, fftBins) { + var maxVolume = -Infinity; + analyser.getFloatFrequencyData(fftBins); - sdp.push('c=IN IP4 0.0.0.0'); - if (desc.bandwidth && desc.bandwidth.type && desc.bandwidth.bandwidth) { - sdp.push('b=' + desc.bandwidth.type + ':' + desc.bandwidth.bandwidth); - } - if (desc.descType == 'rtp') { - sdp.push('a=rtcp:1 IN IP4 0.0.0.0'); + for(var i=4, ii=fftBins.length; i < ii; i++) { + if (fftBins[i] > maxVolume && fftBins[i] < 0) { + maxVolume = fftBins[i]; } + }; - if (transport) { - if (transport.ufrag) { - sdp.push('a=ice-ufrag:' + transport.ufrag); - } - if (transport.pwd) { - sdp.push('a=ice-pwd:' + transport.pwd); - } - - var pushedSetup = false; - fingerprints.forEach(function (fingerprint) { - sdp.push('a=fingerprint:' + fingerprint.hash + ' ' + fingerprint.value); - if (fingerprint.setup && !pushedSetup) { - sdp.push('a=setup:' + fingerprint.setup); - } - }); - - if (transport.sctp) { - transport.sctp.forEach(function (map) { - sdp.push('a=sctpmap:' + map.number + ' ' + map.protocol + ' ' + map.streams); - }); - } - } + return maxVolume; +} - if (desc.descType == 'rtp') { - sdp.push('a=' + (SENDERS[role][direction][content.senders] || 'sendrecv')); - } - sdp.push('a=mid:' + content.name); - if (desc.mux) { - sdp.push('a=rtcp-mux'); - } +var audioContextType = window.AudioContext || window.webkitAudioContext; +// use a single audio context due to hardware limits +var audioContext = null; +module.exports = function(stream, options) { + var harker = new WildEmitter(); - var encryption = desc.encryption || []; - encryption.forEach(function (crypto) { - sdp.push('a=crypto:' + crypto.tag + ' ' + crypto.cipherSuite + ' ' + crypto.keyParams + (crypto.sessionParams ? ' ' + crypto.sessionParams : '')); - }); - if (desc.googConferenceFlag) { - sdp.push('a=x-google-flag:conference'); - } - payloads.forEach(function (payload) { - var rtpmap = 'a=rtpmap:' + payload.id + ' ' + payload.name + '/' + payload.clockrate; - if (payload.channels && payload.channels != '1') { - rtpmap += '/' + payload.channels; - } - sdp.push(rtpmap); + // make it not break in non-supported browsers + if (!audioContextType) return harker; - if (payload.parameters && payload.parameters.length) { - var fmtp = ['a=fmtp:' + payload.id]; - var parameters = []; - payload.parameters.forEach(function (param) { - parameters.push((param.key ? param.key + '=' : '') + param.value); - }); - fmtp.push(parameters.join(';')); - sdp.push(fmtp.join(' ')); - } + //Config + var options = options || {}, + smoothing = (options.smoothing || 0.1), + interval = (options.interval || 50), + threshold = options.threshold, + play = options.play, + history = options.history || 10, + running = true; - if (payload.feedback) { - payload.feedback.forEach(function (fb) { - if (fb.type === 'trr-int') { - sdp.push('a=rtcp-fb:' + payload.id + ' trr-int ' + fb.value ? fb.value : '0'); - } else { - sdp.push('a=rtcp-fb:' + payload.id + ' ' + fb.type + (fb.subtype ? ' ' + fb.subtype : '')); - } - }); - } - }); + //Setup Audio Context + if (!audioContext) { + audioContext = new audioContextType(); + } + var sourceNode, fftBins, analyser; - if (desc.feedback) { - desc.feedback.forEach(function (fb) { - if (fb.type === 'trr-int') { - sdp.push('a=rtcp-fb:* trr-int ' + fb.value ? fb.value : '0'); - } else { - sdp.push('a=rtcp-fb:* ' + fb.type + (fb.subtype ? ' ' + fb.subtype : '')); - } - }); - } + analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = smoothing; + fftBins = new Float32Array(analyser.fftSize); - var hdrExts = desc.headerExtensions || []; - hdrExts.forEach(function (hdr) { - sdp.push('a=extmap:' + hdr.id + (hdr.senders ? '/' + SENDERS[role][direction][hdr.senders] : '') + ' ' + hdr.uri); - }); + if (stream.jquery) stream = stream[0]; + if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) { + //Audio Tag + sourceNode = audioContext.createMediaElementSource(stream); + if (typeof play === 'undefined') play = true; + threshold = threshold || -50; + } else { + //WebRTC Stream + sourceNode = audioContext.createMediaStreamSource(stream); + threshold = threshold || -50; + } - var ssrcGroups = desc.sourceGroups || []; - ssrcGroups.forEach(function (ssrcGroup) { - sdp.push('a=ssrc-group:' + ssrcGroup.semantics + ' ' + ssrcGroup.sources.join(' ')); - }); + sourceNode.connect(analyser); + if (play) analyser.connect(audioContext.destination); - var ssrcs = desc.sources || []; - ssrcs.forEach(function (ssrc) { - for (var i = 0; i < ssrc.parameters.length; i++) { - var param = ssrc.parameters[i]; - sdp.push('a=ssrc:' + (ssrc.ssrc || desc.ssrc) + ' ' + param.key + (param.value ? (':' + param.value) : '')); - } - }); + harker.speaking = false; - var candidates = transport.candidates || []; - candidates.forEach(function (candidate) { - sdp.push(exports.toCandidateSDP(candidate)); - }); + harker.setThreshold = function(t) { + threshold = t; + }; - return sdp.join('\r\n'); -}; + 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'); + } + }; + harker.speakingHistory = []; + for (var i = 0; i < history; i++) { + harker.speakingHistory.push(0); + } -exports.toCandidateSDP = function (candidate) { - var sdp = []; + // 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); - sdp.push(candidate.foundation); - sdp.push(candidate.component); - sdp.push(candidate.protocol.toUpperCase()); - sdp.push(candidate.priority); - sdp.push(candidate.ip); - sdp.push(candidate.port); + harker.emit('volume_change', currentVolume, threshold); - var type = candidate.type; - sdp.push('typ'); - sdp.push(type); - if (type === 'srflx' || type === 'prflx' || type === 'relay') { - if (candidate.relAddr && candidate.relPort) { - sdp.push('raddr'); - sdp.push(candidate.relAddr); - sdp.push('rport'); - sdp.push(candidate.relPort); + var history = 0; + if (currentVolume > threshold && !harker.speaking) { + // trigger quickly, short history + for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; } - } - if (candidate.tcpType && candidate.protocol.toUpperCase() == 'TCP') { - sdp.push('tcptype'); - sdp.push(candidate.tcpType); - } + if (history >= 2) { + harker.speaking = true; + harker.emit('speaking'); + } + } else if (currentVolume < threshold && harker.speaking) { + for (var i = 0; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history == 0) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + } + harker.speakingHistory.shift(); + harker.speakingHistory.push(0 + (currentVolume > threshold)); - sdp.push('generation'); - sdp.push(candidate.generation || '0'); + looper(); + }, interval); + }; + looper(); - // FIXME: apparently this is wrong per spec - // but then, we need this when actually putting this into - // SDP so it's going to stay. - // decision needs to be revisited when browsers dont - // accept this any longer - return 'a=candidate:' + sdp.join(' '); -}; -},{"./senders":24}],24:[function(require,module,exports){ + return harker; +} + +},{"wildemitter":2}],24:[function(require,module,exports){ module.exports = { initiator: { incoming: { @@ -9166,7 +9397,7 @@ exports.bandwidth = function (line) { return parsed; }; -},{}],23:[function(require,module,exports){ +},{}],21:[function(require,module,exports){ // based on https://github.com/ESTOS/strophe.jingle/ // adds wildemitter support var util = require('util'); @@ -9403,6 +9634,6 @@ TraceablePeerConnection.prototype.getStats = function (callback, errback) { module.exports = TraceablePeerConnection; -},{"util":8,"webrtcsupport":4,"wildemitter":3}]},{},[1])(1) +},{"util":8,"webrtcsupport":6,"wildemitter":2}]},{},[1])(1) }); ;