webrtc
Version:
WebRTC abstraction for making it simple to manage multiple peer connections of various types.
1,592 lines (1,384 loc) • 175 kB
JavaScript
(function(e){if("function"==typeof bootstrap)bootstrap("webrtc",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeWebRTC=e}else"undefined"!=typeof window?window.WebRTC=e():global.WebRTC=e()})(function(){var define,ses,bootstrap,module,exports;
return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s<n.length;s++)i(n[s]);return i})({1:[function(require,module,exports){
var util = require('util');
var webrtc = require('webrtcsupport');
var WildEmitter = require('wildemitter');
var mockconsole = require('mockconsole');
var localMedia = require('localmedia');
var Peer = require('./peer');
function WebRTC(opts) {
var self = this;
var options = opts || {};
var config = this.config = {
debug: false,
// makes the entire PC config overridable
peerConnectionConfig: {
iceServers: [{"url": "stun:stun.l.google.com:19302"}]
},
peerConnectionConstraints: {
optional: [
{DtlsSrtpKeyAgreement: true}
]
},
receiveMedia: {
mandatory: {
OfferToReceiveAudio: true,
OfferToReceiveVideo: true
}
},
enableDataChannels: true
};
var item;
// expose screensharing check
this.screenSharingSupport = webrtc.screenSharing;
// We also allow a 'logger' option. It can be any object that implements
// log, warn, and error methods.
// We log nothing by default, following "the rule of silence":
// http://www.linfo.org/rule_of_silence.html
this.logger = function () {
// we assume that if you're in debug mode and you didn't
// pass in a logger, you actually want to log as much as
// possible.
if (opts.debug) {
return opts.logger || console;
} else {
// or we'll use your logger which should have its own logic
// for output. Or we'll return the no-op.
return opts.logger || mockconsole;
}
}();
// set options
for (item in options) {
this.config[item] = options[item];
}
// check for support
if (!webrtc.support) {
this.logger.error('Your browser doesn\'t seem to support WebRTC');
}
// where we'll store our peer connections
this.peers = [];
// call localMedia constructor
localMedia.call(this, this.config);
this.on('speaking', function () {
if (!self.hardMuted) {
// FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
self.peers.forEach(function (peer) {
if (peer.enableDataChannels) {
var dc = peer.getDataChannel('hark');
if (dc.readyState != 'open') return;
dc.send(JSON.stringify({type: 'speaking'}));
}
});
}
});
this.on('stoppedSpeaking', function () {
if (!self.hardMuted) {
// FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
self.peers.forEach(function (peer) {
if (peer.enableDataChannels) {
var dc = peer.getDataChannel('hark');
if (dc.readyState != 'open') return;
dc.send(JSON.stringify({type: 'stoppedSpeaking'}));
}
});
}
});
this.on('volumeChange', function (volume, treshold) {
if (!self.hardMuted) {
// FIXME: should use sendDirectlyToAll, but currently has different semantics wrt payload
self.peers.forEach(function (peer) {
if (peer.enableDataChannels) {
var dc = peer.getDataChannel('hark');
if (dc.readyState != 'open') return;
dc.send(JSON.stringify({type: 'volume', volume: volume }));
}
});
}
});
// log events in debug mode
if (this.config.debug) {
this.on('*', function (event, val1, val2) {
var logger;
// if you didn't pass in a logger and you explicitly turning on debug
// we're just going to assume you're wanting log output with console
if (self.config.logger === mockconsole) {
logger = console;
} else {
logger = self.logger;
}
logger.log('event:', event, val1, val2);
});
}
}
util.inherits(WebRTC, localMedia);
WebRTC.prototype.createPeer = function (opts) {
var peer;
opts.parent = this;
peer = new Peer(opts);
this.peers.push(peer);
return peer;
};
// removes peers
WebRTC.prototype.removePeers = function (id, type) {
this.getPeers(id, type).forEach(function (peer) {
peer.end();
});
};
// fetches all Peer objects by session id and/or type
WebRTC.prototype.getPeers = function (sessionId, type) {
return this.peers.filter(function (peer) {
return (!sessionId || peer.id === sessionId) && (!type || peer.type === type);
});
};
// sends message to all
WebRTC.prototype.sendToAll = function (message, payload) {
this.peers.forEach(function (peer) {
peer.send(message, payload);
});
};
// sends message to all using a datachannel
// only sends to anyone who has an open datachannel
WebRTC.prototype.sendDirectlyToAll = function (channel, message, payload) {
this.peers.forEach(function (peer) {
if (peer.enableDataChannels) {
peer.sendDirectly(channel, message, payload);
}
});
};
module.exports = WebRTC;
},{"./peer":3,"localmedia":7,"mockconsole":6,"util":2,"webrtcsupport":5,"wildemitter":4}],2:[function(require,module,exports){
var events = require('events');
exports.isArray = isArray;
exports.isDate = function(obj){return Object.prototype.toString.call(obj) === '[object Date]'};
exports.isRegExp = function(obj){return Object.prototype.toString.call(obj) === '[object RegExp]'};
exports.print = function () {};
exports.puts = function () {};
exports.debug = function() {};
exports.inspect = function(obj, showHidden, depth, colors) {
var seen = [];
var stylize = function(str, styleType) {
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
var styles =
{ 'bold' : [1, 22],
'italic' : [3, 23],
'underline' : [4, 24],
'inverse' : [7, 27],
'white' : [37, 39],
'grey' : [90, 39],
'black' : [30, 39],
'blue' : [34, 39],
'cyan' : [36, 39],
'green' : [32, 39],
'magenta' : [35, 39],
'red' : [31, 39],
'yellow' : [33, 39] };
var style =
{ 'special': 'cyan',
'number': 'blue',
'boolean': 'yellow',
'undefined': 'grey',
'null': 'bold',
'string': 'green',
'date': 'magenta',
// "name": intentionally not styling
'regexp': 'red' }[styleType];
if (style) {
return '\u001b[' + styles[style][0] + 'm' + str +
'\u001b[' + styles[style][1] + 'm';
} else {
return str;
}
};
if (! colors) {
stylize = function(str, styleType) { return str; };
}
function format(value, recurseTimes) {
// Provide a hook for user-specified inspect functions.
// Check that value is an object with an inspect function on it
if (value && typeof value.inspect === 'function' &&
// Filter out the util module, it's inspect function is special
value !== exports &&
// Also filter out any prototype objects using the circular check.
!(value.constructor && value.constructor.prototype === value)) {
return value.inspect(recurseTimes);
}
// Primitive types cannot have properties
switch (typeof value) {
case 'undefined':
return stylize('undefined', 'undefined');
case 'string':
var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') + '\'';
return stylize(simple, 'string');
case 'number':
return stylize('' + value, 'number');
case 'boolean':
return stylize('' + value, 'boolean');
}
// For some reason typeof null is "object", so special case here.
if (value === null) {
return stylize('null', 'null');
}
// Look up the keys of the object.
var visible_keys = Object_keys(value);
var keys = showHidden ? Object_getOwnPropertyNames(value) : visible_keys;
// Functions without properties can be shortcutted.
if (typeof value === 'function' && keys.length === 0) {
if (isRegExp(value)) {
return stylize('' + value, 'regexp');
} else {
var name = value.name ? ': ' + value.name : '';
return stylize('[Function' + name + ']', 'special');
}
}
// Dates without properties can be shortcutted
if (isDate(value) && keys.length === 0) {
return stylize(value.toUTCString(), 'date');
}
var base, type, braces;
// Determine the object type
if (isArray(value)) {
type = 'Array';
braces = ['[', ']'];
} else {
type = 'Object';
braces = ['{', '}'];
}
// Make functions say that they are functions
if (typeof value === 'function') {
var n = value.name ? ': ' + value.name : '';
base = (isRegExp(value)) ? ' ' + value : ' [Function' + n + ']';
} else {
base = '';
}
// Make dates with properties first say the date
if (isDate(value)) {
base = ' ' + value.toUTCString();
}
if (keys.length === 0) {
return braces[0] + base + braces[1];
}
if (recurseTimes < 0) {
if (isRegExp(value)) {
return stylize('' + value, 'regexp');
} else {
return stylize('[Object]', 'special');
}
}
seen.push(value);
var output = keys.map(function(key) {
var name, str;
if (value.__lookupGetter__) {
if (value.__lookupGetter__(key)) {
if (value.__lookupSetter__(key)) {
str = stylize('[Getter/Setter]', 'special');
} else {
str = stylize('[Getter]', 'special');
}
} else {
if (value.__lookupSetter__(key)) {
str = stylize('[Setter]', 'special');
}
}
}
if (visible_keys.indexOf(key) < 0) {
name = '[' + key + ']';
}
if (!str) {
if (seen.indexOf(value[key]) < 0) {
if (recurseTimes === null) {
str = format(value[key]);
} else {
str = format(value[key], recurseTimes - 1);
}
if (str.indexOf('\n') > -1) {
if (isArray(value)) {
str = str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n').substr(2);
} else {
str = '\n' + str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n');
}
}
} else {
str = stylize('[Circular]', 'special');
}
}
if (typeof name === 'undefined') {
if (type === 'Array' && key.match(/^\d+$/)) {
return str;
}
name = JSON.stringify('' + key);
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
name = name.substr(1, name.length - 2);
name = stylize(name, 'name');
} else {
name = name.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/(^"|"$)/g, "'");
name = stylize(name, 'string');
}
}
return name + ': ' + str;
});
seen.pop();
var numLinesEst = 0;
var length = output.reduce(function(prev, cur) {
numLinesEst++;
if (cur.indexOf('\n') >= 0) numLinesEst++;
return prev + cur.length + 1;
}, 0);
if (length > 50) {
output = braces[0] +
(base === '' ? '' : base + '\n ') +
' ' +
output.join(',\n ') +
' ' +
braces[1];
} else {
output = braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
}
return output;
}
return format(obj, (typeof depth === 'undefined' ? 2 : depth));
};
function isArray(ar) {
return Array.isArray(ar) ||
(typeof ar === 'object' && Object.prototype.toString.call(ar) === '[object Array]');
}
function isRegExp(re) {
typeof re === 'object' && Object.prototype.toString.call(re) === '[object RegExp]';
}
function isDate(d) {
return typeof d === 'object' && Object.prototype.toString.call(d) === '[object Date]';
}
function pad(n) {
return n < 10 ? '0' + n.toString(10) : n.toString(10);
}
var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec'];
// 26 Feb 16:19:34
function timestamp() {
var d = new Date();
var time = [pad(d.getHours()),
pad(d.getMinutes()),
pad(d.getSeconds())].join(':');
return [d.getDate(), months[d.getMonth()], time].join(' ');
}
exports.log = function (msg) {};
exports.pump = null;
var Object_keys = Object.keys || function (obj) {
var res = [];
for (var key in obj) res.push(key);
return res;
};
var Object_getOwnPropertyNames = Object.getOwnPropertyNames || function (obj) {
var res = [];
for (var key in obj) {
if (Object.hasOwnProperty.call(obj, key)) res.push(key);
}
return res;
};
var Object_create = Object.create || function (prototype, properties) {
// from es5-shim
var object;
if (prototype === null) {
object = { '__proto__' : null };
}
else {
if (typeof prototype !== 'object') {
throw new TypeError(
'typeof prototype[' + (typeof prototype) + '] != \'object\''
);
}
var Type = function () {};
Type.prototype = prototype;
object = new Type();
object.__proto__ = prototype;
}
if (typeof properties !== 'undefined' && Object.defineProperties) {
Object.defineProperties(object, properties);
}
return object;
};
exports.inherits = function(ctor, superCtor) {
ctor.super_ = superCtor;
ctor.prototype = Object_create(superCtor.prototype, {
constructor: {
value: ctor,
enumerable: false,
writable: true,
configurable: true
}
});
};
var formatRegExp = /%[sdj%]/g;
exports.format = function(f) {
if (typeof f !== 'string') {
var objects = [];
for (var i = 0; i < arguments.length; i++) {
objects.push(exports.inspect(arguments[i]));
}
return objects.join(' ');
}
var i = 1;
var args = arguments;
var len = args.length;
var str = String(f).replace(formatRegExp, function(x) {
if (x === '%%') return '%';
if (i >= len) return x;
switch (x) {
case '%s': return String(args[i++]);
case '%d': return Number(args[i++]);
case '%j': return JSON.stringify(args[i++]);
default:
return x;
}
});
for(var x = args[i]; i < len; x = args[++i]){
if (x === null || typeof x !== 'object') {
str += ' ' + x;
} else {
str += ' ' + exports.inspect(x);
}
}
return str;
};
},{"events":8}],4:[function(require,module,exports){
/*
WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based
on @visionmedia's Emitter from UI Kit.
Why? I wanted it standalone.
I also wanted support for wildcard emitters like this:
emitter.on('*', function (eventName, other, event, payloads) {
});
emitter.on('somenamespace*', function (eventName, payloads) {
});
Please note that callbacks triggered by wildcard registered events also get
the event name as the first argument.
*/
module.exports = WildEmitter;
function WildEmitter() {
this.callbacks = {};
}
// Listen on the given `event` with `fn`. Store a group name if present.
WildEmitter.prototype.on = function (event, groupName, fn) {
var hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
func._groupName = group;
(this.callbacks[event] = this.callbacks[event] || []).push(func);
return this;
};
// Adds an `event` listener that will be invoked a single
// time then automatically removed.
WildEmitter.prototype.once = function (event, groupName, fn) {
var self = this,
hasGroup = (arguments.length === 3),
group = hasGroup ? arguments[1] : undefined,
func = hasGroup ? arguments[2] : arguments[1];
function on() {
self.off(event, on);
func.apply(this, arguments);
}
this.on(event, group, on);
return this;
};
// Unbinds an entire group
WildEmitter.prototype.releaseGroup = function (groupName) {
var item, i, len, handlers;
for (item in this.callbacks) {
handlers = this.callbacks[item];
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i]._groupName === groupName) {
//console.log('removing');
// remove it and shorten the array we're looping through
handlers.splice(i, 1);
i--;
len--;
}
}
}
return this;
};
// Remove the given callback for `event` or all
// registered callbacks.
WildEmitter.prototype.off = function (event, fn) {
var callbacks = this.callbacks[event],
i;
if (!callbacks) return this;
// remove all handlers
if (arguments.length === 1) {
delete this.callbacks[event];
return this;
}
// remove specific handler
i = callbacks.indexOf(fn);
callbacks.splice(i, 1);
return this;
};
/// Emit `event` with the given args.
// also calls any `*` handlers
WildEmitter.prototype.emit = function (event) {
var args = [].slice.call(arguments, 1),
callbacks = this.callbacks[event],
specialCallbacks = this.getWildcardCallbacks(event),
i,
len,
item,
listeners;
if (callbacks) {
listeners = callbacks.slice();
for (i = 0, len = listeners.length; i < len; ++i) {
if (listeners[i]) {
listeners[i].apply(this, args);
} else {
break;
}
}
}
if (specialCallbacks) {
len = specialCallbacks.length;
listeners = specialCallbacks.slice();
for (i = 0, len = listeners.length; i < len; ++i) {
if (listeners[i]) {
listeners[i].apply(this, [event].concat(args));
} else {
break;
}
}
}
return this;
};
// Helper for for finding special wildcard event handlers that match the event
WildEmitter.prototype.getWildcardCallbacks = function (eventName) {
var item,
split,
result = [];
for (item in this.callbacks) {
split = item.split('*');
if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) {
result = result.concat(this.callbacks[item]);
}
}
return result;
};
},{}],5:[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
};
},{}],6:[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;
},{}],9:[function(require,module,exports){
// shim for using process in browser
var process = module.exports = {};
process.nextTick = (function () {
var canSetImmediate = typeof window !== 'undefined'
&& window.setImmediate;
var canPost = typeof window !== 'undefined'
&& window.postMessage && window.addEventListener
;
if (canSetImmediate) {
return function (f) { return window.setImmediate(f) };
}
if (canPost) {
var queue = [];
window.addEventListener('message', function (ev) {
var source = ev.source;
if ((source === window || source === null) && ev.data === 'process-tick') {
ev.stopPropagation();
if (queue.length > 0) {
var fn = queue.shift();
fn();
}
}
}, true);
return function nextTick(fn) {
queue.push(fn);
window.postMessage('process-tick', '*');
};
}
return function nextTick(fn) {
setTimeout(fn, 0);
};
})();
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
process.binding = function (name) {
throw new Error('process.binding is not supported');
}
// TODO(shtylman)
process.cwd = function () { return '/' };
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
},{}],8:[function(require,module,exports){
var process=require("__browserify_process");if (!process.EventEmitter) process.EventEmitter = function () {};
var EventEmitter = exports.EventEmitter = process.EventEmitter;
var isArray = typeof Array.isArray === 'function'
? Array.isArray
: function (xs) {
return Object.prototype.toString.call(xs) === '[object Array]'
}
;
function indexOf (xs, x) {
if (xs.indexOf) return xs.indexOf(x);
for (var i = 0; i < xs.length; i++) {
if (x === xs[i]) return i;
}
return -1;
}
// By default EventEmitters will print a warning if more than
// 10 listeners are added to it. This is a useful default which
// helps finding memory leaks.
//
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
var defaultMaxListeners = 10;
EventEmitter.prototype.setMaxListeners = function(n) {
if (!this._events) this._events = {};
this._events.maxListeners = n;
};
EventEmitter.prototype.emit = function(type) {
// If there is no 'error' event listener then throw.
if (type === 'error') {
if (!this._events || !this._events.error ||
(isArray(this._events.error) && !this._events.error.length))
{
if (arguments[1] instanceof Error) {
throw arguments[1]; // Unhandled 'error' event
} else {
throw new Error("Uncaught, unspecified 'error' event.");
}
return false;
}
}
if (!this._events) return false;
var handler = this._events[type];
if (!handler) return false;
if (typeof handler == 'function') {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
var args = Array.prototype.slice.call(arguments, 1);
handler.apply(this, args);
}
return true;
} else if (isArray(handler)) {
var args = Array.prototype.slice.call(arguments, 1);
var listeners = handler.slice();
for (var i = 0, l = listeners.length; i < l; i++) {
listeners[i].apply(this, args);
}
return true;
} else {
return false;
}
};
// EventEmitter is defined in src/node_events.cc
// EventEmitter.prototype.emit() is also defined there.
EventEmitter.prototype.addListener = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('addListener only takes instances of Function');
}
if (!this._events) this._events = {};
// To avoid recursion in the case that type == "newListeners"! Before
// adding it to the listeners, first emit "newListeners".
this.emit('newListener', type, listener);
if (!this._events[type]) {
// Optimize the case of one listener. Don't need the extra array object.
this._events[type] = listener;
} else if (isArray(this._events[type])) {
// Check for listener leak
if (!this._events[type].warned) {
var m;
if (this._events.maxListeners !== undefined) {
m = this._events.maxListeners;
} else {
m = defaultMaxListeners;
}
if (m && m > 0 && this._events[type].length > m) {
this._events[type].warned = true;
console.error('(node) warning: possible EventEmitter memory ' +
'leak detected. %d listeners added. ' +
'Use emitter.setMaxListeners() to increase limit.',
this._events[type].length);
console.trace();
}
}
// If we've already got an array, just append.
this._events[type].push(listener);
} else {
// Adding the second element, need to change to array.
this._events[type] = [this._events[type], listener];
}
return this;
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.once = function(type, listener) {
var self = this;
self.on(type, function g() {
self.removeListener(type, g);
listener.apply(this, arguments);
});
return this;
};
EventEmitter.prototype.removeListener = function(type, listener) {
if ('function' !== typeof listener) {
throw new Error('removeListener only takes instances of Function');
}
// does not use listeners(), so no side effect of creating _events[type]
if (!this._events || !this._events[type]) return this;
var list = this._events[type];
if (isArray(list)) {
var i = indexOf(list, listener);
if (i < 0) return this;
list.splice(i, 1);
if (list.length == 0)
delete this._events[type];
} else if (this._events[type] === listener) {
delete this._events[type];
}
return this;
};
EventEmitter.prototype.removeAllListeners = function(type) {
if (arguments.length === 0) {
this._events = {};
return this;
}
// does not use listeners(), so no side effect of creating _events[type]
if (type && this._events && this._events[type]) this._events[type] = null;
return this;
};
EventEmitter.prototype.listeners = function(type) {
if (!this._events) this._events = {};
if (!this._events[type]) this._events[type] = [];
if (!isArray(this._events[type])) {
this._events[type] = [this._events[type]];
}
return this._events[type];
};
EventEmitter.listenerCount = function(emitter, type) {
var ret;
if (!emitter._events || !emitter._events[type])
ret = 0;
else if (typeof emitter._events[type] === 'function')
ret = 1;
else
ret = emitter._events[type].length;
return ret;
};
},{"__browserify_process":9}],3:[function(require,module,exports){
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;
this.id = options.id;
this.parent = options.parent;
this.type = options.type || 'video';
this.oneway = options.oneway || false;
this.sharemyscreen = options.sharemyscreen || false;
this.browserPrefix = options.prefix;
this.stream = options.stream;
this.enableDataChannels = options.enableDataChannels === undefined ? this.parent.config.enableDataChannels : options.enableDataChannels;
this.receiveMedia = options.receiveMedia || this.parent.config.receiveMedia;
this.channels = {};
this.sid = options.sid || Date.now().toString();
// Create an RTCPeerConnection via the polyfill
this.pc = new PeerConnection(this.parent.config.peerConnectionConfig, this.parent.config.peerConnectionConstraints);
this.pc.on('ice', this.onIceCandidate.bind(this));
this.pc.on('offer', function (offer) {
self.send('offer', offer);
});
this.pc.on('answer', function (offer) {
self.send('answer', offer);
});
this.pc.on('addStream', this.handleRemoteStreamAdded.bind(this));
this.pc.on('addChannel', this.handleDataChannelAdded.bind(this));
this.pc.on('removeStream', this.handleStreamRemoved.bind(this));
// Just fire negotiation needed events for now
// When browser re-negotiation handling seems to work
// we can use this as the trigger for starting the offer/answer process
// automatically. We'll just leave it be for now while this stabalizes.
this.pc.on('negotiationNeeded', this.emit.bind(this, 'negotiationNeeded'));
this.pc.on('iceConnectionStateChange', this.emit.bind(this, 'iceConnectionStateChange'));
this.pc.on('iceConnectionStateChange', function () {
switch (self.pc.iceConnectionState) {
case 'failed':
// currently, in chrome only the initiator goes to failed
// so we need to signal this to the peer
if (self.pc.pc.peerconnection.localDescription.type === 'offer') {
self.parent.emit('iceFailed', self);
self.send('connectivityError');
}
break;
}
});
this.pc.on('signalingStateChange', this.emit.bind(this, 'signalingStateChange'));
this.logger = this.parent.logger;
// handle screensharing/broadcast mode
if (options.type === 'screen') {
if (this.parent.localScreen && this.sharemyscreen) {
this.logger.log('adding local screen stream to peer connection');
this.pc.addStream(this.parent.localScreen);
this.broadcaster = options.broadcaster;
}
} else {
this.parent.localStreams.forEach(function (stream) {
self.pc.addStream(stream);
});
}
// 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);
});
}
util.inherits(Peer, WildEmitter);
Peer.prototype.handleMessage = function (message) {
var self = this;
this.logger.log('getting', message.type, message);
if (message.prefix) this.browserPrefix = message.prefix;
if (message.type === 'offer') {
// workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1064247
message.payload.sdp = message.payload.sdp.replace('a=fmtp:0 profile-level-id=0x42e00c;packetization-mode=1\r\n', '');
this.pc.handleOffer(message.payload, function (err) {
if (err) {
return;
}
// auto-accept
self.pc.answer(self.receiveMedia, function (err, sessionDescription) {
//self.send('answer', sessionDescription);
});
});
} else if (message.type === 'answer') {
this.pc.handleAnswer(message.payload);
} else if (message.type === 'candidate') {
this.pc.processIce(message.payload);
} else if (message.type === 'connectivityError') {
this.parent.emit('connectivityError', self);
} else if (message.type === 'mute') {
this.parent.emit('mute', {id: message.from, name: message.payload.name});
} else if (message.type === 'unmute') {
this.parent.emit('unmute', {id: message.from, name: message.payload.name});
}
};
// send via signalling channel
Peer.prototype.send = function (messageType, payload) {
var message = {
to: this.id,
sid: this.sid,
broadcaster: this.broadcaster,
roomType: this.type,
type: messageType,
payload: payload,
prefix: webrtc.prefix
};
this.logger.log('sending', messageType, message);
this.parent.emit('message', message);
};
// send via data channel
// returns true when message was sent and false if channel is not open
Peer.prototype.sendDirectly = function (channel, messageType, payload) {
var message = {
type: messageType,
payload: payload
};
this.logger.log('sending via datachannel', channel, messageType, message);
var dc = this.getDataChannel(channel);
if (dc.readyState != 'open') return false;
dc.send(JSON.stringify(message));
return true;
};
// Internal method registering handlers for a data channel and emitting events on the peer
Peer.prototype._observeDataChannel = function (channel) {
var self = this;
channel.onclose = this.emit.bind(this, 'channelClose', channel);
channel.onerror = this.emit.bind(this, 'channelError', channel);
channel.onmessage = function (event) {
self.emit('channelMessage', self, channel.label, JSON.parse(event.data), channel, event);
};
channel.onopen = this.emit.bind(this, 'channelOpen', channel);
};
// Fetch or create a data channel by the given name
Peer.prototype.getDataChannel = function (name, opts) {
if (!webrtc.supportDataChannel) return this.emit('error', new Error('createDataChannel not supported'));
var channel = this.channels[name];
opts || (opts = {});
if (channel) return channel;
// if we don't have one by this label, create it
channel = this.channels[name] = this.pc.createDataChannel(name, opts);
this._observeDataChannel(channel);
return channel;
};
Peer.prototype.onIceCandidate = function (candidate) {
if (this.closed) return;
if (candidate) {
this.send('candidate', candidate);
} else {
this.logger.log("End of candidates.");
}
};
Peer.prototype.start = function () {
var self = this;
// well, the webrtc api requires that we either
// a) create a datachannel a priori
// b) do a renegotiation later to add the SCTP m-line
// Let's do (a) first...
if (this.enableDataChannels) {
this.getDataChannel('simplewebrtc');
}
this.pc.offer(this.receiveMedia, function (err, sessionDescription) {
//self.send('offer', sessionDescription);
});
};
Peer.prototype.icerestart = function () {
var constraints = this.receiveMedia;
constraints.mandatory.IceRestart = true;
this.pc.offer(constraints, function (err, success) { });
};
Peer.prototype.end = function () {
if (this.closed) return;
this.pc.close();
this.handleStreamRemoved();
};
Peer.prototype.handleRemoteStreamAdded = function (event) {
var self = this;
if (this.stream) {
this.logger.warn('Already have a remote stream');
} else {
this.stream = event.stream;
// FIXME: addEventListener('ended', ...) would be nicer
// but does not work in firefox
this.stream.onended = function () {
self.end();
};
this.parent.emit('peerStreamAdded', this);
}
};
Peer.prototype.handleStreamRemoved = function () {
this.parent.peers.splice(this.parent.peers.indexOf(this), 1);
this.closed = true;
this.parent.emit('peerStreamRemoved', this);
};
Peer.prototype.handleDataChannelAdded = function (channel) {
this.channels[channel.label] = 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;
},{"filetransfer":11,"rtcpeerconnection":10,"util":2,"webrtcsupport":5,"wildemitter":4}],12:[function(require,module,exports){
// getUserMedia helper by @HenrikJoreteg
var func = (window.navigator.getUserMedia ||
window.navigator.webkitGetUserMedia ||
window.navigator.mozGetUserMedia ||
window.navigator.msGetUserMedia);
module.exports = function (constraints, cb) {
var options, error;
var haveOpts = arguments.length === 2;
var defaultOpts = {video: true, audio: true};
var denied = 'PermissionDeniedError';
var notSatisfied = 'ConstraintNotSatisfiedError';
// make constraints optional
if (!haveOpts) {
cb = constraints;
constraints = defaultOpts;
}
// treat lack of browser support like an error
if (!func) {
// throw proper error per spec
error = new Error('MediaStreamError');
error.name = 'NotSupportedError';
// keep all callbacks async
return window.setTimeout(function () {
cb(error);
}, 0);
}
// make requesting media from non-http sources trigger an error
// current browsers silently drop the request instead
var protocol = window.location.protocol;
if (protocol !== 'http:' && protocol !== 'https:') {
error = new Error('MediaStreamError');
error.name = 'NotSupportedError';
// keep all callbacks async
return window.setTimeout(function () {
cb(error);
}, 0);
}
// normalize error handling when no media types are requested
if (!constraints.audio && !constraints.video) {
error = new Error('MediaStreamError');
error.name = 'NoMediaRequestedError';
// keep all callbacks async
return window.setTimeout(function () {
cb(error);
}, 0);
}
if (localStorage && localStorage.useFirefoxFakeDevice === "true") {
constraints.fake = true;
}
func.call(window.navigator, constraints, function (stream) {
cb(null, stream);
}, function (err) {
var error;
// coerce into an error object since FF gives us a string
// there are only two valid names according to the spec
// we coerce all non-denied to "constraint not satisfied".
if (typeof err === 'string') {
error = new Error('MediaStreamError');
if (err === denied) {
error.name = denied;
} else {
error.name = notSatisfied;
}
} else {
// if we get an error object make sure '.name' property is set
// according to spec: http://dev.w3.org/2011/webrtc/editor/getusermedia.html#navigatorusermediaerror-and-navigatorusermediaerrorcallback
error = err;
if (!error.name) {
// this is likely chrome which
// sets a property called "ERROR_DENIED" on the error object
// if so we make sure to set a name
if (error[denied]) {
err.name = denied;
} else {
err.name = notSatisfied;
}
}
}
cb(error);
});
};
},{}],7:[function(require,module,exports){
var util = require('util');
var hark = require('hark');
var webrtc = require('webrtcsupport');
var getUserMedia = require('getusermedia');
var getScreenMedia = require('getscreenmedia');
var WildEmitter = require('wildemitter');
var GainController = require('mediastream-gain');
var mockconsole = require('mockconsole');
function LocalMedia(opts) {
WildEmitter.call(this);
var config = this.config = {
autoAdjustMic: false,
detectSpeakingEvents: true,
media: {
audio: true,
video: true
},
logger: mockconsole
};
var item;
for (item in opts) {
this.config[item] = opts[item];
}
this.logger = config.logger;
this._log = this.logger.log.bind(this.logger, 'LocalMedia:');
this._logerror = this.logger.error.bind(this.logger, 'LocalMedia:');
this.screenSharingSupport = webrtc.screenSharing;
this.localStreams = [];
this.localScreens = [];
if (!webrtc.support) {
this._logerror('Your browser does not support local media capture.');
}
}
util.inherits(LocalMedia, WildEmitter);
LocalMedia.prototype.start = function (mediaConstraints, cb) {
var self = this;
var constraints = mediaConstraints || this.config.media;
getUserMedia(constraints, function (err, stream) {
if (!err) {
if (constraints.audio && self.config.detectSpeakingEvents) {
self.setupAudioMonitor(stream, self.config.harkOptions);
}
self.localStreams.push(stream);
if (self.config.autoAdjustMic) {
self.gainController = new GainController(stream);
// start out somewhat muted if we can track audio
self.setMicIfEnabled(0.5);
}
// TODO: might need to migrate to the video tracks onended
// FIXME: firefox does not seem to trigger this...
stream.onended = function () {
/*
var idx = self.localStreams.indexOf(stream);
if (idx > -1) {
self.localScreens.splice(idx, 1);
}
self.emit('localStreamStopped', stream);
*/
};
self.emit('localStream', stream);
}
if (cb) {
return cb(err, stream);
}
});
};
LocalMedia.prototype.stop = function (stream) {
var self = this;
// FIXME: duplicates cleanup code until fixed in FF
if (stream) {
stream.stop();
self.emit('localStreamStopped', stream);
var idx = self.localStreams.indexOf(stream);
if (idx > -1) {
self.localStreams = self.localStreams.splice(idx, 1);
}
} else {
if (this.audioMonitor) {
this.audioMonitor.stop();
delete this.audioMonitor;
}
this.localStreams.forEach(function (stream) {
stream.stop();
self.emit('localStreamStopped', stream);
});
this.localStreams = [];
}
};
LocalMedia.prototype.startScreenShare = function (cb) {
var self = this;
getScreenMedia(function (err, stream) {
if (!err) {
self.localScreens.push(stream);
// TODO: might need to migrate to the video tracks onended
// Firefox does not support .onended but it does not support
// screensharing either
stream.onended = function () {
var idx = self.localScreens.indexOf(stream);
if (idx > -1) {
self.localScreens.splice(idx, 1);
}
self.emit('localScreenStopped', stream);
};
self.emit('localScreen', stream);
}
// enable the callback
if (cb) {
return cb(err, stream);
}
});
};
LocalMedia.prototype.stopScreenShare = function (stream) {
if (stream) {
stream.stop();
} else {
this.localScreens.forEach(function (stream) {
stream.stop();
});
this.localScreens = [];
}
};
// Audio controls
LocalMedia.prototype.mute = function () {
this._audioEnabled(false);
this.hardMuted = true;
this.emit('audioOff');
};
LocalMedia.prototype.unmute = function () {
this._audioEnabled(true);
this.hardMuted = false;
this.emit('audioOn');
};
LocalMedia.prototype.setupAudioMonitor = function (stream, harkOptions) {
this._log('Setup audio');
var audio = this.audioMonitor = hark(stream, harkOptions);
var self = this;
var timeout;
audio.on('speaking', function () {
self.emit('speaking');
if (self.hardMuted) {
return;
}
self.setMicIfEnabled(1);
});
audio.on('stopped_speaking', function () {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(function () {
self.emit('stoppedSpeaking');
if (self.hardMuted) {
return;
}
self.setMicIfEnabled(0.5);
}, 1000);
});
audio.on('volume_change', function (volume, treshold) {
self.emit('volumeChange', volume, treshold);
});
};
// We do this as a seperate method in order to
// still leave the "setMicVolume" as a working
// method.
LocalMedia.prototype.setMicIfEnabled = function (volume) {
if (!this.config.autoAdjustMic) {
return;
}
this.gainController.setGain(volume);
};
// Video controls
LocalMedia.prototype.pauseVideo = function () {
this._videoEnabled(false);
this.emit('videoOff');
};
LocalMedia.prototype.resumeVideo = function () {
this._videoEnabled(true);
this.emit('videoOn');
};
// Combined controls
LocalMedia.prototype.pause = function () {
this.mute();
this.pauseVideo();
};
LocalMedia.prototype.resume = function () {
this.unmute();
this.resumeVideo();
};
// Internal methods for enabling/disabling audio/video
LocalMedia.prototype._audioEnabled = function (bool) {
// work around for chrome 27 bug where disabling tracks
// doesn't seem to work (works in canary, remove when working)
this.setMicIfEnabled(bool ? 1 : 0);
this.localStreams.forEach(function (stream) {
stream.getAudioTracks().forEach(function (track) {
track.enabled = !!bool;
});
});
};
LocalMedia.prototype._videoEnabled = function (bool) {
this.localStreams.forEach(function (stream) {
stream.getVideoTracks().forEach(function (track) {
track.enabled = !!bool;
});
});
};
// check if all audio streams are enabled
LocalMedia.prototype.isAudioEnabled = function () {
var enabled = true;
this.localStreams.forEach(function (stream) {
stream.getAudioTracks().forEach(function (track) {
enabled = enabled && track.enabled;
});
});
return enabled;
};
// check if all video streams are enabled
LocalMedia.prototype.isVideoEnabled = function () {
var enabled = true;
this.localStreams.forEach(function (stream) {
stream.getVideoTracks().forEach(function (track) {
enabled = enabled && track.enabled;
});
});
return enabled;
};
// Backwards Compat
LocalMedia.prototype.startLocalMedia = LocalMedia.prototype.start;
LocalMedia.prototype.stopLocalMedia = LocalMedia.prototype.stop;
// fallback for old .localStream behaviour
Object.defineProperty(LocalMedia.prototype, 'localStream', {
get: function () {
return this.localStr