UNPKG

webrtc

Version:

WebRTC abstraction for making it simple to manage multiple peer connections of various types.

1,592 lines (1,384 loc) 175 kB
(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