UNPKG

rtc-quickconnect

Version:

Create a WebRTC connection in record time

1,911 lines (1,476 loc) 397 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.quickconnect = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ /* jshint node: true */ /* global location */ 'use strict'; var rtc = require('rtc-tools'); var mbus = require('mbus'); var detectPlugin = require('rtc-core/plugin'); var debug = rtc.logger('rtc-quickconnect'); var extend = require('cog/extend'); /** # rtc-quickconnect This is a high level helper module designed to help you get up an running with WebRTC really, really quickly. By using this module you are trading off some flexibility, so if you need a more flexible configuration you should drill down into lower level components of the [rtc.io](http://www.rtc.io) suite. In particular you should check out [rtc](https://github.com/rtc-io/rtc). ## Example Usage In the simplest case you simply call quickconnect with a single string argument which tells quickconnect which server to use for signaling: <<< examples/simple.js <<< docs/events.md <<< docs/examples.md ## Regarding Signalling and a Signalling Server Signaling is an important part of setting up a WebRTC connection and for our examples we use our own test instance of the [rtc-switchboard](https://github.com/rtc-io/rtc-switchboard). For your testing and development you are more than welcome to use this also, but just be aware that we use this for our testing so it may go up and down a little. If you need something more stable, why not consider deploying an instance of the switchboard yourself - it's pretty easy :) ## Reference ``` quickconnect(signalhost, opts?) => rtc-sigaller instance (+ helpers) ``` ### Valid Quick Connect Options The options provided to the `rtc-quickconnect` module function influence the behaviour of some of the underlying components used from the rtc.io suite. Listed below are some of the commonly used options: - `ns` (default: '') An optional namespace for your signalling room. While quickconnect will generate a unique hash for the room, this can be made to be more unique by providing a namespace. Using a namespace means two demos that have generated the same hash but use a different namespace will be in different rooms. - `room` (default: null) _added 0.6_ Rather than use the internal hash generation (plus optional namespace) for room name generation, simply use this room name instead. __NOTE:__ Use of the `room` option takes precendence over `ns`. - `debug` (default: false) Write rtc.io suite debug output to the browser console. - `expectedLocalStreams` (default: not specified) _added 3.0_ By providing a positive integer value for this option will mean that the created quickconnect instance will wait until the specified number of streams have been added to the quickconnect "template" before announcing to the signaling server. - `manualJoin` (default: `false`) Set this value to `true` if you would prefer to call the `join` function to connecting to the signalling server, rather than having that happen automatically as soon as quickconnect is ready to. #### Options for Peer Connection Creation Options that are passed onto the [rtc.createConnection](https://github.com/rtc-io/rtc#createconnectionopts-constraints) function: - `iceServers` This provides a list of ice servers that can be used to help negotiate a connection between peers. #### Options for P2P negotiation Under the hood, quickconnect uses the [rtc/couple](https://github.com/rtc-io/rtc#rtccouple) logic, and the options passed to quickconnect are also passed onto this function. **/ module.exports = function(signalhost, opts) { var hash = typeof location != 'undefined' && location.hash.slice(1); var signaller = require('rtc-pluggable-signaller')(extend({ signaller: signalhost, // use the primus endpoint as a fallback in case we are talking to an // older switchboard instance endpoints: ['/', '/primus'] }, opts)); var getPeerData = require('./lib/getpeerdata')(signaller.peers); // init configurable vars var ns = (opts || {}).ns || ''; var room = (opts || {}).room; var debugging = (opts || {}).debug; var allowJoin = !(opts || {}).manualJoin; var profile = {}; var announced = false; // initialise iceServers to undefined // we will not announce until these have been properly initialised var iceServers; // collect the local streams var localStreams = []; // create the calls map var calls = signaller.calls = require('./lib/calls')(signaller, opts); // create the known data channels registry var channels = {}; // save the plugins passed to the signaller var plugins = signaller.plugins = (opts || {}).plugins || []; var plugin = detectPlugin(plugins); var pluginReady; // check how many local streams have been expected (default: 0) var expectedLocalStreams = parseInt((opts || {}).expectedLocalStreams, 10) || 0; var announceTimer = 0; var updateTimer = 0; function checkReadyToAnnounce() { clearTimeout(announceTimer); // if we have already announced do nothing! if (announced) { return; } if (! allowJoin) { return; } // if we have a plugin but it's not initialized we aren't ready if (plugin && (! pluginReady)) { return; } // if we have no iceServers we aren't ready if (! iceServers) { return; } // if we are waiting for a set number of streams, then wait until we have // the required number if (expectedLocalStreams && localStreams.length < expectedLocalStreams) { return; } // announce ourselves to our new friend announceTimer = setTimeout(function() { var data = extend({ room: room }, profile); // announce and emit the local announce event signaller.announce(data); announced = true; }, 0); } function connect(id) { var data = getPeerData(id); var pc; var monitor; // if the room is not a match, abort if (data.room !== room) { return; } // end any call to this id so we know we are starting fresh calls.end(id); // create a peer connection // iceServers that have been created using genice taking precendence pc = rtc.createConnection( extend({}, opts, { iceServers: iceServers }), (opts || {}).constraints ); signaller('peer:connect', data.id, pc, data); // add this connection to the calls list calls.create(data.id, pc); // add the local streams localStreams.forEach(function(stream) { pc.addStream(stream); }); // add the data channels // do this differently based on whether the connection is a // master or a slave connection if (signaller.isMaster(data.id)) { debug('is master, creating data channels: ', Object.keys(channels)); // create the channels Object.keys(channels).forEach(function(label) { gotPeerChannel(pc.createDataChannel(label, channels[label]), pc, data); }); } else { pc.ondatachannel = function(evt) { var channel = evt && evt.channel; // if we have no channel, abort if (! channel) { return; } if (channels[channel.label] !== undefined) { gotPeerChannel(channel, pc, getPeerData(id)); } }; } // couple the connections debug('coupling ' + signaller.id + ' to ' + data.id); monitor = rtc.couple(pc, id, signaller, extend({}, opts, { logger: mbus('pc.' + id, signaller) })); signaller('peer:couple', id, pc, data, monitor); // once active, trigger the peer connect event monitor.once('connected', calls.start.bind(null, id, pc, data)); monitor.once('closed', calls.end.bind(null, id)); // if we are the master connnection, create the offer // NOTE: this only really for the sake of politeness, as rtc couple // implementation handles the slave attempting to create an offer if (signaller.isMaster(id)) { monitor.createOffer(); } } function getActiveCall(peerId) { var call = calls.get(peerId); if (! call) { throw new Error('No active call for peer: ' + peerId); } return call; } function gotPeerChannel(channel, pc, data) { var channelMonitor; function channelReady() { var call = calls.get(data.id); var args = [ data.id, channel, data, pc ]; // decouple the channel.onopen listener debug('reporting channel "' + channel.label + '" ready, have call: ' + (!!call)); clearInterval(channelMonitor); channel.onopen = null; // save the channel if (call) { call.channels.set(channel.label, channel); } // trigger the %channel.label%:open event debug('triggering channel:opened events for channel: ' + channel.label); // emit the plain channel:opened event signaller.apply(signaller, ['channel:opened'].concat(args)); // emit the channel:opened:%label% eve signaller.apply( signaller, ['channel:opened:' + channel.label].concat(args) ); } debug('channel ' + channel.label + ' discovered for peer: ' + data.id); if (channel.readyState === 'open') { return channelReady(); } debug('channel not ready, current state = ' + channel.readyState); channel.onopen = channelReady; // monitor the channel open (don't trust the channel open event just yet) channelMonitor = setInterval(function() { debug('checking channel state, current state = ' + channel.readyState); if (channel.readyState === 'open') { channelReady(); } }, 500); } function initPlugin() { return plugin && plugin.init(opts, function(err) { if (err) { return console.error('Could not initialize plugin: ', err); } pluginReady = true; checkReadyToAnnounce(); }); } function handleLocalAnnounce(data) { // if we send an announce with an updated room then update our local room name if (data && typeof data.room != 'undefined') { room = data.room; } } function handlePeerFilter(id, data) { // only connect with the peer if we are ready data.allow = data.allow && (localStreams.length >= expectedLocalStreams); } function handlePeerUpdate(data) { var id = data && data.id; var activeCall = id && calls.get(id); // if we have received an update for a peer that has no active calls, // then pass this onto the announce handler if (id && (! activeCall)) { debug('received peer update from peer ' + id + ', no active calls'); signaller.to(id).send('/reconnect'); return connect(id); } } // if the room is not defined, then generate the room name if (! room) { // if the hash is not assigned, then create a random hash value if (typeof location != 'undefined' && (! hash)) { hash = location.hash = '' + (Math.pow(2, 53) * Math.random()); } room = ns + '#' + hash; } if (debugging) { rtc.logger.enable.apply(rtc.logger, Array.isArray(debug) ? debugging : ['*']); } signaller.on('peer:announce', function(data) { connect(data.id); }); signaller.on('peer:update', handlePeerUpdate); signaller.on('message:reconnect', function(sender) { connect(sender.id); }); /** ### Quickconnect Broadcast and Data Channel Helper Functions The following are functions that are patched into the `rtc-signaller` instance that make working with and creating functional WebRTC applications a lot simpler. **/ /** #### addStream ``` addStream(stream:MediaStream) => qc ``` Add the stream to active calls and also save the stream so that it can be added to future calls. **/ signaller.broadcast = signaller.addStream = function(stream) { localStreams.push(stream); // if we have any active calls, then add the stream calls.values().forEach(function(data) { data.pc.addStream(stream); }); checkReadyToAnnounce(); return signaller; }; /** #### endCalls() The `endCalls` function terminates all the active calls that have been created in this quickconnect instance. Calling `endCalls` does not kill the connection with the signalling server. **/ signaller.endCalls = function() { calls.keys().forEach(calls.end); }; /** #### close() The `close` function provides a convenient way of closing all associated peer connections. This function simply uses the `endCalls` function and the underlying `leave` function of the signaller to do a "full cleanup" of all connections. **/ signaller.close = function() { signaller.endCalls(); signaller.leave(); }; /** #### createDataChannel(label, config) Request that a data channel with the specified `label` is created on the peer connection. When the data channel is open and available, an event will be triggered using the label of the data channel. For example, if a new data channel was requested using the following call: ```js var qc = quickconnect('https://switchboard.rtc.io/').createDataChannel('test'); ``` Then when the data channel is ready for use, a `test:open` event would be emitted by `qc`. **/ signaller.createDataChannel = function(label, opts) { // create a channel on all existing calls calls.keys().forEach(function(peerId) { var call = calls.get(peerId); var dc; // if we are the master connection, create the data channel if (call && call.pc && signaller.isMaster(peerId)) { dc = call.pc.createDataChannel(label, opts); gotPeerChannel(dc, call.pc, getPeerData(peerId)); } }); // save the data channel opts in the local channels dictionary channels[label] = opts || null; return signaller; }; /** #### join() The `join` function is used when `manualJoin` is set to true when creating a quickconnect instance. Call the `join` function once you are ready to join the signalling server and initiate connections with other people. **/ signaller.join = function() { allowJoin = true; checkReadyToAnnounce(); }; /** #### `get(name)` The `get` function returns the property value for the specified property name. **/ signaller.get = function(name) { return profile[name]; }; /** #### `getLocalStreams()` Return a copy of the local streams that have currently been configured **/ signaller.getLocalStreams = function() { return [].concat(localStreams); }; /** #### reactive() Flag that this session will be a reactive connection. **/ signaller.reactive = function() { // add the reactive flag opts = opts || {}; opts.reactive = true; // chain return signaller; }; /** #### removeStream ``` removeStream(stream:MediaStream) ``` Remove the specified stream from both the local streams that are to be connected to new peers, and also from any active calls. **/ signaller.removeStream = function(stream) { var localIndex = localStreams.indexOf(stream); // remove the stream from any active calls calls.values().forEach(function(call) { call.pc.removeStream(stream); }); // remove the stream from the localStreams array if (localIndex >= 0) { localStreams.splice(localIndex, 1); } return signaller; }; /** #### requestChannel ``` requestChannel(targetId, label, callback) ``` This is a function that can be used to respond to remote peers supplying a data channel as part of their configuration. As per the `receiveStream` function this function will either fire the callback immediately if the channel is already available, or once the channel has been discovered on the call. **/ signaller.requestChannel = function(targetId, label, callback) { var call = getActiveCall(targetId); var channel = call && call.channels.get(label); // if we have then channel trigger the callback immediately if (channel) { callback(null, channel); return signaller; } // if not, wait for it signaller.once('channel:opened:' + label, function(id, dc) { callback(null, dc); }); return signaller; }; /** #### requestStream ``` requestStream(targetId, idx, callback) ``` Used to request a remote stream from a quickconnect instance. If the stream is already available in the calls remote streams, then the callback will be triggered immediately, otherwise this function will monitor `stream:added` events and wait for a match. In the case that an unknown target is requested, then an exception will be thrown. **/ signaller.requestStream = function(targetId, idx, callback) { var call = getActiveCall(targetId); var stream; function waitForStream(peerId) { if (peerId !== targetId) { return; } // get the stream stream = call.pc.getRemoteStreams()[idx]; // if we have the stream, then remove the listener and trigger the cb if (stream) { signaller.removeListener('stream:added', waitForStream); callback(null, stream); } } // look for the stream in the remote streams of the call stream = call.pc.getRemoteStreams()[idx]; // if we found the stream then trigger the callback if (stream) { callback(null, stream); return signaller; } // otherwise wait for the stream signaller.on('stream:added', waitForStream); return signaller; }; /** #### profile(data) Update the profile data with the attached information, so when the signaller announces it includes this data in addition to any room and id information. **/ signaller.profile = function(data) { extend(profile, data); // if we have already announced, then reannounce our profile to provide // others a `peer:update` event if (announced) { clearTimeout(updateTimer); updateTimer = setTimeout(function() { signaller.announce(profile); }, (opts || {}).updateDelay || 1000); } return signaller; }; /** #### waitForCall ``` waitForCall(targetId, callback) ``` Wait for a call from the specified targetId. If the call is already active the callback will be fired immediately, otherwise we will wait for a `call:started` event that matches the requested `targetId` **/ signaller.waitForCall = function(targetId, callback) { var call = calls.get(targetId); if (call && call.active) { callback(null, call.pc); return signaller; } signaller.on('call:started', function handleNewCall(id) { if (id === targetId) { signaller.removeListener('call:started', handleNewCall); callback(null, calls.get(id).pc); } }); }; // if we have an expected number of local streams, then use a filter to // check if we should respond if (expectedLocalStreams) { signaller.on('peer:filter', handlePeerFilter); } // respond to local announce messages signaller.on('local:announce', handleLocalAnnounce); // handle ping messages signaller.on('message:ping', calls.ping); // use genice to find our iceServers require('rtc-core/genice')(opts, function(err, servers) { if (err) { return console.error('could not find iceServers: ', err); } iceServers = servers; checkReadyToAnnounce(); }); // if we plugin is active, then initialize it if (plugin) { initPlugin(); } // pass the signaller on return signaller; }; },{"./lib/calls":2,"./lib/getpeerdata":3,"cog/extend":6,"mbus":12,"rtc-core/genice":14,"rtc-core/plugin":16,"rtc-pluggable-signaller":17,"rtc-tools":50}],2:[function(require,module,exports){ (function (process){ var rtc = require('rtc-tools'); var debug = rtc.logger('rtc-quickconnect'); var cleanup = require('rtc-tools/cleanup'); var getable = require('cog/getable'); module.exports = function(signaller, opts) { var calls = getable({}); var getPeerData = require('./getpeerdata')(signaller.peers); var heartbeat; function create(id, pc) { calls.set(id, { active: false, pc: pc, channels: getable({}), streams: [], lastping: Date.now() }); } function createStreamAddHandler(id) { return function(evt) { debug('peer ' + id + ' added stream'); updateRemoteStreams(id); receiveRemoteStream(id)(evt.stream); }; } function createStreamRemoveHandler(id) { return function(evt) { debug('peer ' + id + ' removed stream'); updateRemoteStreams(id); signaller('stream:removed', id, evt.stream); }; } function end(id) { var call = calls.get(id); // if we have no data, then do nothing if (! call) { return; } // if we have no data, then return call.channels.keys().forEach(function(label) { var channel = call.channels.get(label); var args = [id, channel, label]; // emit the plain channel:closed event signaller.apply(signaller, ['channel:closed'].concat(args)); // emit the labelled version of the event signaller.apply(signaller, ['channel:closed:' + label].concat(args)); // decouple the events channel.onopen = null; }); // trigger stream:removed events for each of the remotestreams in the pc call.streams.forEach(function(stream) { signaller('stream:removed', id, stream); }); // delete the call data calls.delete(id); // if we have no more calls, disable the heartbeat if (calls.keys().length === 0) { resetHeartbeat(); } // trigger the call:ended event signaller('call:ended', id, call.pc); // ensure the peer connection is properly cleaned up cleanup(call.pc); } function ping(sender) { var call = calls.get(sender && sender.id); // set the last ping for the data if (call) { call.lastping = Date.now(); } } function receiveRemoteStream(id) { return function(stream) { signaller('stream:added', id, stream, getPeerData(id)); }; } function resetHeartbeat() { clearInterval(heartbeat); heartbeat = 0; } function start(id, pc, data) { var call = calls.get(id); var streams = [].concat(pc.getRemoteStreams()); // flag the call as active call.active = true; call.streams = [].concat(pc.getRemoteStreams()); pc.onaddstream = createStreamAddHandler(id); pc.onremovestream = createStreamRemoveHandler(id); debug(signaller.id + ' - ' + id + ' call start: ' + streams.length + ' streams'); signaller('call:started', id, pc, data); // configure the heartbeat timer heartbeat = heartbeat || require('./heartbeat')(signaller, calls, opts); // examine the existing remote streams after a short delay process.nextTick(function() { // iterate through any remote streams streams.forEach(receiveRemoteStream(id)); }); } function updateRemoteStreams(id) { var call = calls.get(id); if (call && call.pc) { call.streams = [].concat(call.pc.getRemoteStreams()); } } calls.create = create; calls.end = end; calls.ping = ping; calls.start = start; return calls; }; }).call(this,require('_process')) },{"./getpeerdata":3,"./heartbeat":4,"_process":11,"cog/getable":7,"rtc-tools":50,"rtc-tools/cleanup":46}],3:[function(require,module,exports){ module.exports = function(peers) { return function(id) { var peer = peers.get(id); return peer && peer.data; }; }; },{}],4:[function(require,module,exports){ module.exports = function(signaller, calls, opts) { var heartbeat = (opts || {}).heartbeat || 2500; var heartbeatTimer = 0; function send() { var tickInactive = (Date.now() - (heartbeat * 4)); // iterate through our established calls calls.keys().forEach(function(id) { var call = calls.get(id); // if the call ping is too old, end the call if (call.lastping < tickInactive) { return calls.end(id); } // send a ping message signaller.to(id).send('/ping'); }); } if (! heartbeat) { return; } return setInterval(send, heartbeat); }; },{}],5:[function(require,module,exports){ /* jshint node: true */ 'use strict'; /** ## cog/defaults ```js var defaults = require('cog/defaults'); ``` ### defaults(target, *) Shallow copy object properties from the supplied source objects (*) into the target object, returning the target object once completed. Do not, however, overwrite existing keys with new values: ```js defaults({ a: 1, b: 2 }, { c: 3 }, { d: 4 }, { b: 5 })); ``` See an example on [requirebin](http://requirebin.com/?gist=6079475). **/ module.exports = function(target) { // ensure we have a target target = target || {}; // iterate through the sources and copy to the target [].slice.call(arguments, 1).forEach(function(source) { if (! source) { return; } for (var prop in source) { if (target[prop] === void 0) { target[prop] = source[prop]; } } }); return target; }; },{}],6:[function(require,module,exports){ /* jshint node: true */ 'use strict'; /** ## cog/extend ```js var extend = require('cog/extend'); ``` ### extend(target, *) Shallow copy object properties from the supplied source objects (*) into the target object, returning the target object once completed: ```js extend({ a: 1, b: 2 }, { c: 3 }, { d: 4 }, { b: 5 })); ``` See an example on [requirebin](http://requirebin.com/?gist=6079475). **/ module.exports = function(target) { [].slice.call(arguments, 1).forEach(function(source) { if (! source) { return; } for (var prop in source) { target[prop] = source[prop]; } }); return target; }; },{}],7:[function(require,module,exports){ /** ## cog/getable Take an object and provide a wrapper that allows you to `get` and `set` values on that object. **/ module.exports = function(target) { function get(key) { return target[key]; } function set(key, value) { target[key] = value; } function remove(key) { return delete target[key]; } function keys() { return Object.keys(target); }; function values() { return Object.keys(target).map(function(key) { return target[key]; }); }; if (typeof target != 'object') { return target; } return { get: get, set: set, remove: remove, delete: remove, keys: keys, values: values }; }; },{}],8:[function(require,module,exports){ /* jshint node: true */ 'use strict'; /** ## cog/jsonparse ```js var jsonparse = require('cog/jsonparse'); ``` ### jsonparse(input) This function will attempt to automatically detect stringified JSON, and when detected will parse into JSON objects. The function looks for strings that look and smell like stringified JSON, and if found attempts to `JSON.parse` the input into a valid object. **/ module.exports = function(input) { var isString = typeof input == 'string' || (input instanceof String); var reNumeric = /^\-?\d+\.?\d*$/; var shouldParse ; var firstChar; var lastChar; if ((! isString) || input.length < 2) { if (isString && reNumeric.test(input)) { return parseFloat(input); } return input; } // check for true or false if (input === 'true' || input === 'false') { return input === 'true'; } // check for null if (input === 'null') { return null; } // get the first and last characters firstChar = input.charAt(0); lastChar = input.charAt(input.length - 1); // determine whether we should JSON.parse the input shouldParse = (firstChar == '{' && lastChar == '}') || (firstChar == '[' && lastChar == ']') || (firstChar == '"' && lastChar == '"'); if (shouldParse) { try { return JSON.parse(input); } catch (e) { // apparently it wasn't valid json, carry on with regular processing } } return reNumeric.test(input) ? parseFloat(input) : input; }; },{}],9:[function(require,module,exports){ /* jshint node: true */ 'use strict'; /** ## cog/logger ```js var logger = require('cog/logger'); ``` Simple browser logging offering similar functionality to the [debug](https://github.com/visionmedia/debug) module. ### Usage Create your self a new logging instance and give it a name: ```js var debug = logger('phil'); ``` Now do some debugging: ```js debug('hello'); ``` At this stage, no log output will be generated because your logger is currently disabled. Enable it: ```js logger.enable('phil'); ``` Now do some more logger: ```js debug('Oh this is so much nicer :)'); // --> phil: Oh this is some much nicer :) ``` ### Reference **/ var active = []; var unleashListeners = []; var targets = [ console ]; /** #### logger(name) Create a new logging instance. **/ var logger = module.exports = function(name) { // initial enabled check var enabled = checkActive(); function checkActive() { return enabled = active.indexOf('*') >= 0 || active.indexOf(name) >= 0; } // register the check active with the listeners array unleashListeners[unleashListeners.length] = checkActive; // return the actual logging function return function() { var args = [].slice.call(arguments); // if we have a string message if (typeof args[0] == 'string' || (args[0] instanceof String)) { args[0] = name + ': ' + args[0]; } // if not enabled, bail if (! enabled) { return; } // log targets.forEach(function(target) { target.log.apply(target, args); }); }; }; /** #### logger.reset() Reset logging (remove the default console logger, flag all loggers as inactive, etc, etc. **/ logger.reset = function() { // reset targets and active states targets = []; active = []; return logger.enable(); }; /** #### logger.to(target) Add a logging target. The logger must have a `log` method attached. **/ logger.to = function(target) { targets = targets.concat(target || []); return logger; }; /** #### logger.enable(names*) Enable logging via the named logging instances. To enable logging via all instances, you can pass a wildcard: ```js logger.enable('*'); ``` __TODO:__ wildcard enablers **/ logger.enable = function() { // update the active active = active.concat([].slice.call(arguments)); // trigger the unleash listeners unleashListeners.forEach(function(listener) { listener(); }); return logger; }; },{}],10:[function(require,module,exports){ /* jshint node: true */ 'use strict'; /** ## cog/throttle ```js var throttle = require('cog/throttle'); ``` ### throttle(fn, delay, opts) A cherry-pickable throttle function. Used to throttle `fn` to ensure that it can be called at most once every `delay` milliseconds. Will fire first event immediately, ensuring the next event fired will occur at least `delay` milliseconds after the first, and so on. **/ module.exports = function(fn, delay, opts) { var lastExec = (opts || {}).leading !== false ? 0 : Date.now(); var trailing = (opts || {}).trailing; var timer; var queuedArgs; var queuedScope; // trailing defaults to true trailing = trailing || trailing === undefined; function invokeDefered() { fn.apply(queuedScope, queuedArgs || []); lastExec = Date.now(); } return function() { var tick = Date.now(); var elapsed = tick - lastExec; // always clear the defered timer clearTimeout(timer); if (elapsed < delay) { queuedArgs = [].slice.call(arguments, 0); queuedScope = this; return trailing && (timer = setTimeout(invokeDefered, delay - elapsed)); } // call the function lastExec = tick; fn.apply(this, arguments); }; }; },{}],11:[function(require,module,exports){ // shim for using process in browser var process = module.exports = {}; var queue = []; var draining = false; var currentQueue; var queueIndex = -1; function cleanUpNextTick() { draining = false; if (currentQueue.length) { queue = currentQueue.concat(queue); } else { queueIndex = -1; } if (queue.length) { drainQueue(); } } function drainQueue() { if (draining) { return; } var timeout = setTimeout(cleanUpNextTick); draining = true; var len = queue.length; while(len) { currentQueue = queue; queue = []; while (++queueIndex < len) { currentQueue[queueIndex].run(); } queueIndex = -1; len = queue.length; } currentQueue = null; draining = false; clearTimeout(timeout); } process.nextTick = function (fun) { var args = new Array(arguments.length - 1); if (arguments.length > 1) { for (var i = 1; i < arguments.length; i++) { args[i - 1] = arguments[i]; } } queue.push(new Item(fun, args)); if (queue.length === 1 && !draining) { setTimeout(drainQueue, 0); } }; // v8 likes predictible objects function Item(fun, array) { this.fun = fun; this.array = array; } Item.prototype.run = function () { this.fun.apply(null, this.array); }; process.title = 'browser'; process.browser = true; process.env = {}; process.argv = []; process.version = ''; // empty string to avoid regexp issues process.versions = {}; function noop() {} process.on = noop; process.addListener = noop; process.once = noop; process.off = noop; process.removeListener = noop; process.removeAllListeners = noop; process.emit = noop; 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'); }; process.umask = function() { return 0; }; },{}],12:[function(require,module,exports){ var reDelim = /[\.\:]/; /** # mbus If Node's EventEmitter and Eve were to have a child, it might look something like this. No wildcard support at this stage though... ## Example Usage <<< docs/usage.md ## Reference ### `mbus(namespace?, parent?, scope?)` Create a new message bus with `namespace` inheriting from the `parent` mbus instance. If events from this message bus should be triggered with a specific `this` scope, then specify it using the `scope` argument. **/ var createBus = module.exports = function(namespace, parent, scope) { var registry = {}; var feeds = []; function bus(name) { var args = [].slice.call(arguments, 1); var delimited = normalize(name); var handlers = registry[delimited] || []; var results; // send through the feeds feeds.forEach(function(feed) { feed({ name: delimited, args: args }); }); // run the registered handlers results = [].concat(handlers).map(function(handler) { return handler.apply(scope || this, args); }); // run the parent handlers if (bus.parent) { results = results.concat( bus.parent.apply( scope || this, [(namespace ? namespace + '.' : '') + delimited].concat(args) ) ); } return results; } /** ### `mbus#clear()` Reset the handler registry, which essential deregisters all event listeners. _Alias:_ `removeAllListeners` **/ function clear(name) { // if we have a name, reset handlers for that handler if (name) { delete registry[normalize(name)]; } // otherwise, reset the entire handler registry else { registry = {}; } } /** ### `mbus#feed(handler)` Attach a handler function that will see all events that are sent through this bus in an "object stream" format that matches the following format: ``` { name: 'event.name', args: [ 'event', 'args' ] } ``` The feed function returns a function that can be called to stop the feed sending data. **/ function feed(handler) { function stop() { feeds.splice(feeds.indexOf(handler), 1); } feeds.push(handler); return stop; } function normalize(name) { return (Array.isArray(name) ? name : name.split(reDelim)).join('.'); } /** ### `mbus#off(name, handler)` Deregister an event handler. **/ function off(name, handler) { var handlers = registry[normalize(name)] || []; var idx = handlers ? handlers.indexOf(handler._actual || handler) : -1; if (idx >= 0) { handlers.splice(idx, 1); } } /** ### `mbus#on(name, handler)` Register an event handler for the event `name`. **/ function on(name, handler) { var handlers; name = normalize(name); handlers = registry[name]; if (handlers) { handlers.push(handler); } else { registry[name] = [ handler ]; } return bus; } /** ### `mbus#once(name, handler)` Register an event handler for the event `name` that will only trigger once (i.e. the handler will be deregistered immediately after being triggered the first time). **/ function once(name, handler) { function handleEvent() { var result = handler.apply(this, arguments); bus.off(name, handleEvent); return result; } handler._actual = handleEvent; return on(name, handleEvent); } if (typeof namespace == 'function') { parent = namespace; namespace = ''; } namespace = normalize(namespace || ''); bus.clear = bus.removeAllListeners = clear; bus.feed = feed; bus.on = bus.addListener = on; bus.once = once; bus.off = bus.removeListener = off; bus.parent = parent || (namespace && createBus()); return bus; }; },{}],13:[function(require,module,exports){ /* jshint node: true */ /* global window: false */ /* global navigator: false */ 'use strict'; var browser = require('detect-browser'); /** ### `rtc-core/detect` A browser detection helper for accessing prefix-free versions of the various WebRTC types. ### Example Usage If you wanted to get the native `RTCPeerConnection` prototype in any browser you could do the following: ```js var detect = require('rtc-core/detect'); // also available in rtc/detect var RTCPeerConnection = detect('RTCPeerConnection'); ``` This would provide whatever the browser prefixed version of the RTCPeerConnection is available (`webkitRTCPeerConnection`, `mozRTCPeerConnection`, etc). **/ var detect = module.exports = function(target, opts) { var attach = (opts || {}).attach; var prefixIdx; var prefix; var testName; var hostObject = this || (typeof window != 'undefined' ? window : undefined); // initialise to default prefixes // (reverse order as we use a decrementing for loop) var prefixes = ((opts || {}).prefixes || ['ms', 'o', 'moz', 'webkit']).concat(''); // if we have no host object, then abort if (! hostObject) { return; } // iterate through the prefixes and return the class if found in global for (prefixIdx = prefixes.length; prefixIdx--; ) { prefix = prefixes[prefixIdx]; // construct the test class name // if we have a prefix ensure the target has an uppercase first character // such that a test for getUserMedia would result in a // search for webkitGetUserMedia testName = prefix + (prefix ? target.charAt(0).toUpperCase() + target.slice(1) : target); if (typeof hostObject[testName] != 'undefined') { // update the last used prefix detect.browser = detect.browser || prefix.toLowerCase(); if (attach) { hostObject[target] = hostObject[testName]; } return hostObject[testName]; } } }; // detect mozilla (yes, this feels dirty) detect.moz = typeof navigator != 'undefined' && !!navigator.mozGetUserMedia; // set the browser and browser version detect.browser = browser.name; detect.browserVersion = detect.version = browser.version; },{"detect-browser":15}],14:[function(require,module,exports){ /** ### `rtc-core/genice` Respond appropriately to options that are passed to packages like `rtc-quickconnect` and trigger a `callback` (error first) with iceServer values. The function looks for either of the following keys in the options, in the following order or precedence: 1. `ice` - this can either be an array of ice server values or a generator function (in the same format as this function). If this key contains a value then any servers specified in the `iceServers` key (2) will be ignored. 2. `iceServers` - an array of ice server values. **/ module.exports = function(opts, callback) { var ice = (opts || {}).ice; var iceServers = (opts || {}).iceServers; if (typeof ice == 'function') { return ice(opts, callback); } else if (Array.isArray(ice)) { return callback(null, [].concat(ice)); } callback(null, [].concat(iceServers || [])); }; },{}],15:[function(require,module,exports){ var browsers = [ [ 'chrome', /Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/ ], [ 'firefox', /Firefox\/([0-9\.]+)(?:\s|$)/ ], [ 'opera', /Opera\/([0-9\.]+)(?:\s|$)/ ], [ 'ie', /Trident\/7\.0.*rv\:([0-9\.]+)\).*Gecko$/ ], [ 'ie', /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/ ], [ 'ie', /MSIE\s(7\.0)/ ], [ 'bb10', /BB10;\sTouch.*Version\/([0-9\.]+)/ ], [ 'android', /Android\s([0-9\.]+)/ ], [ 'ios', /iPad\;\sCPU\sOS\s([0-9\._]+)/ ], [ 'ios', /iPhone\;\sCPU\siPhone\sOS\s([0-9\._]+)/ ], [ 'safari', /Safari\/([0-9\._]+)/ ] ]; var match = browsers.map(match).filter(isMatch)[0]; var parts = match && match[3].split(/[._]/).slice(0,3); while (parts && parts.length < 3) { parts.push('0'); } // set the name and version exports.name = match && match[0]; exports.version = parts && parts.join('.'); function match(pair) { return pair.concat(pair[1].exec(navigator.userAgent)); } function isMatch(pair) { return !!pair[2]; } },{}],16:[function(require,module,exports){ var detect = require('./detect'); var requiredFunctions = [ 'init' ]; function isSupported(plugin) { return plugin && typeof plugin.supported == 'function' && plugin.supported(detect); } function isValid(plugin) { var supportedFunctions = requiredFunctions.filter(function(fn) { return typeof plugin[fn] == 'function'; }); return supportedFunctions.length === requiredFunctions.length; } module.exports = function(plugins) { return [].concat(plugins || []).filter(isSupported).filter(isValid)[0]; } },{"./detect":13}],17:[function(require,module,exports){ /** # rtc-pluggable-signaller By using `rtc-pluggable-signaller` in your code, you provide the ability for your package to customize which signalling client it uses (and thus have significant control) over how signalling operates in your environment. ## How it Works The pluggable signaller looks in the provided `opts` for a `signaller` attribute. If the value of this attribute is a string, then it is assumed that you wish to use the default [`rtc-signaller`](https://github.com/rtc-io/rtc-signaller) in your package. If, however, it is not a string value then it will be passed straight back as the signaller (assuming that you have provided an object that is compliant with the rtc.io signalling API). **/ module.exports = function(opts) { var signaller = (opts || {}).signaller; var messenger = (opts || {}).messenger || require('rtc-switchboard-messenger'); if (typeof signaller == 'string' || (signaller instanceof String)) { return require('rtc-signaller')(messenger(signaller, opts), opts); } return signaller; }; },{"rtc-signaller":18,"rtc-switchboard-messenger":37}],18:[function(require,module,exports){ /* jshint node: true */ 'use strict'; var detect = require('rtc-core/detect'); var extend = require('cog/extend'); var mbus = require('mbus'); var getable = require('cog/getable'); var uuid = require('cuid'); var pull = require('pull-stream'); var pushable = require('pull-pushable'); var prepare = require('rtc-signal/prepare'); var createQueue = require('pull-pushable'); // ready state constants var RS_DISCONNECTED = 0; var RS_CONNECTING = 1; var RS_CONNECTED = 2; // initialise signaller metadata so we don't have to include the package.json // TODO: make this checkable with some kind of prepublish script var metadata = { version: '6.2.1' }; /** # rtc-signaller The `rtc-signaller` module provides a transportless signalling mechanism for WebRTC. ## Purpose <<< docs/purpose.md ## Getting Started While the signaller is capable of communicating by a number of different messengers (i.e. anything that can send and receive messages over a wire) it comes with support for understanding how to connect to an [rtc-switchboard](https://github.com/rtc-io/rtc-switchboard) out of the box. The following code sample demonstrates how: <<< examples/getting-started.js <<< docs/events.md <<< docs/signalflow-diagrams.md <<< docs/identifying-participants.md ## Reference The `rtc-signaller` module is designed to be used primarily in a functional way and when called it creates a new signaller that will enable you to communicate with other peers via your messaging network. ```js // create a signaller from something that knows how to send messages var signaller = require('rtc-signaller')(messenger); ``` As demonstrated in the getting started guide, you can also pass through a string value instead of a messenger instance if you simply want to connect to an existing `rtc-switchboard` instance. **/ module.exports = function(messenger, opts) { var autoconnect = (opts || {}).autoconnect; var reconnect = (opts || {}).reconnect; var queue = createQueue(); var connectionCount = 0; // create the signaller var signaller = require('rtc-signal/signaller')(opts, bufferMessage); var announced = false; var announceTimer = 0; var readyState = RS_DISCONNECTED; function bufferMessage(message) { queue.push(message); // if we are not connected (and should autoconnect), then attempt connection if (readyState === RS_DISCONNECTED && (autoconnect === undefined || autoconnect)) { connect(); } } function handleDisconnect() { if (reconnect === undefined || reconnect) { setTimeout(connect, 50); } } /** ### `signaller.connect()` Manually connect the signaller using the supplied messenger. __NOTE:__ This should never have to be called if the default setting for `autoconnect` is used. **/ var connect = signaller.connect = function() { // if we are already connecting then do nothing if (readyState === RS_CONNECTING) { return; } // initiate the messenger readyState = RS_CONNECTING; messenger(function(err, source, sink) { if (err) { readyState = RS_DISCONNECTED; return signaller('error', err); } // increment the connection count connectionCount += 1; // flag as connected readyState = RS_CONNECTED; // pass messages to the processor pull( source, // monitor disconnection pull.through(null, function() { queue = createQueue(); readyState = RS_DISCONNECTED; signaller('disconnected'); }), pull.drain(signaller._process) ); // pass the queue to the sink pull(queue, sink); // handle disconnection signaller.removeListener('disconnected', handleDisconnect); signaller.on('disconnected', handleDisconnect); // trigger the connected event signaller('connected'); // if this is a reconnection, then reannounce if (announced && connectionCount > 1) { signaller._announce(); } }); }; /** ### announce(data?) The `announce` function of the signaller will pass an `/announce` message through the messenger network. When no additional data is supplied to this function then only the id of the signaller is sent to all active members of the messenging network. #### Joining Rooms To join a room using an announce call you simply provide the name of the room you wish to join as part of the data block that you annouce, for example: ```js signaller.announce({ room: 'testroom' }); ``` Signalling servers (such as [rtc-switchboard](https://github.com/rtc-io/rtc-switchboard)) will then place your peer connection into a room with other peers that have also announced in this room. Once you have joined a room, the server will only deliver messages that you `send` to other peers within that room. #### Providing Additional Announce Data There may be instances where you wish to send additional data as part of your announce message in your applica