You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
18562 lines
510 KiB
18562 lines
510 KiB
(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.SimpleWebRTC = 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){
|
|
module.exports = after
|
|
|
|
function after(count, callback, err_cb) {
|
|
var bail = false
|
|
err_cb = err_cb || noop
|
|
proxy.count = count
|
|
|
|
return (count === 0) ? callback() : proxy
|
|
|
|
function proxy(err, result) {
|
|
if (proxy.count <= 0) {
|
|
throw new Error('after called too many times')
|
|
}
|
|
--proxy.count
|
|
|
|
// after first error, rest are passed to err_cb
|
|
if (err) {
|
|
bail = true
|
|
callback(err)
|
|
// future error callbacks will go to error handler
|
|
callback = err_cb
|
|
} else if (proxy.count === 0 && !bail) {
|
|
callback(null, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
function noop() {}
|
|
|
|
},{}],2:[function(require,module,exports){
|
|
/**
|
|
* An abstraction for slicing an arraybuffer even when
|
|
* ArrayBuffer.prototype.slice is not supported
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
module.exports = function(arraybuffer, start, end) {
|
|
var bytes = arraybuffer.byteLength;
|
|
start = start || 0;
|
|
end = end || bytes;
|
|
|
|
if (arraybuffer.slice) { return arraybuffer.slice(start, end); }
|
|
|
|
if (start < 0) { start += bytes; }
|
|
if (end < 0) { end += bytes; }
|
|
if (end > bytes) { end = bytes; }
|
|
|
|
if (start >= bytes || start >= end || bytes === 0) {
|
|
return new ArrayBuffer(0);
|
|
}
|
|
|
|
var abv = new Uint8Array(arraybuffer);
|
|
var result = new Uint8Array(end - start);
|
|
for (var i = start, ii = 0; i < end; i++, ii++) {
|
|
result[ii] = abv[i];
|
|
}
|
|
return result.buffer;
|
|
};
|
|
|
|
},{}],3:[function(require,module,exports){
|
|
var adapter = require('webrtc-adapter');
|
|
module.exports = function (stream, el, options) {
|
|
var item;
|
|
var URL = window.URL;
|
|
var element = el;
|
|
var opts = {
|
|
autoplay: true,
|
|
mirror: false,
|
|
muted: false,
|
|
audio: false,
|
|
disableContextMenu: false
|
|
};
|
|
|
|
if (options) {
|
|
for (item in options) {
|
|
opts[item] = options[item];
|
|
}
|
|
}
|
|
|
|
if (!element) {
|
|
element = document.createElement(opts.audio ? 'audio' : 'video');
|
|
} else if (element.tagName.toLowerCase() === 'audio') {
|
|
opts.audio = true;
|
|
}
|
|
|
|
if (opts.disableContextMenu) {
|
|
element.oncontextmenu = function (e) {
|
|
e.preventDefault();
|
|
};
|
|
}
|
|
|
|
if (opts.autoplay) element.autoplay = 'autoplay';
|
|
if (opts.muted) element.muted = true;
|
|
if (!opts.audio && opts.mirror) {
|
|
['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) {
|
|
var styleName = prefix ? prefix + 'Transform' : 'transform';
|
|
element.style[styleName] = 'scaleX(-1)';
|
|
});
|
|
}
|
|
|
|
element.srcObject = stream;
|
|
return element;
|
|
};
|
|
|
|
},{"webrtc-adapter":90}],4:[function(require,module,exports){
|
|
|
|
/**
|
|
* Expose `Backoff`.
|
|
*/
|
|
|
|
module.exports = Backoff;
|
|
|
|
/**
|
|
* Initialize backoff timer with `opts`.
|
|
*
|
|
* - `min` initial timeout in milliseconds [100]
|
|
* - `max` max timeout [10000]
|
|
* - `jitter` [0]
|
|
* - `factor` [2]
|
|
*
|
|
* @param {Object} opts
|
|
* @api public
|
|
*/
|
|
|
|
function Backoff(opts) {
|
|
opts = opts || {};
|
|
this.ms = opts.min || 100;
|
|
this.max = opts.max || 10000;
|
|
this.factor = opts.factor || 2;
|
|
this.jitter = opts.jitter > 0 && opts.jitter <= 1 ? opts.jitter : 0;
|
|
this.attempts = 0;
|
|
}
|
|
|
|
/**
|
|
* Return the backoff duration.
|
|
*
|
|
* @return {Number}
|
|
* @api public
|
|
*/
|
|
|
|
Backoff.prototype.duration = function(){
|
|
var ms = this.ms * Math.pow(this.factor, this.attempts++);
|
|
if (this.jitter) {
|
|
var rand = Math.random();
|
|
var deviation = Math.floor(rand * this.jitter * ms);
|
|
ms = (Math.floor(rand * 10) & 1) == 0 ? ms - deviation : ms + deviation;
|
|
}
|
|
return Math.min(ms, this.max) | 0;
|
|
};
|
|
|
|
/**
|
|
* Reset the number of attempts.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Backoff.prototype.reset = function(){
|
|
this.attempts = 0;
|
|
};
|
|
|
|
/**
|
|
* Set the minimum duration
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Backoff.prototype.setMin = function(min){
|
|
this.ms = min;
|
|
};
|
|
|
|
/**
|
|
* Set the maximum duration
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Backoff.prototype.setMax = function(max){
|
|
this.max = max;
|
|
};
|
|
|
|
/**
|
|
* Set the jitter
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Backoff.prototype.setJitter = function(jitter){
|
|
this.jitter = jitter;
|
|
};
|
|
|
|
|
|
},{}],5:[function(require,module,exports){
|
|
/*
|
|
* base64-arraybuffer
|
|
* https://github.com/niklasvh/base64-arraybuffer
|
|
*
|
|
* Copyright (c) 2012 Niklas von Hertzen
|
|
* Licensed under the MIT license.
|
|
*/
|
|
(function(chars){
|
|
"use strict";
|
|
|
|
exports.encode = function(arraybuffer) {
|
|
var bytes = new Uint8Array(arraybuffer),
|
|
i, len = bytes.length, base64 = "";
|
|
|
|
for (i = 0; i < len; i+=3) {
|
|
base64 += chars[bytes[i] >> 2];
|
|
base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
|
|
base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
|
|
base64 += chars[bytes[i + 2] & 63];
|
|
}
|
|
|
|
if ((len % 3) === 2) {
|
|
base64 = base64.substring(0, base64.length - 1) + "=";
|
|
} else if (len % 3 === 1) {
|
|
base64 = base64.substring(0, base64.length - 2) + "==";
|
|
}
|
|
|
|
return base64;
|
|
};
|
|
|
|
exports.decode = function(base64) {
|
|
var bufferLength = base64.length * 0.75,
|
|
len = base64.length, i, p = 0,
|
|
encoded1, encoded2, encoded3, encoded4;
|
|
|
|
if (base64[base64.length - 1] === "=") {
|
|
bufferLength--;
|
|
if (base64[base64.length - 2] === "=") {
|
|
bufferLength--;
|
|
}
|
|
}
|
|
|
|
var arraybuffer = new ArrayBuffer(bufferLength),
|
|
bytes = new Uint8Array(arraybuffer);
|
|
|
|
for (i = 0; i < len; i+=4) {
|
|
encoded1 = chars.indexOf(base64[i]);
|
|
encoded2 = chars.indexOf(base64[i+1]);
|
|
encoded3 = chars.indexOf(base64[i+2]);
|
|
encoded4 = chars.indexOf(base64[i+3]);
|
|
|
|
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
|
|
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
|
|
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
|
|
}
|
|
|
|
return arraybuffer;
|
|
};
|
|
})("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/");
|
|
|
|
},{}],6:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* Create a blob builder even when vendor prefixes exist
|
|
*/
|
|
|
|
var BlobBuilder = global.BlobBuilder
|
|
|| global.WebKitBlobBuilder
|
|
|| global.MSBlobBuilder
|
|
|| global.MozBlobBuilder;
|
|
|
|
/**
|
|
* Check if Blob constructor is supported
|
|
*/
|
|
|
|
var blobSupported = (function() {
|
|
try {
|
|
var a = new Blob(['hi']);
|
|
return a.size === 2;
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* Check if Blob constructor supports ArrayBufferViews
|
|
* Fails in Safari 6, so we need to map to ArrayBuffers there.
|
|
*/
|
|
|
|
var blobSupportsArrayBufferView = blobSupported && (function() {
|
|
try {
|
|
var b = new Blob([new Uint8Array([1,2])]);
|
|
return b.size === 2;
|
|
} catch(e) {
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
/**
|
|
* Check if BlobBuilder is supported
|
|
*/
|
|
|
|
var blobBuilderSupported = BlobBuilder
|
|
&& BlobBuilder.prototype.append
|
|
&& BlobBuilder.prototype.getBlob;
|
|
|
|
/**
|
|
* Helper function that maps ArrayBufferViews to ArrayBuffers
|
|
* Used by BlobBuilder constructor and old browsers that didn't
|
|
* support it in the Blob constructor.
|
|
*/
|
|
|
|
function mapArrayBufferViews(ary) {
|
|
for (var i = 0; i < ary.length; i++) {
|
|
var chunk = ary[i];
|
|
if (chunk.buffer instanceof ArrayBuffer) {
|
|
var buf = chunk.buffer;
|
|
|
|
// if this is a subarray, make a copy so we only
|
|
// include the subarray region from the underlying buffer
|
|
if (chunk.byteLength !== buf.byteLength) {
|
|
var copy = new Uint8Array(chunk.byteLength);
|
|
copy.set(new Uint8Array(buf, chunk.byteOffset, chunk.byteLength));
|
|
buf = copy.buffer;
|
|
}
|
|
|
|
ary[i] = buf;
|
|
}
|
|
}
|
|
}
|
|
|
|
function BlobBuilderConstructor(ary, options) {
|
|
options = options || {};
|
|
|
|
var bb = new BlobBuilder();
|
|
mapArrayBufferViews(ary);
|
|
|
|
for (var i = 0; i < ary.length; i++) {
|
|
bb.append(ary[i]);
|
|
}
|
|
|
|
return (options.type) ? bb.getBlob(options.type) : bb.getBlob();
|
|
};
|
|
|
|
function BlobConstructor(ary, options) {
|
|
mapArrayBufferViews(ary);
|
|
return new Blob(ary, options || {});
|
|
};
|
|
|
|
module.exports = (function() {
|
|
if (blobSupported) {
|
|
return blobSupportsArrayBufferView ? global.Blob : BlobConstructor;
|
|
} else if (blobBuilderSupported) {
|
|
return BlobBuilderConstructor;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
})();
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{}],7:[function(require,module,exports){
|
|
/**
|
|
* Slice reference.
|
|
*/
|
|
|
|
var slice = [].slice;
|
|
|
|
/**
|
|
* Bind `obj` to `fn`.
|
|
*
|
|
* @param {Object} obj
|
|
* @param {Function|String} fn or string
|
|
* @return {Function}
|
|
* @api public
|
|
*/
|
|
|
|
module.exports = function(obj, fn){
|
|
if ('string' == typeof fn) fn = obj[fn];
|
|
if ('function' != typeof fn) throw new Error('bind() requires a function');
|
|
var args = slice.call(arguments, 2);
|
|
return function(){
|
|
return fn.apply(obj, args.concat(slice.call(arguments)));
|
|
}
|
|
};
|
|
|
|
},{}],8:[function(require,module,exports){
|
|
|
|
/**
|
|
* Expose `Emitter`.
|
|
*/
|
|
|
|
module.exports = Emitter;
|
|
|
|
/**
|
|
* Initialize a new `Emitter`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function Emitter(obj) {
|
|
if (obj) return mixin(obj);
|
|
};
|
|
|
|
/**
|
|
* Mixin the emitter properties.
|
|
*
|
|
* @param {Object} obj
|
|
* @return {Object}
|
|
* @api private
|
|
*/
|
|
|
|
function mixin(obj) {
|
|
for (var key in Emitter.prototype) {
|
|
obj[key] = Emitter.prototype[key];
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Listen on the given `event` with `fn`.
|
|
*
|
|
* @param {String} event
|
|
* @param {Function} fn
|
|
* @return {Emitter}
|
|
* @api public
|
|
*/
|
|
|
|
Emitter.prototype.on =
|
|
Emitter.prototype.addEventListener = function(event, fn){
|
|
this._callbacks = this._callbacks || {};
|
|
(this._callbacks[event] = this._callbacks[event] || [])
|
|
.push(fn);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Adds an `event` listener that will be invoked a single
|
|
* time then automatically removed.
|
|
*
|
|
* @param {String} event
|
|
* @param {Function} fn
|
|
* @return {Emitter}
|
|
* @api public
|
|
*/
|
|
|
|
Emitter.prototype.once = function(event, fn){
|
|
var self = this;
|
|
this._callbacks = this._callbacks || {};
|
|
|
|
function on() {
|
|
self.off(event, on);
|
|
fn.apply(this, arguments);
|
|
}
|
|
|
|
on.fn = fn;
|
|
this.on(event, on);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Remove the given callback for `event` or all
|
|
* registered callbacks.
|
|
*
|
|
* @param {String} event
|
|
* @param {Function} fn
|
|
* @return {Emitter}
|
|
* @api public
|
|
*/
|
|
|
|
Emitter.prototype.off =
|
|
Emitter.prototype.removeListener =
|
|
Emitter.prototype.removeAllListeners =
|
|
Emitter.prototype.removeEventListener = function(event, fn){
|
|
this._callbacks = this._callbacks || {};
|
|
|
|
// all
|
|
if (0 == arguments.length) {
|
|
this._callbacks = {};
|
|
return this;
|
|
}
|
|
|
|
// specific event
|
|
var callbacks = this._callbacks[event];
|
|
if (!callbacks) return this;
|
|
|
|
// remove all handlers
|
|
if (1 == arguments.length) {
|
|
delete this._callbacks[event];
|
|
return this;
|
|
}
|
|
|
|
// remove specific handler
|
|
var cb;
|
|
for (var i = 0; i < callbacks.length; i++) {
|
|
cb = callbacks[i];
|
|
if (cb === fn || cb.fn === fn) {
|
|
callbacks.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Emit `event` with the given args.
|
|
*
|
|
* @param {String} event
|
|
* @param {Mixed} ...
|
|
* @return {Emitter}
|
|
*/
|
|
|
|
Emitter.prototype.emit = function(event){
|
|
this._callbacks = this._callbacks || {};
|
|
var args = [].slice.call(arguments, 1)
|
|
, callbacks = this._callbacks[event];
|
|
|
|
if (callbacks) {
|
|
callbacks = callbacks.slice(0);
|
|
for (var i = 0, len = callbacks.length; i < len; ++i) {
|
|
callbacks[i].apply(this, args);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Return array of callbacks for `event`.
|
|
*
|
|
* @param {String} event
|
|
* @return {Array}
|
|
* @api public
|
|
*/
|
|
|
|
Emitter.prototype.listeners = function(event){
|
|
this._callbacks = this._callbacks || {};
|
|
return this._callbacks[event] || [];
|
|
};
|
|
|
|
/**
|
|
* Check if this emitter has `event` handlers.
|
|
*
|
|
* @param {String} event
|
|
* @return {Boolean}
|
|
* @api public
|
|
*/
|
|
|
|
Emitter.prototype.hasListeners = function(event){
|
|
return !! this.listeners(event).length;
|
|
};
|
|
|
|
},{}],9:[function(require,module,exports){
|
|
|
|
module.exports = function(a, b){
|
|
var fn = function(){};
|
|
fn.prototype = b.prototype;
|
|
a.prototype = new fn;
|
|
a.prototype.constructor = a;
|
|
};
|
|
},{}],10:[function(require,module,exports){
|
|
|
|
module.exports = require('./lib/');
|
|
|
|
},{"./lib/":11}],11:[function(require,module,exports){
|
|
|
|
module.exports = require('./socket');
|
|
|
|
/**
|
|
* Exports parser
|
|
*
|
|
* @api public
|
|
*
|
|
*/
|
|
module.exports.parser = require('engine.io-parser');
|
|
|
|
},{"./socket":12,"engine.io-parser":24}],12:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var transports = require('./transports');
|
|
var Emitter = require('component-emitter');
|
|
var debug = require('debug')('engine.io-client:socket');
|
|
var index = require('indexof');
|
|
var parser = require('engine.io-parser');
|
|
var parseuri = require('parseuri');
|
|
var parsejson = require('parsejson');
|
|
var parseqs = require('parseqs');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = Socket;
|
|
|
|
/**
|
|
* Noop function.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
function noop(){}
|
|
|
|
/**
|
|
* Socket constructor.
|
|
*
|
|
* @param {String|Object} uri or options
|
|
* @param {Object} options
|
|
* @api public
|
|
*/
|
|
|
|
function Socket(uri, opts){
|
|
if (!(this instanceof Socket)) return new Socket(uri, opts);
|
|
|
|
opts = opts || {};
|
|
|
|
if (uri && 'object' == typeof uri) {
|
|
opts = uri;
|
|
uri = null;
|
|
}
|
|
|
|
if (uri) {
|
|
uri = parseuri(uri);
|
|
opts.host = uri.host;
|
|
opts.secure = uri.protocol == 'https' || uri.protocol == 'wss';
|
|
opts.port = uri.port;
|
|
if (uri.query) opts.query = uri.query;
|
|
}
|
|
|
|
this.secure = null != opts.secure ? opts.secure :
|
|
(global.location && 'https:' == location.protocol);
|
|
|
|
if (opts.host) {
|
|
var pieces = opts.host.split(':');
|
|
opts.hostname = pieces.shift();
|
|
if (pieces.length) {
|
|
opts.port = pieces.pop();
|
|
} else if (!opts.port) {
|
|
// if no port is specified manually, use the protocol default
|
|
opts.port = this.secure ? '443' : '80';
|
|
}
|
|
}
|
|
|
|
this.agent = opts.agent || false;
|
|
this.hostname = opts.hostname ||
|
|
(global.location ? location.hostname : 'localhost');
|
|
this.port = opts.port || (global.location && location.port ?
|
|
location.port :
|
|
(this.secure ? 443 : 80));
|
|
this.query = opts.query || {};
|
|
if ('string' == typeof this.query) this.query = parseqs.decode(this.query);
|
|
this.upgrade = false !== opts.upgrade;
|
|
this.path = (opts.path || '/engine.io').replace(/\/$/, '') + '/';
|
|
this.forceJSONP = !!opts.forceJSONP;
|
|
this.jsonp = false !== opts.jsonp;
|
|
this.forceBase64 = !!opts.forceBase64;
|
|
this.enablesXDR = !!opts.enablesXDR;
|
|
this.timestampParam = opts.timestampParam || 't';
|
|
this.timestampRequests = opts.timestampRequests;
|
|
this.transports = opts.transports || ['polling', 'websocket'];
|
|
this.readyState = '';
|
|
this.writeBuffer = [];
|
|
this.callbackBuffer = [];
|
|
this.policyPort = opts.policyPort || 843;
|
|
this.rememberUpgrade = opts.rememberUpgrade || false;
|
|
this.binaryType = null;
|
|
this.onlyBinaryUpgrades = opts.onlyBinaryUpgrades;
|
|
|
|
// SSL options for Node.js client
|
|
this.pfx = opts.pfx || null;
|
|
this.key = opts.key || null;
|
|
this.passphrase = opts.passphrase || null;
|
|
this.cert = opts.cert || null;
|
|
this.ca = opts.ca || null;
|
|
this.ciphers = opts.ciphers || null;
|
|
this.rejectUnauthorized = opts.rejectUnauthorized || null;
|
|
|
|
this.open();
|
|
}
|
|
|
|
Socket.priorWebsocketSuccess = false;
|
|
|
|
/**
|
|
* Mix in `Emitter`.
|
|
*/
|
|
|
|
Emitter(Socket.prototype);
|
|
|
|
/**
|
|
* Protocol version.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Socket.protocol = parser.protocol; // this is an int
|
|
|
|
/**
|
|
* Expose deps for legacy compatibility
|
|
* and standalone browser access.
|
|
*/
|
|
|
|
Socket.Socket = Socket;
|
|
Socket.Transport = require('./transport');
|
|
Socket.transports = require('./transports');
|
|
Socket.parser = require('engine.io-parser');
|
|
|
|
/**
|
|
* Creates transport of the given type.
|
|
*
|
|
* @param {String} transport name
|
|
* @return {Transport}
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.createTransport = function (name) {
|
|
debug('creating transport "%s"', name);
|
|
var query = clone(this.query);
|
|
|
|
// append engine.io protocol identifier
|
|
query.EIO = parser.protocol;
|
|
|
|
// transport name
|
|
query.transport = name;
|
|
|
|
// session id if we already have one
|
|
if (this.id) query.sid = this.id;
|
|
|
|
var transport = new transports[name]({
|
|
agent: this.agent,
|
|
hostname: this.hostname,
|
|
port: this.port,
|
|
secure: this.secure,
|
|
path: this.path,
|
|
query: query,
|
|
forceJSONP: this.forceJSONP,
|
|
jsonp: this.jsonp,
|
|
forceBase64: this.forceBase64,
|
|
enablesXDR: this.enablesXDR,
|
|
timestampRequests: this.timestampRequests,
|
|
timestampParam: this.timestampParam,
|
|
policyPort: this.policyPort,
|
|
socket: this,
|
|
pfx: this.pfx,
|
|
key: this.key,
|
|
passphrase: this.passphrase,
|
|
cert: this.cert,
|
|
ca: this.ca,
|
|
ciphers: this.ciphers,
|
|
rejectUnauthorized: this.rejectUnauthorized
|
|
});
|
|
|
|
return transport;
|
|
};
|
|
|
|
function clone (obj) {
|
|
var o = {};
|
|
for (var i in obj) {
|
|
if (obj.hasOwnProperty(i)) {
|
|
o[i] = obj[i];
|
|
}
|
|
}
|
|
return o;
|
|
}
|
|
|
|
/**
|
|
* Initializes transport to use and starts probe.
|
|
*
|
|
* @api private
|
|
*/
|
|
Socket.prototype.open = function () {
|
|
var transport;
|
|
if (this.rememberUpgrade && Socket.priorWebsocketSuccess && this.transports.indexOf('websocket') != -1) {
|
|
transport = 'websocket';
|
|
} else if (0 == this.transports.length) {
|
|
// Emit error on next tick so it can be listened to
|
|
var self = this;
|
|
setTimeout(function() {
|
|
self.emit('error', 'No transports available');
|
|
}, 0);
|
|
return;
|
|
} else {
|
|
transport = this.transports[0];
|
|
}
|
|
this.readyState = 'opening';
|
|
|
|
// Retry with the next transport if the transport is disabled (jsonp: false)
|
|
var transport;
|
|
try {
|
|
transport = this.createTransport(transport);
|
|
} catch (e) {
|
|
this.transports.shift();
|
|
this.open();
|
|
return;
|
|
}
|
|
|
|
transport.open();
|
|
this.setTransport(transport);
|
|
};
|
|
|
|
/**
|
|
* Sets the current transport. Disables the existing one (if any).
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.setTransport = function(transport){
|
|
debug('setting transport %s', transport.name);
|
|
var self = this;
|
|
|
|
if (this.transport) {
|
|
debug('clearing existing transport %s', this.transport.name);
|
|
this.transport.removeAllListeners();
|
|
}
|
|
|
|
// set up transport
|
|
this.transport = transport;
|
|
|
|
// set up transport listeners
|
|
transport
|
|
.on('drain', function(){
|
|
self.onDrain();
|
|
})
|
|
.on('packet', function(packet){
|
|
self.onPacket(packet);
|
|
})
|
|
.on('error', function(e){
|
|
self.onError(e);
|
|
})
|
|
.on('close', function(){
|
|
self.onClose('transport close');
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Probes a transport.
|
|
*
|
|
* @param {String} transport name
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.probe = function (name) {
|
|
debug('probing transport "%s"', name);
|
|
var transport = this.createTransport(name, { probe: 1 })
|
|
, failed = false
|
|
, self = this;
|
|
|
|
Socket.priorWebsocketSuccess = false;
|
|
|
|
function onTransportOpen(){
|
|
if (self.onlyBinaryUpgrades) {
|
|
var upgradeLosesBinary = !this.supportsBinary && self.transport.supportsBinary;
|
|
failed = failed || upgradeLosesBinary;
|
|
}
|
|
if (failed) return;
|
|
|
|
debug('probe transport "%s" opened', name);
|
|
transport.send([{ type: 'ping', data: 'probe' }]);
|
|
transport.once('packet', function (msg) {
|
|
if (failed) return;
|
|
if ('pong' == msg.type && 'probe' == msg.data) {
|
|
debug('probe transport "%s" pong', name);
|
|
self.upgrading = true;
|
|
self.emit('upgrading', transport);
|
|
if (!transport) return;
|
|
Socket.priorWebsocketSuccess = 'websocket' == transport.name;
|
|
|
|
debug('pausing current transport "%s"', self.transport.name);
|
|
self.transport.pause(function () {
|
|
if (failed) return;
|
|
if ('closed' == self.readyState) return;
|
|
debug('changing transport and sending upgrade packet');
|
|
|
|
cleanup();
|
|
|
|
self.setTransport(transport);
|
|
transport.send([{ type: 'upgrade' }]);
|
|
self.emit('upgrade', transport);
|
|
transport = null;
|
|
self.upgrading = false;
|
|
self.flush();
|
|
});
|
|
} else {
|
|
debug('probe transport "%s" failed', name);
|
|
var err = new Error('probe error');
|
|
err.transport = transport.name;
|
|
self.emit('upgradeError', err);
|
|
}
|
|
});
|
|
}
|
|
|
|
function freezeTransport() {
|
|
if (failed) return;
|
|
|
|
// Any callback called by transport should be ignored since now
|
|
failed = true;
|
|
|
|
cleanup();
|
|
|
|
transport.close();
|
|
transport = null;
|
|
}
|
|
|
|
//Handle any error that happens while probing
|
|
function onerror(err) {
|
|
var error = new Error('probe error: ' + err);
|
|
error.transport = transport.name;
|
|
|
|
freezeTransport();
|
|
|
|
debug('probe transport "%s" failed because of error: %s', name, err);
|
|
|
|
self.emit('upgradeError', error);
|
|
}
|
|
|
|
function onTransportClose(){
|
|
onerror("transport closed");
|
|
}
|
|
|
|
//When the socket is closed while we're probing
|
|
function onclose(){
|
|
onerror("socket closed");
|
|
}
|
|
|
|
//When the socket is upgraded while we're probing
|
|
function onupgrade(to){
|
|
if (transport && to.name != transport.name) {
|
|
debug('"%s" works - aborting "%s"', to.name, transport.name);
|
|
freezeTransport();
|
|
}
|
|
}
|
|
|
|
//Remove all listeners on the transport and on self
|
|
function cleanup(){
|
|
transport.removeListener('open', onTransportOpen);
|
|
transport.removeListener('error', onerror);
|
|
transport.removeListener('close', onTransportClose);
|
|
self.removeListener('close', onclose);
|
|
self.removeListener('upgrading', onupgrade);
|
|
}
|
|
|
|
transport.once('open', onTransportOpen);
|
|
transport.once('error', onerror);
|
|
transport.once('close', onTransportClose);
|
|
|
|
this.once('close', onclose);
|
|
this.once('upgrading', onupgrade);
|
|
|
|
transport.open();
|
|
|
|
};
|
|
|
|
/**
|
|
* Called when connection is deemed open.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.onOpen = function () {
|
|
debug('socket open');
|
|
this.readyState = 'open';
|
|
Socket.priorWebsocketSuccess = 'websocket' == this.transport.name;
|
|
this.emit('open');
|
|
this.flush();
|
|
|
|
// we check for `readyState` in case an `open`
|
|
// listener already closed the socket
|
|
if ('open' == this.readyState && this.upgrade && this.transport.pause) {
|
|
debug('starting upgrade probes');
|
|
for (var i = 0, l = this.upgrades.length; i < l; i++) {
|
|
this.probe(this.upgrades[i]);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles a packet.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onPacket = function (packet) {
|
|
if ('opening' == this.readyState || 'open' == this.readyState) {
|
|
debug('socket receive: type "%s", data "%s"', packet.type, packet.data);
|
|
|
|
this.emit('packet', packet);
|
|
|
|
// Socket is live - any packet counts
|
|
this.emit('heartbeat');
|
|
|
|
switch (packet.type) {
|
|
case 'open':
|
|
this.onHandshake(parsejson(packet.data));
|
|
break;
|
|
|
|
case 'pong':
|
|
this.setPing();
|
|
break;
|
|
|
|
case 'error':
|
|
var err = new Error('server error');
|
|
err.code = packet.data;
|
|
this.emit('error', err);
|
|
break;
|
|
|
|
case 'message':
|
|
this.emit('data', packet.data);
|
|
this.emit('message', packet.data);
|
|
break;
|
|
}
|
|
} else {
|
|
debug('packet received with socket readyState "%s"', this.readyState);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon handshake completion.
|
|
*
|
|
* @param {Object} handshake obj
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onHandshake = function (data) {
|
|
this.emit('handshake', data);
|
|
this.id = data.sid;
|
|
this.transport.query.sid = data.sid;
|
|
this.upgrades = this.filterUpgrades(data.upgrades);
|
|
this.pingInterval = data.pingInterval;
|
|
this.pingTimeout = data.pingTimeout;
|
|
this.onOpen();
|
|
// In case open handler closes socket
|
|
if ('closed' == this.readyState) return;
|
|
this.setPing();
|
|
|
|
// Prolong liveness of socket on heartbeat
|
|
this.removeListener('heartbeat', this.onHeartbeat);
|
|
this.on('heartbeat', this.onHeartbeat);
|
|
};
|
|
|
|
/**
|
|
* Resets ping timeout.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onHeartbeat = function (timeout) {
|
|
clearTimeout(this.pingTimeoutTimer);
|
|
var self = this;
|
|
self.pingTimeoutTimer = setTimeout(function () {
|
|
if ('closed' == self.readyState) return;
|
|
self.onClose('ping timeout');
|
|
}, timeout || (self.pingInterval + self.pingTimeout));
|
|
};
|
|
|
|
/**
|
|
* Pings server every `this.pingInterval` and expects response
|
|
* within `this.pingTimeout` or closes connection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.setPing = function () {
|
|
var self = this;
|
|
clearTimeout(self.pingIntervalTimer);
|
|
self.pingIntervalTimer = setTimeout(function () {
|
|
debug('writing ping packet - expecting pong within %sms', self.pingTimeout);
|
|
self.ping();
|
|
self.onHeartbeat(self.pingTimeout);
|
|
}, self.pingInterval);
|
|
};
|
|
|
|
/**
|
|
* Sends a ping packet.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.ping = function () {
|
|
this.sendPacket('ping');
|
|
};
|
|
|
|
/**
|
|
* Called on `drain` event
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onDrain = function() {
|
|
for (var i = 0; i < this.prevBufferLen; i++) {
|
|
if (this.callbackBuffer[i]) {
|
|
this.callbackBuffer[i]();
|
|
}
|
|
}
|
|
|
|
this.writeBuffer.splice(0, this.prevBufferLen);
|
|
this.callbackBuffer.splice(0, this.prevBufferLen);
|
|
|
|
// setting prevBufferLen = 0 is very important
|
|
// for example, when upgrading, upgrade packet is sent over,
|
|
// and a nonzero prevBufferLen could cause problems on `drain`
|
|
this.prevBufferLen = 0;
|
|
|
|
if (this.writeBuffer.length == 0) {
|
|
this.emit('drain');
|
|
} else {
|
|
this.flush();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Flush write buffers.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.flush = function () {
|
|
if ('closed' != this.readyState && this.transport.writable &&
|
|
!this.upgrading && this.writeBuffer.length) {
|
|
debug('flushing %d packets in socket', this.writeBuffer.length);
|
|
this.transport.send(this.writeBuffer);
|
|
// keep track of current length of writeBuffer
|
|
// splice writeBuffer and callbackBuffer on `drain`
|
|
this.prevBufferLen = this.writeBuffer.length;
|
|
this.emit('flush');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sends a message.
|
|
*
|
|
* @param {String} message.
|
|
* @param {Function} callback function.
|
|
* @return {Socket} for chaining.
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.write =
|
|
Socket.prototype.send = function (msg, fn) {
|
|
this.sendPacket('message', msg, fn);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sends a packet.
|
|
*
|
|
* @param {String} packet type.
|
|
* @param {String} data.
|
|
* @param {Function} callback function.
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.sendPacket = function (type, data, fn) {
|
|
if ('closing' == this.readyState || 'closed' == this.readyState) {
|
|
return;
|
|
}
|
|
|
|
var packet = { type: type, data: data };
|
|
this.emit('packetCreate', packet);
|
|
this.writeBuffer.push(packet);
|
|
this.callbackBuffer.push(fn);
|
|
this.flush();
|
|
};
|
|
|
|
/**
|
|
* Closes the connection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.close = function () {
|
|
if ('opening' == this.readyState || 'open' == this.readyState) {
|
|
this.readyState = 'closing';
|
|
|
|
var self = this;
|
|
|
|
function close() {
|
|
self.onClose('forced close');
|
|
debug('socket closing - telling transport to close');
|
|
self.transport.close();
|
|
}
|
|
|
|
function cleanupAndClose() {
|
|
self.removeListener('upgrade', cleanupAndClose);
|
|
self.removeListener('upgradeError', cleanupAndClose);
|
|
close();
|
|
}
|
|
|
|
function waitForUpgrade() {
|
|
// wait for upgrade to finish since we can't send packets while pausing a transport
|
|
self.once('upgrade', cleanupAndClose);
|
|
self.once('upgradeError', cleanupAndClose);
|
|
}
|
|
|
|
if (this.writeBuffer.length) {
|
|
this.once('drain', function() {
|
|
if (this.upgrading) {
|
|
waitForUpgrade();
|
|
} else {
|
|
close();
|
|
}
|
|
});
|
|
} else if (this.upgrading) {
|
|
waitForUpgrade();
|
|
} else {
|
|
close();
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Called upon transport error
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onError = function (err) {
|
|
debug('socket error %j', err);
|
|
Socket.priorWebsocketSuccess = false;
|
|
this.emit('error', err);
|
|
this.onClose('transport error', err);
|
|
};
|
|
|
|
/**
|
|
* Called upon transport close.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onClose = function (reason, desc) {
|
|
if ('opening' == this.readyState || 'open' == this.readyState || 'closing' == this.readyState) {
|
|
debug('socket close with reason: "%s"', reason);
|
|
var self = this;
|
|
|
|
// clear timers
|
|
clearTimeout(this.pingIntervalTimer);
|
|
clearTimeout(this.pingTimeoutTimer);
|
|
|
|
// clean buffers in next tick, so developers can still
|
|
// grab the buffers on `close` event
|
|
setTimeout(function() {
|
|
self.writeBuffer = [];
|
|
self.callbackBuffer = [];
|
|
self.prevBufferLen = 0;
|
|
}, 0);
|
|
|
|
// stop event from firing again for transport
|
|
this.transport.removeAllListeners('close');
|
|
|
|
// ensure transport won't stay open
|
|
this.transport.close();
|
|
|
|
// ignore further transport communication
|
|
this.transport.removeAllListeners();
|
|
|
|
// set ready state
|
|
this.readyState = 'closed';
|
|
|
|
// clear session id
|
|
this.id = null;
|
|
|
|
// emit close event
|
|
this.emit('close', reason, desc);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Filters upgrades, returning only those matching client transports.
|
|
*
|
|
* @param {Array} server upgrades
|
|
* @api private
|
|
*
|
|
*/
|
|
|
|
Socket.prototype.filterUpgrades = function (upgrades) {
|
|
var filteredUpgrades = [];
|
|
for (var i = 0, j = upgrades.length; i<j; i++) {
|
|
if (~index(this.transports, upgrades[i])) filteredUpgrades.push(upgrades[i]);
|
|
}
|
|
return filteredUpgrades;
|
|
};
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./transport":13,"./transports":14,"component-emitter":8,"debug":20,"engine.io-parser":24,"indexof":44,"parsejson":52,"parseqs":53,"parseuri":23}],13:[function(require,module,exports){
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var parser = require('engine.io-parser');
|
|
var Emitter = require('component-emitter');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = Transport;
|
|
|
|
/**
|
|
* Transport abstract constructor.
|
|
*
|
|
* @param {Object} options.
|
|
* @api private
|
|
*/
|
|
|
|
function Transport (opts) {
|
|
this.path = opts.path;
|
|
this.hostname = opts.hostname;
|
|
this.port = opts.port;
|
|
this.secure = opts.secure;
|
|
this.query = opts.query;
|
|
this.timestampParam = opts.timestampParam;
|
|
this.timestampRequests = opts.timestampRequests;
|
|
this.readyState = '';
|
|
this.agent = opts.agent || false;
|
|
this.socket = opts.socket;
|
|
this.enablesXDR = opts.enablesXDR;
|
|
|
|
// SSL options for Node.js client
|
|
this.pfx = opts.pfx;
|
|
this.key = opts.key;
|
|
this.passphrase = opts.passphrase;
|
|
this.cert = opts.cert;
|
|
this.ca = opts.ca;
|
|
this.ciphers = opts.ciphers;
|
|
this.rejectUnauthorized = opts.rejectUnauthorized;
|
|
}
|
|
|
|
/**
|
|
* Mix in `Emitter`.
|
|
*/
|
|
|
|
Emitter(Transport.prototype);
|
|
|
|
/**
|
|
* A counter used to prevent collisions in the timestamps used
|
|
* for cache busting.
|
|
*/
|
|
|
|
Transport.timestamps = 0;
|
|
|
|
/**
|
|
* Emits an error.
|
|
*
|
|
* @param {String} str
|
|
* @return {Transport} for chaining
|
|
* @api public
|
|
*/
|
|
|
|
Transport.prototype.onError = function (msg, desc) {
|
|
var err = new Error(msg);
|
|
err.type = 'TransportError';
|
|
err.description = desc;
|
|
this.emit('error', err);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Opens the transport.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Transport.prototype.open = function () {
|
|
if ('closed' == this.readyState || '' == this.readyState) {
|
|
this.readyState = 'opening';
|
|
this.doOpen();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Closes the transport.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Transport.prototype.close = function () {
|
|
if ('opening' == this.readyState || 'open' == this.readyState) {
|
|
this.doClose();
|
|
this.onClose();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sends multiple packets.
|
|
*
|
|
* @param {Array} packets
|
|
* @api private
|
|
*/
|
|
|
|
Transport.prototype.send = function(packets){
|
|
if ('open' == this.readyState) {
|
|
this.write(packets);
|
|
} else {
|
|
throw new Error('Transport not open');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon open
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Transport.prototype.onOpen = function () {
|
|
this.readyState = 'open';
|
|
this.writable = true;
|
|
this.emit('open');
|
|
};
|
|
|
|
/**
|
|
* Called with data.
|
|
*
|
|
* @param {String} data
|
|
* @api private
|
|
*/
|
|
|
|
Transport.prototype.onData = function(data){
|
|
var packet = parser.decodePacket(data, this.socket.binaryType);
|
|
this.onPacket(packet);
|
|
};
|
|
|
|
/**
|
|
* Called with a decoded packet.
|
|
*/
|
|
|
|
Transport.prototype.onPacket = function (packet) {
|
|
this.emit('packet', packet);
|
|
};
|
|
|
|
/**
|
|
* Called upon close.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Transport.prototype.onClose = function () {
|
|
this.readyState = 'closed';
|
|
this.emit('close');
|
|
};
|
|
|
|
},{"component-emitter":8,"engine.io-parser":24}],14:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* Module dependencies
|
|
*/
|
|
|
|
var XMLHttpRequest = require('xmlhttprequest');
|
|
var XHR = require('./polling-xhr');
|
|
var JSONP = require('./polling-jsonp');
|
|
var websocket = require('./websocket');
|
|
|
|
/**
|
|
* Export transports.
|
|
*/
|
|
|
|
exports.polling = polling;
|
|
exports.websocket = websocket;
|
|
|
|
/**
|
|
* Polling transport polymorphic constructor.
|
|
* Decides on xhr vs jsonp based on feature detection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
function polling(opts){
|
|
var xhr;
|
|
var xd = false;
|
|
var xs = false;
|
|
var jsonp = false !== opts.jsonp;
|
|
|
|
if (global.location) {
|
|
var isSSL = 'https:' == location.protocol;
|
|
var port = location.port;
|
|
|
|
// some user agents have empty `location.port`
|
|
if (!port) {
|
|
port = isSSL ? 443 : 80;
|
|
}
|
|
|
|
xd = opts.hostname != location.hostname || port != opts.port;
|
|
xs = opts.secure != isSSL;
|
|
}
|
|
|
|
opts.xdomain = xd;
|
|
opts.xscheme = xs;
|
|
xhr = new XMLHttpRequest(opts);
|
|
|
|
if ('open' in xhr && !opts.forceJSONP) {
|
|
return new XHR(opts);
|
|
} else {
|
|
if (!jsonp) throw new Error('JSONP disabled');
|
|
return new JSONP(opts);
|
|
}
|
|
}
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./polling-jsonp":15,"./polling-xhr":16,"./websocket":18,"xmlhttprequest":19}],15:[function(require,module,exports){
|
|
(function (global){
|
|
|
|
/**
|
|
* Module requirements.
|
|
*/
|
|
|
|
var Polling = require('./polling');
|
|
var inherit = require('component-inherit');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = JSONPPolling;
|
|
|
|
/**
|
|
* Cached regular expressions.
|
|
*/
|
|
|
|
var rNewline = /\n/g;
|
|
var rEscapedNewline = /\\n/g;
|
|
|
|
/**
|
|
* Global JSONP callbacks.
|
|
*/
|
|
|
|
var callbacks;
|
|
|
|
/**
|
|
* Callbacks count.
|
|
*/
|
|
|
|
var index = 0;
|
|
|
|
/**
|
|
* Noop.
|
|
*/
|
|
|
|
function empty () { }
|
|
|
|
/**
|
|
* JSONP Polling constructor.
|
|
*
|
|
* @param {Object} opts.
|
|
* @api public
|
|
*/
|
|
|
|
function JSONPPolling (opts) {
|
|
Polling.call(this, opts);
|
|
|
|
this.query = this.query || {};
|
|
|
|
// define global callbacks array if not present
|
|
// we do this here (lazily) to avoid unneeded global pollution
|
|
if (!callbacks) {
|
|
// we need to consider multiple engines in the same page
|
|
if (!global.___eio) global.___eio = [];
|
|
callbacks = global.___eio;
|
|
}
|
|
|
|
// callback identifier
|
|
this.index = callbacks.length;
|
|
|
|
// add callback to jsonp global
|
|
var self = this;
|
|
callbacks.push(function (msg) {
|
|
self.onData(msg);
|
|
});
|
|
|
|
// append to query string
|
|
this.query.j = this.index;
|
|
|
|
// prevent spurious errors from being emitted when the window is unloaded
|
|
if (global.document && global.addEventListener) {
|
|
global.addEventListener('beforeunload', function () {
|
|
if (self.script) self.script.onerror = empty;
|
|
}, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inherits from Polling.
|
|
*/
|
|
|
|
inherit(JSONPPolling, Polling);
|
|
|
|
/*
|
|
* JSONP only supports binary as base64 encoded strings
|
|
*/
|
|
|
|
JSONPPolling.prototype.supportsBinary = false;
|
|
|
|
/**
|
|
* Closes the socket.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
JSONPPolling.prototype.doClose = function () {
|
|
if (this.script) {
|
|
this.script.parentNode.removeChild(this.script);
|
|
this.script = null;
|
|
}
|
|
|
|
if (this.form) {
|
|
this.form.parentNode.removeChild(this.form);
|
|
this.form = null;
|
|
this.iframe = null;
|
|
}
|
|
|
|
Polling.prototype.doClose.call(this);
|
|
};
|
|
|
|
/**
|
|
* Starts a poll cycle.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
JSONPPolling.prototype.doPoll = function () {
|
|
var self = this;
|
|
var script = document.createElement('script');
|
|
|
|
if (this.script) {
|
|
this.script.parentNode.removeChild(this.script);
|
|
this.script = null;
|
|
}
|
|
|
|
script.async = true;
|
|
script.src = this.uri();
|
|
script.onerror = function(e){
|
|
self.onError('jsonp poll error',e);
|
|
};
|
|
|
|
var insertAt = document.getElementsByTagName('script')[0];
|
|
insertAt.parentNode.insertBefore(script, insertAt);
|
|
this.script = script;
|
|
|
|
var isUAgecko = 'undefined' != typeof navigator && /gecko/i.test(navigator.userAgent);
|
|
|
|
if (isUAgecko) {
|
|
setTimeout(function () {
|
|
var iframe = document.createElement('iframe');
|
|
document.body.appendChild(iframe);
|
|
document.body.removeChild(iframe);
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Writes with a hidden iframe.
|
|
*
|
|
* @param {String} data to send
|
|
* @param {Function} called upon flush.
|
|
* @api private
|
|
*/
|
|
|
|
JSONPPolling.prototype.doWrite = function (data, fn) {
|
|
var self = this;
|
|
|
|
if (!this.form) {
|
|
var form = document.createElement('form');
|
|
var area = document.createElement('textarea');
|
|
var id = this.iframeId = 'eio_iframe_' + this.index;
|
|
var iframe;
|
|
|
|
form.className = 'socketio';
|
|
form.style.position = 'absolute';
|
|
form.style.top = '-1000px';
|
|
form.style.left = '-1000px';
|
|
form.target = id;
|
|
form.method = 'POST';
|
|
form.setAttribute('accept-charset', 'utf-8');
|
|
area.name = 'd';
|
|
form.appendChild(area);
|
|
document.body.appendChild(form);
|
|
|
|
this.form = form;
|
|
this.area = area;
|
|
}
|
|
|
|
this.form.action = this.uri();
|
|
|
|
function complete () {
|
|
initIframe();
|
|
fn();
|
|
}
|
|
|
|
function initIframe () {
|
|
if (self.iframe) {
|
|
try {
|
|
self.form.removeChild(self.iframe);
|
|
} catch (e) {
|
|
self.onError('jsonp polling iframe removal error', e);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// ie6 dynamic iframes with target="" support (thanks Chris Lambacher)
|
|
var html = '<iframe src="javascript:0" name="'+ self.iframeId +'">';
|
|
iframe = document.createElement(html);
|
|
} catch (e) {
|
|
iframe = document.createElement('iframe');
|
|
iframe.name = self.iframeId;
|
|
iframe.src = 'javascript:0';
|
|
}
|
|
|
|
iframe.id = self.iframeId;
|
|
|
|
self.form.appendChild(iframe);
|
|
self.iframe = iframe;
|
|
}
|
|
|
|
initIframe();
|
|
|
|
// escape \n to prevent it from being converted into \r\n by some UAs
|
|
// double escaping is required for escaped new lines because unescaping of new lines can be done safely on server-side
|
|
data = data.replace(rEscapedNewline, '\\\n');
|
|
this.area.value = data.replace(rNewline, '\\n');
|
|
|
|
try {
|
|
this.form.submit();
|
|
} catch(e) {}
|
|
|
|
if (this.iframe.attachEvent) {
|
|
this.iframe.onreadystatechange = function(){
|
|
if (self.iframe.readyState == 'complete') {
|
|
complete();
|
|
}
|
|
};
|
|
} else {
|
|
this.iframe.onload = complete;
|
|
}
|
|
};
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./polling":17,"component-inherit":9}],16:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* Module requirements.
|
|
*/
|
|
|
|
var XMLHttpRequest = require('xmlhttprequest');
|
|
var Polling = require('./polling');
|
|
var Emitter = require('component-emitter');
|
|
var inherit = require('component-inherit');
|
|
var debug = require('debug')('engine.io-client:polling-xhr');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = XHR;
|
|
module.exports.Request = Request;
|
|
|
|
/**
|
|
* Empty function
|
|
*/
|
|
|
|
function empty(){}
|
|
|
|
/**
|
|
* XHR Polling constructor.
|
|
*
|
|
* @param {Object} opts
|
|
* @api public
|
|
*/
|
|
|
|
function XHR(opts){
|
|
Polling.call(this, opts);
|
|
|
|
if (global.location) {
|
|
var isSSL = 'https:' == location.protocol;
|
|
var port = location.port;
|
|
|
|
// some user agents have empty `location.port`
|
|
if (!port) {
|
|
port = isSSL ? 443 : 80;
|
|
}
|
|
|
|
this.xd = opts.hostname != global.location.hostname ||
|
|
port != opts.port;
|
|
this.xs = opts.secure != isSSL;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Inherits from Polling.
|
|
*/
|
|
|
|
inherit(XHR, Polling);
|
|
|
|
/**
|
|
* XHR supports binary
|
|
*/
|
|
|
|
XHR.prototype.supportsBinary = true;
|
|
|
|
/**
|
|
* Creates a request.
|
|
*
|
|
* @param {String} method
|
|
* @api private
|
|
*/
|
|
|
|
XHR.prototype.request = function(opts){
|
|
opts = opts || {};
|
|
opts.uri = this.uri();
|
|
opts.xd = this.xd;
|
|
opts.xs = this.xs;
|
|
opts.agent = this.agent || false;
|
|
opts.supportsBinary = this.supportsBinary;
|
|
opts.enablesXDR = this.enablesXDR;
|
|
|
|
// SSL options for Node.js client
|
|
opts.pfx = this.pfx;
|
|
opts.key = this.key;
|
|
opts.passphrase = this.passphrase;
|
|
opts.cert = this.cert;
|
|
opts.ca = this.ca;
|
|
opts.ciphers = this.ciphers;
|
|
opts.rejectUnauthorized = this.rejectUnauthorized;
|
|
|
|
return new Request(opts);
|
|
};
|
|
|
|
/**
|
|
* Sends data.
|
|
*
|
|
* @param {String} data to send.
|
|
* @param {Function} called upon flush.
|
|
* @api private
|
|
*/
|
|
|
|
XHR.prototype.doWrite = function(data, fn){
|
|
var isBinary = typeof data !== 'string' && data !== undefined;
|
|
var req = this.request({ method: 'POST', data: data, isBinary: isBinary });
|
|
var self = this;
|
|
req.on('success', fn);
|
|
req.on('error', function(err){
|
|
self.onError('xhr post error', err);
|
|
});
|
|
this.sendXhr = req;
|
|
};
|
|
|
|
/**
|
|
* Starts a poll cycle.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
XHR.prototype.doPoll = function(){
|
|
debug('xhr poll');
|
|
var req = this.request();
|
|
var self = this;
|
|
req.on('data', function(data){
|
|
self.onData(data);
|
|
});
|
|
req.on('error', function(err){
|
|
self.onError('xhr poll error', err);
|
|
});
|
|
this.pollXhr = req;
|
|
};
|
|
|
|
/**
|
|
* Request constructor
|
|
*
|
|
* @param {Object} options
|
|
* @api public
|
|
*/
|
|
|
|
function Request(opts){
|
|
this.method = opts.method || 'GET';
|
|
this.uri = opts.uri;
|
|
this.xd = !!opts.xd;
|
|
this.xs = !!opts.xs;
|
|
this.async = false !== opts.async;
|
|
this.data = undefined != opts.data ? opts.data : null;
|
|
this.agent = opts.agent;
|
|
this.isBinary = opts.isBinary;
|
|
this.supportsBinary = opts.supportsBinary;
|
|
this.enablesXDR = opts.enablesXDR;
|
|
|
|
// SSL options for Node.js client
|
|
this.pfx = opts.pfx;
|
|
this.key = opts.key;
|
|
this.passphrase = opts.passphrase;
|
|
this.cert = opts.cert;
|
|
this.ca = opts.ca;
|
|
this.ciphers = opts.ciphers;
|
|
this.rejectUnauthorized = opts.rejectUnauthorized;
|
|
|
|
this.create();
|
|
}
|
|
|
|
/**
|
|
* Mix in `Emitter`.
|
|
*/
|
|
|
|
Emitter(Request.prototype);
|
|
|
|
/**
|
|
* Creates the XHR object and sends the request.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.create = function(){
|
|
var opts = { agent: this.agent, xdomain: this.xd, xscheme: this.xs, enablesXDR: this.enablesXDR };
|
|
|
|
// SSL options for Node.js client
|
|
opts.pfx = this.pfx;
|
|
opts.key = this.key;
|
|
opts.passphrase = this.passphrase;
|
|
opts.cert = this.cert;
|
|
opts.ca = this.ca;
|
|
opts.ciphers = this.ciphers;
|
|
opts.rejectUnauthorized = this.rejectUnauthorized;
|
|
|
|
var xhr = this.xhr = new XMLHttpRequest(opts);
|
|
var self = this;
|
|
|
|
try {
|
|
debug('xhr open %s: %s', this.method, this.uri);
|
|
xhr.open(this.method, this.uri, this.async);
|
|
if (this.supportsBinary) {
|
|
// This has to be done after open because Firefox is stupid
|
|
// http://stackoverflow.com/questions/13216903/get-binary-data-with-xmlhttprequest-in-a-firefox-extension
|
|
xhr.responseType = 'arraybuffer';
|
|
}
|
|
|
|
if ('POST' == this.method) {
|
|
try {
|
|
if (this.isBinary) {
|
|
xhr.setRequestHeader('Content-type', 'application/octet-stream');
|
|
} else {
|
|
xhr.setRequestHeader('Content-type', 'text/plain;charset=UTF-8');
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
// ie6 check
|
|
if ('withCredentials' in xhr) {
|
|
xhr.withCredentials = true;
|
|
}
|
|
|
|
if (this.hasXDR()) {
|
|
xhr.onload = function(){
|
|
self.onLoad();
|
|
};
|
|
xhr.onerror = function(){
|
|
self.onError(xhr.responseText);
|
|
};
|
|
} else {
|
|
xhr.onreadystatechange = function(){
|
|
if (4 != xhr.readyState) return;
|
|
if (200 == xhr.status || 1223 == xhr.status) {
|
|
self.onLoad();
|
|
} else {
|
|
// make sure the `error` event handler that's user-set
|
|
// does not throw in the same tick and gets caught here
|
|
setTimeout(function(){
|
|
self.onError(xhr.status);
|
|
}, 0);
|
|
}
|
|
};
|
|
}
|
|
|
|
debug('xhr data %s', this.data);
|
|
xhr.send(this.data);
|
|
} catch (e) {
|
|
// Need to defer since .create() is called directly fhrom the constructor
|
|
// and thus the 'error' event can only be only bound *after* this exception
|
|
// occurs. Therefore, also, we cannot throw here at all.
|
|
setTimeout(function() {
|
|
self.onError(e);
|
|
}, 0);
|
|
return;
|
|
}
|
|
|
|
if (global.document) {
|
|
this.index = Request.requestsCount++;
|
|
Request.requests[this.index] = this;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon successful response.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.onSuccess = function(){
|
|
this.emit('success');
|
|
this.cleanup();
|
|
};
|
|
|
|
/**
|
|
* Called if we have data.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.onData = function(data){
|
|
this.emit('data', data);
|
|
this.onSuccess();
|
|
};
|
|
|
|
/**
|
|
* Called upon error.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.onError = function(err){
|
|
this.emit('error', err);
|
|
this.cleanup(true);
|
|
};
|
|
|
|
/**
|
|
* Cleans up house.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.cleanup = function(fromError){
|
|
if ('undefined' == typeof this.xhr || null === this.xhr) {
|
|
return;
|
|
}
|
|
// xmlhttprequest
|
|
if (this.hasXDR()) {
|
|
this.xhr.onload = this.xhr.onerror = empty;
|
|
} else {
|
|
this.xhr.onreadystatechange = empty;
|
|
}
|
|
|
|
if (fromError) {
|
|
try {
|
|
this.xhr.abort();
|
|
} catch(e) {}
|
|
}
|
|
|
|
if (global.document) {
|
|
delete Request.requests[this.index];
|
|
}
|
|
|
|
this.xhr = null;
|
|
};
|
|
|
|
/**
|
|
* Called upon load.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.onLoad = function(){
|
|
var data;
|
|
try {
|
|
var contentType;
|
|
try {
|
|
contentType = this.xhr.getResponseHeader('Content-Type').split(';')[0];
|
|
} catch (e) {}
|
|
if (contentType === 'application/octet-stream') {
|
|
data = this.xhr.response;
|
|
} else {
|
|
if (!this.supportsBinary) {
|
|
data = this.xhr.responseText;
|
|
} else {
|
|
data = 'ok';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.onError(e);
|
|
}
|
|
if (null != data) {
|
|
this.onData(data);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if it has XDomainRequest.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Request.prototype.hasXDR = function(){
|
|
return 'undefined' !== typeof global.XDomainRequest && !this.xs && this.enablesXDR;
|
|
};
|
|
|
|
/**
|
|
* Aborts the request.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Request.prototype.abort = function(){
|
|
this.cleanup();
|
|
};
|
|
|
|
/**
|
|
* Aborts pending requests when unloading the window. This is needed to prevent
|
|
* memory leaks (e.g. when using IE) and to ensure that no spurious error is
|
|
* emitted.
|
|
*/
|
|
|
|
if (global.document) {
|
|
Request.requestsCount = 0;
|
|
Request.requests = {};
|
|
if (global.attachEvent) {
|
|
global.attachEvent('onunload', unloadHandler);
|
|
} else if (global.addEventListener) {
|
|
global.addEventListener('beforeunload', unloadHandler, false);
|
|
}
|
|
}
|
|
|
|
function unloadHandler() {
|
|
for (var i in Request.requests) {
|
|
if (Request.requests.hasOwnProperty(i)) {
|
|
Request.requests[i].abort();
|
|
}
|
|
}
|
|
}
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./polling":17,"component-emitter":8,"component-inherit":9,"debug":20,"xmlhttprequest":19}],17:[function(require,module,exports){
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var Transport = require('../transport');
|
|
var parseqs = require('parseqs');
|
|
var parser = require('engine.io-parser');
|
|
var inherit = require('component-inherit');
|
|
var debug = require('debug')('engine.io-client:polling');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = Polling;
|
|
|
|
/**
|
|
* Is XHR2 supported?
|
|
*/
|
|
|
|
var hasXHR2 = (function() {
|
|
var XMLHttpRequest = require('xmlhttprequest');
|
|
var xhr = new XMLHttpRequest({ xdomain: false });
|
|
return null != xhr.responseType;
|
|
})();
|
|
|
|
/**
|
|
* Polling interface.
|
|
*
|
|
* @param {Object} opts
|
|
* @api private
|
|
*/
|
|
|
|
function Polling(opts){
|
|
var forceBase64 = (opts && opts.forceBase64);
|
|
if (!hasXHR2 || forceBase64) {
|
|
this.supportsBinary = false;
|
|
}
|
|
Transport.call(this, opts);
|
|
}
|
|
|
|
/**
|
|
* Inherits from Transport.
|
|
*/
|
|
|
|
inherit(Polling, Transport);
|
|
|
|
/**
|
|
* Transport name.
|
|
*/
|
|
|
|
Polling.prototype.name = 'polling';
|
|
|
|
/**
|
|
* Opens the socket (triggers polling). We write a PING message to determine
|
|
* when the transport is open.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Polling.prototype.doOpen = function(){
|
|
this.poll();
|
|
};
|
|
|
|
/**
|
|
* Pauses polling.
|
|
*
|
|
* @param {Function} callback upon buffers are flushed and transport is paused
|
|
* @api private
|
|
*/
|
|
|
|
Polling.prototype.pause = function(onPause){
|
|
var pending = 0;
|
|
var self = this;
|
|
|
|
this.readyState = 'pausing';
|
|
|
|
function pause(){
|
|
debug('paused');
|
|
self.readyState = 'paused';
|
|
onPause();
|
|
}
|
|
|
|
if (this.polling || !this.writable) {
|
|
var total = 0;
|
|
|
|
if (this.polling) {
|
|
debug('we are currently polling - waiting to pause');
|
|
total++;
|
|
this.once('pollComplete', function(){
|
|
debug('pre-pause polling complete');
|
|
--total || pause();
|
|
});
|
|
}
|
|
|
|
if (!this.writable) {
|
|
debug('we are currently writing - waiting to pause');
|
|
total++;
|
|
this.once('drain', function(){
|
|
debug('pre-pause writing complete');
|
|
--total || pause();
|
|
});
|
|
}
|
|
} else {
|
|
pause();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Starts polling cycle.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Polling.prototype.poll = function(){
|
|
debug('polling');
|
|
this.polling = true;
|
|
this.doPoll();
|
|
this.emit('poll');
|
|
};
|
|
|
|
/**
|
|
* Overloads onData to detect payloads.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Polling.prototype.onData = function(data){
|
|
var self = this;
|
|
debug('polling got data %s', data);
|
|
var callback = function(packet, index, total) {
|
|
// if its the first message we consider the transport open
|
|
if ('opening' == self.readyState) {
|
|
self.onOpen();
|
|
}
|
|
|
|
// if its a close packet, we close the ongoing requests
|
|
if ('close' == packet.type) {
|
|
self.onClose();
|
|
return false;
|
|
}
|
|
|
|
// otherwise bypass onData and handle the message
|
|
self.onPacket(packet);
|
|
};
|
|
|
|
// decode payload
|
|
parser.decodePayload(data, this.socket.binaryType, callback);
|
|
|
|
// if an event did not trigger closing
|
|
if ('closed' != this.readyState) {
|
|
// if we got data we're not polling
|
|
this.polling = false;
|
|
this.emit('pollComplete');
|
|
|
|
if ('open' == this.readyState) {
|
|
this.poll();
|
|
} else {
|
|
debug('ignoring poll - transport state "%s"', this.readyState);
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* For polling, send a close packet.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Polling.prototype.doClose = function(){
|
|
var self = this;
|
|
|
|
function close(){
|
|
debug('writing close packet');
|
|
self.write([{ type: 'close' }]);
|
|
}
|
|
|
|
if ('open' == this.readyState) {
|
|
debug('transport open - closing');
|
|
close();
|
|
} else {
|
|
// in case we're trying to close while
|
|
// handshaking is in progress (GH-164)
|
|
debug('transport not open - deferring close');
|
|
this.once('open', close);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Writes a packets payload.
|
|
*
|
|
* @param {Array} data packets
|
|
* @param {Function} drain callback
|
|
* @api private
|
|
*/
|
|
|
|
Polling.prototype.write = function(packets){
|
|
var self = this;
|
|
this.writable = false;
|
|
var callbackfn = function() {
|
|
self.writable = true;
|
|
self.emit('drain');
|
|
};
|
|
|
|
var self = this;
|
|
parser.encodePayload(packets, this.supportsBinary, function(data) {
|
|
self.doWrite(data, callbackfn);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Generates uri for connection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Polling.prototype.uri = function(){
|
|
var query = this.query || {};
|
|
var schema = this.secure ? 'https' : 'http';
|
|
var port = '';
|
|
|
|
// cache busting is forced
|
|
if (false !== this.timestampRequests) {
|
|
query[this.timestampParam] = +new Date + '-' + Transport.timestamps++;
|
|
}
|
|
|
|
if (!this.supportsBinary && !query.sid) {
|
|
query.b64 = 1;
|
|
}
|
|
|
|
query = parseqs.encode(query);
|
|
|
|
// avoid port if default for schema
|
|
if (this.port && (('https' == schema && this.port != 443) ||
|
|
('http' == schema && this.port != 80))) {
|
|
port = ':' + this.port;
|
|
}
|
|
|
|
// prepend ? to query
|
|
if (query.length) {
|
|
query = '?' + query;
|
|
}
|
|
|
|
return schema + '://' + this.hostname + port + this.path + query;
|
|
};
|
|
|
|
},{"../transport":13,"component-inherit":9,"debug":20,"engine.io-parser":24,"parseqs":53,"xmlhttprequest":19}],18:[function(require,module,exports){
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var Transport = require('../transport');
|
|
var parser = require('engine.io-parser');
|
|
var parseqs = require('parseqs');
|
|
var inherit = require('component-inherit');
|
|
var debug = require('debug')('engine.io-client:websocket');
|
|
|
|
/**
|
|
* `ws` exposes a WebSocket-compatible interface in
|
|
* Node, or the `WebSocket` or `MozWebSocket` globals
|
|
* in the browser.
|
|
*/
|
|
|
|
var WebSocket = require('ws');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = WS;
|
|
|
|
/**
|
|
* WebSocket transport constructor.
|
|
*
|
|
* @api {Object} connection options
|
|
* @api public
|
|
*/
|
|
|
|
function WS(opts){
|
|
var forceBase64 = (opts && opts.forceBase64);
|
|
if (forceBase64) {
|
|
this.supportsBinary = false;
|
|
}
|
|
Transport.call(this, opts);
|
|
}
|
|
|
|
/**
|
|
* Inherits from Transport.
|
|
*/
|
|
|
|
inherit(WS, Transport);
|
|
|
|
/**
|
|
* Transport name.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
WS.prototype.name = 'websocket';
|
|
|
|
/*
|
|
* WebSockets support binary
|
|
*/
|
|
|
|
WS.prototype.supportsBinary = true;
|
|
|
|
/**
|
|
* Opens socket.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
WS.prototype.doOpen = function(){
|
|
if (!this.check()) {
|
|
// let probe timeout
|
|
return;
|
|
}
|
|
|
|
var self = this;
|
|
var uri = this.uri();
|
|
var protocols = void(0);
|
|
var opts = { agent: this.agent };
|
|
|
|
// SSL options for Node.js client
|
|
opts.pfx = this.pfx;
|
|
opts.key = this.key;
|
|
opts.passphrase = this.passphrase;
|
|
opts.cert = this.cert;
|
|
opts.ca = this.ca;
|
|
opts.ciphers = this.ciphers;
|
|
opts.rejectUnauthorized = this.rejectUnauthorized;
|
|
|
|
this.ws = new WebSocket(uri, protocols, opts);
|
|
|
|
if (this.ws.binaryType === undefined) {
|
|
this.supportsBinary = false;
|
|
}
|
|
|
|
this.ws.binaryType = 'arraybuffer';
|
|
this.addEventListeners();
|
|
};
|
|
|
|
/**
|
|
* Adds event listeners to the socket
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
WS.prototype.addEventListeners = function(){
|
|
var self = this;
|
|
|
|
this.ws.onopen = function(){
|
|
self.onOpen();
|
|
};
|
|
this.ws.onclose = function(){
|
|
self.onClose();
|
|
};
|
|
this.ws.onmessage = function(ev){
|
|
self.onData(ev.data);
|
|
};
|
|
this.ws.onerror = function(e){
|
|
self.onError('websocket error', e);
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Override `onData` to use a timer on iOS.
|
|
* See: https://gist.github.com/mloughran/2052006
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
if ('undefined' != typeof navigator
|
|
&& /iPad|iPhone|iPod/i.test(navigator.userAgent)) {
|
|
WS.prototype.onData = function(data){
|
|
var self = this;
|
|
setTimeout(function(){
|
|
Transport.prototype.onData.call(self, data);
|
|
}, 0);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Writes data to socket.
|
|
*
|
|
* @param {Array} array of packets.
|
|
* @api private
|
|
*/
|
|
|
|
WS.prototype.write = function(packets){
|
|
var self = this;
|
|
this.writable = false;
|
|
// encodePacket efficient as it uses WS framing
|
|
// no need for encodePayload
|
|
for (var i = 0, l = packets.length; i < l; i++) {
|
|
parser.encodePacket(packets[i], this.supportsBinary, function(data) {
|
|
//Sometimes the websocket has already been closed but the browser didn't
|
|
//have a chance of informing us about it yet, in that case send will
|
|
//throw an error
|
|
try {
|
|
self.ws.send(data);
|
|
} catch (e){
|
|
debug('websocket closed before onclose event');
|
|
}
|
|
});
|
|
}
|
|
|
|
function ondrain() {
|
|
self.writable = true;
|
|
self.emit('drain');
|
|
}
|
|
// fake drain
|
|
// defer to next tick to allow Socket to clear writeBuffer
|
|
setTimeout(ondrain, 0);
|
|
};
|
|
|
|
/**
|
|
* Called upon close
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
WS.prototype.onClose = function(){
|
|
Transport.prototype.onClose.call(this);
|
|
};
|
|
|
|
/**
|
|
* Closes socket.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
WS.prototype.doClose = function(){
|
|
if (typeof this.ws !== 'undefined') {
|
|
this.ws.close();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generates uri for connection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
WS.prototype.uri = function(){
|
|
var query = this.query || {};
|
|
var schema = this.secure ? 'wss' : 'ws';
|
|
var port = '';
|
|
|
|
// avoid port if default for schema
|
|
if (this.port && (('wss' == schema && this.port != 443)
|
|
|| ('ws' == schema && this.port != 80))) {
|
|
port = ':' + this.port;
|
|
}
|
|
|
|
// append timestamp to URI
|
|
if (this.timestampRequests) {
|
|
query[this.timestampParam] = +new Date;
|
|
}
|
|
|
|
// communicate binary support capabilities
|
|
if (!this.supportsBinary) {
|
|
query.b64 = 1;
|
|
}
|
|
|
|
query = parseqs.encode(query);
|
|
|
|
// prepend ? to query
|
|
if (query.length) {
|
|
query = '?' + query;
|
|
}
|
|
|
|
return schema + '://' + this.hostname + port + this.path + query;
|
|
};
|
|
|
|
/**
|
|
* Feature detection for WebSocket.
|
|
*
|
|
* @return {Boolean} whether this transport is available.
|
|
* @api public
|
|
*/
|
|
|
|
WS.prototype.check = function(){
|
|
return !!WebSocket && !('__initialize' in WebSocket && this.name === WS.prototype.name);
|
|
};
|
|
|
|
},{"../transport":13,"component-inherit":9,"debug":20,"engine.io-parser":24,"parseqs":53,"ws":101}],19:[function(require,module,exports){
|
|
// browser shim for xmlhttprequest module
|
|
var hasCORS = require('has-cors');
|
|
|
|
module.exports = function(opts) {
|
|
var xdomain = opts.xdomain;
|
|
|
|
// scheme must be same when usign XDomainRequest
|
|
// http://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx
|
|
var xscheme = opts.xscheme;
|
|
|
|
// XDomainRequest has a flow of not sending cookie, therefore it should be disabled as a default.
|
|
// https://github.com/Automattic/engine.io-client/pull/217
|
|
var enablesXDR = opts.enablesXDR;
|
|
|
|
// XMLHttpRequest can be disabled on IE
|
|
try {
|
|
if ('undefined' != typeof XMLHttpRequest && (!xdomain || hasCORS)) {
|
|
return new XMLHttpRequest();
|
|
}
|
|
} catch (e) { }
|
|
|
|
// Use XDomainRequest for IE8 if enablesXDR is true
|
|
// because loading bar keeps flashing when using jsonp-polling
|
|
// https://github.com/yujiosaka/socke.io-ie8-loading-example
|
|
try {
|
|
if ('undefined' != typeof XDomainRequest && !xscheme && enablesXDR) {
|
|
return new XDomainRequest();
|
|
}
|
|
} catch (e) { }
|
|
|
|
if (!xdomain) {
|
|
try {
|
|
return new ActiveXObject('Microsoft.XMLHTTP');
|
|
} catch(e) { }
|
|
}
|
|
}
|
|
|
|
},{"has-cors":43}],20:[function(require,module,exports){
|
|
|
|
/**
|
|
* This is the web browser implementation of `debug()`.
|
|
*
|
|
* Expose `debug()` as the module.
|
|
*/
|
|
|
|
exports = module.exports = require('./debug');
|
|
exports.log = log;
|
|
exports.formatArgs = formatArgs;
|
|
exports.save = save;
|
|
exports.load = load;
|
|
exports.useColors = useColors;
|
|
|
|
/**
|
|
* Colors.
|
|
*/
|
|
|
|
exports.colors = [
|
|
'lightseagreen',
|
|
'forestgreen',
|
|
'goldenrod',
|
|
'dodgerblue',
|
|
'darkorchid',
|
|
'crimson'
|
|
];
|
|
|
|
/**
|
|
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
|
|
* and the Firebug extension (any Firefox version) are known
|
|
* to support "%c" CSS customizations.
|
|
*
|
|
* TODO: add a `localStorage` variable to explicitly enable/disable colors
|
|
*/
|
|
|
|
function useColors() {
|
|
// is webkit? http://stackoverflow.com/a/16459606/376773
|
|
return ('WebkitAppearance' in document.documentElement.style) ||
|
|
// is firebug? http://stackoverflow.com/a/398120/376773
|
|
(window.console && (console.firebug || (console.exception && console.table))) ||
|
|
// is firefox >= v31?
|
|
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
|
|
(navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31);
|
|
}
|
|
|
|
/**
|
|
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
|
|
*/
|
|
|
|
exports.formatters.j = function(v) {
|
|
return JSON.stringify(v);
|
|
};
|
|
|
|
|
|
/**
|
|
* Colorize log arguments if enabled.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function formatArgs() {
|
|
var args = arguments;
|
|
var useColors = this.useColors;
|
|
|
|
args[0] = (useColors ? '%c' : '')
|
|
+ this.namespace
|
|
+ (useColors ? ' %c' : ' ')
|
|
+ args[0]
|
|
+ (useColors ? '%c ' : ' ')
|
|
+ '+' + exports.humanize(this.diff);
|
|
|
|
if (!useColors) return args;
|
|
|
|
var c = 'color: ' + this.color;
|
|
args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1));
|
|
|
|
// the final "%c" is somewhat tricky, because there could be other
|
|
// arguments passed either before or after the %c, so we need to
|
|
// figure out the correct index to insert the CSS into
|
|
var index = 0;
|
|
var lastC = 0;
|
|
args[0].replace(/%[a-z%]/g, function(match) {
|
|
if ('%%' === match) return;
|
|
index++;
|
|
if ('%c' === match) {
|
|
// we only are interested in the *last* %c
|
|
// (the user may have provided their own)
|
|
lastC = index;
|
|
}
|
|
});
|
|
|
|
args.splice(lastC, 0, c);
|
|
return args;
|
|
}
|
|
|
|
/**
|
|
* Invokes `console.log()` when available.
|
|
* No-op when `console.log` is not a "function".
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function log() {
|
|
// This hackery is required for IE8,
|
|
// where the `console.log` function doesn't have 'apply'
|
|
return 'object' == typeof console
|
|
&& 'function' == typeof console.log
|
|
&& Function.prototype.apply.call(console.log, console, arguments);
|
|
}
|
|
|
|
/**
|
|
* Save `namespaces`.
|
|
*
|
|
* @param {String} namespaces
|
|
* @api private
|
|
*/
|
|
|
|
function save(namespaces) {
|
|
try {
|
|
if (null == namespaces) {
|
|
localStorage.removeItem('debug');
|
|
} else {
|
|
localStorage.debug = namespaces;
|
|
}
|
|
} catch(e) {}
|
|
}
|
|
|
|
/**
|
|
* Load `namespaces`.
|
|
*
|
|
* @return {String} returns the previously persisted debug modes
|
|
* @api private
|
|
*/
|
|
|
|
function load() {
|
|
var r;
|
|
try {
|
|
r = localStorage.debug;
|
|
} catch(e) {}
|
|
return r;
|
|
}
|
|
|
|
/**
|
|
* Enable namespaces listed in `localStorage.debug` initially.
|
|
*/
|
|
|
|
exports.enable(load());
|
|
|
|
},{"./debug":21}],21:[function(require,module,exports){
|
|
|
|
/**
|
|
* This is the common logic for both the Node.js and web browser
|
|
* implementations of `debug()`.
|
|
*
|
|
* Expose `debug()` as the module.
|
|
*/
|
|
|
|
exports = module.exports = debug;
|
|
exports.coerce = coerce;
|
|
exports.disable = disable;
|
|
exports.enable = enable;
|
|
exports.enabled = enabled;
|
|
exports.humanize = require('ms');
|
|
|
|
/**
|
|
* The currently active debug mode names, and names to skip.
|
|
*/
|
|
|
|
exports.names = [];
|
|
exports.skips = [];
|
|
|
|
/**
|
|
* Map of special "%n" handling functions, for the debug "format" argument.
|
|
*
|
|
* Valid key names are a single, lowercased letter, i.e. "n".
|
|
*/
|
|
|
|
exports.formatters = {};
|
|
|
|
/**
|
|
* Previously assigned color.
|
|
*/
|
|
|
|
var prevColor = 0;
|
|
|
|
/**
|
|
* Previous log timestamp.
|
|
*/
|
|
|
|
var prevTime;
|
|
|
|
/**
|
|
* Select a color.
|
|
*
|
|
* @return {Number}
|
|
* @api private
|
|
*/
|
|
|
|
function selectColor() {
|
|
return exports.colors[prevColor++ % exports.colors.length];
|
|
}
|
|
|
|
/**
|
|
* Create a debugger with the given `namespace`.
|
|
*
|
|
* @param {String} namespace
|
|
* @return {Function}
|
|
* @api public
|
|
*/
|
|
|
|
function debug(namespace) {
|
|
|
|
// define the `disabled` version
|
|
function disabled() {
|
|
}
|
|
disabled.enabled = false;
|
|
|
|
// define the `enabled` version
|
|
function enabled() {
|
|
|
|
var self = enabled;
|
|
|
|
// set `diff` timestamp
|
|
var curr = +new Date();
|
|
var ms = curr - (prevTime || curr);
|
|
self.diff = ms;
|
|
self.prev = prevTime;
|
|
self.curr = curr;
|
|
prevTime = curr;
|
|
|
|
// add the `color` if not set
|
|
if (null == self.useColors) self.useColors = exports.useColors();
|
|
if (null == self.color && self.useColors) self.color = selectColor();
|
|
|
|
var args = Array.prototype.slice.call(arguments);
|
|
|
|
args[0] = exports.coerce(args[0]);
|
|
|
|
if ('string' !== typeof args[0]) {
|
|
// anything else let's inspect with %o
|
|
args = ['%o'].concat(args);
|
|
}
|
|
|
|
// apply any `formatters` transformations
|
|
var index = 0;
|
|
args[0] = args[0].replace(/%([a-z%])/g, function(match, format) {
|
|
// if we encounter an escaped % then don't increase the array index
|
|
if (match === '%%') return match;
|
|
index++;
|
|
var formatter = exports.formatters[format];
|
|
if ('function' === typeof formatter) {
|
|
var val = args[index];
|
|
match = formatter.call(self, val);
|
|
|
|
// now we need to remove `args[index]` since it's inlined in the `format`
|
|
args.splice(index, 1);
|
|
index--;
|
|
}
|
|
return match;
|
|
});
|
|
|
|
if ('function' === typeof exports.formatArgs) {
|
|
args = exports.formatArgs.apply(self, args);
|
|
}
|
|
var logFn = enabled.log || exports.log || console.log.bind(console);
|
|
logFn.apply(self, args);
|
|
}
|
|
enabled.enabled = true;
|
|
|
|
var fn = exports.enabled(namespace) ? enabled : disabled;
|
|
|
|
fn.namespace = namespace;
|
|
|
|
return fn;
|
|
}
|
|
|
|
/**
|
|
* Enables a debug mode by namespaces. This can include modes
|
|
* separated by a colon and wildcards.
|
|
*
|
|
* @param {String} namespaces
|
|
* @api public
|
|
*/
|
|
|
|
function enable(namespaces) {
|
|
exports.save(namespaces);
|
|
|
|
var split = (namespaces || '').split(/[\s,]+/);
|
|
var len = split.length;
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
if (!split[i]) continue; // ignore empty strings
|
|
namespaces = split[i].replace(/\*/g, '.*?');
|
|
if (namespaces[0] === '-') {
|
|
exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
|
|
} else {
|
|
exports.names.push(new RegExp('^' + namespaces + '$'));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disable debug output.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function disable() {
|
|
exports.enable('');
|
|
}
|
|
|
|
/**
|
|
* Returns true if the given mode name is enabled, false otherwise.
|
|
*
|
|
* @param {String} name
|
|
* @return {Boolean}
|
|
* @api public
|
|
*/
|
|
|
|
function enabled(name) {
|
|
var i, len;
|
|
for (i = 0, len = exports.skips.length; i < len; i++) {
|
|
if (exports.skips[i].test(name)) {
|
|
return false;
|
|
}
|
|
}
|
|
for (i = 0, len = exports.names.length; i < len; i++) {
|
|
if (exports.names[i].test(name)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Coerce `val`.
|
|
*
|
|
* @param {Mixed} val
|
|
* @return {Mixed}
|
|
* @api private
|
|
*/
|
|
|
|
function coerce(val) {
|
|
if (val instanceof Error) return val.stack || val.message;
|
|
return val;
|
|
}
|
|
|
|
},{"ms":22}],22:[function(require,module,exports){
|
|
/**
|
|
* Helpers.
|
|
*/
|
|
|
|
var s = 1000;
|
|
var m = s * 60;
|
|
var h = m * 60;
|
|
var d = h * 24;
|
|
var y = d * 365.25;
|
|
|
|
/**
|
|
* Parse or format the given `val`.
|
|
*
|
|
* Options:
|
|
*
|
|
* - `long` verbose formatting [false]
|
|
*
|
|
* @param {String|Number} val
|
|
* @param {Object} options
|
|
* @return {String|Number}
|
|
* @api public
|
|
*/
|
|
|
|
module.exports = function(val, options){
|
|
options = options || {};
|
|
if ('string' == typeof val) return parse(val);
|
|
return options.long
|
|
? long(val)
|
|
: short(val);
|
|
};
|
|
|
|
/**
|
|
* Parse the given `str` and return milliseconds.
|
|
*
|
|
* @param {String} str
|
|
* @return {Number}
|
|
* @api private
|
|
*/
|
|
|
|
function parse(str) {
|
|
var match = /^((?:\d+)?\.?\d+) *(ms|seconds?|s|minutes?|m|hours?|h|days?|d|years?|y)?$/i.exec(str);
|
|
if (!match) return;
|
|
var n = parseFloat(match[1]);
|
|
var type = (match[2] || 'ms').toLowerCase();
|
|
switch (type) {
|
|
case 'years':
|
|
case 'year':
|
|
case 'y':
|
|
return n * y;
|
|
case 'days':
|
|
case 'day':
|
|
case 'd':
|
|
return n * d;
|
|
case 'hours':
|
|
case 'hour':
|
|
case 'h':
|
|
return n * h;
|
|
case 'minutes':
|
|
case 'minute':
|
|
case 'm':
|
|
return n * m;
|
|
case 'seconds':
|
|
case 'second':
|
|
case 's':
|
|
return n * s;
|
|
case 'ms':
|
|
return n;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Short format for `ms`.
|
|
*
|
|
* @param {Number} ms
|
|
* @return {String}
|
|
* @api private
|
|
*/
|
|
|
|
function short(ms) {
|
|
if (ms >= d) return Math.round(ms / d) + 'd';
|
|
if (ms >= h) return Math.round(ms / h) + 'h';
|
|
if (ms >= m) return Math.round(ms / m) + 'm';
|
|
if (ms >= s) return Math.round(ms / s) + 's';
|
|
return ms + 'ms';
|
|
}
|
|
|
|
/**
|
|
* Long format for `ms`.
|
|
*
|
|
* @param {Number} ms
|
|
* @return {String}
|
|
* @api private
|
|
*/
|
|
|
|
function long(ms) {
|
|
return plural(ms, d, 'day')
|
|
|| plural(ms, h, 'hour')
|
|
|| plural(ms, m, 'minute')
|
|
|| plural(ms, s, 'second')
|
|
|| ms + ' ms';
|
|
}
|
|
|
|
/**
|
|
* Pluralization helper.
|
|
*/
|
|
|
|
function plural(ms, n, name) {
|
|
if (ms < n) return;
|
|
if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name;
|
|
return Math.ceil(ms / n) + ' ' + name + 's';
|
|
}
|
|
|
|
},{}],23:[function(require,module,exports){
|
|
/**
|
|
* Parses an URI
|
|
*
|
|
* @author Steven Levithan <stevenlevithan.com> (MIT license)
|
|
* @api private
|
|
*/
|
|
|
|
var re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
|
|
|
|
var parts = [
|
|
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
|
|
];
|
|
|
|
module.exports = function parseuri(str) {
|
|
var src = str,
|
|
b = str.indexOf('['),
|
|
e = str.indexOf(']');
|
|
|
|
if (b != -1 && e != -1) {
|
|
str = str.substring(0, b) + str.substring(b, e).replace(/:/g, ';') + str.substring(e, str.length);
|
|
}
|
|
|
|
var m = re.exec(str || ''),
|
|
uri = {},
|
|
i = 14;
|
|
|
|
while (i--) {
|
|
uri[parts[i]] = m[i] || '';
|
|
}
|
|
|
|
if (b != -1 && e != -1) {
|
|
uri.source = src;
|
|
uri.host = uri.host.substring(1, uri.host.length - 1).replace(/;/g, ':');
|
|
uri.authority = uri.authority.replace('[', '').replace(']', '').replace(/;/g, ':');
|
|
uri.ipv6uri = true;
|
|
}
|
|
|
|
return uri;
|
|
};
|
|
|
|
},{}],24:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var keys = require('./keys');
|
|
var hasBinary = require('has-binary');
|
|
var sliceBuffer = require('arraybuffer.slice');
|
|
var base64encoder = require('base64-arraybuffer');
|
|
var after = require('after');
|
|
var utf8 = require('utf8');
|
|
|
|
/**
|
|
* Check if we are running an android browser. That requires us to use
|
|
* ArrayBuffer with polling transports...
|
|
*
|
|
* http://ghinda.net/jpeg-blob-ajax-android/
|
|
*/
|
|
|
|
var isAndroid = navigator.userAgent.match(/Android/i);
|
|
|
|
/**
|
|
* Check if we are running in PhantomJS.
|
|
* Uploading a Blob with PhantomJS does not work correctly, as reported here:
|
|
* https://github.com/ariya/phantomjs/issues/11395
|
|
* @type boolean
|
|
*/
|
|
var isPhantomJS = /PhantomJS/i.test(navigator.userAgent);
|
|
|
|
/**
|
|
* When true, avoids using Blobs to encode payloads.
|
|
* @type boolean
|
|
*/
|
|
var dontSendBlobs = isAndroid || isPhantomJS;
|
|
|
|
/**
|
|
* Current protocol version.
|
|
*/
|
|
|
|
exports.protocol = 3;
|
|
|
|
/**
|
|
* Packet types.
|
|
*/
|
|
|
|
var packets = exports.packets = {
|
|
open: 0 // non-ws
|
|
, close: 1 // non-ws
|
|
, ping: 2
|
|
, pong: 3
|
|
, message: 4
|
|
, upgrade: 5
|
|
, noop: 6
|
|
};
|
|
|
|
var packetslist = keys(packets);
|
|
|
|
/**
|
|
* Premade error packet.
|
|
*/
|
|
|
|
var err = { type: 'error', data: 'parser error' };
|
|
|
|
/**
|
|
* Create a blob api even for blob builder when vendor prefixes exist
|
|
*/
|
|
|
|
var Blob = require('blob');
|
|
|
|
/**
|
|
* Encodes a packet.
|
|
*
|
|
* <packet type id> [ <data> ]
|
|
*
|
|
* Example:
|
|
*
|
|
* 5hello world
|
|
* 3
|
|
* 4
|
|
*
|
|
* Binary is encoded in an identical principle
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
exports.encodePacket = function (packet, supportsBinary, utf8encode, callback) {
|
|
if ('function' == typeof supportsBinary) {
|
|
callback = supportsBinary;
|
|
supportsBinary = false;
|
|
}
|
|
|
|
if ('function' == typeof utf8encode) {
|
|
callback = utf8encode;
|
|
utf8encode = null;
|
|
}
|
|
|
|
var data = (packet.data === undefined)
|
|
? undefined
|
|
: packet.data.buffer || packet.data;
|
|
|
|
if (global.ArrayBuffer && data instanceof ArrayBuffer) {
|
|
return encodeArrayBuffer(packet, supportsBinary, callback);
|
|
} else if (Blob && data instanceof global.Blob) {
|
|
return encodeBlob(packet, supportsBinary, callback);
|
|
}
|
|
|
|
// might be an object with { base64: true, data: dataAsBase64String }
|
|
if (data && data.base64) {
|
|
return encodeBase64Object(packet, callback);
|
|
}
|
|
|
|
// Sending data as a utf-8 string
|
|
var encoded = packets[packet.type];
|
|
|
|
// data fragment is optional
|
|
if (undefined !== packet.data) {
|
|
encoded += utf8encode ? utf8.encode(String(packet.data)) : String(packet.data);
|
|
}
|
|
|
|
return callback('' + encoded);
|
|
|
|
};
|
|
|
|
function encodeBase64Object(packet, callback) {
|
|
// packet data is an object { base64: true, data: dataAsBase64String }
|
|
var message = 'b' + exports.packets[packet.type] + packet.data.data;
|
|
return callback(message);
|
|
}
|
|
|
|
/**
|
|
* Encode packet helpers for binary types
|
|
*/
|
|
|
|
function encodeArrayBuffer(packet, supportsBinary, callback) {
|
|
if (!supportsBinary) {
|
|
return exports.encodeBase64Packet(packet, callback);
|
|
}
|
|
|
|
var data = packet.data;
|
|
var contentArray = new Uint8Array(data);
|
|
var resultBuffer = new Uint8Array(1 + data.byteLength);
|
|
|
|
resultBuffer[0] = packets[packet.type];
|
|
for (var i = 0; i < contentArray.length; i++) {
|
|
resultBuffer[i+1] = contentArray[i];
|
|
}
|
|
|
|
return callback(resultBuffer.buffer);
|
|
}
|
|
|
|
function encodeBlobAsArrayBuffer(packet, supportsBinary, callback) {
|
|
if (!supportsBinary) {
|
|
return exports.encodeBase64Packet(packet, callback);
|
|
}
|
|
|
|
var fr = new FileReader();
|
|
fr.onload = function() {
|
|
packet.data = fr.result;
|
|
exports.encodePacket(packet, supportsBinary, true, callback);
|
|
};
|
|
return fr.readAsArrayBuffer(packet.data);
|
|
}
|
|
|
|
function encodeBlob(packet, supportsBinary, callback) {
|
|
if (!supportsBinary) {
|
|
return exports.encodeBase64Packet(packet, callback);
|
|
}
|
|
|
|
if (dontSendBlobs) {
|
|
return encodeBlobAsArrayBuffer(packet, supportsBinary, callback);
|
|
}
|
|
|
|
var length = new Uint8Array(1);
|
|
length[0] = packets[packet.type];
|
|
var blob = new Blob([length.buffer, packet.data]);
|
|
|
|
return callback(blob);
|
|
}
|
|
|
|
/**
|
|
* Encodes a packet with binary data in a base64 string
|
|
*
|
|
* @param {Object} packet, has `type` and `data`
|
|
* @return {String} base64 encoded message
|
|
*/
|
|
|
|
exports.encodeBase64Packet = function(packet, callback) {
|
|
var message = 'b' + exports.packets[packet.type];
|
|
if (Blob && packet.data instanceof Blob) {
|
|
var fr = new FileReader();
|
|
fr.onload = function() {
|
|
var b64 = fr.result.split(',')[1];
|
|
callback(message + b64);
|
|
};
|
|
return fr.readAsDataURL(packet.data);
|
|
}
|
|
|
|
var b64data;
|
|
try {
|
|
b64data = String.fromCharCode.apply(null, new Uint8Array(packet.data));
|
|
} catch (e) {
|
|
// iPhone Safari doesn't let you apply with typed arrays
|
|
var typed = new Uint8Array(packet.data);
|
|
var basic = new Array(typed.length);
|
|
for (var i = 0; i < typed.length; i++) {
|
|
basic[i] = typed[i];
|
|
}
|
|
b64data = String.fromCharCode.apply(null, basic);
|
|
}
|
|
message += global.btoa(b64data);
|
|
return callback(message);
|
|
};
|
|
|
|
/**
|
|
* Decodes a packet. Changes format to Blob if requested.
|
|
*
|
|
* @return {Object} with `type` and `data` (if any)
|
|
* @api private
|
|
*/
|
|
|
|
exports.decodePacket = function (data, binaryType, utf8decode) {
|
|
// String data
|
|
if (typeof data == 'string' || data === undefined) {
|
|
if (data.charAt(0) == 'b') {
|
|
return exports.decodeBase64Packet(data.substr(1), binaryType);
|
|
}
|
|
|
|
if (utf8decode) {
|
|
try {
|
|
data = utf8.decode(data);
|
|
} catch (e) {
|
|
return err;
|
|
}
|
|
}
|
|
var type = data.charAt(0);
|
|
|
|
if (Number(type) != type || !packetslist[type]) {
|
|
return err;
|
|
}
|
|
|
|
if (data.length > 1) {
|
|
return { type: packetslist[type], data: data.substring(1) };
|
|
} else {
|
|
return { type: packetslist[type] };
|
|
}
|
|
}
|
|
|
|
var asArray = new Uint8Array(data);
|
|
var type = asArray[0];
|
|
var rest = sliceBuffer(data, 1);
|
|
if (Blob && binaryType === 'blob') {
|
|
rest = new Blob([rest]);
|
|
}
|
|
return { type: packetslist[type], data: rest };
|
|
};
|
|
|
|
/**
|
|
* Decodes a packet encoded in a base64 string
|
|
*
|
|
* @param {String} base64 encoded message
|
|
* @return {Object} with `type` and `data` (if any)
|
|
*/
|
|
|
|
exports.decodeBase64Packet = function(msg, binaryType) {
|
|
var type = packetslist[msg.charAt(0)];
|
|
if (!global.ArrayBuffer) {
|
|
return { type: type, data: { base64: true, data: msg.substr(1) } };
|
|
}
|
|
|
|
var data = base64encoder.decode(msg.substr(1));
|
|
|
|
if (binaryType === 'blob' && Blob) {
|
|
data = new Blob([data]);
|
|
}
|
|
|
|
return { type: type, data: data };
|
|
};
|
|
|
|
/**
|
|
* Encodes multiple messages (payload).
|
|
*
|
|
* <length>:data
|
|
*
|
|
* Example:
|
|
*
|
|
* 11:hello world2:hi
|
|
*
|
|
* If any contents are binary, they will be encoded as base64 strings. Base64
|
|
* encoded strings are marked with a b before the length specifier
|
|
*
|
|
* @param {Array} packets
|
|
* @api private
|
|
*/
|
|
|
|
exports.encodePayload = function (packets, supportsBinary, callback) {
|
|
if (typeof supportsBinary == 'function') {
|
|
callback = supportsBinary;
|
|
supportsBinary = null;
|
|
}
|
|
|
|
var isBinary = hasBinary(packets);
|
|
|
|
if (supportsBinary && isBinary) {
|
|
if (Blob && !dontSendBlobs) {
|
|
return exports.encodePayloadAsBlob(packets, callback);
|
|
}
|
|
|
|
return exports.encodePayloadAsArrayBuffer(packets, callback);
|
|
}
|
|
|
|
if (!packets.length) {
|
|
return callback('0:');
|
|
}
|
|
|
|
function setLengthHeader(message) {
|
|
return message.length + ':' + message;
|
|
}
|
|
|
|
function encodeOne(packet, doneCallback) {
|
|
exports.encodePacket(packet, !isBinary ? false : supportsBinary, true, function(message) {
|
|
doneCallback(null, setLengthHeader(message));
|
|
});
|
|
}
|
|
|
|
map(packets, encodeOne, function(err, results) {
|
|
return callback(results.join(''));
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Async array map using after
|
|
*/
|
|
|
|
function map(ary, each, done) {
|
|
var result = new Array(ary.length);
|
|
var next = after(ary.length, done);
|
|
|
|
var eachWithIndex = function(i, el, cb) {
|
|
each(el, function(error, msg) {
|
|
result[i] = msg;
|
|
cb(error, result);
|
|
});
|
|
};
|
|
|
|
for (var i = 0; i < ary.length; i++) {
|
|
eachWithIndex(i, ary[i], next);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Decodes data when a payload is maybe expected. Possible binary contents are
|
|
* decoded from their base64 representation
|
|
*
|
|
* @param {String} data, callback method
|
|
* @api public
|
|
*/
|
|
|
|
exports.decodePayload = function (data, binaryType, callback) {
|
|
if (typeof data != 'string') {
|
|
return exports.decodePayloadAsBinary(data, binaryType, callback);
|
|
}
|
|
|
|
if (typeof binaryType === 'function') {
|
|
callback = binaryType;
|
|
binaryType = null;
|
|
}
|
|
|
|
var packet;
|
|
if (data == '') {
|
|
// parser error - ignoring payload
|
|
return callback(err, 0, 1);
|
|
}
|
|
|
|
var length = ''
|
|
, n, msg;
|
|
|
|
for (var i = 0, l = data.length; i < l; i++) {
|
|
var chr = data.charAt(i);
|
|
|
|
if (':' != chr) {
|
|
length += chr;
|
|
} else {
|
|
if ('' == length || (length != (n = Number(length)))) {
|
|
// parser error - ignoring payload
|
|
return callback(err, 0, 1);
|
|
}
|
|
|
|
msg = data.substr(i + 1, n);
|
|
|
|
if (length != msg.length) {
|
|
// parser error - ignoring payload
|
|
return callback(err, 0, 1);
|
|
}
|
|
|
|
if (msg.length) {
|
|
packet = exports.decodePacket(msg, binaryType, true);
|
|
|
|
if (err.type == packet.type && err.data == packet.data) {
|
|
// parser error in individual packet - ignoring payload
|
|
return callback(err, 0, 1);
|
|
}
|
|
|
|
var ret = callback(packet, i + n, l);
|
|
if (false === ret) return;
|
|
}
|
|
|
|
// advance cursor
|
|
i += n;
|
|
length = '';
|
|
}
|
|
}
|
|
|
|
if (length != '') {
|
|
// parser error - ignoring payload
|
|
return callback(err, 0, 1);
|
|
}
|
|
|
|
};
|
|
|
|
/**
|
|
* Encodes multiple messages (payload) as binary.
|
|
*
|
|
* <1 = binary, 0 = string><number from 0-9><number from 0-9>[...]<number
|
|
* 255><data>
|
|
*
|
|
* Example:
|
|
* 1 3 255 1 2 3, if the binary contents are interpreted as 8 bit integers
|
|
*
|
|
* @param {Array} packets
|
|
* @return {ArrayBuffer} encoded payload
|
|
* @api private
|
|
*/
|
|
|
|
exports.encodePayloadAsArrayBuffer = function(packets, callback) {
|
|
if (!packets.length) {
|
|
return callback(new ArrayBuffer(0));
|
|
}
|
|
|
|
function encodeOne(packet, doneCallback) {
|
|
exports.encodePacket(packet, true, true, function(data) {
|
|
return doneCallback(null, data);
|
|
});
|
|
}
|
|
|
|
map(packets, encodeOne, function(err, encodedPackets) {
|
|
var totalLength = encodedPackets.reduce(function(acc, p) {
|
|
var len;
|
|
if (typeof p === 'string'){
|
|
len = p.length;
|
|
} else {
|
|
len = p.byteLength;
|
|
}
|
|
return acc + len.toString().length + len + 2; // string/binary identifier + separator = 2
|
|
}, 0);
|
|
|
|
var resultArray = new Uint8Array(totalLength);
|
|
|
|
var bufferIndex = 0;
|
|
encodedPackets.forEach(function(p) {
|
|
var isString = typeof p === 'string';
|
|
var ab = p;
|
|
if (isString) {
|
|
var view = new Uint8Array(p.length);
|
|
for (var i = 0; i < p.length; i++) {
|
|
view[i] = p.charCodeAt(i);
|
|
}
|
|
ab = view.buffer;
|
|
}
|
|
|
|
if (isString) { // not true binary
|
|
resultArray[bufferIndex++] = 0;
|
|
} else { // true binary
|
|
resultArray[bufferIndex++] = 1;
|
|
}
|
|
|
|
var lenStr = ab.byteLength.toString();
|
|
for (var i = 0; i < lenStr.length; i++) {
|
|
resultArray[bufferIndex++] = parseInt(lenStr[i]);
|
|
}
|
|
resultArray[bufferIndex++] = 255;
|
|
|
|
var view = new Uint8Array(ab);
|
|
for (var i = 0; i < view.length; i++) {
|
|
resultArray[bufferIndex++] = view[i];
|
|
}
|
|
});
|
|
|
|
return callback(resultArray.buffer);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Encode as Blob
|
|
*/
|
|
|
|
exports.encodePayloadAsBlob = function(packets, callback) {
|
|
function encodeOne(packet, doneCallback) {
|
|
exports.encodePacket(packet, true, true, function(encoded) {
|
|
var binaryIdentifier = new Uint8Array(1);
|
|
binaryIdentifier[0] = 1;
|
|
if (typeof encoded === 'string') {
|
|
var view = new Uint8Array(encoded.length);
|
|
for (var i = 0; i < encoded.length; i++) {
|
|
view[i] = encoded.charCodeAt(i);
|
|
}
|
|
encoded = view.buffer;
|
|
binaryIdentifier[0] = 0;
|
|
}
|
|
|
|
var len = (encoded instanceof ArrayBuffer)
|
|
? encoded.byteLength
|
|
: encoded.size;
|
|
|
|
var lenStr = len.toString();
|
|
var lengthAry = new Uint8Array(lenStr.length + 1);
|
|
for (var i = 0; i < lenStr.length; i++) {
|
|
lengthAry[i] = parseInt(lenStr[i]);
|
|
}
|
|
lengthAry[lenStr.length] = 255;
|
|
|
|
if (Blob) {
|
|
var blob = new Blob([binaryIdentifier.buffer, lengthAry.buffer, encoded]);
|
|
doneCallback(null, blob);
|
|
}
|
|
});
|
|
}
|
|
|
|
map(packets, encodeOne, function(err, results) {
|
|
return callback(new Blob(results));
|
|
});
|
|
};
|
|
|
|
/*
|
|
* Decodes data when a payload is maybe expected. Strings are decoded by
|
|
* interpreting each byte as a key code for entries marked to start with 0. See
|
|
* description of encodePayloadAsBinary
|
|
*
|
|
* @param {ArrayBuffer} data, callback method
|
|
* @api public
|
|
*/
|
|
|
|
exports.decodePayloadAsBinary = function (data, binaryType, callback) {
|
|
if (typeof binaryType === 'function') {
|
|
callback = binaryType;
|
|
binaryType = null;
|
|
}
|
|
|
|
var bufferTail = data;
|
|
var buffers = [];
|
|
|
|
var numberTooLong = false;
|
|
while (bufferTail.byteLength > 0) {
|
|
var tailArray = new Uint8Array(bufferTail);
|
|
var isString = tailArray[0] === 0;
|
|
var msgLength = '';
|
|
|
|
for (var i = 1; ; i++) {
|
|
if (tailArray[i] == 255) break;
|
|
|
|
if (msgLength.length > 310) {
|
|
numberTooLong = true;
|
|
break;
|
|
}
|
|
|
|
msgLength += tailArray[i];
|
|
}
|
|
|
|
if(numberTooLong) return callback(err, 0, 1);
|
|
|
|
bufferTail = sliceBuffer(bufferTail, 2 + msgLength.length);
|
|
msgLength = parseInt(msgLength);
|
|
|
|
var msg = sliceBuffer(bufferTail, 0, msgLength);
|
|
if (isString) {
|
|
try {
|
|
msg = String.fromCharCode.apply(null, new Uint8Array(msg));
|
|
} catch (e) {
|
|
// iPhone Safari doesn't let you apply to typed arrays
|
|
var typed = new Uint8Array(msg);
|
|
msg = '';
|
|
for (var i = 0; i < typed.length; i++) {
|
|
msg += String.fromCharCode(typed[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
buffers.push(msg);
|
|
bufferTail = sliceBuffer(bufferTail, msgLength);
|
|
}
|
|
|
|
var total = buffers.length;
|
|
buffers.forEach(function(buffer, i) {
|
|
callback(exports.decodePacket(buffer, binaryType, true), i, total);
|
|
});
|
|
};
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./keys":25,"after":1,"arraybuffer.slice":2,"base64-arraybuffer":5,"blob":6,"has-binary":41,"utf8":86}],25:[function(require,module,exports){
|
|
|
|
/**
|
|
* Gets the keys for an object.
|
|
*
|
|
* @return {Array} keys
|
|
* @api private
|
|
*/
|
|
|
|
module.exports = Object.keys || function keys (obj){
|
|
var arr = [];
|
|
var has = Object.prototype.hasOwnProperty;
|
|
|
|
for (var i in obj) {
|
|
if (has.call(obj, i)) {
|
|
arr.push(i);
|
|
}
|
|
}
|
|
return arr;
|
|
};
|
|
|
|
},{}],26:[function(require,module,exports){
|
|
var WildEmitter = require('wildemitter');
|
|
var util = require('util');
|
|
|
|
function Sender(opts) {
|
|
WildEmitter.call(this);
|
|
var options = opts || {};
|
|
this.config = {
|
|
chunksize: 16384,
|
|
pacing: 0
|
|
};
|
|
// set our config from options
|
|
var item;
|
|
for (item in options) {
|
|
this.config[item] = options[item];
|
|
}
|
|
|
|
this.file = null;
|
|
this.channel = null;
|
|
}
|
|
util.inherits(Sender, WildEmitter);
|
|
|
|
Sender.prototype.send = function (file, channel) {
|
|
var self = this;
|
|
this.file = file;
|
|
this.channel = channel;
|
|
var usePoll = typeof channel.bufferedAmountLowThreshold !== 'number';
|
|
var offset = 0;
|
|
var sliceFile = function() {
|
|
var reader = new window.FileReader();
|
|
reader.onload = (function() {
|
|
return function(e) {
|
|
self.channel.send(e.target.result);
|
|
self.emit('progress', offset, file.size, e.target.result);
|
|
|
|
if (file.size > offset + e.target.result.byteLength) {
|
|
if (usePoll) {
|
|
window.setTimeout(sliceFile, self.config.pacing);
|
|
} else if (channel.bufferedAmount <= channel.bufferedAmountLowThreshold) {
|
|
window.setTimeout(sliceFile, 0);
|
|
} else {
|
|
// wait for bufferedAmountLow to fire
|
|
}
|
|
} else {
|
|
self.emit('progress', file.size, file.size, null);
|
|
self.emit('sentFile');
|
|
}
|
|
offset = offset + self.config.chunksize;
|
|
};
|
|
})(file);
|
|
var slice = file.slice(offset, offset + self.config.chunksize);
|
|
reader.readAsArrayBuffer(slice);
|
|
};
|
|
if (!usePoll) {
|
|
channel.bufferedAmountLowThreshold = 8 * this.config.chunksize;
|
|
channel.addEventListener('bufferedamountlow', sliceFile);
|
|
}
|
|
window.setTimeout(sliceFile, 0);
|
|
};
|
|
|
|
function Receiver() {
|
|
WildEmitter.call(this);
|
|
|
|
this.receiveBuffer = [];
|
|
this.received = 0;
|
|
this.metadata = {};
|
|
this.channel = null;
|
|
}
|
|
util.inherits(Receiver, WildEmitter);
|
|
|
|
Receiver.prototype.receive = function (metadata, channel) {
|
|
var self = this;
|
|
|
|
if (metadata) {
|
|
this.metadata = metadata;
|
|
}
|
|
this.channel = channel;
|
|
// chrome only supports arraybuffers and those make it easier to calc the hash
|
|
channel.binaryType = 'arraybuffer';
|
|
this.channel.onmessage = function (event) {
|
|
var len = event.data.byteLength;
|
|
self.received += len;
|
|
self.receiveBuffer.push(event.data);
|
|
|
|
self.emit('progress', self.received, self.metadata.size, event.data);
|
|
if (self.received === self.metadata.size) {
|
|
self.emit('receivedFile', new window.Blob(self.receiveBuffer), self.metadata);
|
|
self.receiveBuffer = []; // discard receivebuffer
|
|
} else if (self.received > self.metadata.size) {
|
|
// FIXME
|
|
console.error('received more than expected, discarding...');
|
|
self.receiveBuffer = []; // just discard...
|
|
|
|
}
|
|
};
|
|
};
|
|
|
|
module.exports = {};
|
|
module.exports.support = typeof window !== 'undefined' && window && window.File && window.FileReader && window.Blob;
|
|
module.exports.Sender = Sender;
|
|
module.exports.Receiver = Receiver;
|
|
|
|
},{"util":89,"wildemitter":100}],27:[function(require,module,exports){
|
|
// getScreenMedia helper by @HenrikJoreteg
|
|
var getUserMedia = require('getusermedia');
|
|
|
|
// cache for constraints and callback
|
|
var cache = {};
|
|
|
|
module.exports = function (mode, constraints, cb) {
|
|
var hasConstraints = arguments.length === 3;
|
|
var callback = hasConstraints ? cb : constraints;
|
|
var error;
|
|
|
|
if (typeof window === 'undefined' || window.location.protocol === 'http:') {
|
|
error = new Error('NavigatorUserMediaError');
|
|
error.name = 'HTTPS_REQUIRED';
|
|
return callback(error);
|
|
}
|
|
|
|
if (window.navigator.userAgent.match('Chrome')) {
|
|
var chromever = parseInt(window.navigator.userAgent.match(/Chrome\/(\d+)\./)[1], 10);
|
|
var maxver = 33;
|
|
// Chrome 71 dropped support for "window.chrome.webstore;".
|
|
var isCef = (chromever < 71) && !window.chrome.webstore;
|
|
// "known" crash in chrome 34 and 35 on linux
|
|
if (window.navigator.userAgent.match('Linux')) maxver = 35;
|
|
|
|
// check that the extension is installed by looking for a
|
|
// sessionStorage variable that contains the extension id
|
|
// this has to be set after installation unless the contest
|
|
// script does that
|
|
if (sessionStorage.getScreenMediaJSExtensionId) {
|
|
chrome.runtime.sendMessage(sessionStorage.getScreenMediaJSExtensionId,
|
|
{type:'getScreen', id: 1}, null,
|
|
function (data) {
|
|
if (!data || data.sourceId === '') { // user canceled
|
|
var error = new Error('NavigatorUserMediaError');
|
|
error.name = 'PERMISSION_DENIED';
|
|
callback(error);
|
|
} else {
|
|
constraints = (hasConstraints && constraints) || {audio: false, video: {
|
|
mandatory: {
|
|
chromeMediaSource: 'desktop',
|
|
maxWidth: window.screen.width,
|
|
maxHeight: window.screen.height,
|
|
maxFrameRate: 3
|
|
}
|
|
}};
|
|
constraints.video.mandatory.chromeMediaSourceId = data.sourceId;
|
|
getUserMedia(constraints, callback);
|
|
}
|
|
}
|
|
);
|
|
} else if (window.cefGetScreenMedia) {
|
|
//window.cefGetScreenMedia is experimental - may be removed without notice
|
|
window.cefGetScreenMedia(function(sourceId) {
|
|
if (!sourceId) {
|
|
var error = new Error('cefGetScreenMediaError');
|
|
error.name = 'CEF_GETSCREENMEDIA_CANCELED';
|
|
callback(error);
|
|
} else {
|
|
constraints = (hasConstraints && constraints) || {audio: false, video: {
|
|
mandatory: {
|
|
chromeMediaSource: 'desktop',
|
|
maxWidth: window.screen.width,
|
|
maxHeight: window.screen.height,
|
|
maxFrameRate: 3
|
|
},
|
|
optional: [
|
|
{googLeakyBucket: true},
|
|
{googTemporalLayeredScreencast: true}
|
|
]
|
|
}};
|
|
constraints.video.mandatory.chromeMediaSourceId = sourceId;
|
|
getUserMedia(constraints, callback);
|
|
}
|
|
});
|
|
} else if (isCef || (chromever >= 26 && chromever <= maxver)) {
|
|
// chrome 26 - chrome 33 way to do it -- requires bad chrome://flags
|
|
// note: this is basically in maintenance mode and will go away soon
|
|
constraints = (hasConstraints && constraints) || {
|
|
video: {
|
|
mandatory: {
|
|
googLeakyBucket: true,
|
|
maxWidth: window.screen.width,
|
|
maxHeight: window.screen.height,
|
|
maxFrameRate: 3,
|
|
chromeMediaSource: 'screen'
|
|
}
|
|
}
|
|
};
|
|
getUserMedia(constraints, callback);
|
|
} else {
|
|
// chrome 34+ way requiring an extension
|
|
var pending = window.setTimeout(function () {
|
|
error = new Error('NavigatorUserMediaError');
|
|
error.name = 'EXTENSION_UNAVAILABLE';
|
|
return callback(error);
|
|
}, 1000);
|
|
cache[pending] = [callback, hasConstraints ? constraints : null];
|
|
window.postMessage({ type: 'getScreen', id: pending }, '*');
|
|
}
|
|
} else if (window.navigator.userAgent.match('Firefox')) {
|
|
var ffver = parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10);
|
|
if (ffver >= 52) {
|
|
mode = mode || 'window';
|
|
constraints = (hasConstraints && constraints) || {
|
|
video: {
|
|
mozMediaSource: mode,
|
|
mediaSource: mode
|
|
}
|
|
};
|
|
getUserMedia(constraints, function (err, stream) {
|
|
callback(err, stream);
|
|
if (err) {
|
|
return;
|
|
}
|
|
// workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1045810
|
|
var lastTime = stream.currentTime;
|
|
var polly = window.setInterval(function () {
|
|
if (!stream) window.clearInterval(polly);
|
|
if (stream.currentTime == lastTime) {
|
|
window.clearInterval(polly);
|
|
if (stream.onended) {
|
|
stream.onended();
|
|
}
|
|
}
|
|
lastTime = stream.currentTime;
|
|
}, 500);
|
|
});
|
|
} else {
|
|
error = new Error('NavigatorUserMediaError');
|
|
error.name = 'FF52_REQUIRED';
|
|
return callback(error);
|
|
}
|
|
}
|
|
};
|
|
|
|
typeof window !== 'undefined' && window.addEventListener('message', function (event) {
|
|
if (event.origin != window.location.origin && !event.isTrusted) {
|
|
return;
|
|
}
|
|
if (event.data.type == 'gotScreen' && cache[event.data.id]) {
|
|
var data = cache[event.data.id];
|
|
var constraints = data[1];
|
|
var callback = data[0];
|
|
delete cache[event.data.id];
|
|
|
|
if (event.data.sourceId === '') { // user canceled
|
|
var error = new Error('NavigatorUserMediaError');
|
|
error.name = 'PERMISSION_DENIED';
|
|
callback(error);
|
|
} else {
|
|
constraints = constraints || {audio: false, video: {
|
|
mandatory: {
|
|
chromeMediaSource: 'desktop',
|
|
maxWidth: window.screen.width,
|
|
maxHeight: window.screen.height,
|
|
maxFrameRate: 3
|
|
},
|
|
optional: [
|
|
{googLeakyBucket: true},
|
|
{googTemporalLayeredScreencast: true}
|
|
]
|
|
}};
|
|
constraints.video.mandatory.chromeMediaSourceId = event.data.sourceId;
|
|
getUserMedia(constraints, callback);
|
|
}
|
|
} else if (event.data.type == 'getScreenPending') {
|
|
window.clearTimeout(event.data.id);
|
|
}
|
|
});
|
|
|
|
},{"getusermedia":28}],28:[function(require,module,exports){
|
|
// getUserMedia helper by @HenrikJoreteg used for navigator.getUserMedia shim
|
|
var adapter = require('webrtc-adapter');
|
|
|
|
module.exports = function (constraints, cb) {
|
|
var error;
|
|
var haveOpts = arguments.length === 2;
|
|
var defaultOpts = {video: true, audio: true};
|
|
|
|
var denied = 'PermissionDeniedError';
|
|
var altDenied = 'PERMISSION_DENIED';
|
|
var notSatisfied = 'ConstraintNotSatisfiedError';
|
|
|
|
// make constraints optional
|
|
if (!haveOpts) {
|
|
cb = constraints;
|
|
constraints = defaultOpts;
|
|
}
|
|
|
|
// treat lack of browser support like an error
|
|
if (typeof navigator === 'undefined' || !navigator.getUserMedia) {
|
|
// throw proper error per spec
|
|
error = new Error('MediaStreamError');
|
|
error.name = 'NotSupportedError';
|
|
|
|
// keep all callbacks async
|
|
return 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 setTimeout(function () {
|
|
cb(error);
|
|
}, 0);
|
|
}
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|
.then(function (stream) {
|
|
cb(null, stream);
|
|
}).catch(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 || err === altDenied) {
|
|
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);
|
|
});
|
|
};
|
|
|
|
},{"webrtc-adapter":90}],29:[function(require,module,exports){
|
|
// getUserMedia helper by @HenrikJoreteg used for navigator.getUserMedia shim
|
|
var adapter = require('webrtc-adapter');
|
|
|
|
module.exports = function (constraints, cb) {
|
|
var error;
|
|
var haveOpts = arguments.length === 2;
|
|
var defaultOpts = {video: true, audio: true};
|
|
|
|
var denied = 'PermissionDeniedError';
|
|
var altDenied = 'PERMISSION_DENIED';
|
|
var notSatisfied = 'ConstraintNotSatisfiedError';
|
|
|
|
// make constraints optional
|
|
if (!haveOpts) {
|
|
cb = constraints;
|
|
constraints = defaultOpts;
|
|
}
|
|
|
|
// treat lack of browser support like an error
|
|
if (typeof navigator === 'undefined' || !navigator.getUserMedia) {
|
|
// throw proper error per spec
|
|
error = new Error('MediaStreamError');
|
|
error.name = 'NotSupportedError';
|
|
|
|
// keep all callbacks async
|
|
return 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 setTimeout(function () {
|
|
cb(error);
|
|
}, 0);
|
|
}
|
|
|
|
// testing support -- note: using the about:config pref is better
|
|
// for Firefox 39+, this might get removed in the future
|
|
if (localStorage && localStorage.useFirefoxFakeDevice === 'true') {
|
|
constraints.fake = true;
|
|
}
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|
.then(function (stream) {
|
|
cb(null, stream);
|
|
}).catch(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 || err === altDenied) {
|
|
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);
|
|
});
|
|
};
|
|
|
|
},{"webrtc-adapter":30}],30:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
|
|
'use strict';
|
|
|
|
// Shimming starts here.
|
|
(function() {
|
|
// Utils.
|
|
var logging = require('./utils').log;
|
|
var browserDetails = require('./utils').browserDetails;
|
|
// Export to the adapter global object visible in the browser.
|
|
module.exports.browserDetails = browserDetails;
|
|
module.exports.extractVersion = require('./utils').extractVersion;
|
|
module.exports.disableLog = require('./utils').disableLog;
|
|
|
|
// Uncomment the line below if you want logging to occur, including logging
|
|
// for the switch statement below. Can also be turned on in the browser via
|
|
// adapter.disableLog(false), but then logging from the switch statement below
|
|
// will not appear.
|
|
// require('./utils').disableLog(false);
|
|
|
|
// Browser shims.
|
|
var chromeShim = require('./chrome/chrome_shim') || null;
|
|
var edgeShim = require('./edge/edge_shim') || null;
|
|
var firefoxShim = require('./firefox/firefox_shim') || null;
|
|
var safariShim = require('./safari/safari_shim') || null;
|
|
|
|
// Shim browser if found.
|
|
switch (browserDetails.browser) {
|
|
case 'opera': // fallthrough as it uses chrome shims
|
|
case 'chrome':
|
|
if (!chromeShim || !chromeShim.shimPeerConnection) {
|
|
logging('Chrome shim is not included in this adapter release.');
|
|
return;
|
|
}
|
|
logging('adapter.js shimming chrome.');
|
|
// Export to the adapter global object visible in the browser.
|
|
module.exports.browserShim = chromeShim;
|
|
|
|
chromeShim.shimGetUserMedia();
|
|
chromeShim.shimMediaStream();
|
|
chromeShim.shimSourceObject();
|
|
chromeShim.shimPeerConnection();
|
|
chromeShim.shimOnTrack();
|
|
break;
|
|
case 'firefox':
|
|
if (!firefoxShim || !firefoxShim.shimPeerConnection) {
|
|
logging('Firefox shim is not included in this adapter release.');
|
|
return;
|
|
}
|
|
logging('adapter.js shimming firefox.');
|
|
// Export to the adapter global object visible in the browser.
|
|
module.exports.browserShim = firefoxShim;
|
|
|
|
firefoxShim.shimGetUserMedia();
|
|
firefoxShim.shimSourceObject();
|
|
firefoxShim.shimPeerConnection();
|
|
firefoxShim.shimOnTrack();
|
|
break;
|
|
case 'edge':
|
|
if (!edgeShim || !edgeShim.shimPeerConnection) {
|
|
logging('MS edge shim is not included in this adapter release.');
|
|
return;
|
|
}
|
|
logging('adapter.js shimming edge.');
|
|
// Export to the adapter global object visible in the browser.
|
|
module.exports.browserShim = edgeShim;
|
|
|
|
edgeShim.shimGetUserMedia();
|
|
edgeShim.shimPeerConnection();
|
|
break;
|
|
case 'safari':
|
|
if (!safariShim) {
|
|
logging('Safari shim is not included in this adapter release.');
|
|
return;
|
|
}
|
|
logging('adapter.js shimming safari.');
|
|
// Export to the adapter global object visible in the browser.
|
|
module.exports.browserShim = safariShim;
|
|
|
|
safariShim.shimGetUserMedia();
|
|
break;
|
|
default:
|
|
logging('Unsupported browser!');
|
|
}
|
|
})();
|
|
|
|
},{"./chrome/chrome_shim":31,"./edge/edge_shim":33,"./firefox/firefox_shim":35,"./safari/safari_shim":37,"./utils":38}],31:[function(require,module,exports){
|
|
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
var logging = require('../utils.js').log;
|
|
var browserDetails = require('../utils.js').browserDetails;
|
|
|
|
var chromeShim = {
|
|
shimMediaStream: function() {
|
|
window.MediaStream = window.MediaStream || window.webkitMediaStream;
|
|
},
|
|
|
|
shimOnTrack: function() {
|
|
if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
|
|
window.RTCPeerConnection.prototype)) {
|
|
Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
|
|
get: function() {
|
|
return this._ontrack;
|
|
},
|
|
set: function(f) {
|
|
var self = this;
|
|
if (this._ontrack) {
|
|
this.removeEventListener('track', this._ontrack);
|
|
this.removeEventListener('addstream', this._ontrackpoly);
|
|
}
|
|
this.addEventListener('track', this._ontrack = f);
|
|
this.addEventListener('addstream', this._ontrackpoly = function(e) {
|
|
// onaddstream does not fire when a track is added to an existing
|
|
// stream. But stream.onaddtrack is implemented so we use that.
|
|
e.stream.addEventListener('addtrack', function(te) {
|
|
var event = new Event('track');
|
|
event.track = te.track;
|
|
event.receiver = {track: te.track};
|
|
event.streams = [e.stream];
|
|
self.dispatchEvent(event);
|
|
});
|
|
e.stream.getTracks().forEach(function(track) {
|
|
var event = new Event('track');
|
|
event.track = track;
|
|
event.receiver = {track: track};
|
|
event.streams = [e.stream];
|
|
this.dispatchEvent(event);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
shimSourceObject: function() {
|
|
if (typeof window === 'object') {
|
|
if (window.HTMLMediaElement &&
|
|
!('srcObject' in window.HTMLMediaElement.prototype)) {
|
|
// Shim the srcObject property, once, when HTMLMediaElement is found.
|
|
Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
|
|
get: function() {
|
|
return this._srcObject;
|
|
},
|
|
set: function(stream) {
|
|
var self = this;
|
|
// Use _srcObject as a private property for this shim
|
|
this._srcObject = stream;
|
|
if (this.src) {
|
|
URL.revokeObjectURL(this.src);
|
|
}
|
|
|
|
if (!stream) {
|
|
this.src = '';
|
|
return;
|
|
}
|
|
this.src = URL.createObjectURL(stream);
|
|
// We need to recreate the blob url when a track is added or
|
|
// removed. Doing it manually since we want to avoid a recursion.
|
|
stream.addEventListener('addtrack', function() {
|
|
if (self.src) {
|
|
URL.revokeObjectURL(self.src);
|
|
}
|
|
self.src = URL.createObjectURL(stream);
|
|
});
|
|
stream.addEventListener('removetrack', function() {
|
|
if (self.src) {
|
|
URL.revokeObjectURL(self.src);
|
|
}
|
|
self.src = URL.createObjectURL(stream);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
shimPeerConnection: function() {
|
|
// The RTCPeerConnection object.
|
|
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
|
|
// Translate iceTransportPolicy to iceTransports,
|
|
// see https://code.google.com/p/webrtc/issues/detail?id=4869
|
|
logging('PeerConnection');
|
|
if (pcConfig && pcConfig.iceTransportPolicy) {
|
|
pcConfig.iceTransports = pcConfig.iceTransportPolicy;
|
|
}
|
|
|
|
var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints);
|
|
var origGetStats = pc.getStats.bind(pc);
|
|
pc.getStats = function(selector, successCallback, errorCallback) {
|
|
var self = this;
|
|
var args = arguments;
|
|
|
|
// If selector is a function then we are in the old style stats so just
|
|
// pass back the original getStats format to avoid breaking old users.
|
|
if (arguments.length > 0 && typeof selector === 'function') {
|
|
return origGetStats(selector, successCallback);
|
|
}
|
|
|
|
var fixChromeStats_ = function(response) {
|
|
var standardReport = {};
|
|
var reports = response.result();
|
|
reports.forEach(function(report) {
|
|
var standardStats = {
|
|
id: report.id,
|
|
timestamp: report.timestamp,
|
|
type: report.type
|
|
};
|
|
report.names().forEach(function(name) {
|
|
standardStats[name] = report.stat(name);
|
|
});
|
|
standardReport[standardStats.id] = standardStats;
|
|
});
|
|
|
|
return standardReport;
|
|
};
|
|
|
|
// shim getStats with maplike support
|
|
var makeMapStats = function(stats, legacyStats) {
|
|
var map = new Map(Object.keys(stats).map(function(key) {
|
|
return[key, stats[key]];
|
|
}));
|
|
legacyStats = legacyStats || stats;
|
|
Object.keys(legacyStats).forEach(function(key) {
|
|
map[key] = legacyStats[key];
|
|
});
|
|
return map;
|
|
};
|
|
|
|
if (arguments.length >= 2) {
|
|
var successCallbackWrapper_ = function(response) {
|
|
args[1](makeMapStats(fixChromeStats_(response)));
|
|
};
|
|
|
|
return origGetStats.apply(this, [successCallbackWrapper_,
|
|
arguments[0]]);
|
|
}
|
|
|
|
// promise-support
|
|
return new Promise(function(resolve, reject) {
|
|
if (args.length === 1 && typeof selector === 'object') {
|
|
origGetStats.apply(self, [
|
|
function(response) {
|
|
resolve(makeMapStats(fixChromeStats_(response)));
|
|
}, reject]);
|
|
} else {
|
|
// Preserve legacy chrome stats only on legacy access of stats obj
|
|
origGetStats.apply(self, [
|
|
function(response) {
|
|
resolve(makeMapStats(fixChromeStats_(response),
|
|
response.result()));
|
|
}, reject]);
|
|
}
|
|
}).then(successCallback, errorCallback);
|
|
};
|
|
|
|
return pc;
|
|
};
|
|
window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype;
|
|
|
|
// wrap static methods. Currently just generateCertificate.
|
|
if (webkitRTCPeerConnection.generateCertificate) {
|
|
Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
|
|
get: function() {
|
|
return webkitRTCPeerConnection.generateCertificate;
|
|
}
|
|
});
|
|
}
|
|
|
|
['createOffer', 'createAnswer'].forEach(function(method) {
|
|
var nativeMethod = webkitRTCPeerConnection.prototype[method];
|
|
webkitRTCPeerConnection.prototype[method] = function() {
|
|
var self = this;
|
|
if (arguments.length < 1 || (arguments.length === 1 &&
|
|
typeof arguments[0] === 'object')) {
|
|
var opts = arguments.length === 1 ? arguments[0] : undefined;
|
|
return new Promise(function(resolve, reject) {
|
|
nativeMethod.apply(self, [resolve, reject, opts]);
|
|
});
|
|
}
|
|
return nativeMethod.apply(this, arguments);
|
|
};
|
|
});
|
|
|
|
// add promise support -- natively available in Chrome 51
|
|
if (browserDetails.version < 51) {
|
|
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
|
|
.forEach(function(method) {
|
|
var nativeMethod = webkitRTCPeerConnection.prototype[method];
|
|
webkitRTCPeerConnection.prototype[method] = function() {
|
|
var args = arguments;
|
|
var self = this;
|
|
var promise = new Promise(function(resolve, reject) {
|
|
nativeMethod.apply(self, [args[0], resolve, reject]);
|
|
});
|
|
if (args.length < 2) {
|
|
return promise;
|
|
}
|
|
return promise.then(function() {
|
|
args[1].apply(null, []);
|
|
},
|
|
function(err) {
|
|
if (args.length >= 3) {
|
|
args[2].apply(null, [err]);
|
|
}
|
|
});
|
|
};
|
|
});
|
|
}
|
|
|
|
// support for addIceCandidate(null)
|
|
var nativeAddIceCandidate =
|
|
RTCPeerConnection.prototype.addIceCandidate;
|
|
RTCPeerConnection.prototype.addIceCandidate = function() {
|
|
return arguments[0] === null ? Promise.resolve()
|
|
: nativeAddIceCandidate.apply(this, arguments);
|
|
};
|
|
|
|
// shim implicit creation of RTCSessionDescription/RTCIceCandidate
|
|
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
|
|
.forEach(function(method) {
|
|
var nativeMethod = webkitRTCPeerConnection.prototype[method];
|
|
webkitRTCPeerConnection.prototype[method] = function() {
|
|
arguments[0] = new ((method === 'addIceCandidate') ?
|
|
RTCIceCandidate : RTCSessionDescription)(arguments[0]);
|
|
return nativeMethod.apply(this, arguments);
|
|
};
|
|
});
|
|
},
|
|
|
|
// Attach a media stream to an element.
|
|
attachMediaStream: function(element, stream) {
|
|
logging('DEPRECATED, attachMediaStream will soon be removed.');
|
|
if (browserDetails.version >= 43) {
|
|
element.srcObject = stream;
|
|
} else if (typeof element.src !== 'undefined') {
|
|
element.src = URL.createObjectURL(stream);
|
|
} else {
|
|
logging('Error attaching stream to element.');
|
|
}
|
|
},
|
|
|
|
reattachMediaStream: function(to, from) {
|
|
logging('DEPRECATED, reattachMediaStream will soon be removed.');
|
|
if (browserDetails.version >= 43) {
|
|
to.srcObject = from.srcObject;
|
|
} else {
|
|
to.src = from.src;
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimMediaStream: chromeShim.shimMediaStream,
|
|
shimOnTrack: chromeShim.shimOnTrack,
|
|
shimSourceObject: chromeShim.shimSourceObject,
|
|
shimPeerConnection: chromeShim.shimPeerConnection,
|
|
shimGetUserMedia: require('./getusermedia'),
|
|
attachMediaStream: chromeShim.attachMediaStream,
|
|
reattachMediaStream: chromeShim.reattachMediaStream
|
|
};
|
|
|
|
},{"../utils.js":38,"./getusermedia":32}],32:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
var logging = require('../utils.js').log;
|
|
|
|
// Expose public methods.
|
|
module.exports = function() {
|
|
var constraintsToChrome_ = function(c) {
|
|
if (typeof c !== 'object' || c.mandatory || c.optional) {
|
|
return c;
|
|
}
|
|
var cc = {};
|
|
Object.keys(c).forEach(function(key) {
|
|
if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
|
|
return;
|
|
}
|
|
var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
|
|
if (r.exact !== undefined && typeof r.exact === 'number') {
|
|
r.min = r.max = r.exact;
|
|
}
|
|
var oldname_ = function(prefix, name) {
|
|
if (prefix) {
|
|
return prefix + name.charAt(0).toUpperCase() + name.slice(1);
|
|
}
|
|
return (name === 'deviceId') ? 'sourceId' : name;
|
|
};
|
|
if (r.ideal !== undefined) {
|
|
cc.optional = cc.optional || [];
|
|
var oc = {};
|
|
if (typeof r.ideal === 'number') {
|
|
oc[oldname_('min', key)] = r.ideal;
|
|
cc.optional.push(oc);
|
|
oc = {};
|
|
oc[oldname_('max', key)] = r.ideal;
|
|
cc.optional.push(oc);
|
|
} else {
|
|
oc[oldname_('', key)] = r.ideal;
|
|
cc.optional.push(oc);
|
|
}
|
|
}
|
|
if (r.exact !== undefined && typeof r.exact !== 'number') {
|
|
cc.mandatory = cc.mandatory || {};
|
|
cc.mandatory[oldname_('', key)] = r.exact;
|
|
} else {
|
|
['min', 'max'].forEach(function(mix) {
|
|
if (r[mix] !== undefined) {
|
|
cc.mandatory = cc.mandatory || {};
|
|
cc.mandatory[oldname_(mix, key)] = r[mix];
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (c.advanced) {
|
|
cc.optional = (cc.optional || []).concat(c.advanced);
|
|
}
|
|
return cc;
|
|
};
|
|
|
|
var shimConstraints_ = function(constraints, func) {
|
|
constraints = JSON.parse(JSON.stringify(constraints));
|
|
if (constraints && constraints.audio) {
|
|
constraints.audio = constraintsToChrome_(constraints.audio);
|
|
}
|
|
if (constraints && typeof constraints.video === 'object') {
|
|
// Shim facingMode for mobile, where it defaults to "user".
|
|
var face = constraints.video.facingMode;
|
|
face = face && ((typeof face === 'object') ? face : {ideal: face});
|
|
|
|
if ((face && (face.exact === 'user' || face.exact === 'environment' ||
|
|
face.ideal === 'user' || face.ideal === 'environment')) &&
|
|
!(navigator.mediaDevices.getSupportedConstraints &&
|
|
navigator.mediaDevices.getSupportedConstraints().facingMode)) {
|
|
delete constraints.video.facingMode;
|
|
if (face.exact === 'environment' || face.ideal === 'environment') {
|
|
// Look for "back" in label, or use last cam (typically back cam).
|
|
return navigator.mediaDevices.enumerateDevices()
|
|
.then(function(devices) {
|
|
devices = devices.filter(function(d) {
|
|
return d.kind === 'videoinput';
|
|
});
|
|
var back = devices.find(function(d) {
|
|
return d.label.toLowerCase().indexOf('back') !== -1;
|
|
}) || (devices.length && devices[devices.length - 1]);
|
|
if (back) {
|
|
constraints.video.deviceId = face.exact ? {exact: back.deviceId} :
|
|
{ideal: back.deviceId};
|
|
}
|
|
constraints.video = constraintsToChrome_(constraints.video);
|
|
logging('chrome: ' + JSON.stringify(constraints));
|
|
return func(constraints);
|
|
});
|
|
}
|
|
}
|
|
constraints.video = constraintsToChrome_(constraints.video);
|
|
}
|
|
logging('chrome: ' + JSON.stringify(constraints));
|
|
return func(constraints);
|
|
};
|
|
|
|
var shimError_ = function(e) {
|
|
return {
|
|
name: {
|
|
PermissionDeniedError: 'NotAllowedError',
|
|
ConstraintNotSatisfiedError: 'OverconstrainedError'
|
|
}[e.name] || e.name,
|
|
message: e.message,
|
|
constraint: e.constraintName,
|
|
toString: function() {
|
|
return this.name + (this.message && ': ') + this.message;
|
|
}
|
|
};
|
|
};
|
|
|
|
var getUserMedia_ = function(constraints, onSuccess, onError) {
|
|
shimConstraints_(constraints, function(c) {
|
|
navigator.webkitGetUserMedia(c, onSuccess, function(e) {
|
|
onError(shimError_(e));
|
|
});
|
|
});
|
|
};
|
|
|
|
navigator.getUserMedia = getUserMedia_;
|
|
|
|
// Returns the result of getUserMedia as a Promise.
|
|
var getUserMediaPromise_ = function(constraints) {
|
|
return new Promise(function(resolve, reject) {
|
|
navigator.getUserMedia(constraints, resolve, reject);
|
|
});
|
|
};
|
|
|
|
if (!navigator.mediaDevices) {
|
|
navigator.mediaDevices = {
|
|
getUserMedia: getUserMediaPromise_,
|
|
enumerateDevices: function() {
|
|
return new Promise(function(resolve) {
|
|
var kinds = {audio: 'audioinput', video: 'videoinput'};
|
|
return MediaStreamTrack.getSources(function(devices) {
|
|
resolve(devices.map(function(device) {
|
|
return {label: device.label,
|
|
kind: kinds[device.kind],
|
|
deviceId: device.id,
|
|
groupId: ''};
|
|
}));
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
// A shim for getUserMedia method on the mediaDevices object.
|
|
// TODO(KaptenJansson) remove once implemented in Chrome stable.
|
|
if (!navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia = function(constraints) {
|
|
return getUserMediaPromise_(constraints);
|
|
};
|
|
} else {
|
|
// Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
|
|
// function which returns a Promise, it does not accept spec-style
|
|
// constraints.
|
|
var origGetUserMedia = navigator.mediaDevices.getUserMedia.
|
|
bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.getUserMedia = function(cs) {
|
|
return shimConstraints_(cs, function(c) {
|
|
return origGetUserMedia(c).catch(function(e) {
|
|
return Promise.reject(shimError_(e));
|
|
});
|
|
});
|
|
};
|
|
}
|
|
|
|
// Dummy devicechange event methods.
|
|
// TODO(KaptenJansson) remove once implemented in Chrome stable.
|
|
if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
|
|
navigator.mediaDevices.addEventListener = function() {
|
|
logging('Dummy mediaDevices.addEventListener called.');
|
|
};
|
|
}
|
|
if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
|
|
navigator.mediaDevices.removeEventListener = function() {
|
|
logging('Dummy mediaDevices.removeEventListener called.');
|
|
};
|
|
}
|
|
};
|
|
|
|
},{"../utils.js":38}],33:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var SDPUtils = require('sdp');
|
|
var logging = require('../utils').log;
|
|
|
|
var edgeShim = {
|
|
shimPeerConnection: function() {
|
|
if (window.RTCIceGatherer) {
|
|
// ORTC defines an RTCIceCandidate object but no constructor.
|
|
// Not implemented in Edge.
|
|
if (!window.RTCIceCandidate) {
|
|
window.RTCIceCandidate = function(args) {
|
|
return args;
|
|
};
|
|
}
|
|
// ORTC does not have a session description object but
|
|
// other browsers (i.e. Chrome) that will support both PC and ORTC
|
|
// in the future might have this defined already.
|
|
if (!window.RTCSessionDescription) {
|
|
window.RTCSessionDescription = function(args) {
|
|
return args;
|
|
};
|
|
}
|
|
}
|
|
|
|
window.RTCPeerConnection = function(config) {
|
|
var self = this;
|
|
|
|
var _eventTarget = document.createDocumentFragment();
|
|
['addEventListener', 'removeEventListener', 'dispatchEvent']
|
|
.forEach(function(method) {
|
|
self[method] = _eventTarget[method].bind(_eventTarget);
|
|
});
|
|
|
|
this.onicecandidate = null;
|
|
this.onaddstream = null;
|
|
this.ontrack = null;
|
|
this.onremovestream = null;
|
|
this.onsignalingstatechange = null;
|
|
this.oniceconnectionstatechange = null;
|
|
this.onnegotiationneeded = null;
|
|
this.ondatachannel = null;
|
|
|
|
this.localStreams = [];
|
|
this.remoteStreams = [];
|
|
this.getLocalStreams = function() {
|
|
return self.localStreams;
|
|
};
|
|
this.getRemoteStreams = function() {
|
|
return self.remoteStreams;
|
|
};
|
|
|
|
this.localDescription = new RTCSessionDescription({
|
|
type: '',
|
|
sdp: ''
|
|
});
|
|
this.remoteDescription = new RTCSessionDescription({
|
|
type: '',
|
|
sdp: ''
|
|
});
|
|
this.signalingState = 'stable';
|
|
this.iceConnectionState = 'new';
|
|
this.iceGatheringState = 'new';
|
|
|
|
this.iceOptions = {
|
|
gatherPolicy: 'all',
|
|
iceServers: []
|
|
};
|
|
if (config && config.iceTransportPolicy) {
|
|
switch (config.iceTransportPolicy) {
|
|
case 'all':
|
|
case 'relay':
|
|
this.iceOptions.gatherPolicy = config.iceTransportPolicy;
|
|
break;
|
|
case 'none':
|
|
// FIXME: remove once implementation and spec have added this.
|
|
throw new TypeError('iceTransportPolicy "none" not supported');
|
|
default:
|
|
// don't set iceTransportPolicy.
|
|
break;
|
|
}
|
|
}
|
|
this.usingBundle = config && config.bundlePolicy === 'max-bundle';
|
|
|
|
if (config && config.iceServers) {
|
|
// Edge does not like
|
|
// 1) stun:
|
|
// 2) turn: that does not have all of turn:host:port?transport=udp
|
|
var iceServers = JSON.parse(JSON.stringify(config.iceServers));
|
|
this.iceOptions.iceServers = iceServers.filter(function(server) {
|
|
if (server && server.urls) {
|
|
var urls = server.urls;
|
|
if (typeof urls === 'string') {
|
|
urls = [urls];
|
|
}
|
|
urls = urls.filter(function(url) {
|
|
return url.indexOf('turn:') === 0 &&
|
|
url.indexOf('transport=udp') !== -1;
|
|
})[0];
|
|
return !!urls;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
|
|
// everything that is needed to describe a SDP m-line.
|
|
this.transceivers = [];
|
|
|
|
// since the iceGatherer is currently created in createOffer but we
|
|
// must not emit candidates until after setLocalDescription we buffer
|
|
// them in this array.
|
|
this._localIceCandidatesBuffer = [];
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype._emitBufferedCandidates = function() {
|
|
var self = this;
|
|
var sections = SDPUtils.splitSections(self.localDescription.sdp);
|
|
// FIXME: need to apply ice candidates in a way which is async but
|
|
// in-order
|
|
this._localIceCandidatesBuffer.forEach(function(event) {
|
|
var end = !event.candidate || Object.keys(event.candidate).length === 0;
|
|
if (end) {
|
|
for (var j = 1; j < sections.length; j++) {
|
|
if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) {
|
|
sections[j] += 'a=end-of-candidates\r\n';
|
|
}
|
|
}
|
|
} else if (event.candidate.candidate.indexOf('typ endOfCandidates')
|
|
=== -1) {
|
|
sections[event.candidate.sdpMLineIndex + 1] +=
|
|
'a=' + event.candidate.candidate + '\r\n';
|
|
}
|
|
self.localDescription.sdp = sections.join('');
|
|
self.dispatchEvent(event);
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(event);
|
|
}
|
|
if (!event.candidate && self.iceGatheringState !== 'complete') {
|
|
var complete = self.transceivers.every(function(transceiver) {
|
|
return transceiver.iceGatherer &&
|
|
transceiver.iceGatherer.state === 'completed';
|
|
});
|
|
if (complete) {
|
|
self.iceGatheringState = 'complete';
|
|
}
|
|
}
|
|
});
|
|
this._localIceCandidatesBuffer = [];
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.addStream = function(stream) {
|
|
// Clone is necessary for local demos mostly, attaching directly
|
|
// to two different senders does not work (build 10547).
|
|
this.localStreams.push(stream.clone());
|
|
this._maybeFireNegotiationNeeded();
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.removeStream = function(stream) {
|
|
var idx = this.localStreams.indexOf(stream);
|
|
if (idx > -1) {
|
|
this.localStreams.splice(idx, 1);
|
|
this._maybeFireNegotiationNeeded();
|
|
}
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getSenders = function() {
|
|
return this.transceivers.filter(function(transceiver) {
|
|
return !!transceiver.rtpSender;
|
|
})
|
|
.map(function(transceiver) {
|
|
return transceiver.rtpSender;
|
|
});
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getReceivers = function() {
|
|
return this.transceivers.filter(function(transceiver) {
|
|
return !!transceiver.rtpReceiver;
|
|
})
|
|
.map(function(transceiver) {
|
|
return transceiver.rtpReceiver;
|
|
});
|
|
};
|
|
|
|
// Determines the intersection of local and remote capabilities.
|
|
window.RTCPeerConnection.prototype._getCommonCapabilities =
|
|
function(localCapabilities, remoteCapabilities) {
|
|
var commonCapabilities = {
|
|
codecs: [],
|
|
headerExtensions: [],
|
|
fecMechanisms: []
|
|
};
|
|
localCapabilities.codecs.forEach(function(lCodec) {
|
|
for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
|
|
var rCodec = remoteCapabilities.codecs[i];
|
|
if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
|
|
lCodec.clockRate === rCodec.clockRate &&
|
|
lCodec.numChannels === rCodec.numChannels) {
|
|
// push rCodec so we reply with offerer payload type
|
|
commonCapabilities.codecs.push(rCodec);
|
|
|
|
// FIXME: also need to determine intersection between
|
|
// .rtcpFeedback and .parameters
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
localCapabilities.headerExtensions
|
|
.forEach(function(lHeaderExtension) {
|
|
for (var i = 0; i < remoteCapabilities.headerExtensions.length;
|
|
i++) {
|
|
var rHeaderExtension = remoteCapabilities.headerExtensions[i];
|
|
if (lHeaderExtension.uri === rHeaderExtension.uri) {
|
|
commonCapabilities.headerExtensions.push(rHeaderExtension);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// FIXME: fecMechanisms
|
|
return commonCapabilities;
|
|
};
|
|
|
|
// Create ICE gatherer, ICE transport and DTLS transport.
|
|
window.RTCPeerConnection.prototype._createIceAndDtlsTransports =
|
|
function(mid, sdpMLineIndex) {
|
|
var self = this;
|
|
var iceGatherer = new RTCIceGatherer(self.iceOptions);
|
|
var iceTransport = new RTCIceTransport(iceGatherer);
|
|
iceGatherer.onlocalcandidate = function(evt) {
|
|
var event = new Event('icecandidate');
|
|
event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
|
|
|
|
var cand = evt.candidate;
|
|
var end = !cand || Object.keys(cand).length === 0;
|
|
// Edge emits an empty object for RTCIceCandidateComplete‥
|
|
if (end) {
|
|
// polyfill since RTCIceGatherer.state is not implemented in
|
|
// Edge 10547 yet.
|
|
if (iceGatherer.state === undefined) {
|
|
iceGatherer.state = 'completed';
|
|
}
|
|
|
|
// Emit a candidate with type endOfCandidates to make the samples
|
|
// work. Edge requires addIceCandidate with this empty candidate
|
|
// to start checking. The real solution is to signal
|
|
// end-of-candidates to the other side when getting the null
|
|
// candidate but some apps (like the samples) don't do that.
|
|
event.candidate.candidate =
|
|
'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates';
|
|
} else {
|
|
// RTCIceCandidate doesn't have a component, needs to be added
|
|
cand.component = iceTransport.component === 'RTCP' ? 2 : 1;
|
|
event.candidate.candidate = SDPUtils.writeCandidate(cand);
|
|
}
|
|
|
|
// update local description.
|
|
var sections = SDPUtils.splitSections(self.localDescription.sdp);
|
|
if (event.candidate.candidate.indexOf('typ endOfCandidates')
|
|
=== -1) {
|
|
sections[event.candidate.sdpMLineIndex + 1] +=
|
|
'a=' + event.candidate.candidate + '\r\n';
|
|
} else {
|
|
sections[event.candidate.sdpMLineIndex + 1] +=
|
|
'a=end-of-candidates\r\n';
|
|
}
|
|
self.localDescription.sdp = sections.join('');
|
|
|
|
var complete = self.transceivers.every(function(transceiver) {
|
|
return transceiver.iceGatherer &&
|
|
transceiver.iceGatherer.state === 'completed';
|
|
});
|
|
|
|
// Emit candidate if localDescription is set.
|
|
// Also emits null candidate when all gatherers are complete.
|
|
switch (self.iceGatheringState) {
|
|
case 'new':
|
|
self._localIceCandidatesBuffer.push(event);
|
|
if (end && complete) {
|
|
self._localIceCandidatesBuffer.push(
|
|
new Event('icecandidate'));
|
|
}
|
|
break;
|
|
case 'gathering':
|
|
self._emitBufferedCandidates();
|
|
self.dispatchEvent(event);
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(event);
|
|
}
|
|
if (complete) {
|
|
self.dispatchEvent(new Event('icecandidate'));
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(new Event('icecandidate'));
|
|
}
|
|
self.iceGatheringState = 'complete';
|
|
}
|
|
break;
|
|
case 'complete':
|
|
// should not happen... currently!
|
|
break;
|
|
default: // no-op.
|
|
break;
|
|
}
|
|
};
|
|
iceTransport.onicestatechange = function() {
|
|
self._updateConnectionState();
|
|
};
|
|
|
|
var dtlsTransport = new RTCDtlsTransport(iceTransport);
|
|
dtlsTransport.ondtlsstatechange = function() {
|
|
self._updateConnectionState();
|
|
};
|
|
dtlsTransport.onerror = function() {
|
|
// onerror does not set state to failed by itself.
|
|
dtlsTransport.state = 'failed';
|
|
self._updateConnectionState();
|
|
};
|
|
|
|
return {
|
|
iceGatherer: iceGatherer,
|
|
iceTransport: iceTransport,
|
|
dtlsTransport: dtlsTransport
|
|
};
|
|
};
|
|
|
|
// Start the RTP Sender and Receiver for a transceiver.
|
|
window.RTCPeerConnection.prototype._transceive = function(transceiver,
|
|
send, recv) {
|
|
var params = this._getCommonCapabilities(transceiver.localCapabilities,
|
|
transceiver.remoteCapabilities);
|
|
if (send && transceiver.rtpSender) {
|
|
params.encodings = transceiver.sendEncodingParameters;
|
|
params.rtcp = {
|
|
cname: SDPUtils.localCName
|
|
};
|
|
if (transceiver.recvEncodingParameters.length) {
|
|
params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
|
|
}
|
|
transceiver.rtpSender.send(params);
|
|
}
|
|
if (recv && transceiver.rtpReceiver) {
|
|
params.encodings = transceiver.recvEncodingParameters;
|
|
params.rtcp = {
|
|
cname: transceiver.cname
|
|
};
|
|
if (transceiver.sendEncodingParameters.length) {
|
|
params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
|
|
}
|
|
transceiver.rtpReceiver.receive(params);
|
|
}
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.setLocalDescription =
|
|
function(description) {
|
|
var self = this;
|
|
var sections;
|
|
var sessionpart;
|
|
if (description.type === 'offer') {
|
|
// FIXME: What was the purpose of this empty if statement?
|
|
// if (!this._pendingOffer) {
|
|
// } else {
|
|
if (this._pendingOffer) {
|
|
// VERY limited support for SDP munging. Limited to:
|
|
// * changing the order of codecs
|
|
sections = SDPUtils.splitSections(description.sdp);
|
|
sessionpart = sections.shift();
|
|
sections.forEach(function(mediaSection, sdpMLineIndex) {
|
|
var caps = SDPUtils.parseRtpParameters(mediaSection);
|
|
self._pendingOffer[sdpMLineIndex].localCapabilities = caps;
|
|
});
|
|
this.transceivers = this._pendingOffer;
|
|
delete this._pendingOffer;
|
|
}
|
|
} else if (description.type === 'answer') {
|
|
sections = SDPUtils.splitSections(self.remoteDescription.sdp);
|
|
sessionpart = sections.shift();
|
|
var isIceLite = SDPUtils.matchPrefix(sessionpart,
|
|
'a=ice-lite').length > 0;
|
|
sections.forEach(function(mediaSection, sdpMLineIndex) {
|
|
var transceiver = self.transceivers[sdpMLineIndex];
|
|
var iceGatherer = transceiver.iceGatherer;
|
|
var iceTransport = transceiver.iceTransport;
|
|
var dtlsTransport = transceiver.dtlsTransport;
|
|
var localCapabilities = transceiver.localCapabilities;
|
|
var remoteCapabilities = transceiver.remoteCapabilities;
|
|
var rejected = mediaSection.split('\n', 1)[0]
|
|
.split(' ', 2)[1] === '0';
|
|
|
|
if (!rejected) {
|
|
var remoteIceParameters = SDPUtils.getIceParameters(
|
|
mediaSection, sessionpart);
|
|
if (isIceLite) {
|
|
var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
|
|
.map(function(cand) {
|
|
return SDPUtils.parseCandidate(cand);
|
|
})
|
|
.filter(function(cand) {
|
|
return cand.component === '1';
|
|
});
|
|
// ice-lite only includes host candidates in the SDP so we can
|
|
// use setRemoteCandidates (which implies an
|
|
// RTCIceCandidateComplete)
|
|
if (cands.length) {
|
|
iceTransport.setRemoteCandidates(cands);
|
|
}
|
|
}
|
|
var remoteDtlsParameters = SDPUtils.getDtlsParameters(
|
|
mediaSection, sessionpart);
|
|
if (isIceLite) {
|
|
remoteDtlsParameters.role = 'server';
|
|
}
|
|
|
|
if (!self.usingBundle || sdpMLineIndex === 0) {
|
|
iceTransport.start(iceGatherer, remoteIceParameters,
|
|
isIceLite ? 'controlling' : 'controlled');
|
|
dtlsTransport.start(remoteDtlsParameters);
|
|
}
|
|
|
|
// Calculate intersection of capabilities.
|
|
var params = self._getCommonCapabilities(localCapabilities,
|
|
remoteCapabilities);
|
|
|
|
// Start the RTCRtpSender. The RTCRtpReceiver for this
|
|
// transceiver has already been started in setRemoteDescription.
|
|
self._transceive(transceiver,
|
|
params.codecs.length > 0,
|
|
false);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.localDescription = {
|
|
type: description.type,
|
|
sdp: description.sdp
|
|
};
|
|
switch (description.type) {
|
|
case 'offer':
|
|
this._updateSignalingState('have-local-offer');
|
|
break;
|
|
case 'answer':
|
|
this._updateSignalingState('stable');
|
|
break;
|
|
default:
|
|
throw new TypeError('unsupported type "' + description.type +
|
|
'"');
|
|
}
|
|
|
|
// If a success callback was provided, emit ICE candidates after it
|
|
// has been executed. Otherwise, emit callback after the Promise is
|
|
// resolved.
|
|
var hasCallback = arguments.length > 1 &&
|
|
typeof arguments[1] === 'function';
|
|
if (hasCallback) {
|
|
var cb = arguments[1];
|
|
window.setTimeout(function() {
|
|
cb();
|
|
if (self.iceGatheringState === 'new') {
|
|
self.iceGatheringState = 'gathering';
|
|
}
|
|
self._emitBufferedCandidates();
|
|
}, 0);
|
|
}
|
|
var p = Promise.resolve();
|
|
p.then(function() {
|
|
if (!hasCallback) {
|
|
if (self.iceGatheringState === 'new') {
|
|
self.iceGatheringState = 'gathering';
|
|
}
|
|
// Usually candidates will be emitted earlier.
|
|
window.setTimeout(self._emitBufferedCandidates.bind(self), 500);
|
|
}
|
|
});
|
|
return p;
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.setRemoteDescription =
|
|
function(description) {
|
|
var self = this;
|
|
var stream = new MediaStream();
|
|
var receiverList = [];
|
|
var sections = SDPUtils.splitSections(description.sdp);
|
|
var sessionpart = sections.shift();
|
|
var isIceLite = SDPUtils.matchPrefix(sessionpart,
|
|
'a=ice-lite').length > 0;
|
|
this.usingBundle = SDPUtils.matchPrefix(sessionpart,
|
|
'a=group:BUNDLE ').length > 0;
|
|
sections.forEach(function(mediaSection, sdpMLineIndex) {
|
|
var lines = SDPUtils.splitLines(mediaSection);
|
|
var mline = lines[0].substr(2).split(' ');
|
|
var kind = mline[0];
|
|
var rejected = mline[1] === '0';
|
|
var direction = SDPUtils.getDirection(mediaSection, sessionpart);
|
|
|
|
var transceiver;
|
|
var iceGatherer;
|
|
var iceTransport;
|
|
var dtlsTransport;
|
|
var rtpSender;
|
|
var rtpReceiver;
|
|
var sendEncodingParameters;
|
|
var recvEncodingParameters;
|
|
var localCapabilities;
|
|
|
|
var track;
|
|
// FIXME: ensure the mediaSection has rtcp-mux set.
|
|
var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
|
|
var remoteIceParameters;
|
|
var remoteDtlsParameters;
|
|
if (!rejected) {
|
|
remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
|
|
sessionpart);
|
|
remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
|
|
sessionpart);
|
|
remoteDtlsParameters.role = 'client';
|
|
}
|
|
recvEncodingParameters =
|
|
SDPUtils.parseRtpEncodingParameters(mediaSection);
|
|
|
|
var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:');
|
|
if (mid.length) {
|
|
mid = mid[0].substr(6);
|
|
} else {
|
|
mid = SDPUtils.generateIdentifier();
|
|
}
|
|
|
|
var cname;
|
|
// Gets the first SSRC. Note that with RTX there might be multiple
|
|
// SSRCs.
|
|
var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
|
|
.map(function(line) {
|
|
return SDPUtils.parseSsrcMedia(line);
|
|
})
|
|
.filter(function(obj) {
|
|
return obj.attribute === 'cname';
|
|
})[0];
|
|
if (remoteSsrc) {
|
|
cname = remoteSsrc.value;
|
|
}
|
|
|
|
var isComplete = SDPUtils.matchPrefix(mediaSection,
|
|
'a=end-of-candidates').length > 0;
|
|
var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
|
|
.map(function(cand) {
|
|
return SDPUtils.parseCandidate(cand);
|
|
})
|
|
.filter(function(cand) {
|
|
return cand.component === '1';
|
|
});
|
|
if (description.type === 'offer' && !rejected) {
|
|
var transports = self.usingBundle && sdpMLineIndex > 0 ? {
|
|
iceGatherer: self.transceivers[0].iceGatherer,
|
|
iceTransport: self.transceivers[0].iceTransport,
|
|
dtlsTransport: self.transceivers[0].dtlsTransport
|
|
} : self._createIceAndDtlsTransports(mid, sdpMLineIndex);
|
|
|
|
if (isComplete) {
|
|
transports.iceTransport.setRemoteCandidates(cands);
|
|
}
|
|
|
|
localCapabilities = RTCRtpReceiver.getCapabilities(kind);
|
|
sendEncodingParameters = [{
|
|
ssrc: (2 * sdpMLineIndex + 2) * 1001
|
|
}];
|
|
|
|
rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
|
|
|
|
track = rtpReceiver.track;
|
|
receiverList.push([track, rtpReceiver]);
|
|
// FIXME: not correct when there are multiple streams but that is
|
|
// not currently supported in this shim.
|
|
stream.addTrack(track);
|
|
|
|
// FIXME: look at direction.
|
|
if (self.localStreams.length > 0 &&
|
|
self.localStreams[0].getTracks().length >= sdpMLineIndex) {
|
|
// FIXME: actually more complicated, needs to match types etc
|
|
var localtrack = self.localStreams[0]
|
|
.getTracks()[sdpMLineIndex];
|
|
rtpSender = new RTCRtpSender(localtrack,
|
|
transports.dtlsTransport);
|
|
}
|
|
|
|
self.transceivers[sdpMLineIndex] = {
|
|
iceGatherer: transports.iceGatherer,
|
|
iceTransport: transports.iceTransport,
|
|
dtlsTransport: transports.dtlsTransport,
|
|
localCapabilities: localCapabilities,
|
|
remoteCapabilities: remoteCapabilities,
|
|
rtpSender: rtpSender,
|
|
rtpReceiver: rtpReceiver,
|
|
kind: kind,
|
|
mid: mid,
|
|
cname: cname,
|
|
sendEncodingParameters: sendEncodingParameters,
|
|
recvEncodingParameters: recvEncodingParameters
|
|
};
|
|
// Start the RTCRtpReceiver now. The RTPSender is started in
|
|
// setLocalDescription.
|
|
self._transceive(self.transceivers[sdpMLineIndex],
|
|
false,
|
|
direction === 'sendrecv' || direction === 'sendonly');
|
|
} else if (description.type === 'answer' && !rejected) {
|
|
transceiver = self.transceivers[sdpMLineIndex];
|
|
iceGatherer = transceiver.iceGatherer;
|
|
iceTransport = transceiver.iceTransport;
|
|
dtlsTransport = transceiver.dtlsTransport;
|
|
rtpSender = transceiver.rtpSender;
|
|
rtpReceiver = transceiver.rtpReceiver;
|
|
sendEncodingParameters = transceiver.sendEncodingParameters;
|
|
localCapabilities = transceiver.localCapabilities;
|
|
|
|
self.transceivers[sdpMLineIndex].recvEncodingParameters =
|
|
recvEncodingParameters;
|
|
self.transceivers[sdpMLineIndex].remoteCapabilities =
|
|
remoteCapabilities;
|
|
self.transceivers[sdpMLineIndex].cname = cname;
|
|
|
|
if ((isIceLite || isComplete) && cands.length) {
|
|
iceTransport.setRemoteCandidates(cands);
|
|
}
|
|
if (!self.usingBundle || sdpMLineIndex === 0) {
|
|
iceTransport.start(iceGatherer, remoteIceParameters,
|
|
'controlling');
|
|
dtlsTransport.start(remoteDtlsParameters);
|
|
}
|
|
|
|
self._transceive(transceiver,
|
|
direction === 'sendrecv' || direction === 'recvonly',
|
|
direction === 'sendrecv' || direction === 'sendonly');
|
|
|
|
if (rtpReceiver &&
|
|
(direction === 'sendrecv' || direction === 'sendonly')) {
|
|
track = rtpReceiver.track;
|
|
receiverList.push([track, rtpReceiver]);
|
|
stream.addTrack(track);
|
|
} else {
|
|
// FIXME: actually the receiver should be created later.
|
|
delete transceiver.rtpReceiver;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.remoteDescription = {
|
|
type: description.type,
|
|
sdp: description.sdp
|
|
};
|
|
switch (description.type) {
|
|
case 'offer':
|
|
this._updateSignalingState('have-remote-offer');
|
|
break;
|
|
case 'answer':
|
|
this._updateSignalingState('stable');
|
|
break;
|
|
default:
|
|
throw new TypeError('unsupported type "' + description.type +
|
|
'"');
|
|
}
|
|
if (stream.getTracks().length) {
|
|
self.remoteStreams.push(stream);
|
|
window.setTimeout(function() {
|
|
var event = new Event('addstream');
|
|
event.stream = stream;
|
|
self.dispatchEvent(event);
|
|
if (self.onaddstream !== null) {
|
|
window.setTimeout(function() {
|
|
self.onaddstream(event);
|
|
}, 0);
|
|
}
|
|
|
|
receiverList.forEach(function(item) {
|
|
var track = item[0];
|
|
var receiver = item[1];
|
|
var trackEvent = new Event('track');
|
|
trackEvent.track = track;
|
|
trackEvent.receiver = receiver;
|
|
trackEvent.streams = [stream];
|
|
self.dispatchEvent(event);
|
|
if (self.ontrack !== null) {
|
|
window.setTimeout(function() {
|
|
self.ontrack(trackEvent);
|
|
}, 0);
|
|
}
|
|
});
|
|
}, 0);
|
|
}
|
|
if (arguments.length > 1 && typeof arguments[1] === 'function') {
|
|
window.setTimeout(arguments[1], 0);
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.close = function() {
|
|
this.transceivers.forEach(function(transceiver) {
|
|
/* not yet
|
|
if (transceiver.iceGatherer) {
|
|
transceiver.iceGatherer.close();
|
|
}
|
|
*/
|
|
if (transceiver.iceTransport) {
|
|
transceiver.iceTransport.stop();
|
|
}
|
|
if (transceiver.dtlsTransport) {
|
|
transceiver.dtlsTransport.stop();
|
|
}
|
|
if (transceiver.rtpSender) {
|
|
transceiver.rtpSender.stop();
|
|
}
|
|
if (transceiver.rtpReceiver) {
|
|
transceiver.rtpReceiver.stop();
|
|
}
|
|
});
|
|
// FIXME: clean up tracks, local streams, remote streams, etc
|
|
this._updateSignalingState('closed');
|
|
};
|
|
|
|
// Update the signaling state.
|
|
window.RTCPeerConnection.prototype._updateSignalingState =
|
|
function(newState) {
|
|
this.signalingState = newState;
|
|
var event = new Event('signalingstatechange');
|
|
this.dispatchEvent(event);
|
|
if (this.onsignalingstatechange !== null) {
|
|
this.onsignalingstatechange(event);
|
|
}
|
|
};
|
|
|
|
// Determine whether to fire the negotiationneeded event.
|
|
window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded =
|
|
function() {
|
|
// Fire away (for now).
|
|
var event = new Event('negotiationneeded');
|
|
this.dispatchEvent(event);
|
|
if (this.onnegotiationneeded !== null) {
|
|
this.onnegotiationneeded(event);
|
|
}
|
|
};
|
|
|
|
// Update the connection state.
|
|
window.RTCPeerConnection.prototype._updateConnectionState = function() {
|
|
var self = this;
|
|
var newState;
|
|
var states = {
|
|
'new': 0,
|
|
closed: 0,
|
|
connecting: 0,
|
|
checking: 0,
|
|
connected: 0,
|
|
completed: 0,
|
|
failed: 0
|
|
};
|
|
this.transceivers.forEach(function(transceiver) {
|
|
states[transceiver.iceTransport.state]++;
|
|
states[transceiver.dtlsTransport.state]++;
|
|
});
|
|
// ICETransport.completed and connected are the same for this purpose.
|
|
states.connected += states.completed;
|
|
|
|
newState = 'new';
|
|
if (states.failed > 0) {
|
|
newState = 'failed';
|
|
} else if (states.connecting > 0 || states.checking > 0) {
|
|
newState = 'connecting';
|
|
} else if (states.disconnected > 0) {
|
|
newState = 'disconnected';
|
|
} else if (states.new > 0) {
|
|
newState = 'new';
|
|
} else if (states.connected > 0 || states.completed > 0) {
|
|
newState = 'connected';
|
|
}
|
|
|
|
if (newState !== self.iceConnectionState) {
|
|
self.iceConnectionState = newState;
|
|
var event = new Event('iceconnectionstatechange');
|
|
this.dispatchEvent(event);
|
|
if (this.oniceconnectionstatechange !== null) {
|
|
this.oniceconnectionstatechange(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.createOffer = function() {
|
|
var self = this;
|
|
if (this._pendingOffer) {
|
|
throw new Error('createOffer called while there is a pending offer.');
|
|
}
|
|
var offerOptions;
|
|
if (arguments.length === 1 && typeof arguments[0] !== 'function') {
|
|
offerOptions = arguments[0];
|
|
} else if (arguments.length === 3) {
|
|
offerOptions = arguments[2];
|
|
}
|
|
|
|
var tracks = [];
|
|
var numAudioTracks = 0;
|
|
var numVideoTracks = 0;
|
|
// Default to sendrecv.
|
|
if (this.localStreams.length) {
|
|
numAudioTracks = this.localStreams[0].getAudioTracks().length;
|
|
numVideoTracks = this.localStreams[0].getVideoTracks().length;
|
|
}
|
|
// Determine number of audio and video tracks we need to send/recv.
|
|
if (offerOptions) {
|
|
// Reject Chrome legacy constraints.
|
|
if (offerOptions.mandatory || offerOptions.optional) {
|
|
throw new TypeError(
|
|
'Legacy mandatory/optional constraints not supported.');
|
|
}
|
|
if (offerOptions.offerToReceiveAudio !== undefined) {
|
|
numAudioTracks = offerOptions.offerToReceiveAudio;
|
|
}
|
|
if (offerOptions.offerToReceiveVideo !== undefined) {
|
|
numVideoTracks = offerOptions.offerToReceiveVideo;
|
|
}
|
|
}
|
|
if (this.localStreams.length) {
|
|
// Push local streams.
|
|
this.localStreams[0].getTracks().forEach(function(track) {
|
|
tracks.push({
|
|
kind: track.kind,
|
|
track: track,
|
|
wantReceive: track.kind === 'audio' ?
|
|
numAudioTracks > 0 : numVideoTracks > 0
|
|
});
|
|
if (track.kind === 'audio') {
|
|
numAudioTracks--;
|
|
} else if (track.kind === 'video') {
|
|
numVideoTracks--;
|
|
}
|
|
});
|
|
}
|
|
// Create M-lines for recvonly streams.
|
|
while (numAudioTracks > 0 || numVideoTracks > 0) {
|
|
if (numAudioTracks > 0) {
|
|
tracks.push({
|
|
kind: 'audio',
|
|
wantReceive: true
|
|
});
|
|
numAudioTracks--;
|
|
}
|
|
if (numVideoTracks > 0) {
|
|
tracks.push({
|
|
kind: 'video',
|
|
wantReceive: true
|
|
});
|
|
numVideoTracks--;
|
|
}
|
|
}
|
|
|
|
var sdp = SDPUtils.writeSessionBoilerplate();
|
|
var transceivers = [];
|
|
tracks.forEach(function(mline, sdpMLineIndex) {
|
|
// For each track, create an ice gatherer, ice transport,
|
|
// dtls transport, potentially rtpsender and rtpreceiver.
|
|
var track = mline.track;
|
|
var kind = mline.kind;
|
|
var mid = SDPUtils.generateIdentifier();
|
|
|
|
var transports = self.usingBundle && sdpMLineIndex > 0 ? {
|
|
iceGatherer: transceivers[0].iceGatherer,
|
|
iceTransport: transceivers[0].iceTransport,
|
|
dtlsTransport: transceivers[0].dtlsTransport
|
|
} : self._createIceAndDtlsTransports(mid, sdpMLineIndex);
|
|
|
|
var localCapabilities = RTCRtpSender.getCapabilities(kind);
|
|
var rtpSender;
|
|
var rtpReceiver;
|
|
|
|
// generate an ssrc now, to be used later in rtpSender.send
|
|
var sendEncodingParameters = [{
|
|
ssrc: (2 * sdpMLineIndex + 1) * 1001
|
|
}];
|
|
if (track) {
|
|
rtpSender = new RTCRtpSender(track, transports.dtlsTransport);
|
|
}
|
|
|
|
if (mline.wantReceive) {
|
|
rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
|
|
}
|
|
|
|
transceivers[sdpMLineIndex] = {
|
|
iceGatherer: transports.iceGatherer,
|
|
iceTransport: transports.iceTransport,
|
|
dtlsTransport: transports.dtlsTransport,
|
|
localCapabilities: localCapabilities,
|
|
remoteCapabilities: null,
|
|
rtpSender: rtpSender,
|
|
rtpReceiver: rtpReceiver,
|
|
kind: kind,
|
|
mid: mid,
|
|
sendEncodingParameters: sendEncodingParameters,
|
|
recvEncodingParameters: null
|
|
};
|
|
});
|
|
if (this.usingBundle) {
|
|
sdp += 'a=group:BUNDLE ' + transceivers.map(function(t) {
|
|
return t.mid;
|
|
}).join(' ') + '\r\n';
|
|
}
|
|
tracks.forEach(function(mline, sdpMLineIndex) {
|
|
var transceiver = transceivers[sdpMLineIndex];
|
|
sdp += SDPUtils.writeMediaSection(transceiver,
|
|
transceiver.localCapabilities, 'offer', self.localStreams[0]);
|
|
});
|
|
|
|
this._pendingOffer = transceivers;
|
|
var desc = new RTCSessionDescription({
|
|
type: 'offer',
|
|
sdp: sdp
|
|
});
|
|
if (arguments.length && typeof arguments[0] === 'function') {
|
|
window.setTimeout(arguments[0], 0, desc);
|
|
}
|
|
return Promise.resolve(desc);
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.createAnswer = function() {
|
|
var self = this;
|
|
|
|
var sdp = SDPUtils.writeSessionBoilerplate();
|
|
if (this.usingBundle) {
|
|
sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
|
|
return t.mid;
|
|
}).join(' ') + '\r\n';
|
|
}
|
|
this.transceivers.forEach(function(transceiver) {
|
|
// Calculate intersection of capabilities.
|
|
var commonCapabilities = self._getCommonCapabilities(
|
|
transceiver.localCapabilities,
|
|
transceiver.remoteCapabilities);
|
|
|
|
sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities,
|
|
'answer', self.localStreams[0]);
|
|
});
|
|
|
|
var desc = new RTCSessionDescription({
|
|
type: 'answer',
|
|
sdp: sdp
|
|
});
|
|
if (arguments.length && typeof arguments[0] === 'function') {
|
|
window.setTimeout(arguments[0], 0, desc);
|
|
}
|
|
return Promise.resolve(desc);
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
|
|
if (candidate === null) {
|
|
this.transceivers.forEach(function(transceiver) {
|
|
transceiver.iceTransport.addRemoteCandidate({});
|
|
});
|
|
} else {
|
|
var mLineIndex = candidate.sdpMLineIndex;
|
|
if (candidate.sdpMid) {
|
|
for (var i = 0; i < this.transceivers.length; i++) {
|
|
if (this.transceivers[i].mid === candidate.sdpMid) {
|
|
mLineIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var transceiver = this.transceivers[mLineIndex];
|
|
if (transceiver) {
|
|
var cand = Object.keys(candidate.candidate).length > 0 ?
|
|
SDPUtils.parseCandidate(candidate.candidate) : {};
|
|
// Ignore Chrome's invalid candidates since Edge does not like them.
|
|
if (cand.protocol === 'tcp' && cand.port === 0) {
|
|
return;
|
|
}
|
|
// Ignore RTCP candidates, we assume RTCP-MUX.
|
|
if (cand.component !== '1') {
|
|
return;
|
|
}
|
|
// A dirty hack to make samples work.
|
|
if (cand.type === 'endOfCandidates') {
|
|
cand = {};
|
|
}
|
|
transceiver.iceTransport.addRemoteCandidate(cand);
|
|
|
|
// update the remoteDescription.
|
|
var sections = SDPUtils.splitSections(this.remoteDescription.sdp);
|
|
sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim()
|
|
: 'a=end-of-candidates') + '\r\n';
|
|
this.remoteDescription.sdp = sections.join('');
|
|
}
|
|
}
|
|
if (arguments.length > 1 && typeof arguments[1] === 'function') {
|
|
window.setTimeout(arguments[1], 0);
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getStats = function() {
|
|
var promises = [];
|
|
this.transceivers.forEach(function(transceiver) {
|
|
['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
|
|
'dtlsTransport'].forEach(function(method) {
|
|
if (transceiver[method]) {
|
|
promises.push(transceiver[method].getStats());
|
|
}
|
|
});
|
|
});
|
|
var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
|
|
arguments[1];
|
|
return new Promise(function(resolve) {
|
|
// shim getStats with maplike support
|
|
var results = new Map();
|
|
Promise.all(promises).then(function(res) {
|
|
res.forEach(function(result) {
|
|
Object.keys(result).forEach(function(id) {
|
|
results.set(id, result[id]);
|
|
results[id] = result[id];
|
|
});
|
|
});
|
|
if (cb) {
|
|
window.setTimeout(cb, 0, results);
|
|
}
|
|
resolve(results);
|
|
});
|
|
});
|
|
};
|
|
},
|
|
|
|
// Attach a media stream to an element.
|
|
attachMediaStream: function(element, stream) {
|
|
logging('DEPRECATED, attachMediaStream will soon be removed.');
|
|
element.srcObject = stream;
|
|
},
|
|
|
|
reattachMediaStream: function(to, from) {
|
|
logging('DEPRECATED, reattachMediaStream will soon be removed.');
|
|
to.srcObject = from.srcObject;
|
|
}
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimPeerConnection: edgeShim.shimPeerConnection,
|
|
shimGetUserMedia: require('./getusermedia'),
|
|
attachMediaStream: edgeShim.attachMediaStream,
|
|
reattachMediaStream: edgeShim.reattachMediaStream
|
|
};
|
|
|
|
},{"../utils":38,"./getusermedia":34,"sdp":62}],34:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
// Expose public methods.
|
|
module.exports = function() {
|
|
var shimError_ = function(e) {
|
|
return {
|
|
name: {PermissionDeniedError: 'NotAllowedError'}[e.name] || e.name,
|
|
message: e.message,
|
|
constraint: e.constraint,
|
|
toString: function() {
|
|
return this.name;
|
|
}
|
|
};
|
|
};
|
|
|
|
// getUserMedia error shim.
|
|
var origGetUserMedia = navigator.mediaDevices.getUserMedia.
|
|
bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.getUserMedia = function(c) {
|
|
return origGetUserMedia(c).catch(function(e) {
|
|
return Promise.reject(shimError_(e));
|
|
});
|
|
};
|
|
};
|
|
|
|
},{}],35:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var logging = require('../utils').log;
|
|
var browserDetails = require('../utils').browserDetails;
|
|
|
|
var firefoxShim = {
|
|
shimOnTrack: function() {
|
|
if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
|
|
window.RTCPeerConnection.prototype)) {
|
|
Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
|
|
get: function() {
|
|
return this._ontrack;
|
|
},
|
|
set: function(f) {
|
|
if (this._ontrack) {
|
|
this.removeEventListener('track', this._ontrack);
|
|
this.removeEventListener('addstream', this._ontrackpoly);
|
|
}
|
|
this.addEventListener('track', this._ontrack = f);
|
|
this.addEventListener('addstream', this._ontrackpoly = function(e) {
|
|
e.stream.getTracks().forEach(function(track) {
|
|
var event = new Event('track');
|
|
event.track = track;
|
|
event.receiver = {track: track};
|
|
event.streams = [e.stream];
|
|
this.dispatchEvent(event);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
shimSourceObject: function() {
|
|
// Firefox has supported mozSrcObject since FF22, unprefixed in 42.
|
|
if (typeof window === 'object') {
|
|
if (window.HTMLMediaElement &&
|
|
!('srcObject' in window.HTMLMediaElement.prototype)) {
|
|
// Shim the srcObject property, once, when HTMLMediaElement is found.
|
|
Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
|
|
get: function() {
|
|
return this.mozSrcObject;
|
|
},
|
|
set: function(stream) {
|
|
this.mozSrcObject = stream;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
shimPeerConnection: function() {
|
|
if (typeof window !== 'object' || !(window.RTCPeerConnection ||
|
|
window.mozRTCPeerConnection)) {
|
|
return; // probably media.peerconnection.enabled=false in about:config
|
|
}
|
|
// The RTCPeerConnection object.
|
|
if (!window.RTCPeerConnection) {
|
|
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
|
|
if (browserDetails.version < 38) {
|
|
// .urls is not supported in FF < 38.
|
|
// create RTCIceServers with a single url.
|
|
if (pcConfig && pcConfig.iceServers) {
|
|
var newIceServers = [];
|
|
for (var i = 0; i < pcConfig.iceServers.length; i++) {
|
|
var server = pcConfig.iceServers[i];
|
|
if (server.hasOwnProperty('urls')) {
|
|
for (var j = 0; j < server.urls.length; j++) {
|
|
var newServer = {
|
|
url: server.urls[j]
|
|
};
|
|
if (server.urls[j].indexOf('turn') === 0) {
|
|
newServer.username = server.username;
|
|
newServer.credential = server.credential;
|
|
}
|
|
newIceServers.push(newServer);
|
|
}
|
|
} else {
|
|
newIceServers.push(pcConfig.iceServers[i]);
|
|
}
|
|
}
|
|
pcConfig.iceServers = newIceServers;
|
|
}
|
|
}
|
|
return new mozRTCPeerConnection(pcConfig, pcConstraints);
|
|
};
|
|
window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype;
|
|
|
|
// wrap static methods. Currently just generateCertificate.
|
|
if (mozRTCPeerConnection.generateCertificate) {
|
|
Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
|
|
get: function() {
|
|
return mozRTCPeerConnection.generateCertificate;
|
|
}
|
|
});
|
|
}
|
|
|
|
window.RTCSessionDescription = mozRTCSessionDescription;
|
|
window.RTCIceCandidate = mozRTCIceCandidate;
|
|
}
|
|
|
|
// shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
|
|
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
|
|
.forEach(function(method) {
|
|
var nativeMethod = RTCPeerConnection.prototype[method];
|
|
RTCPeerConnection.prototype[method] = function() {
|
|
arguments[0] = new ((method === 'addIceCandidate') ?
|
|
RTCIceCandidate : RTCSessionDescription)(arguments[0]);
|
|
return nativeMethod.apply(this, arguments);
|
|
};
|
|
});
|
|
|
|
// support for addIceCandidate(null)
|
|
var nativeAddIceCandidate =
|
|
RTCPeerConnection.prototype.addIceCandidate;
|
|
RTCPeerConnection.prototype.addIceCandidate = function() {
|
|
return arguments[0] === null ? Promise.resolve()
|
|
: nativeAddIceCandidate.apply(this, arguments);
|
|
};
|
|
|
|
// shim getStats with maplike support
|
|
var makeMapStats = function(stats) {
|
|
var map = new Map();
|
|
Object.keys(stats).forEach(function(key) {
|
|
map.set(key, stats[key]);
|
|
map[key] = stats[key];
|
|
});
|
|
return map;
|
|
};
|
|
|
|
var nativeGetStats = RTCPeerConnection.prototype.getStats;
|
|
RTCPeerConnection.prototype.getStats = function(selector, onSucc, onErr) {
|
|
return nativeGetStats.apply(this, [selector || null])
|
|
.then(function(stats) {
|
|
return makeMapStats(stats);
|
|
})
|
|
.then(onSucc, onErr);
|
|
};
|
|
},
|
|
|
|
// Attach a media stream to an element.
|
|
attachMediaStream: function(element, stream) {
|
|
logging('DEPRECATED, attachMediaStream will soon be removed.');
|
|
element.srcObject = stream;
|
|
},
|
|
|
|
reattachMediaStream: function(to, from) {
|
|
logging('DEPRECATED, reattachMediaStream will soon be removed.');
|
|
to.srcObject = from.srcObject;
|
|
}
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimOnTrack: firefoxShim.shimOnTrack,
|
|
shimSourceObject: firefoxShim.shimSourceObject,
|
|
shimPeerConnection: firefoxShim.shimPeerConnection,
|
|
shimGetUserMedia: require('./getusermedia'),
|
|
attachMediaStream: firefoxShim.attachMediaStream,
|
|
reattachMediaStream: firefoxShim.reattachMediaStream
|
|
};
|
|
|
|
},{"../utils":38,"./getusermedia":36}],36:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var logging = require('../utils').log;
|
|
var browserDetails = require('../utils').browserDetails;
|
|
|
|
// Expose public methods.
|
|
module.exports = function() {
|
|
var shimError_ = function(e) {
|
|
return {
|
|
name: {
|
|
SecurityError: 'NotAllowedError',
|
|
PermissionDeniedError: 'NotAllowedError'
|
|
}[e.name] || e.name,
|
|
message: {
|
|
'The operation is insecure.': 'The request is not allowed by the ' +
|
|
'user agent or the platform in the current context.'
|
|
}[e.message] || e.message,
|
|
constraint: e.constraint,
|
|
toString: function() {
|
|
return this.name + (this.message && ': ') + this.message;
|
|
}
|
|
};
|
|
};
|
|
|
|
// getUserMedia constraints shim.
|
|
var getUserMedia_ = function(constraints, onSuccess, onError) {
|
|
var constraintsToFF37_ = function(c) {
|
|
if (typeof c !== 'object' || c.require) {
|
|
return c;
|
|
}
|
|
var require = [];
|
|
Object.keys(c).forEach(function(key) {
|
|
if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
|
|
return;
|
|
}
|
|
var r = c[key] = (typeof c[key] === 'object') ?
|
|
c[key] : {ideal: c[key]};
|
|
if (r.min !== undefined ||
|
|
r.max !== undefined || r.exact !== undefined) {
|
|
require.push(key);
|
|
}
|
|
if (r.exact !== undefined) {
|
|
if (typeof r.exact === 'number') {
|
|
r. min = r.max = r.exact;
|
|
} else {
|
|
c[key] = r.exact;
|
|
}
|
|
delete r.exact;
|
|
}
|
|
if (r.ideal !== undefined) {
|
|
c.advanced = c.advanced || [];
|
|
var oc = {};
|
|
if (typeof r.ideal === 'number') {
|
|
oc[key] = {min: r.ideal, max: r.ideal};
|
|
} else {
|
|
oc[key] = r.ideal;
|
|
}
|
|
c.advanced.push(oc);
|
|
delete r.ideal;
|
|
if (!Object.keys(r).length) {
|
|
delete c[key];
|
|
}
|
|
}
|
|
});
|
|
if (require.length) {
|
|
c.require = require;
|
|
}
|
|
return c;
|
|
};
|
|
constraints = JSON.parse(JSON.stringify(constraints));
|
|
if (browserDetails.version < 38) {
|
|
logging('spec: ' + JSON.stringify(constraints));
|
|
if (constraints.audio) {
|
|
constraints.audio = constraintsToFF37_(constraints.audio);
|
|
}
|
|
if (constraints.video) {
|
|
constraints.video = constraintsToFF37_(constraints.video);
|
|
}
|
|
logging('ff37: ' + JSON.stringify(constraints));
|
|
}
|
|
return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
|
|
onError(shimError_(e));
|
|
});
|
|
};
|
|
|
|
// Returns the result of getUserMedia as a Promise.
|
|
var getUserMediaPromise_ = function(constraints) {
|
|
return new Promise(function(resolve, reject) {
|
|
getUserMedia_(constraints, resolve, reject);
|
|
});
|
|
};
|
|
|
|
// Shim for mediaDevices on older versions.
|
|
if (!navigator.mediaDevices) {
|
|
navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
|
|
addEventListener: function() { },
|
|
removeEventListener: function() { }
|
|
};
|
|
}
|
|
navigator.mediaDevices.enumerateDevices =
|
|
navigator.mediaDevices.enumerateDevices || function() {
|
|
return new Promise(function(resolve) {
|
|
var infos = [
|
|
{kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
|
|
{kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
|
|
];
|
|
resolve(infos);
|
|
});
|
|
};
|
|
|
|
if (browserDetails.version < 41) {
|
|
// Work around http://bugzil.la/1169665
|
|
var orgEnumerateDevices =
|
|
navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.enumerateDevices = function() {
|
|
return orgEnumerateDevices().then(undefined, function(e) {
|
|
if (e.name === 'NotFoundError') {
|
|
return [];
|
|
}
|
|
throw e;
|
|
});
|
|
};
|
|
}
|
|
if (browserDetails.version < 49) {
|
|
var origGetUserMedia = navigator.mediaDevices.getUserMedia.
|
|
bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.getUserMedia = function(c) {
|
|
return origGetUserMedia(c).catch(function(e) {
|
|
return Promise.reject(shimError_(e));
|
|
});
|
|
};
|
|
}
|
|
navigator.getUserMedia = function(constraints, onSuccess, onError) {
|
|
if (browserDetails.version < 44) {
|
|
return getUserMedia_(constraints, onSuccess, onError);
|
|
}
|
|
// Replace Firefox 44+'s deprecation warning with unprefixed version.
|
|
console.warn('navigator.getUserMedia has been replaced by ' +
|
|
'navigator.mediaDevices.getUserMedia');
|
|
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
|
|
};
|
|
};
|
|
|
|
},{"../utils":38}],37:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
'use strict';
|
|
var safariShim = {
|
|
// TODO: DrAlex, should be here, double check against LayoutTests
|
|
// shimOnTrack: function() { },
|
|
|
|
// TODO: DrAlex
|
|
// attachMediaStream: function(element, stream) { },
|
|
// reattachMediaStream: function(to, from) { },
|
|
|
|
// TODO: once the back-end for the mac port is done, add.
|
|
// TODO: check for webkitGTK+
|
|
// shimPeerConnection: function() { },
|
|
|
|
shimGetUserMedia: function() {
|
|
navigator.getUserMedia = navigator.webkitGetUserMedia;
|
|
}
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimGetUserMedia: safariShim.shimGetUserMedia
|
|
// TODO
|
|
// shimOnTrack: safariShim.shimOnTrack,
|
|
// shimPeerConnection: safariShim.shimPeerConnection,
|
|
// attachMediaStream: safariShim.attachMediaStream,
|
|
// reattachMediaStream: safariShim.reattachMediaStream
|
|
};
|
|
|
|
},{}],38:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var logDisabled_ = true;
|
|
|
|
// Utility methods.
|
|
var utils = {
|
|
disableLog: function(bool) {
|
|
if (typeof bool !== 'boolean') {
|
|
return new Error('Argument type: ' + typeof bool +
|
|
'. Please use a boolean.');
|
|
}
|
|
logDisabled_ = bool;
|
|
return (bool) ? 'adapter.js logging disabled' :
|
|
'adapter.js logging enabled';
|
|
},
|
|
|
|
log: function() {
|
|
if (typeof window === 'object') {
|
|
if (logDisabled_) {
|
|
return;
|
|
}
|
|
if (typeof console !== 'undefined' && typeof console.log === 'function') {
|
|
console.log.apply(console, arguments);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Extract browser version out of the provided user agent string.
|
|
*
|
|
* @param {!string} uastring userAgent string.
|
|
* @param {!string} expr Regular expression used as match criteria.
|
|
* @param {!number} pos position in the version string to be returned.
|
|
* @return {!number} browser version.
|
|
*/
|
|
extractVersion: function(uastring, expr, pos) {
|
|
var match = uastring.match(expr);
|
|
return match && match.length >= pos && parseInt(match[pos], 10);
|
|
},
|
|
|
|
/**
|
|
* Browser detector.
|
|
*
|
|
* @return {object} result containing browser, version and minVersion
|
|
* properties.
|
|
*/
|
|
detectBrowser: function() {
|
|
// Returned result object.
|
|
var result = {};
|
|
result.browser = null;
|
|
result.version = null;
|
|
result.minVersion = null;
|
|
|
|
// Fail early if it's not a browser
|
|
if (typeof window === 'undefined' || !window.navigator) {
|
|
result.browser = 'Not a browser.';
|
|
return result;
|
|
}
|
|
|
|
// Firefox.
|
|
if (navigator.mozGetUserMedia) {
|
|
result.browser = 'firefox';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/Firefox\/([0-9]+)\./, 1);
|
|
result.minVersion = 31;
|
|
|
|
// all webkit-based browsers
|
|
} else if (navigator.webkitGetUserMedia) {
|
|
// Chrome, Chromium, Webview, Opera, all use the chrome shim for now
|
|
if (window.webkitRTCPeerConnection) {
|
|
result.browser = 'chrome';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/Chrom(e|ium)\/([0-9]+)\./, 2);
|
|
result.minVersion = 38;
|
|
|
|
// Safari or unknown webkit-based
|
|
// for the time being Safari has support for MediaStreams but not webRTC
|
|
} else {
|
|
// Safari UA substrings of interest for reference:
|
|
// - webkit version: AppleWebKit/602.1.25 (also used in Op,Cr)
|
|
// - safari UI version: Version/9.0.3 (unique to Safari)
|
|
// - safari UI webkit version: Safari/601.4.4 (also used in Op,Cr)
|
|
//
|
|
// if the webkit version and safari UI webkit versions are equals,
|
|
// ... this is a stable version.
|
|
//
|
|
// only the internal webkit version is important today to know if
|
|
// media streams are supported
|
|
//
|
|
if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
|
|
result.browser = 'safari';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/AppleWebKit\/([0-9]+)\./, 1);
|
|
result.minVersion = 602;
|
|
|
|
// unknown webkit-based browser
|
|
} else {
|
|
result.browser = 'Unsupported webkit-based browser ' +
|
|
'with GUM support but no WebRTC support.';
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Edge.
|
|
} else if (navigator.mediaDevices &&
|
|
navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) {
|
|
result.browser = 'edge';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/Edge\/(\d+).(\d+)$/, 2);
|
|
result.minVersion = 10547;
|
|
|
|
// Default fallthrough: not supported.
|
|
} else {
|
|
result.browser = 'Not a supported browser.';
|
|
return result;
|
|
}
|
|
|
|
// Warn if version is less than minVersion.
|
|
if (result.version < result.minVersion) {
|
|
utils.log('Browser: ' + result.browser + ' Version: ' + result.version +
|
|
' < minimum supported version: ' + result.minVersion +
|
|
'\n some things might not work!');
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Export.
|
|
module.exports = {
|
|
log: utils.log,
|
|
disableLog: utils.disableLog,
|
|
browserDetails: utils.detectBrowser(),
|
|
extractVersion: utils.extractVersion
|
|
};
|
|
|
|
},{}],39:[function(require,module,exports){
|
|
|
|
/**
|
|
* Returns `this`. Execute this without a "context" (i.e. without it being
|
|
* attached to an object of the left-hand side), and `this` points to the
|
|
* "global" scope of the current JS execution.
|
|
*/
|
|
|
|
module.exports = (function () { return this; })();
|
|
|
|
},{}],40:[function(require,module,exports){
|
|
var WildEmitter = require('wildemitter');
|
|
|
|
function getMaxVolume (analyser, fftBins) {
|
|
var maxVolume = -Infinity;
|
|
analyser.getFloatFrequencyData(fftBins);
|
|
|
|
for(var i=4, ii=fftBins.length; i < ii; i++) {
|
|
if (fftBins[i] > maxVolume && fftBins[i] < 0) {
|
|
maxVolume = fftBins[i];
|
|
}
|
|
};
|
|
|
|
return maxVolume;
|
|
}
|
|
|
|
|
|
var audioContextType;
|
|
if (typeof window !== 'undefined') {
|
|
audioContextType = window.AudioContext || window.webkitAudioContext;
|
|
}
|
|
// use a single audio context due to hardware limits
|
|
var audioContext = null;
|
|
module.exports = function(stream, options) {
|
|
var harker = new WildEmitter();
|
|
|
|
|
|
// make it not break in non-supported browsers
|
|
if (!audioContextType) return harker;
|
|
|
|
//Config
|
|
var options = options || {},
|
|
smoothing = (options.smoothing || 0.1),
|
|
interval = (options.interval || 50),
|
|
threshold = options.threshold,
|
|
play = options.play,
|
|
history = options.history || 10,
|
|
running = true;
|
|
|
|
//Setup Audio Context
|
|
if (!audioContext) {
|
|
audioContext = new audioContextType();
|
|
}
|
|
var sourceNode, fftBins, analyser;
|
|
|
|
analyser = audioContext.createAnalyser();
|
|
analyser.fftSize = 512;
|
|
analyser.smoothingTimeConstant = smoothing;
|
|
fftBins = new Float32Array(analyser.frequencyBinCount);
|
|
|
|
if (stream.jquery) stream = stream[0];
|
|
if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) {
|
|
//Audio Tag
|
|
sourceNode = audioContext.createMediaElementSource(stream);
|
|
if (typeof play === 'undefined') play = true;
|
|
threshold = threshold || -50;
|
|
} else {
|
|
//WebRTC Stream
|
|
sourceNode = audioContext.createMediaStreamSource(stream);
|
|
threshold = threshold || -50;
|
|
}
|
|
|
|
sourceNode.connect(analyser);
|
|
if (play) analyser.connect(audioContext.destination);
|
|
|
|
harker.speaking = false;
|
|
|
|
harker.setThreshold = function(t) {
|
|
threshold = t;
|
|
};
|
|
|
|
harker.setInterval = function(i) {
|
|
interval = i;
|
|
};
|
|
|
|
harker.stop = function() {
|
|
running = false;
|
|
harker.emit('volume_change', -100, threshold);
|
|
if (harker.speaking) {
|
|
harker.speaking = false;
|
|
harker.emit('stopped_speaking');
|
|
}
|
|
analyser.disconnect();
|
|
sourceNode.disconnect();
|
|
};
|
|
harker.speakingHistory = [];
|
|
for (var i = 0; i < history; i++) {
|
|
harker.speakingHistory.push(0);
|
|
}
|
|
|
|
// Poll the analyser node to determine if speaking
|
|
// and emit events if changed
|
|
var looper = function() {
|
|
setTimeout(function() {
|
|
|
|
//check if stop has been called
|
|
if(!running) {
|
|
return;
|
|
}
|
|
|
|
var currentVolume = getMaxVolume(analyser, fftBins);
|
|
|
|
harker.emit('volume_change', currentVolume, threshold);
|
|
|
|
var history = 0;
|
|
if (currentVolume > threshold && !harker.speaking) {
|
|
// trigger quickly, short history
|
|
for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) {
|
|
history += harker.speakingHistory[i];
|
|
}
|
|
if (history >= 2) {
|
|
harker.speaking = true;
|
|
harker.emit('speaking');
|
|
}
|
|
} else if (currentVolume < threshold && harker.speaking) {
|
|
for (var i = 0; i < harker.speakingHistory.length; i++) {
|
|
history += harker.speakingHistory[i];
|
|
}
|
|
if (history == 0) {
|
|
harker.speaking = false;
|
|
harker.emit('stopped_speaking');
|
|
}
|
|
}
|
|
harker.speakingHistory.shift();
|
|
harker.speakingHistory.push(0 + (currentVolume > threshold));
|
|
|
|
looper();
|
|
}, interval);
|
|
};
|
|
looper();
|
|
|
|
|
|
return harker;
|
|
}
|
|
|
|
},{"wildemitter":100}],41:[function(require,module,exports){
|
|
(function (global){
|
|
|
|
/*
|
|
* Module requirements.
|
|
*/
|
|
|
|
var isArray = require('isarray');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = hasBinary;
|
|
|
|
/**
|
|
* Checks for binary data.
|
|
*
|
|
* Right now only Buffer and ArrayBuffer are supported..
|
|
*
|
|
* @param {Object} anything
|
|
* @api public
|
|
*/
|
|
|
|
function hasBinary(data) {
|
|
|
|
function _hasBinary(obj) {
|
|
if (!obj) return false;
|
|
|
|
if ( (global.Buffer && global.Buffer.isBuffer(obj)) ||
|
|
(global.ArrayBuffer && obj instanceof ArrayBuffer) ||
|
|
(global.Blob && obj instanceof Blob) ||
|
|
(global.File && obj instanceof File)
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (isArray(obj)) {
|
|
for (var i = 0; i < obj.length; i++) {
|
|
if (_hasBinary(obj[i])) {
|
|
return true;
|
|
}
|
|
}
|
|
} else if (obj && 'object' == typeof obj) {
|
|
if (obj.toJSON) {
|
|
obj = obj.toJSON();
|
|
}
|
|
|
|
for (var key in obj) {
|
|
if (Object.prototype.hasOwnProperty.call(obj, key) && _hasBinary(obj[key])) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return _hasBinary(data);
|
|
}
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"isarray":42}],42:[function(require,module,exports){
|
|
module.exports = Array.isArray || function (arr) {
|
|
return Object.prototype.toString.call(arr) == '[object Array]';
|
|
};
|
|
|
|
},{}],43:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var global = require('global');
|
|
|
|
/**
|
|
* Module exports.
|
|
*
|
|
* Logic borrowed from Modernizr:
|
|
*
|
|
* - https://github.com/Modernizr/Modernizr/blob/master/feature-detects/cors.js
|
|
*/
|
|
|
|
try {
|
|
module.exports = 'XMLHttpRequest' in global &&
|
|
'withCredentials' in new global.XMLHttpRequest();
|
|
} catch (err) {
|
|
// if XMLHttp support is disabled in IE then it will throw
|
|
// when trying to create
|
|
module.exports = false;
|
|
}
|
|
|
|
},{"global":39}],44:[function(require,module,exports){
|
|
|
|
var indexOf = [].indexOf;
|
|
|
|
module.exports = function(arr, obj){
|
|
if (indexOf) return arr.indexOf(obj);
|
|
for (var i = 0; i < arr.length; ++i) {
|
|
if (arr[i] === obj) return i;
|
|
}
|
|
return -1;
|
|
};
|
|
},{}],45:[function(require,module,exports){
|
|
/*! JSON v3.2.6 | http://bestiejs.github.io/json3 | Copyright 2012-2013, Kit Cambridge | http://kit.mit-license.org */
|
|
;(function (window) {
|
|
// Convenience aliases.
|
|
var getClass = {}.toString, isProperty, forEach, undef;
|
|
|
|
// Detect the `define` function exposed by asynchronous module loaders. The
|
|
// strict `define` check is necessary for compatibility with `r.js`.
|
|
var isLoader = typeof define === "function" && define.amd;
|
|
|
|
// Detect native implementations.
|
|
var nativeJSON = typeof JSON == "object" && JSON;
|
|
|
|
// Set up the JSON 3 namespace, preferring the CommonJS `exports` object if
|
|
// available.
|
|
var JSON3 = typeof exports == "object" && exports && !exports.nodeType && exports;
|
|
|
|
if (JSON3 && nativeJSON) {
|
|
// Explicitly delegate to the native `stringify` and `parse`
|
|
// implementations in CommonJS environments.
|
|
JSON3.stringify = nativeJSON.stringify;
|
|
JSON3.parse = nativeJSON.parse;
|
|
} else {
|
|
// Export for web browsers, JavaScript engines, and asynchronous module
|
|
// loaders, using the global `JSON` object if available.
|
|
JSON3 = window.JSON = nativeJSON || {};
|
|
}
|
|
|
|
// Test the `Date#getUTC*` methods. Based on work by @Yaffle.
|
|
var isExtended = new Date(-3509827334573292);
|
|
try {
|
|
// The `getUTCFullYear`, `Month`, and `Date` methods return nonsensical
|
|
// results for certain dates in Opera >= 10.53.
|
|
isExtended = isExtended.getUTCFullYear() == -109252 && isExtended.getUTCMonth() === 0 && isExtended.getUTCDate() === 1 &&
|
|
// Safari < 2.0.2 stores the internal millisecond time value correctly,
|
|
// but clips the values returned by the date methods to the range of
|
|
// signed 32-bit integers ([-2 ** 31, 2 ** 31 - 1]).
|
|
isExtended.getUTCHours() == 10 && isExtended.getUTCMinutes() == 37 && isExtended.getUTCSeconds() == 6 && isExtended.getUTCMilliseconds() == 708;
|
|
} catch (exception) {}
|
|
|
|
// Internal: Determines whether the native `JSON.stringify` and `parse`
|
|
// implementations are spec-compliant. Based on work by Ken Snyder.
|
|
function has(name) {
|
|
if (has[name] !== undef) {
|
|
// Return cached feature test result.
|
|
return has[name];
|
|
}
|
|
|
|
var isSupported;
|
|
if (name == "bug-string-char-index") {
|
|
// IE <= 7 doesn't support accessing string characters using square
|
|
// bracket notation. IE 8 only supports this for primitives.
|
|
isSupported = "a"[0] != "a";
|
|
} else if (name == "json") {
|
|
// Indicates whether both `JSON.stringify` and `JSON.parse` are
|
|
// supported.
|
|
isSupported = has("json-stringify") && has("json-parse");
|
|
} else {
|
|
var value, serialized = '{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}';
|
|
// Test `JSON.stringify`.
|
|
if (name == "json-stringify") {
|
|
var stringify = JSON3.stringify, stringifySupported = typeof stringify == "function" && isExtended;
|
|
if (stringifySupported) {
|
|
// A test function object with a custom `toJSON` method.
|
|
(value = function () {
|
|
return 1;
|
|
}).toJSON = value;
|
|
try {
|
|
stringifySupported =
|
|
// Firefox 3.1b1 and b2 serialize string, number, and boolean
|
|
// primitives as object literals.
|
|
stringify(0) === "0" &&
|
|
// FF 3.1b1, b2, and JSON 2 serialize wrapped primitives as object
|
|
// literals.
|
|
stringify(new Number()) === "0" &&
|
|
stringify(new String()) == '""' &&
|
|
// FF 3.1b1, 2 throw an error if the value is `null`, `undefined`, or
|
|
// does not define a canonical JSON representation (this applies to
|
|
// objects with `toJSON` properties as well, *unless* they are nested
|
|
// within an object or array).
|
|
stringify(getClass) === undef &&
|
|
// IE 8 serializes `undefined` as `"undefined"`. Safari <= 5.1.7 and
|
|
// FF 3.1b3 pass this test.
|
|
stringify(undef) === undef &&
|
|
// Safari <= 5.1.7 and FF 3.1b3 throw `Error`s and `TypeError`s,
|
|
// respectively, if the value is omitted entirely.
|
|
stringify() === undef &&
|
|
// FF 3.1b1, 2 throw an error if the given value is not a number,
|
|
// string, array, object, Boolean, or `null` literal. This applies to
|
|
// objects with custom `toJSON` methods as well, unless they are nested
|
|
// inside object or array literals. YUI 3.0.0b1 ignores custom `toJSON`
|
|
// methods entirely.
|
|
stringify(value) === "1" &&
|
|
stringify([value]) == "[1]" &&
|
|
// Prototype <= 1.6.1 serializes `[undefined]` as `"[]"` instead of
|
|
// `"[null]"`.
|
|
stringify([undef]) == "[null]" &&
|
|
// YUI 3.0.0b1 fails to serialize `null` literals.
|
|
stringify(null) == "null" &&
|
|
// FF 3.1b1, 2 halts serialization if an array contains a function:
|
|
// `[1, true, getClass, 1]` serializes as "[1,true,],". FF 3.1b3
|
|
// elides non-JSON values from objects and arrays, unless they
|
|
// define custom `toJSON` methods.
|
|
stringify([undef, getClass, null]) == "[null,null,null]" &&
|
|
// Simple serialization test. FF 3.1b1 uses Unicode escape sequences
|
|
// where character escape codes are expected (e.g., `\b` => `\u0008`).
|
|
stringify({ "a": [value, true, false, null, "\x00\b\n\f\r\t"] }) == serialized &&
|
|
// FF 3.1b1 and b2 ignore the `filter` and `width` arguments.
|
|
stringify(null, value) === "1" &&
|
|
stringify([1, 2], null, 1) == "[\n 1,\n 2\n]" &&
|
|
// JSON 2, Prototype <= 1.7, and older WebKit builds incorrectly
|
|
// serialize extended years.
|
|
stringify(new Date(-8.64e15)) == '"-271821-04-20T00:00:00.000Z"' &&
|
|
// The milliseconds are optional in ES 5, but required in 5.1.
|
|
stringify(new Date(8.64e15)) == '"+275760-09-13T00:00:00.000Z"' &&
|
|
// Firefox <= 11.0 incorrectly serializes years prior to 0 as negative
|
|
// four-digit years instead of six-digit years. Credits: @Yaffle.
|
|
stringify(new Date(-621987552e5)) == '"-000001-01-01T00:00:00.000Z"' &&
|
|
// Safari <= 5.1.5 and Opera >= 10.53 incorrectly serialize millisecond
|
|
// values less than 1000. Credits: @Yaffle.
|
|
stringify(new Date(-1)) == '"1969-12-31T23:59:59.999Z"';
|
|
} catch (exception) {
|
|
stringifySupported = false;
|
|
}
|
|
}
|
|
isSupported = stringifySupported;
|
|
}
|
|
// Test `JSON.parse`.
|
|
if (name == "json-parse") {
|
|
var parse = JSON3.parse;
|
|
if (typeof parse == "function") {
|
|
try {
|
|
// FF 3.1b1, b2 will throw an exception if a bare literal is provided.
|
|
// Conforming implementations should also coerce the initial argument to
|
|
// a string prior to parsing.
|
|
if (parse("0") === 0 && !parse(false)) {
|
|
// Simple parsing test.
|
|
value = parse(serialized);
|
|
var parseSupported = value["a"].length == 5 && value["a"][0] === 1;
|
|
if (parseSupported) {
|
|
try {
|
|
// Safari <= 5.1.2 and FF 3.1b1 allow unescaped tabs in strings.
|
|
parseSupported = !parse('"\t"');
|
|
} catch (exception) {}
|
|
if (parseSupported) {
|
|
try {
|
|
// FF 4.0 and 4.0.1 allow leading `+` signs and leading
|
|
// decimal points. FF 4.0, 4.0.1, and IE 9-10 also allow
|
|
// certain octal literals.
|
|
parseSupported = parse("01") !== 1;
|
|
} catch (exception) {}
|
|
}
|
|
if (parseSupported) {
|
|
try {
|
|
// FF 4.0, 4.0.1, and Rhino 1.7R3-R4 allow trailing decimal
|
|
// points. These environments, along with FF 3.1b1 and 2,
|
|
// also allow trailing commas in JSON objects and arrays.
|
|
parseSupported = parse("1.") !== 1;
|
|
} catch (exception) {}
|
|
}
|
|
}
|
|
}
|
|
} catch (exception) {
|
|
parseSupported = false;
|
|
}
|
|
}
|
|
isSupported = parseSupported;
|
|
}
|
|
}
|
|
return has[name] = !!isSupported;
|
|
}
|
|
|
|
if (!has("json")) {
|
|
// Common `[[Class]]` name aliases.
|
|
var functionClass = "[object Function]";
|
|
var dateClass = "[object Date]";
|
|
var numberClass = "[object Number]";
|
|
var stringClass = "[object String]";
|
|
var arrayClass = "[object Array]";
|
|
var booleanClass = "[object Boolean]";
|
|
|
|
// Detect incomplete support for accessing string characters by index.
|
|
var charIndexBuggy = has("bug-string-char-index");
|
|
|
|
// Define additional utility methods if the `Date` methods are buggy.
|
|
if (!isExtended) {
|
|
var floor = Math.floor;
|
|
// A mapping between the months of the year and the number of days between
|
|
// January 1st and the first of the respective month.
|
|
var Months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
|
|
// Internal: Calculates the number of days between the Unix epoch and the
|
|
// first day of the given month.
|
|
var getDay = function (year, month) {
|
|
return Months[month] + 365 * (year - 1970) + floor((year - 1969 + (month = +(month > 1))) / 4) - floor((year - 1901 + month) / 100) + floor((year - 1601 + month) / 400);
|
|
};
|
|
}
|
|
|
|
// Internal: Determines if a property is a direct property of the given
|
|
// object. Delegates to the native `Object#hasOwnProperty` method.
|
|
if (!(isProperty = {}.hasOwnProperty)) {
|
|
isProperty = function (property) {
|
|
var members = {}, constructor;
|
|
if ((members.__proto__ = null, members.__proto__ = {
|
|
// The *proto* property cannot be set multiple times in recent
|
|
// versions of Firefox and SeaMonkey.
|
|
"toString": 1
|
|
}, members).toString != getClass) {
|
|
// Safari <= 2.0.3 doesn't implement `Object#hasOwnProperty`, but
|
|
// supports the mutable *proto* property.
|
|
isProperty = function (property) {
|
|
// Capture and break the object's prototype chain (see section 8.6.2
|
|
// of the ES 5.1 spec). The parenthesized expression prevents an
|
|
// unsafe transformation by the Closure Compiler.
|
|
var original = this.__proto__, result = property in (this.__proto__ = null, this);
|
|
// Restore the original prototype chain.
|
|
this.__proto__ = original;
|
|
return result;
|
|
};
|
|
} else {
|
|
// Capture a reference to the top-level `Object` constructor.
|
|
constructor = members.constructor;
|
|
// Use the `constructor` property to simulate `Object#hasOwnProperty` in
|
|
// other environments.
|
|
isProperty = function (property) {
|
|
var parent = (this.constructor || constructor).prototype;
|
|
return property in this && !(property in parent && this[property] === parent[property]);
|
|
};
|
|
}
|
|
members = null;
|
|
return isProperty.call(this, property);
|
|
};
|
|
}
|
|
|
|
// Internal: A set of primitive types used by `isHostType`.
|
|
var PrimitiveTypes = {
|
|
'boolean': 1,
|
|
'number': 1,
|
|
'string': 1,
|
|
'undefined': 1
|
|
};
|
|
|
|
// Internal: Determines if the given object `property` value is a
|
|
// non-primitive.
|
|
var isHostType = function (object, property) {
|
|
var type = typeof object[property];
|
|
return type == 'object' ? !!object[property] : !PrimitiveTypes[type];
|
|
};
|
|
|
|
// Internal: Normalizes the `for...in` iteration algorithm across
|
|
// environments. Each enumerated key is yielded to a `callback` function.
|
|
forEach = function (object, callback) {
|
|
var size = 0, Properties, members, property;
|
|
|
|
// Tests for bugs in the current environment's `for...in` algorithm. The
|
|
// `valueOf` property inherits the non-enumerable flag from
|
|
// `Object.prototype` in older versions of IE, Netscape, and Mozilla.
|
|
(Properties = function () {
|
|
this.valueOf = 0;
|
|
}).prototype.valueOf = 0;
|
|
|
|
// Iterate over a new instance of the `Properties` class.
|
|
members = new Properties();
|
|
for (property in members) {
|
|
// Ignore all properties inherited from `Object.prototype`.
|
|
if (isProperty.call(members, property)) {
|
|
size++;
|
|
}
|
|
}
|
|
Properties = members = null;
|
|
|
|
// Normalize the iteration algorithm.
|
|
if (!size) {
|
|
// A list of non-enumerable properties inherited from `Object.prototype`.
|
|
members = ["valueOf", "toString", "toLocaleString", "propertyIsEnumerable", "isPrototypeOf", "hasOwnProperty", "constructor"];
|
|
// IE <= 8, Mozilla 1.0, and Netscape 6.2 ignore shadowed non-enumerable
|
|
// properties.
|
|
forEach = function (object, callback) {
|
|
var isFunction = getClass.call(object) == functionClass, property, length;
|
|
var hasProperty = !isFunction && typeof object.constructor != 'function' && isHostType(object, 'hasOwnProperty') ? object.hasOwnProperty : isProperty;
|
|
for (property in object) {
|
|
// Gecko <= 1.0 enumerates the `prototype` property of functions under
|
|
// certain conditions; IE does not.
|
|
if (!(isFunction && property == "prototype") && hasProperty.call(object, property)) {
|
|
callback(property);
|
|
}
|
|
}
|
|
// Manually invoke the callback for each non-enumerable property.
|
|
for (length = members.length; property = members[--length]; hasProperty.call(object, property) && callback(property));
|
|
};
|
|
} else if (size == 2) {
|
|
// Safari <= 2.0.4 enumerates shadowed properties twice.
|
|
forEach = function (object, callback) {
|
|
// Create a set of iterated properties.
|
|
var members = {}, isFunction = getClass.call(object) == functionClass, property;
|
|
for (property in object) {
|
|
// Store each property name to prevent double enumeration. The
|
|
// `prototype` property of functions is not enumerated due to cross-
|
|
// environment inconsistencies.
|
|
if (!(isFunction && property == "prototype") && !isProperty.call(members, property) && (members[property] = 1) && isProperty.call(object, property)) {
|
|
callback(property);
|
|
}
|
|
}
|
|
};
|
|
} else {
|
|
// No bugs detected; use the standard `for...in` algorithm.
|
|
forEach = function (object, callback) {
|
|
var isFunction = getClass.call(object) == functionClass, property, isConstructor;
|
|
for (property in object) {
|
|
if (!(isFunction && property == "prototype") && isProperty.call(object, property) && !(isConstructor = property === "constructor")) {
|
|
callback(property);
|
|
}
|
|
}
|
|
// Manually invoke the callback for the `constructor` property due to
|
|
// cross-environment inconsistencies.
|
|
if (isConstructor || isProperty.call(object, (property = "constructor"))) {
|
|
callback(property);
|
|
}
|
|
};
|
|
}
|
|
return forEach(object, callback);
|
|
};
|
|
|
|
// Public: Serializes a JavaScript `value` as a JSON string. The optional
|
|
// `filter` argument may specify either a function that alters how object and
|
|
// array members are serialized, or an array of strings and numbers that
|
|
// indicates which properties should be serialized. The optional `width`
|
|
// argument may be either a string or number that specifies the indentation
|
|
// level of the output.
|
|
if (!has("json-stringify")) {
|
|
// Internal: A map of control characters and their escaped equivalents.
|
|
var Escapes = {
|
|
92: "\\\\",
|
|
34: '\\"',
|
|
8: "\\b",
|
|
12: "\\f",
|
|
10: "\\n",
|
|
13: "\\r",
|
|
9: "\\t"
|
|
};
|
|
|
|
// Internal: Converts `value` into a zero-padded string such that its
|
|
// length is at least equal to `width`. The `width` must be <= 6.
|
|
var leadingZeroes = "000000";
|
|
var toPaddedString = function (width, value) {
|
|
// The `|| 0` expression is necessary to work around a bug in
|
|
// Opera <= 7.54u2 where `0 == -0`, but `String(-0) !== "0"`.
|
|
return (leadingZeroes + (value || 0)).slice(-width);
|
|
};
|
|
|
|
// Internal: Double-quotes a string `value`, replacing all ASCII control
|
|
// characters (characters with code unit values between 0 and 31) with
|
|
// their escaped equivalents. This is an implementation of the
|
|
// `Quote(value)` operation defined in ES 5.1 section 15.12.3.
|
|
var unicodePrefix = "\\u00";
|
|
var quote = function (value) {
|
|
var result = '"', index = 0, length = value.length, isLarge = length > 10 && charIndexBuggy, symbols;
|
|
if (isLarge) {
|
|
symbols = value.split("");
|
|
}
|
|
for (; index < length; index++) {
|
|
var charCode = value.charCodeAt(index);
|
|
// If the character is a control character, append its Unicode or
|
|
// shorthand escape sequence; otherwise, append the character as-is.
|
|
switch (charCode) {
|
|
case 8: case 9: case 10: case 12: case 13: case 34: case 92:
|
|
result += Escapes[charCode];
|
|
break;
|
|
default:
|
|
if (charCode < 32) {
|
|
result += unicodePrefix + toPaddedString(2, charCode.toString(16));
|
|
break;
|
|
}
|
|
result += isLarge ? symbols[index] : charIndexBuggy ? value.charAt(index) : value[index];
|
|
}
|
|
}
|
|
return result + '"';
|
|
};
|
|
|
|
// Internal: Recursively serializes an object. Implements the
|
|
// `Str(key, holder)`, `JO(value)`, and `JA(value)` operations.
|
|
var serialize = function (property, object, callback, properties, whitespace, indentation, stack) {
|
|
var value, className, year, month, date, time, hours, minutes, seconds, milliseconds, results, element, index, length, prefix, result;
|
|
try {
|
|
// Necessary for host object support.
|
|
value = object[property];
|
|
} catch (exception) {}
|
|
if (typeof value == "object" && value) {
|
|
className = getClass.call(value);
|
|
if (className == dateClass && !isProperty.call(value, "toJSON")) {
|
|
if (value > -1 / 0 && value < 1 / 0) {
|
|
// Dates are serialized according to the `Date#toJSON` method
|
|
// specified in ES 5.1 section 15.9.5.44. See section 15.9.1.15
|
|
// for the ISO 8601 date time string format.
|
|
if (getDay) {
|
|
// Manually compute the year, month, date, hours, minutes,
|
|
// seconds, and milliseconds if the `getUTC*` methods are
|
|
// buggy. Adapted from @Yaffle's `date-shim` project.
|
|
date = floor(value / 864e5);
|
|
for (year = floor(date / 365.2425) + 1970 - 1; getDay(year + 1, 0) <= date; year++);
|
|
for (month = floor((date - getDay(year, 0)) / 30.42); getDay(year, month + 1) <= date; month++);
|
|
date = 1 + date - getDay(year, month);
|
|
// The `time` value specifies the time within the day (see ES
|
|
// 5.1 section 15.9.1.2). The formula `(A % B + B) % B` is used
|
|
// to compute `A modulo B`, as the `%` operator does not
|
|
// correspond to the `modulo` operation for negative numbers.
|
|
time = (value % 864e5 + 864e5) % 864e5;
|
|
// The hours, minutes, seconds, and milliseconds are obtained by
|
|
// decomposing the time within the day. See section 15.9.1.10.
|
|
hours = floor(time / 36e5) % 24;
|
|
minutes = floor(time / 6e4) % 60;
|
|
seconds = floor(time / 1e3) % 60;
|
|
milliseconds = time % 1e3;
|
|
} else {
|
|
year = value.getUTCFullYear();
|
|
month = value.getUTCMonth();
|
|
date = value.getUTCDate();
|
|
hours = value.getUTCHours();
|
|
minutes = value.getUTCMinutes();
|
|
seconds = value.getUTCSeconds();
|
|
milliseconds = value.getUTCMilliseconds();
|
|
}
|
|
// Serialize extended years correctly.
|
|
value = (year <= 0 || year >= 1e4 ? (year < 0 ? "-" : "+") + toPaddedString(6, year < 0 ? -year : year) : toPaddedString(4, year)) +
|
|
"-" + toPaddedString(2, month + 1) + "-" + toPaddedString(2, date) +
|
|
// Months, dates, hours, minutes, and seconds should have two
|
|
// digits; milliseconds should have three.
|
|
"T" + toPaddedString(2, hours) + ":" + toPaddedString(2, minutes) + ":" + toPaddedString(2, seconds) +
|
|
// Milliseconds are optional in ES 5.0, but required in 5.1.
|
|
"." + toPaddedString(3, milliseconds) + "Z";
|
|
} else {
|
|
value = null;
|
|
}
|
|
} else if (typeof value.toJSON == "function" && ((className != numberClass && className != stringClass && className != arrayClass) || isProperty.call(value, "toJSON"))) {
|
|
// Prototype <= 1.6.1 adds non-standard `toJSON` methods to the
|
|
// `Number`, `String`, `Date`, and `Array` prototypes. JSON 3
|
|
// ignores all `toJSON` methods on these objects unless they are
|
|
// defined directly on an instance.
|
|
value = value.toJSON(property);
|
|
}
|
|
}
|
|
if (callback) {
|
|
// If a replacement function was provided, call it to obtain the value
|
|
// for serialization.
|
|
value = callback.call(object, property, value);
|
|
}
|
|
if (value === null) {
|
|
return "null";
|
|
}
|
|
className = getClass.call(value);
|
|
if (className == booleanClass) {
|
|
// Booleans are represented literally.
|
|
return "" + value;
|
|
} else if (className == numberClass) {
|
|
// JSON numbers must be finite. `Infinity` and `NaN` are serialized as
|
|
// `"null"`.
|
|
return value > -1 / 0 && value < 1 / 0 ? "" + value : "null";
|
|
} else if (className == stringClass) {
|
|
// Strings are double-quoted and escaped.
|
|
return quote("" + value);
|
|
}
|
|
// Recursively serialize objects and arrays.
|
|
if (typeof value == "object") {
|
|
// Check for cyclic structures. This is a linear search; performance
|
|
// is inversely proportional to the number of unique nested objects.
|
|
for (length = stack.length; length--;) {
|
|
if (stack[length] === value) {
|
|
// Cyclic structures cannot be serialized by `JSON.stringify`.
|
|
throw TypeError();
|
|
}
|
|
}
|
|
// Add the object to the stack of traversed objects.
|
|
stack.push(value);
|
|
results = [];
|
|
// Save the current indentation level and indent one additional level.
|
|
prefix = indentation;
|
|
indentation += whitespace;
|
|
if (className == arrayClass) {
|
|
// Recursively serialize array elements.
|
|
for (index = 0, length = value.length; index < length; index++) {
|
|
element = serialize(index, value, callback, properties, whitespace, indentation, stack);
|
|
results.push(element === undef ? "null" : element);
|
|
}
|
|
result = results.length ? (whitespace ? "[\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "]" : ("[" + results.join(",") + "]")) : "[]";
|
|
} else {
|
|
// Recursively serialize object members. Members are selected from
|
|
// either a user-specified list of property names, or the object
|
|
// itself.
|
|
forEach(properties || value, function (property) {
|
|
var element = serialize(property, value, callback, properties, whitespace, indentation, stack);
|
|
if (element !== undef) {
|
|
// According to ES 5.1 section 15.12.3: "If `gap` {whitespace}
|
|
// is not the empty string, let `member` {quote(property) + ":"}
|
|
// be the concatenation of `member` and the `space` character."
|
|
// The "`space` character" refers to the literal space
|
|
// character, not the `space` {width} argument provided to
|
|
// `JSON.stringify`.
|
|
results.push(quote(property) + ":" + (whitespace ? " " : "") + element);
|
|
}
|
|
});
|
|
result = results.length ? (whitespace ? "{\n" + indentation + results.join(",\n" + indentation) + "\n" + prefix + "}" : ("{" + results.join(",") + "}")) : "{}";
|
|
}
|
|
// Remove the object from the traversed object stack.
|
|
stack.pop();
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Public: `JSON.stringify`. See ES 5.1 section 15.12.3.
|
|
JSON3.stringify = function (source, filter, width) {
|
|
var whitespace, callback, properties, className;
|
|
if (typeof filter == "function" || typeof filter == "object" && filter) {
|
|
if ((className = getClass.call(filter)) == functionClass) {
|
|
callback = filter;
|
|
} else if (className == arrayClass) {
|
|
// Convert the property names array into a makeshift set.
|
|
properties = {};
|
|
for (var index = 0, length = filter.length, value; index < length; value = filter[index++], ((className = getClass.call(value)), className == stringClass || className == numberClass) && (properties[value] = 1));
|
|
}
|
|
}
|
|
if (width) {
|
|
if ((className = getClass.call(width)) == numberClass) {
|
|
// Convert the `width` to an integer and create a string containing
|
|
// `width` number of space characters.
|
|
if ((width -= width % 1) > 0) {
|
|
for (whitespace = "", width > 10 && (width = 10); whitespace.length < width; whitespace += " ");
|
|
}
|
|
} else if (className == stringClass) {
|
|
whitespace = width.length <= 10 ? width : width.slice(0, 10);
|
|
}
|
|
}
|
|
// Opera <= 7.54u2 discards the values associated with empty string keys
|
|
// (`""`) only if they are used directly within an object member list
|
|
// (e.g., `!("" in { "": 1})`).
|
|
return serialize("", (value = {}, value[""] = source, value), callback, properties, whitespace, "", []);
|
|
};
|
|
}
|
|
|
|
// Public: Parses a JSON source string.
|
|
if (!has("json-parse")) {
|
|
var fromCharCode = String.fromCharCode;
|
|
|
|
// Internal: A map of escaped control characters and their unescaped
|
|
// equivalents.
|
|
var Unescapes = {
|
|
92: "\\",
|
|
34: '"',
|
|
47: "/",
|
|
98: "\b",
|
|
116: "\t",
|
|
110: "\n",
|
|
102: "\f",
|
|
114: "\r"
|
|
};
|
|
|
|
// Internal: Stores the parser state.
|
|
var Index, Source;
|
|
|
|
// Internal: Resets the parser state and throws a `SyntaxError`.
|
|
var abort = function() {
|
|
Index = Source = null;
|
|
throw SyntaxError();
|
|
};
|
|
|
|
// Internal: Returns the next token, or `"$"` if the parser has reached
|
|
// the end of the source string. A token may be a string, number, `null`
|
|
// literal, or Boolean literal.
|
|
var lex = function () {
|
|
var source = Source, length = source.length, value, begin, position, isSigned, charCode;
|
|
while (Index < length) {
|
|
charCode = source.charCodeAt(Index);
|
|
switch (charCode) {
|
|
case 9: case 10: case 13: case 32:
|
|
// Skip whitespace tokens, including tabs, carriage returns, line
|
|
// feeds, and space characters.
|
|
Index++;
|
|
break;
|
|
case 123: case 125: case 91: case 93: case 58: case 44:
|
|
// Parse a punctuator token (`{`, `}`, `[`, `]`, `:`, or `,`) at
|
|
// the current position.
|
|
value = charIndexBuggy ? source.charAt(Index) : source[Index];
|
|
Index++;
|
|
return value;
|
|
case 34:
|
|
// `"` delimits a JSON string; advance to the next character and
|
|
// begin parsing the string. String tokens are prefixed with the
|
|
// sentinel `@` character to distinguish them from punctuators and
|
|
// end-of-string tokens.
|
|
for (value = "@", Index++; Index < length;) {
|
|
charCode = source.charCodeAt(Index);
|
|
if (charCode < 32) {
|
|
// Unescaped ASCII control characters (those with a code unit
|
|
// less than the space character) are not permitted.
|
|
abort();
|
|
} else if (charCode == 92) {
|
|
// A reverse solidus (`\`) marks the beginning of an escaped
|
|
// control character (including `"`, `\`, and `/`) or Unicode
|
|
// escape sequence.
|
|
charCode = source.charCodeAt(++Index);
|
|
switch (charCode) {
|
|
case 92: case 34: case 47: case 98: case 116: case 110: case 102: case 114:
|
|
// Revive escaped control characters.
|
|
value += Unescapes[charCode];
|
|
Index++;
|
|
break;
|
|
case 117:
|
|
// `\u` marks the beginning of a Unicode escape sequence.
|
|
// Advance to the first character and validate the
|
|
// four-digit code point.
|
|
begin = ++Index;
|
|
for (position = Index + 4; Index < position; Index++) {
|
|
charCode = source.charCodeAt(Index);
|
|
// A valid sequence comprises four hexdigits (case-
|
|
// insensitive) that form a single hexadecimal value.
|
|
if (!(charCode >= 48 && charCode <= 57 || charCode >= 97 && charCode <= 102 || charCode >= 65 && charCode <= 70)) {
|
|
// Invalid Unicode escape sequence.
|
|
abort();
|
|
}
|
|
}
|
|
// Revive the escaped character.
|
|
value += fromCharCode("0x" + source.slice(begin, Index));
|
|
break;
|
|
default:
|
|
// Invalid escape sequence.
|
|
abort();
|
|
}
|
|
} else {
|
|
if (charCode == 34) {
|
|
// An unescaped double-quote character marks the end of the
|
|
// string.
|
|
break;
|
|
}
|
|
charCode = source.charCodeAt(Index);
|
|
begin = Index;
|
|
// Optimize for the common case where a string is valid.
|
|
while (charCode >= 32 && charCode != 92 && charCode != 34) {
|
|
charCode = source.charCodeAt(++Index);
|
|
}
|
|
// Append the string as-is.
|
|
value += source.slice(begin, Index);
|
|
}
|
|
}
|
|
if (source.charCodeAt(Index) == 34) {
|
|
// Advance to the next character and return the revived string.
|
|
Index++;
|
|
return value;
|
|
}
|
|
// Unterminated string.
|
|
abort();
|
|
default:
|
|
// Parse numbers and literals.
|
|
begin = Index;
|
|
// Advance past the negative sign, if one is specified.
|
|
if (charCode == 45) {
|
|
isSigned = true;
|
|
charCode = source.charCodeAt(++Index);
|
|
}
|
|
// Parse an integer or floating-point value.
|
|
if (charCode >= 48 && charCode <= 57) {
|
|
// Leading zeroes are interpreted as octal literals.
|
|
if (charCode == 48 && ((charCode = source.charCodeAt(Index + 1)), charCode >= 48 && charCode <= 57)) {
|
|
// Illegal octal literal.
|
|
abort();
|
|
}
|
|
isSigned = false;
|
|
// Parse the integer component.
|
|
for (; Index < length && ((charCode = source.charCodeAt(Index)), charCode >= 48 && charCode <= 57); Index++);
|
|
// Floats cannot contain a leading decimal point; however, this
|
|
// case is already accounted for by the parser.
|
|
if (source.charCodeAt(Index) == 46) {
|
|
position = ++Index;
|
|
// Parse the decimal component.
|
|
for (; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++);
|
|
if (position == Index) {
|
|
// Illegal trailing decimal.
|
|
abort();
|
|
}
|
|
Index = position;
|
|
}
|
|
// Parse exponents. The `e` denoting the exponent is
|
|
// case-insensitive.
|
|
charCode = source.charCodeAt(Index);
|
|
if (charCode == 101 || charCode == 69) {
|
|
charCode = source.charCodeAt(++Index);
|
|
// Skip past the sign following the exponent, if one is
|
|
// specified.
|
|
if (charCode == 43 || charCode == 45) {
|
|
Index++;
|
|
}
|
|
// Parse the exponential component.
|
|
for (position = Index; position < length && ((charCode = source.charCodeAt(position)), charCode >= 48 && charCode <= 57); position++);
|
|
if (position == Index) {
|
|
// Illegal empty exponent.
|
|
abort();
|
|
}
|
|
Index = position;
|
|
}
|
|
// Coerce the parsed value to a JavaScript number.
|
|
return +source.slice(begin, Index);
|
|
}
|
|
// A negative sign may only precede numbers.
|
|
if (isSigned) {
|
|
abort();
|
|
}
|
|
// `true`, `false`, and `null` literals.
|
|
if (source.slice(Index, Index + 4) == "true") {
|
|
Index += 4;
|
|
return true;
|
|
} else if (source.slice(Index, Index + 5) == "false") {
|
|
Index += 5;
|
|
return false;
|
|
} else if (source.slice(Index, Index + 4) == "null") {
|
|
Index += 4;
|
|
return null;
|
|
}
|
|
// Unrecognized token.
|
|
abort();
|
|
}
|
|
}
|
|
// Return the sentinel `$` character if the parser has reached the end
|
|
// of the source string.
|
|
return "$";
|
|
};
|
|
|
|
// Internal: Parses a JSON `value` token.
|
|
var get = function (value) {
|
|
var results, hasMembers;
|
|
if (value == "$") {
|
|
// Unexpected end of input.
|
|
abort();
|
|
}
|
|
if (typeof value == "string") {
|
|
if ((charIndexBuggy ? value.charAt(0) : value[0]) == "@") {
|
|
// Remove the sentinel `@` character.
|
|
return value.slice(1);
|
|
}
|
|
// Parse object and array literals.
|
|
if (value == "[") {
|
|
// Parses a JSON array, returning a new JavaScript array.
|
|
results = [];
|
|
for (;; hasMembers || (hasMembers = true)) {
|
|
value = lex();
|
|
// A closing square bracket marks the end of the array literal.
|
|
if (value == "]") {
|
|
break;
|
|
}
|
|
// If the array literal contains elements, the current token
|
|
// should be a comma separating the previous element from the
|
|
// next.
|
|
if (hasMembers) {
|
|
if (value == ",") {
|
|
value = lex();
|
|
if (value == "]") {
|
|
// Unexpected trailing `,` in array literal.
|
|
abort();
|
|
}
|
|
} else {
|
|
// A `,` must separate each array element.
|
|
abort();
|
|
}
|
|
}
|
|
// Elisions and leading commas are not permitted.
|
|
if (value == ",") {
|
|
abort();
|
|
}
|
|
results.push(get(value));
|
|
}
|
|
return results;
|
|
} else if (value == "{") {
|
|
// Parses a JSON object, returning a new JavaScript object.
|
|
results = {};
|
|
for (;; hasMembers || (hasMembers = true)) {
|
|
value = lex();
|
|
// A closing curly brace marks the end of the object literal.
|
|
if (value == "}") {
|
|
break;
|
|
}
|
|
// If the object literal contains members, the current token
|
|
// should be a comma separator.
|
|
if (hasMembers) {
|
|
if (value == ",") {
|
|
value = lex();
|
|
if (value == "}") {
|
|
// Unexpected trailing `,` in object literal.
|
|
abort();
|
|
}
|
|
} else {
|
|
// A `,` must separate each object member.
|
|
abort();
|
|
}
|
|
}
|
|
// Leading commas are not permitted, object property names must be
|
|
// double-quoted strings, and a `:` must separate each property
|
|
// name and value.
|
|
if (value == "," || typeof value != "string" || (charIndexBuggy ? value.charAt(0) : value[0]) != "@" || lex() != ":") {
|
|
abort();
|
|
}
|
|
results[value.slice(1)] = get(lex());
|
|
}
|
|
return results;
|
|
}
|
|
// Unexpected token encountered.
|
|
abort();
|
|
}
|
|
return value;
|
|
};
|
|
|
|
// Internal: Updates a traversed object member.
|
|
var update = function(source, property, callback) {
|
|
var element = walk(source, property, callback);
|
|
if (element === undef) {
|
|
delete source[property];
|
|
} else {
|
|
source[property] = element;
|
|
}
|
|
};
|
|
|
|
// Internal: Recursively traverses a parsed JSON object, invoking the
|
|
// `callback` function for each value. This is an implementation of the
|
|
// `Walk(holder, name)` operation defined in ES 5.1 section 15.12.2.
|
|
var walk = function (source, property, callback) {
|
|
var value = source[property], length;
|
|
if (typeof value == "object" && value) {
|
|
// `forEach` can't be used to traverse an array in Opera <= 8.54
|
|
// because its `Object#hasOwnProperty` implementation returns `false`
|
|
// for array indices (e.g., `![1, 2, 3].hasOwnProperty("0")`).
|
|
if (getClass.call(value) == arrayClass) {
|
|
for (length = value.length; length--;) {
|
|
update(value, length, callback);
|
|
}
|
|
} else {
|
|
forEach(value, function (property) {
|
|
update(value, property, callback);
|
|
});
|
|
}
|
|
}
|
|
return callback.call(source, property, value);
|
|
};
|
|
|
|
// Public: `JSON.parse`. See ES 5.1 section 15.12.2.
|
|
JSON3.parse = function (source, callback) {
|
|
var result, value;
|
|
Index = 0;
|
|
Source = "" + source;
|
|
result = get(lex());
|
|
// If a JSON string contains multiple tokens, it is invalid.
|
|
if (lex() != "$") {
|
|
abort();
|
|
}
|
|
// Reset the parser state.
|
|
Index = Source = null;
|
|
return callback && getClass.call(callback) == functionClass ? walk((value = {}, value[""] = result, value), "", callback) : result;
|
|
};
|
|
}
|
|
}
|
|
|
|
// Export for asynchronous module loaders.
|
|
if (isLoader) {
|
|
define(function () {
|
|
return JSON3;
|
|
});
|
|
}
|
|
}(this));
|
|
|
|
},{}],46:[function(require,module,exports){
|
|
var util = require('util');
|
|
var hark = require('hark');
|
|
var webrtcSupport = require('webrtcsupport');
|
|
var getUserMedia = require('getusermedia');
|
|
var getScreenMedia = require('getscreenmedia');
|
|
var WildEmitter = require('wildemitter');
|
|
var GainController = require('mediastream-gain');
|
|
var mockconsole = require('mockconsole');
|
|
|
|
|
|
function isAllTracksEnded(stream) {
|
|
var isAllTracksEnded = true;
|
|
stream.getTracks().forEach(function (t) {
|
|
isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
|
|
});
|
|
return isAllTracksEnded;
|
|
}
|
|
|
|
function LocalMedia(opts) {
|
|
WildEmitter.call(this);
|
|
|
|
var config = this.config = {
|
|
autoAdjustMic: false,
|
|
detectSpeakingEvents: false,
|
|
audioFallback: false,
|
|
media: {
|
|
audio: true,
|
|
video: true
|
|
},
|
|
logger: mockconsole
|
|
};
|
|
|
|
var item;
|
|
for (item in opts) {
|
|
if (opts.hasOwnProperty(item)) {
|
|
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 = webrtcSupport.supportScreenSharing;
|
|
|
|
this.localStreams = [];
|
|
this.localScreens = [];
|
|
|
|
if (!webrtcSupport.supportGetUserMedia) {
|
|
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);
|
|
}
|
|
|
|
stream.getTracks().forEach(function (track) {
|
|
track.addEventListener('ended', function () {
|
|
if (isAllTracksEnded(stream)) {
|
|
self.emit('localStreamStopped', stream);
|
|
}
|
|
});
|
|
});
|
|
|
|
self.emit('localStream', stream);
|
|
} else {
|
|
// Fallback for users without a camera
|
|
if (self.config.audioFallback && (err.name === 'DevicesNotFoundError' || err.name === 'NotFoundError') && constraints.video !== false) {
|
|
constraints.video = false;
|
|
self.start(constraints, cb);
|
|
return;
|
|
}
|
|
}
|
|
if (cb) {
|
|
return cb(err, stream);
|
|
}
|
|
});
|
|
};
|
|
|
|
LocalMedia.prototype.stop = function (stream) {
|
|
// FIXME: duplicates cleanup code until fixed in FF
|
|
if (stream) {
|
|
stream.getTracks().forEach(function (track) { track.stop(); });
|
|
var idx = this.localStreams.indexOf(stream);
|
|
if (idx > -1) {
|
|
this.localStreams.splice(idx, 1);
|
|
if (webrtcSupport.prefix === 'moz') {
|
|
this.emit('localStreamStopped', stream);
|
|
}
|
|
} else {
|
|
idx = this.localScreens.indexOf(stream);
|
|
if (idx > -1) {
|
|
this.localScreens.splice(idx, 1);
|
|
if (webrtcSupport.prefix === 'moz') {
|
|
this.emit('localScreenStopped', stream);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this.stopStreams();
|
|
this.stopScreenShare();
|
|
}
|
|
};
|
|
|
|
LocalMedia.prototype.stopStreams = function () {
|
|
var self = this;
|
|
if (this.audioMonitor) {
|
|
this.audioMonitor.stop();
|
|
delete this.audioMonitor;
|
|
}
|
|
var oldLocalStream = this.localStreams;
|
|
this.localStreams = [];
|
|
|
|
oldLocalStream.forEach(function (stream) {
|
|
stream.getTracks().forEach(function (track) { track.stop(); });
|
|
if (webrtcSupport.prefix === 'moz') {
|
|
self.emit('localStreamStopped', stream);
|
|
}
|
|
});
|
|
};
|
|
|
|
LocalMedia.prototype.startScreenShare = function (mode, cb) {
|
|
var self = this;
|
|
getScreenMedia(mode, function (err, stream) {
|
|
if (!err) {
|
|
self.localScreens.push(stream);
|
|
|
|
stream.getTracks().forEach(function (track) {
|
|
track.addEventListener('ended', function () {
|
|
if (isAllTracksEnded(stream)) {
|
|
self.emit('localScreenStopped', stream);
|
|
}
|
|
});
|
|
});
|
|
|
|
self.emit('localScreen', stream);
|
|
}
|
|
|
|
// enable the callback
|
|
if (cb) {
|
|
return cb(err, stream);
|
|
}
|
|
});
|
|
};
|
|
|
|
LocalMedia.prototype.stopScreenShare = function (stream) {
|
|
var self = this;
|
|
if (stream) {
|
|
stream.getTracks().forEach(function (track) { track.stop(); });
|
|
var idx = this.localScreens.indexOf(stream);
|
|
if (idx > -1) {
|
|
this.localScreens.splice(idx, 1);
|
|
if (webrtcSupport.prefix === 'moz') {
|
|
this.emit('localScreenStopped', stream);
|
|
}
|
|
}
|
|
} else {
|
|
var oldLocalScreens = this.localScreens;
|
|
this.localScreens = [];
|
|
|
|
oldLocalScreens.forEach(function (stream) {
|
|
stream.getTracks().forEach(function (track) { track.stop(); });
|
|
if (webrtcSupport.prefix === 'moz') {
|
|
self.emit('localScreenStopped', stream);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// 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;
|
|
var hasAudioTracks = false;
|
|
this.localStreams.forEach(function (stream) {
|
|
var audioTracks = stream.getAudioTracks();
|
|
if (audioTracks.length > 0) {
|
|
hasAudioTracks = true;
|
|
audioTracks.forEach(function (track) {
|
|
enabled = enabled && track.enabled;
|
|
});
|
|
}
|
|
});
|
|
|
|
// If no audioTracks were found, that means there is no microphone device.
|
|
// In that case, isAudioEnabled should return false.
|
|
if (!hasAudioTracks) {
|
|
return false;
|
|
}
|
|
|
|
return enabled;
|
|
};
|
|
|
|
// check if all video streams are enabled
|
|
LocalMedia.prototype.isVideoEnabled = function () {
|
|
var enabled = true;
|
|
var hasVideoTracks = false;
|
|
this.localStreams.forEach(function (stream) {
|
|
var videoTracks = stream.getVideoTracks();
|
|
if (videoTracks.length > 0) {
|
|
hasVideoTracks = true;
|
|
videoTracks.forEach(function (track) {
|
|
enabled = enabled && track.enabled;
|
|
});
|
|
}
|
|
});
|
|
|
|
// If no videoTracks were found, that means there is no camera device.
|
|
// In that case, isVideoEnabled should return false.
|
|
if (!hasVideoTracks) {
|
|
return false;
|
|
}
|
|
|
|
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.localStreams.length > 0 ? this.localStreams[0] : null;
|
|
}
|
|
});
|
|
// fallback for old .localScreen behaviour
|
|
Object.defineProperty(LocalMedia.prototype, 'localScreen', {
|
|
get: function () {
|
|
return this.localScreens.length > 0 ? this.localScreens[0] : null;
|
|
}
|
|
});
|
|
|
|
module.exports = LocalMedia;
|
|
|
|
},{"getscreenmedia":27,"getusermedia":29,"hark":40,"mediastream-gain":48,"mockconsole":50,"util":89,"webrtcsupport":99,"wildemitter":100}],47:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* lodash (Custom Build) <https://lodash.com/>
|
|
* Build: `lodash modularize exports="npm" -o ./`
|
|
* Copyright jQuery Foundation and other contributors <https://jquery.org/>
|
|
* Released under MIT license <https://lodash.com/license>
|
|
* Based on Underscore.js 1.8.3 <http://underscorejs.org/LICENSE>
|
|
* Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
|
|
*/
|
|
|
|
/** Used as the size to enable large array optimizations. */
|
|
var LARGE_ARRAY_SIZE = 200;
|
|
|
|
/** Used to stand-in for `undefined` hash values. */
|
|
var HASH_UNDEFINED = '__lodash_hash_undefined__';
|
|
|
|
/** Used as references for various `Number` constants. */
|
|
var MAX_SAFE_INTEGER = 9007199254740991;
|
|
|
|
/** `Object#toString` result references. */
|
|
var argsTag = '[object Arguments]',
|
|
arrayTag = '[object Array]',
|
|
boolTag = '[object Boolean]',
|
|
dateTag = '[object Date]',
|
|
errorTag = '[object Error]',
|
|
funcTag = '[object Function]',
|
|
genTag = '[object GeneratorFunction]',
|
|
mapTag = '[object Map]',
|
|
numberTag = '[object Number]',
|
|
objectTag = '[object Object]',
|
|
promiseTag = '[object Promise]',
|
|
regexpTag = '[object RegExp]',
|
|
setTag = '[object Set]',
|
|
stringTag = '[object String]',
|
|
symbolTag = '[object Symbol]',
|
|
weakMapTag = '[object WeakMap]';
|
|
|
|
var arrayBufferTag = '[object ArrayBuffer]',
|
|
dataViewTag = '[object DataView]',
|
|
float32Tag = '[object Float32Array]',
|
|
float64Tag = '[object Float64Array]',
|
|
int8Tag = '[object Int8Array]',
|
|
int16Tag = '[object Int16Array]',
|
|
int32Tag = '[object Int32Array]',
|
|
uint8Tag = '[object Uint8Array]',
|
|
uint8ClampedTag = '[object Uint8ClampedArray]',
|
|
uint16Tag = '[object Uint16Array]',
|
|
uint32Tag = '[object Uint32Array]';
|
|
|
|
/**
|
|
* Used to match `RegExp`
|
|
* [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
|
|
*/
|
|
var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
|
|
|
|
/** Used to match `RegExp` flags from their coerced string values. */
|
|
var reFlags = /\w*$/;
|
|
|
|
/** Used to detect host constructors (Safari). */
|
|
var reIsHostCtor = /^\[object .+?Constructor\]$/;
|
|
|
|
/** Used to detect unsigned integer values. */
|
|
var reIsUint = /^(?:0|[1-9]\d*)$/;
|
|
|
|
/** Used to identify `toStringTag` values supported by `_.clone`. */
|
|
var cloneableTags = {};
|
|
cloneableTags[argsTag] = cloneableTags[arrayTag] =
|
|
cloneableTags[arrayBufferTag] = cloneableTags[dataViewTag] =
|
|
cloneableTags[boolTag] = cloneableTags[dateTag] =
|
|
cloneableTags[float32Tag] = cloneableTags[float64Tag] =
|
|
cloneableTags[int8Tag] = cloneableTags[int16Tag] =
|
|
cloneableTags[int32Tag] = cloneableTags[mapTag] =
|
|
cloneableTags[numberTag] = cloneableTags[objectTag] =
|
|
cloneableTags[regexpTag] = cloneableTags[setTag] =
|
|
cloneableTags[stringTag] = cloneableTags[symbolTag] =
|
|
cloneableTags[uint8Tag] = cloneableTags[uint8ClampedTag] =
|
|
cloneableTags[uint16Tag] = cloneableTags[uint32Tag] = true;
|
|
cloneableTags[errorTag] = cloneableTags[funcTag] =
|
|
cloneableTags[weakMapTag] = false;
|
|
|
|
/** Detect free variable `global` from Node.js. */
|
|
var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
|
|
|
|
/** Detect free variable `self`. */
|
|
var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
|
|
|
|
/** Used as a reference to the global object. */
|
|
var root = freeGlobal || freeSelf || Function('return this')();
|
|
|
|
/** Detect free variable `exports`. */
|
|
var freeExports = typeof exports == 'object' && exports && !exports.nodeType && exports;
|
|
|
|
/** Detect free variable `module`. */
|
|
var freeModule = freeExports && typeof module == 'object' && module && !module.nodeType && module;
|
|
|
|
/** Detect the popular CommonJS extension `module.exports`. */
|
|
var moduleExports = freeModule && freeModule.exports === freeExports;
|
|
|
|
/**
|
|
* Adds the key-value `pair` to `map`.
|
|
*
|
|
* @private
|
|
* @param {Object} map The map to modify.
|
|
* @param {Array} pair The key-value pair to add.
|
|
* @returns {Object} Returns `map`.
|
|
*/
|
|
function addMapEntry(map, pair) {
|
|
// Don't return `map.set` because it's not chainable in IE 11.
|
|
map.set(pair[0], pair[1]);
|
|
return map;
|
|
}
|
|
|
|
/**
|
|
* Adds `value` to `set`.
|
|
*
|
|
* @private
|
|
* @param {Object} set The set to modify.
|
|
* @param {*} value The value to add.
|
|
* @returns {Object} Returns `set`.
|
|
*/
|
|
function addSetEntry(set, value) {
|
|
// Don't return `set.add` because it's not chainable in IE 11.
|
|
set.add(value);
|
|
return set;
|
|
}
|
|
|
|
/**
|
|
* A specialized version of `_.forEach` for arrays without support for
|
|
* iteratee shorthands.
|
|
*
|
|
* @private
|
|
* @param {Array} [array] The array to iterate over.
|
|
* @param {Function} iteratee The function invoked per iteration.
|
|
* @returns {Array} Returns `array`.
|
|
*/
|
|
function arrayEach(array, iteratee) {
|
|
var index = -1,
|
|
length = array ? array.length : 0;
|
|
|
|
while (++index < length) {
|
|
if (iteratee(array[index], index, array) === false) {
|
|
break;
|
|
}
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Appends the elements of `values` to `array`.
|
|
*
|
|
* @private
|
|
* @param {Array} array The array to modify.
|
|
* @param {Array} values The values to append.
|
|
* @returns {Array} Returns `array`.
|
|
*/
|
|
function arrayPush(array, values) {
|
|
var index = -1,
|
|
length = values.length,
|
|
offset = array.length;
|
|
|
|
while (++index < length) {
|
|
array[offset + index] = values[index];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* A specialized version of `_.reduce` for arrays without support for
|
|
* iteratee shorthands.
|
|
*
|
|
* @private
|
|
* @param {Array} [array] The array to iterate over.
|
|
* @param {Function} iteratee The function invoked per iteration.
|
|
* @param {*} [accumulator] The initial value.
|
|
* @param {boolean} [initAccum] Specify using the first element of `array` as
|
|
* the initial value.
|
|
* @returns {*} Returns the accumulated value.
|
|
*/
|
|
function arrayReduce(array, iteratee, accumulator, initAccum) {
|
|
var index = -1,
|
|
length = array ? array.length : 0;
|
|
|
|
if (initAccum && length) {
|
|
accumulator = array[++index];
|
|
}
|
|
while (++index < length) {
|
|
accumulator = iteratee(accumulator, array[index], index, array);
|
|
}
|
|
return accumulator;
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `_.times` without support for iteratee shorthands
|
|
* or max array length checks.
|
|
*
|
|
* @private
|
|
* @param {number} n The number of times to invoke `iteratee`.
|
|
* @param {Function} iteratee The function invoked per iteration.
|
|
* @returns {Array} Returns the array of results.
|
|
*/
|
|
function baseTimes(n, iteratee) {
|
|
var index = -1,
|
|
result = Array(n);
|
|
|
|
while (++index < n) {
|
|
result[index] = iteratee(index);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Gets the value at `key` of `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} [object] The object to query.
|
|
* @param {string} key The key of the property to get.
|
|
* @returns {*} Returns the property value.
|
|
*/
|
|
function getValue(object, key) {
|
|
return object == null ? undefined : object[key];
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is a host object in IE < 9.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is a host object, else `false`.
|
|
*/
|
|
function isHostObject(value) {
|
|
// Many host objects are `Object` objects that can coerce to strings
|
|
// despite having improperly defined `toString` methods.
|
|
var result = false;
|
|
if (value != null && typeof value.toString != 'function') {
|
|
try {
|
|
result = !!(value + '');
|
|
} catch (e) {}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Converts `map` to its key-value pairs.
|
|
*
|
|
* @private
|
|
* @param {Object} map The map to convert.
|
|
* @returns {Array} Returns the key-value pairs.
|
|
*/
|
|
function mapToArray(map) {
|
|
var index = -1,
|
|
result = Array(map.size);
|
|
|
|
map.forEach(function(value, key) {
|
|
result[++index] = [key, value];
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates a unary function that invokes `func` with its argument transformed.
|
|
*
|
|
* @private
|
|
* @param {Function} func The function to wrap.
|
|
* @param {Function} transform The argument transform.
|
|
* @returns {Function} Returns the new function.
|
|
*/
|
|
function overArg(func, transform) {
|
|
return function(arg) {
|
|
return func(transform(arg));
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts `set` to an array of its values.
|
|
*
|
|
* @private
|
|
* @param {Object} set The set to convert.
|
|
* @returns {Array} Returns the values.
|
|
*/
|
|
function setToArray(set) {
|
|
var index = -1,
|
|
result = Array(set.size);
|
|
|
|
set.forEach(function(value) {
|
|
result[++index] = value;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/** Used for built-in method references. */
|
|
var arrayProto = Array.prototype,
|
|
funcProto = Function.prototype,
|
|
objectProto = Object.prototype;
|
|
|
|
/** Used to detect overreaching core-js shims. */
|
|
var coreJsData = root['__core-js_shared__'];
|
|
|
|
/** Used to detect methods masquerading as native. */
|
|
var maskSrcKey = (function() {
|
|
var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
|
|
return uid ? ('Symbol(src)_1.' + uid) : '';
|
|
}());
|
|
|
|
/** Used to resolve the decompiled source of functions. */
|
|
var funcToString = funcProto.toString;
|
|
|
|
/** Used to check objects for own properties. */
|
|
var hasOwnProperty = objectProto.hasOwnProperty;
|
|
|
|
/**
|
|
* Used to resolve the
|
|
* [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
|
|
* of values.
|
|
*/
|
|
var objectToString = objectProto.toString;
|
|
|
|
/** Used to detect if a method is native. */
|
|
var reIsNative = RegExp('^' +
|
|
funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
|
|
.replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
|
|
);
|
|
|
|
/** Built-in value references. */
|
|
var Buffer = moduleExports ? root.Buffer : undefined,
|
|
Symbol = root.Symbol,
|
|
Uint8Array = root.Uint8Array,
|
|
getPrototype = overArg(Object.getPrototypeOf, Object),
|
|
objectCreate = Object.create,
|
|
propertyIsEnumerable = objectProto.propertyIsEnumerable,
|
|
splice = arrayProto.splice;
|
|
|
|
/* Built-in method references for those with the same name as other `lodash` methods. */
|
|
var nativeGetSymbols = Object.getOwnPropertySymbols,
|
|
nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,
|
|
nativeKeys = overArg(Object.keys, Object);
|
|
|
|
/* Built-in method references that are verified to be native. */
|
|
var DataView = getNative(root, 'DataView'),
|
|
Map = getNative(root, 'Map'),
|
|
Promise = getNative(root, 'Promise'),
|
|
Set = getNative(root, 'Set'),
|
|
WeakMap = getNative(root, 'WeakMap'),
|
|
nativeCreate = getNative(Object, 'create');
|
|
|
|
/** Used to detect maps, sets, and weakmaps. */
|
|
var dataViewCtorString = toSource(DataView),
|
|
mapCtorString = toSource(Map),
|
|
promiseCtorString = toSource(Promise),
|
|
setCtorString = toSource(Set),
|
|
weakMapCtorString = toSource(WeakMap);
|
|
|
|
/** Used to convert symbols to primitives and strings. */
|
|
var symbolProto = Symbol ? Symbol.prototype : undefined,
|
|
symbolValueOf = symbolProto ? symbolProto.valueOf : undefined;
|
|
|
|
/**
|
|
* Creates a hash object.
|
|
*
|
|
* @private
|
|
* @constructor
|
|
* @param {Array} [entries] The key-value pairs to cache.
|
|
*/
|
|
function Hash(entries) {
|
|
var index = -1,
|
|
length = entries ? entries.length : 0;
|
|
|
|
this.clear();
|
|
while (++index < length) {
|
|
var entry = entries[index];
|
|
this.set(entry[0], entry[1]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all key-value entries from the hash.
|
|
*
|
|
* @private
|
|
* @name clear
|
|
* @memberOf Hash
|
|
*/
|
|
function hashClear() {
|
|
this.__data__ = nativeCreate ? nativeCreate(null) : {};
|
|
}
|
|
|
|
/**
|
|
* Removes `key` and its value from the hash.
|
|
*
|
|
* @private
|
|
* @name delete
|
|
* @memberOf Hash
|
|
* @param {Object} hash The hash to modify.
|
|
* @param {string} key The key of the value to remove.
|
|
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
|
|
*/
|
|
function hashDelete(key) {
|
|
return this.has(key) && delete this.__data__[key];
|
|
}
|
|
|
|
/**
|
|
* Gets the hash value for `key`.
|
|
*
|
|
* @private
|
|
* @name get
|
|
* @memberOf Hash
|
|
* @param {string} key The key of the value to get.
|
|
* @returns {*} Returns the entry value.
|
|
*/
|
|
function hashGet(key) {
|
|
var data = this.__data__;
|
|
if (nativeCreate) {
|
|
var result = data[key];
|
|
return result === HASH_UNDEFINED ? undefined : result;
|
|
}
|
|
return hasOwnProperty.call(data, key) ? data[key] : undefined;
|
|
}
|
|
|
|
/**
|
|
* Checks if a hash value for `key` exists.
|
|
*
|
|
* @private
|
|
* @name has
|
|
* @memberOf Hash
|
|
* @param {string} key The key of the entry to check.
|
|
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
|
|
*/
|
|
function hashHas(key) {
|
|
var data = this.__data__;
|
|
return nativeCreate ? data[key] !== undefined : hasOwnProperty.call(data, key);
|
|
}
|
|
|
|
/**
|
|
* Sets the hash `key` to `value`.
|
|
*
|
|
* @private
|
|
* @name set
|
|
* @memberOf Hash
|
|
* @param {string} key The key of the value to set.
|
|
* @param {*} value The value to set.
|
|
* @returns {Object} Returns the hash instance.
|
|
*/
|
|
function hashSet(key, value) {
|
|
var data = this.__data__;
|
|
data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
|
|
return this;
|
|
}
|
|
|
|
// Add methods to `Hash`.
|
|
Hash.prototype.clear = hashClear;
|
|
Hash.prototype['delete'] = hashDelete;
|
|
Hash.prototype.get = hashGet;
|
|
Hash.prototype.has = hashHas;
|
|
Hash.prototype.set = hashSet;
|
|
|
|
/**
|
|
* Creates an list cache object.
|
|
*
|
|
* @private
|
|
* @constructor
|
|
* @param {Array} [entries] The key-value pairs to cache.
|
|
*/
|
|
function ListCache(entries) {
|
|
var index = -1,
|
|
length = entries ? entries.length : 0;
|
|
|
|
this.clear();
|
|
while (++index < length) {
|
|
var entry = entries[index];
|
|
this.set(entry[0], entry[1]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all key-value entries from the list cache.
|
|
*
|
|
* @private
|
|
* @name clear
|
|
* @memberOf ListCache
|
|
*/
|
|
function listCacheClear() {
|
|
this.__data__ = [];
|
|
}
|
|
|
|
/**
|
|
* Removes `key` and its value from the list cache.
|
|
*
|
|
* @private
|
|
* @name delete
|
|
* @memberOf ListCache
|
|
* @param {string} key The key of the value to remove.
|
|
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
|
|
*/
|
|
function listCacheDelete(key) {
|
|
var data = this.__data__,
|
|
index = assocIndexOf(data, key);
|
|
|
|
if (index < 0) {
|
|
return false;
|
|
}
|
|
var lastIndex = data.length - 1;
|
|
if (index == lastIndex) {
|
|
data.pop();
|
|
} else {
|
|
splice.call(data, index, 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Gets the list cache value for `key`.
|
|
*
|
|
* @private
|
|
* @name get
|
|
* @memberOf ListCache
|
|
* @param {string} key The key of the value to get.
|
|
* @returns {*} Returns the entry value.
|
|
*/
|
|
function listCacheGet(key) {
|
|
var data = this.__data__,
|
|
index = assocIndexOf(data, key);
|
|
|
|
return index < 0 ? undefined : data[index][1];
|
|
}
|
|
|
|
/**
|
|
* Checks if a list cache value for `key` exists.
|
|
*
|
|
* @private
|
|
* @name has
|
|
* @memberOf ListCache
|
|
* @param {string} key The key of the entry to check.
|
|
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
|
|
*/
|
|
function listCacheHas(key) {
|
|
return assocIndexOf(this.__data__, key) > -1;
|
|
}
|
|
|
|
/**
|
|
* Sets the list cache `key` to `value`.
|
|
*
|
|
* @private
|
|
* @name set
|
|
* @memberOf ListCache
|
|
* @param {string} key The key of the value to set.
|
|
* @param {*} value The value to set.
|
|
* @returns {Object} Returns the list cache instance.
|
|
*/
|
|
function listCacheSet(key, value) {
|
|
var data = this.__data__,
|
|
index = assocIndexOf(data, key);
|
|
|
|
if (index < 0) {
|
|
data.push([key, value]);
|
|
} else {
|
|
data[index][1] = value;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
// Add methods to `ListCache`.
|
|
ListCache.prototype.clear = listCacheClear;
|
|
ListCache.prototype['delete'] = listCacheDelete;
|
|
ListCache.prototype.get = listCacheGet;
|
|
ListCache.prototype.has = listCacheHas;
|
|
ListCache.prototype.set = listCacheSet;
|
|
|
|
/**
|
|
* Creates a map cache object to store key-value pairs.
|
|
*
|
|
* @private
|
|
* @constructor
|
|
* @param {Array} [entries] The key-value pairs to cache.
|
|
*/
|
|
function MapCache(entries) {
|
|
var index = -1,
|
|
length = entries ? entries.length : 0;
|
|
|
|
this.clear();
|
|
while (++index < length) {
|
|
var entry = entries[index];
|
|
this.set(entry[0], entry[1]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all key-value entries from the map.
|
|
*
|
|
* @private
|
|
* @name clear
|
|
* @memberOf MapCache
|
|
*/
|
|
function mapCacheClear() {
|
|
this.__data__ = {
|
|
'hash': new Hash,
|
|
'map': new (Map || ListCache),
|
|
'string': new Hash
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Removes `key` and its value from the map.
|
|
*
|
|
* @private
|
|
* @name delete
|
|
* @memberOf MapCache
|
|
* @param {string} key The key of the value to remove.
|
|
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
|
|
*/
|
|
function mapCacheDelete(key) {
|
|
return getMapData(this, key)['delete'](key);
|
|
}
|
|
|
|
/**
|
|
* Gets the map value for `key`.
|
|
*
|
|
* @private
|
|
* @name get
|
|
* @memberOf MapCache
|
|
* @param {string} key The key of the value to get.
|
|
* @returns {*} Returns the entry value.
|
|
*/
|
|
function mapCacheGet(key) {
|
|
return getMapData(this, key).get(key);
|
|
}
|
|
|
|
/**
|
|
* Checks if a map value for `key` exists.
|
|
*
|
|
* @private
|
|
* @name has
|
|
* @memberOf MapCache
|
|
* @param {string} key The key of the entry to check.
|
|
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
|
|
*/
|
|
function mapCacheHas(key) {
|
|
return getMapData(this, key).has(key);
|
|
}
|
|
|
|
/**
|
|
* Sets the map `key` to `value`.
|
|
*
|
|
* @private
|
|
* @name set
|
|
* @memberOf MapCache
|
|
* @param {string} key The key of the value to set.
|
|
* @param {*} value The value to set.
|
|
* @returns {Object} Returns the map cache instance.
|
|
*/
|
|
function mapCacheSet(key, value) {
|
|
getMapData(this, key).set(key, value);
|
|
return this;
|
|
}
|
|
|
|
// Add methods to `MapCache`.
|
|
MapCache.prototype.clear = mapCacheClear;
|
|
MapCache.prototype['delete'] = mapCacheDelete;
|
|
MapCache.prototype.get = mapCacheGet;
|
|
MapCache.prototype.has = mapCacheHas;
|
|
MapCache.prototype.set = mapCacheSet;
|
|
|
|
/**
|
|
* Creates a stack cache object to store key-value pairs.
|
|
*
|
|
* @private
|
|
* @constructor
|
|
* @param {Array} [entries] The key-value pairs to cache.
|
|
*/
|
|
function Stack(entries) {
|
|
this.__data__ = new ListCache(entries);
|
|
}
|
|
|
|
/**
|
|
* Removes all key-value entries from the stack.
|
|
*
|
|
* @private
|
|
* @name clear
|
|
* @memberOf Stack
|
|
*/
|
|
function stackClear() {
|
|
this.__data__ = new ListCache;
|
|
}
|
|
|
|
/**
|
|
* Removes `key` and its value from the stack.
|
|
*
|
|
* @private
|
|
* @name delete
|
|
* @memberOf Stack
|
|
* @param {string} key The key of the value to remove.
|
|
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
|
|
*/
|
|
function stackDelete(key) {
|
|
return this.__data__['delete'](key);
|
|
}
|
|
|
|
/**
|
|
* Gets the stack value for `key`.
|
|
*
|
|
* @private
|
|
* @name get
|
|
* @memberOf Stack
|
|
* @param {string} key The key of the value to get.
|
|
* @returns {*} Returns the entry value.
|
|
*/
|
|
function stackGet(key) {
|
|
return this.__data__.get(key);
|
|
}
|
|
|
|
/**
|
|
* Checks if a stack value for `key` exists.
|
|
*
|
|
* @private
|
|
* @name has
|
|
* @memberOf Stack
|
|
* @param {string} key The key of the entry to check.
|
|
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
|
|
*/
|
|
function stackHas(key) {
|
|
return this.__data__.has(key);
|
|
}
|
|
|
|
/**
|
|
* Sets the stack `key` to `value`.
|
|
*
|
|
* @private
|
|
* @name set
|
|
* @memberOf Stack
|
|
* @param {string} key The key of the value to set.
|
|
* @param {*} value The value to set.
|
|
* @returns {Object} Returns the stack cache instance.
|
|
*/
|
|
function stackSet(key, value) {
|
|
var cache = this.__data__;
|
|
if (cache instanceof ListCache) {
|
|
var pairs = cache.__data__;
|
|
if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
|
|
pairs.push([key, value]);
|
|
return this;
|
|
}
|
|
cache = this.__data__ = new MapCache(pairs);
|
|
}
|
|
cache.set(key, value);
|
|
return this;
|
|
}
|
|
|
|
// Add methods to `Stack`.
|
|
Stack.prototype.clear = stackClear;
|
|
Stack.prototype['delete'] = stackDelete;
|
|
Stack.prototype.get = stackGet;
|
|
Stack.prototype.has = stackHas;
|
|
Stack.prototype.set = stackSet;
|
|
|
|
/**
|
|
* Creates an array of the enumerable property names of the array-like `value`.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to query.
|
|
* @param {boolean} inherited Specify returning inherited property names.
|
|
* @returns {Array} Returns the array of property names.
|
|
*/
|
|
function arrayLikeKeys(value, inherited) {
|
|
// Safari 8.1 makes `arguments.callee` enumerable in strict mode.
|
|
// Safari 9 makes `arguments.length` enumerable in strict mode.
|
|
var result = (isArray(value) || isArguments(value))
|
|
? baseTimes(value.length, String)
|
|
: [];
|
|
|
|
var length = result.length,
|
|
skipIndexes = !!length;
|
|
|
|
for (var key in value) {
|
|
if ((inherited || hasOwnProperty.call(value, key)) &&
|
|
!(skipIndexes && (key == 'length' || isIndex(key, length)))) {
|
|
result.push(key);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Assigns `value` to `key` of `object` if the existing value is not equivalent
|
|
* using [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
|
|
* for equality comparisons.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to modify.
|
|
* @param {string} key The key of the property to assign.
|
|
* @param {*} value The value to assign.
|
|
*/
|
|
function assignValue(object, key, value) {
|
|
var objValue = object[key];
|
|
if (!(hasOwnProperty.call(object, key) && eq(objValue, value)) ||
|
|
(value === undefined && !(key in object))) {
|
|
object[key] = value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the index at which the `key` is found in `array` of key-value pairs.
|
|
*
|
|
* @private
|
|
* @param {Array} array The array to inspect.
|
|
* @param {*} key The key to search for.
|
|
* @returns {number} Returns the index of the matched value, else `-1`.
|
|
*/
|
|
function assocIndexOf(array, key) {
|
|
var length = array.length;
|
|
while (length--) {
|
|
if (eq(array[length][0], key)) {
|
|
return length;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `_.assign` without support for multiple sources
|
|
* or `customizer` functions.
|
|
*
|
|
* @private
|
|
* @param {Object} object The destination object.
|
|
* @param {Object} source The source object.
|
|
* @returns {Object} Returns `object`.
|
|
*/
|
|
function baseAssign(object, source) {
|
|
return object && copyObject(source, keys(source), object);
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `_.clone` and `_.cloneDeep` which tracks
|
|
* traversed objects.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to clone.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @param {boolean} [isFull] Specify a clone including symbols.
|
|
* @param {Function} [customizer] The function to customize cloning.
|
|
* @param {string} [key] The key of `value`.
|
|
* @param {Object} [object] The parent object of `value`.
|
|
* @param {Object} [stack] Tracks traversed objects and their clone counterparts.
|
|
* @returns {*} Returns the cloned value.
|
|
*/
|
|
function baseClone(value, isDeep, isFull, customizer, key, object, stack) {
|
|
var result;
|
|
if (customizer) {
|
|
result = object ? customizer(value, key, object, stack) : customizer(value);
|
|
}
|
|
if (result !== undefined) {
|
|
return result;
|
|
}
|
|
if (!isObject(value)) {
|
|
return value;
|
|
}
|
|
var isArr = isArray(value);
|
|
if (isArr) {
|
|
result = initCloneArray(value);
|
|
if (!isDeep) {
|
|
return copyArray(value, result);
|
|
}
|
|
} else {
|
|
var tag = getTag(value),
|
|
isFunc = tag == funcTag || tag == genTag;
|
|
|
|
if (isBuffer(value)) {
|
|
return cloneBuffer(value, isDeep);
|
|
}
|
|
if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
|
|
if (isHostObject(value)) {
|
|
return object ? value : {};
|
|
}
|
|
result = initCloneObject(isFunc ? {} : value);
|
|
if (!isDeep) {
|
|
return copySymbols(value, baseAssign(result, value));
|
|
}
|
|
} else {
|
|
if (!cloneableTags[tag]) {
|
|
return object ? value : {};
|
|
}
|
|
result = initCloneByTag(value, tag, baseClone, isDeep);
|
|
}
|
|
}
|
|
// Check for circular references and return its corresponding clone.
|
|
stack || (stack = new Stack);
|
|
var stacked = stack.get(value);
|
|
if (stacked) {
|
|
return stacked;
|
|
}
|
|
stack.set(value, result);
|
|
|
|
if (!isArr) {
|
|
var props = isFull ? getAllKeys(value) : keys(value);
|
|
}
|
|
arrayEach(props || value, function(subValue, key) {
|
|
if (props) {
|
|
key = subValue;
|
|
subValue = value[key];
|
|
}
|
|
// Recursively populate clone (susceptible to call stack limits).
|
|
assignValue(result, key, baseClone(subValue, isDeep, isFull, customizer, key, value, stack));
|
|
});
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `_.create` without support for assigning
|
|
* properties to the created object.
|
|
*
|
|
* @private
|
|
* @param {Object} prototype The object to inherit from.
|
|
* @returns {Object} Returns the new object.
|
|
*/
|
|
function baseCreate(proto) {
|
|
return isObject(proto) ? objectCreate(proto) : {};
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `getAllKeys` and `getAllKeysIn` which uses
|
|
* `keysFunc` and `symbolsFunc` to get the enumerable property names and
|
|
* symbols of `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to query.
|
|
* @param {Function} keysFunc The function to get the keys of `object`.
|
|
* @param {Function} symbolsFunc The function to get the symbols of `object`.
|
|
* @returns {Array} Returns the array of property names and symbols.
|
|
*/
|
|
function baseGetAllKeys(object, keysFunc, symbolsFunc) {
|
|
var result = keysFunc(object);
|
|
return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `getTag`.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to query.
|
|
* @returns {string} Returns the `toStringTag`.
|
|
*/
|
|
function baseGetTag(value) {
|
|
return objectToString.call(value);
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `_.isNative` without bad shim checks.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is a native function,
|
|
* else `false`.
|
|
*/
|
|
function baseIsNative(value) {
|
|
if (!isObject(value) || isMasked(value)) {
|
|
return false;
|
|
}
|
|
var pattern = (isFunction(value) || isHostObject(value)) ? reIsNative : reIsHostCtor;
|
|
return pattern.test(toSource(value));
|
|
}
|
|
|
|
/**
|
|
* The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to query.
|
|
* @returns {Array} Returns the array of property names.
|
|
*/
|
|
function baseKeys(object) {
|
|
if (!isPrototype(object)) {
|
|
return nativeKeys(object);
|
|
}
|
|
var result = [];
|
|
for (var key in Object(object)) {
|
|
if (hasOwnProperty.call(object, key) && key != 'constructor') {
|
|
result.push(key);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `buffer`.
|
|
*
|
|
* @private
|
|
* @param {Buffer} buffer The buffer to clone.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @returns {Buffer} Returns the cloned buffer.
|
|
*/
|
|
function cloneBuffer(buffer, isDeep) {
|
|
if (isDeep) {
|
|
return buffer.slice();
|
|
}
|
|
var result = new buffer.constructor(buffer.length);
|
|
buffer.copy(result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `arrayBuffer`.
|
|
*
|
|
* @private
|
|
* @param {ArrayBuffer} arrayBuffer The array buffer to clone.
|
|
* @returns {ArrayBuffer} Returns the cloned array buffer.
|
|
*/
|
|
function cloneArrayBuffer(arrayBuffer) {
|
|
var result = new arrayBuffer.constructor(arrayBuffer.byteLength);
|
|
new Uint8Array(result).set(new Uint8Array(arrayBuffer));
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `dataView`.
|
|
*
|
|
* @private
|
|
* @param {Object} dataView The data view to clone.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @returns {Object} Returns the cloned data view.
|
|
*/
|
|
function cloneDataView(dataView, isDeep) {
|
|
var buffer = isDeep ? cloneArrayBuffer(dataView.buffer) : dataView.buffer;
|
|
return new dataView.constructor(buffer, dataView.byteOffset, dataView.byteLength);
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `map`.
|
|
*
|
|
* @private
|
|
* @param {Object} map The map to clone.
|
|
* @param {Function} cloneFunc The function to clone values.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @returns {Object} Returns the cloned map.
|
|
*/
|
|
function cloneMap(map, isDeep, cloneFunc) {
|
|
var array = isDeep ? cloneFunc(mapToArray(map), true) : mapToArray(map);
|
|
return arrayReduce(array, addMapEntry, new map.constructor);
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `regexp`.
|
|
*
|
|
* @private
|
|
* @param {Object} regexp The regexp to clone.
|
|
* @returns {Object} Returns the cloned regexp.
|
|
*/
|
|
function cloneRegExp(regexp) {
|
|
var result = new regexp.constructor(regexp.source, reFlags.exec(regexp));
|
|
result.lastIndex = regexp.lastIndex;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `set`.
|
|
*
|
|
* @private
|
|
* @param {Object} set The set to clone.
|
|
* @param {Function} cloneFunc The function to clone values.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @returns {Object} Returns the cloned set.
|
|
*/
|
|
function cloneSet(set, isDeep, cloneFunc) {
|
|
var array = isDeep ? cloneFunc(setToArray(set), true) : setToArray(set);
|
|
return arrayReduce(array, addSetEntry, new set.constructor);
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of the `symbol` object.
|
|
*
|
|
* @private
|
|
* @param {Object} symbol The symbol object to clone.
|
|
* @returns {Object} Returns the cloned symbol object.
|
|
*/
|
|
function cloneSymbol(symbol) {
|
|
return symbolValueOf ? Object(symbolValueOf.call(symbol)) : {};
|
|
}
|
|
|
|
/**
|
|
* Creates a clone of `typedArray`.
|
|
*
|
|
* @private
|
|
* @param {Object} typedArray The typed array to clone.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @returns {Object} Returns the cloned typed array.
|
|
*/
|
|
function cloneTypedArray(typedArray, isDeep) {
|
|
var buffer = isDeep ? cloneArrayBuffer(typedArray.buffer) : typedArray.buffer;
|
|
return new typedArray.constructor(buffer, typedArray.byteOffset, typedArray.length);
|
|
}
|
|
|
|
/**
|
|
* Copies the values of `source` to `array`.
|
|
*
|
|
* @private
|
|
* @param {Array} source The array to copy values from.
|
|
* @param {Array} [array=[]] The array to copy values to.
|
|
* @returns {Array} Returns `array`.
|
|
*/
|
|
function copyArray(source, array) {
|
|
var index = -1,
|
|
length = source.length;
|
|
|
|
array || (array = Array(length));
|
|
while (++index < length) {
|
|
array[index] = source[index];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
/**
|
|
* Copies properties of `source` to `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} source The object to copy properties from.
|
|
* @param {Array} props The property identifiers to copy.
|
|
* @param {Object} [object={}] The object to copy properties to.
|
|
* @param {Function} [customizer] The function to customize copied values.
|
|
* @returns {Object} Returns `object`.
|
|
*/
|
|
function copyObject(source, props, object, customizer) {
|
|
object || (object = {});
|
|
|
|
var index = -1,
|
|
length = props.length;
|
|
|
|
while (++index < length) {
|
|
var key = props[index];
|
|
|
|
var newValue = customizer
|
|
? customizer(object[key], source[key], key, object, source)
|
|
: undefined;
|
|
|
|
assignValue(object, key, newValue === undefined ? source[key] : newValue);
|
|
}
|
|
return object;
|
|
}
|
|
|
|
/**
|
|
* Copies own symbol properties of `source` to `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} source The object to copy symbols from.
|
|
* @param {Object} [object={}] The object to copy symbols to.
|
|
* @returns {Object} Returns `object`.
|
|
*/
|
|
function copySymbols(source, object) {
|
|
return copyObject(source, getSymbols(source), object);
|
|
}
|
|
|
|
/**
|
|
* Creates an array of own enumerable property names and symbols of `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to query.
|
|
* @returns {Array} Returns the array of property names and symbols.
|
|
*/
|
|
function getAllKeys(object) {
|
|
return baseGetAllKeys(object, keys, getSymbols);
|
|
}
|
|
|
|
/**
|
|
* Gets the data for `map`.
|
|
*
|
|
* @private
|
|
* @param {Object} map The map to query.
|
|
* @param {string} key The reference key.
|
|
* @returns {*} Returns the map data.
|
|
*/
|
|
function getMapData(map, key) {
|
|
var data = map.__data__;
|
|
return isKeyable(key)
|
|
? data[typeof key == 'string' ? 'string' : 'hash']
|
|
: data.map;
|
|
}
|
|
|
|
/**
|
|
* Gets the native function at `key` of `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to query.
|
|
* @param {string} key The key of the method to get.
|
|
* @returns {*} Returns the function if it's native, else `undefined`.
|
|
*/
|
|
function getNative(object, key) {
|
|
var value = getValue(object, key);
|
|
return baseIsNative(value) ? value : undefined;
|
|
}
|
|
|
|
/**
|
|
* Creates an array of the own enumerable symbol properties of `object`.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to query.
|
|
* @returns {Array} Returns the array of symbols.
|
|
*/
|
|
var getSymbols = nativeGetSymbols ? overArg(nativeGetSymbols, Object) : stubArray;
|
|
|
|
/**
|
|
* Gets the `toStringTag` of `value`.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to query.
|
|
* @returns {string} Returns the `toStringTag`.
|
|
*/
|
|
var getTag = baseGetTag;
|
|
|
|
// Fallback for data views, maps, sets, and weak maps in IE 11,
|
|
// for data views in Edge < 14, and promises in Node.js.
|
|
if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
|
|
(Map && getTag(new Map) != mapTag) ||
|
|
(Promise && getTag(Promise.resolve()) != promiseTag) ||
|
|
(Set && getTag(new Set) != setTag) ||
|
|
(WeakMap && getTag(new WeakMap) != weakMapTag)) {
|
|
getTag = function(value) {
|
|
var result = objectToString.call(value),
|
|
Ctor = result == objectTag ? value.constructor : undefined,
|
|
ctorString = Ctor ? toSource(Ctor) : undefined;
|
|
|
|
if (ctorString) {
|
|
switch (ctorString) {
|
|
case dataViewCtorString: return dataViewTag;
|
|
case mapCtorString: return mapTag;
|
|
case promiseCtorString: return promiseTag;
|
|
case setCtorString: return setTag;
|
|
case weakMapCtorString: return weakMapTag;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Initializes an array clone.
|
|
*
|
|
* @private
|
|
* @param {Array} array The array to clone.
|
|
* @returns {Array} Returns the initialized clone.
|
|
*/
|
|
function initCloneArray(array) {
|
|
var length = array.length,
|
|
result = array.constructor(length);
|
|
|
|
// Add properties assigned by `RegExp#exec`.
|
|
if (length && typeof array[0] == 'string' && hasOwnProperty.call(array, 'index')) {
|
|
result.index = array.index;
|
|
result.input = array.input;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Initializes an object clone.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to clone.
|
|
* @returns {Object} Returns the initialized clone.
|
|
*/
|
|
function initCloneObject(object) {
|
|
return (typeof object.constructor == 'function' && !isPrototype(object))
|
|
? baseCreate(getPrototype(object))
|
|
: {};
|
|
}
|
|
|
|
/**
|
|
* Initializes an object clone based on its `toStringTag`.
|
|
*
|
|
* **Note:** This function only supports cloning values with tags of
|
|
* `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
|
|
*
|
|
* @private
|
|
* @param {Object} object The object to clone.
|
|
* @param {string} tag The `toStringTag` of the object to clone.
|
|
* @param {Function} cloneFunc The function to clone values.
|
|
* @param {boolean} [isDeep] Specify a deep clone.
|
|
* @returns {Object} Returns the initialized clone.
|
|
*/
|
|
function initCloneByTag(object, tag, cloneFunc, isDeep) {
|
|
var Ctor = object.constructor;
|
|
switch (tag) {
|
|
case arrayBufferTag:
|
|
return cloneArrayBuffer(object);
|
|
|
|
case boolTag:
|
|
case dateTag:
|
|
return new Ctor(+object);
|
|
|
|
case dataViewTag:
|
|
return cloneDataView(object, isDeep);
|
|
|
|
case float32Tag: case float64Tag:
|
|
case int8Tag: case int16Tag: case int32Tag:
|
|
case uint8Tag: case uint8ClampedTag: case uint16Tag: case uint32Tag:
|
|
return cloneTypedArray(object, isDeep);
|
|
|
|
case mapTag:
|
|
return cloneMap(object, isDeep, cloneFunc);
|
|
|
|
case numberTag:
|
|
case stringTag:
|
|
return new Ctor(object);
|
|
|
|
case regexpTag:
|
|
return cloneRegExp(object);
|
|
|
|
case setTag:
|
|
return cloneSet(object, isDeep, cloneFunc);
|
|
|
|
case symbolTag:
|
|
return cloneSymbol(object);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is a valid array-like index.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to check.
|
|
* @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
|
|
* @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
|
|
*/
|
|
function isIndex(value, length) {
|
|
length = length == null ? MAX_SAFE_INTEGER : length;
|
|
return !!length &&
|
|
(typeof value == 'number' || reIsUint.test(value)) &&
|
|
(value > -1 && value % 1 == 0 && value < length);
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is suitable for use as unique object key.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
|
|
*/
|
|
function isKeyable(value) {
|
|
var type = typeof value;
|
|
return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
|
|
? (value !== '__proto__')
|
|
: (value === null);
|
|
}
|
|
|
|
/**
|
|
* Checks if `func` has its source masked.
|
|
*
|
|
* @private
|
|
* @param {Function} func The function to check.
|
|
* @returns {boolean} Returns `true` if `func` is masked, else `false`.
|
|
*/
|
|
function isMasked(func) {
|
|
return !!maskSrcKey && (maskSrcKey in func);
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is likely a prototype object.
|
|
*
|
|
* @private
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
|
|
*/
|
|
function isPrototype(value) {
|
|
var Ctor = value && value.constructor,
|
|
proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
|
|
|
|
return value === proto;
|
|
}
|
|
|
|
/**
|
|
* Converts `func` to its source code.
|
|
*
|
|
* @private
|
|
* @param {Function} func The function to process.
|
|
* @returns {string} Returns the source code.
|
|
*/
|
|
function toSource(func) {
|
|
if (func != null) {
|
|
try {
|
|
return funcToString.call(func);
|
|
} catch (e) {}
|
|
try {
|
|
return (func + '');
|
|
} catch (e) {}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* This method is like `_.clone` except that it recursively clones `value`.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 1.0.0
|
|
* @category Lang
|
|
* @param {*} value The value to recursively clone.
|
|
* @returns {*} Returns the deep cloned value.
|
|
* @see _.clone
|
|
* @example
|
|
*
|
|
* var objects = [{ 'a': 1 }, { 'b': 2 }];
|
|
*
|
|
* var deep = _.cloneDeep(objects);
|
|
* console.log(deep[0] === objects[0]);
|
|
* // => false
|
|
*/
|
|
function cloneDeep(value) {
|
|
return baseClone(value, true, true);
|
|
}
|
|
|
|
/**
|
|
* Performs a
|
|
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
|
|
* comparison between two values to determine if they are equivalent.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.0.0
|
|
* @category Lang
|
|
* @param {*} value The value to compare.
|
|
* @param {*} other The other value to compare.
|
|
* @returns {boolean} Returns `true` if the values are equivalent, else `false`.
|
|
* @example
|
|
*
|
|
* var object = { 'a': 1 };
|
|
* var other = { 'a': 1 };
|
|
*
|
|
* _.eq(object, object);
|
|
* // => true
|
|
*
|
|
* _.eq(object, other);
|
|
* // => false
|
|
*
|
|
* _.eq('a', 'a');
|
|
* // => true
|
|
*
|
|
* _.eq('a', Object('a'));
|
|
* // => false
|
|
*
|
|
* _.eq(NaN, NaN);
|
|
* // => true
|
|
*/
|
|
function eq(value, other) {
|
|
return value === other || (value !== value && other !== other);
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is likely an `arguments` object.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 0.1.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is an `arguments` object,
|
|
* else `false`.
|
|
* @example
|
|
*
|
|
* _.isArguments(function() { return arguments; }());
|
|
* // => true
|
|
*
|
|
* _.isArguments([1, 2, 3]);
|
|
* // => false
|
|
*/
|
|
function isArguments(value) {
|
|
// Safari 8.1 makes `arguments.callee` enumerable in strict mode.
|
|
return isArrayLikeObject(value) && hasOwnProperty.call(value, 'callee') &&
|
|
(!propertyIsEnumerable.call(value, 'callee') || objectToString.call(value) == argsTag);
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is classified as an `Array` object.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 0.1.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is an array, else `false`.
|
|
* @example
|
|
*
|
|
* _.isArray([1, 2, 3]);
|
|
* // => true
|
|
*
|
|
* _.isArray(document.body.children);
|
|
* // => false
|
|
*
|
|
* _.isArray('abc');
|
|
* // => false
|
|
*
|
|
* _.isArray(_.noop);
|
|
* // => false
|
|
*/
|
|
var isArray = Array.isArray;
|
|
|
|
/**
|
|
* Checks if `value` is array-like. A value is considered array-like if it's
|
|
* not a function and has a `value.length` that's an integer greater than or
|
|
* equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.0.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is array-like, else `false`.
|
|
* @example
|
|
*
|
|
* _.isArrayLike([1, 2, 3]);
|
|
* // => true
|
|
*
|
|
* _.isArrayLike(document.body.children);
|
|
* // => true
|
|
*
|
|
* _.isArrayLike('abc');
|
|
* // => true
|
|
*
|
|
* _.isArrayLike(_.noop);
|
|
* // => false
|
|
*/
|
|
function isArrayLike(value) {
|
|
return value != null && isLength(value.length) && !isFunction(value);
|
|
}
|
|
|
|
/**
|
|
* This method is like `_.isArrayLike` except that it also checks if `value`
|
|
* is an object.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.0.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is an array-like object,
|
|
* else `false`.
|
|
* @example
|
|
*
|
|
* _.isArrayLikeObject([1, 2, 3]);
|
|
* // => true
|
|
*
|
|
* _.isArrayLikeObject(document.body.children);
|
|
* // => true
|
|
*
|
|
* _.isArrayLikeObject('abc');
|
|
* // => false
|
|
*
|
|
* _.isArrayLikeObject(_.noop);
|
|
* // => false
|
|
*/
|
|
function isArrayLikeObject(value) {
|
|
return isObjectLike(value) && isArrayLike(value);
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is a buffer.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.3.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
|
|
* @example
|
|
*
|
|
* _.isBuffer(new Buffer(2));
|
|
* // => true
|
|
*
|
|
* _.isBuffer(new Uint8Array(2));
|
|
* // => false
|
|
*/
|
|
var isBuffer = nativeIsBuffer || stubFalse;
|
|
|
|
/**
|
|
* Checks if `value` is classified as a `Function` object.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 0.1.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is a function, else `false`.
|
|
* @example
|
|
*
|
|
* _.isFunction(_);
|
|
* // => true
|
|
*
|
|
* _.isFunction(/abc/);
|
|
* // => false
|
|
*/
|
|
function isFunction(value) {
|
|
// The use of `Object#toString` avoids issues with the `typeof` operator
|
|
// in Safari 8-9 which returns 'object' for typed array and other constructors.
|
|
var tag = isObject(value) ? objectToString.call(value) : '';
|
|
return tag == funcTag || tag == genTag;
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is a valid array-like length.
|
|
*
|
|
* **Note:** This method is loosely based on
|
|
* [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.0.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
|
|
* @example
|
|
*
|
|
* _.isLength(3);
|
|
* // => true
|
|
*
|
|
* _.isLength(Number.MIN_VALUE);
|
|
* // => false
|
|
*
|
|
* _.isLength(Infinity);
|
|
* // => false
|
|
*
|
|
* _.isLength('3');
|
|
* // => false
|
|
*/
|
|
function isLength(value) {
|
|
return typeof value == 'number' &&
|
|
value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is the
|
|
* [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
|
|
* of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 0.1.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is an object, else `false`.
|
|
* @example
|
|
*
|
|
* _.isObject({});
|
|
* // => true
|
|
*
|
|
* _.isObject([1, 2, 3]);
|
|
* // => true
|
|
*
|
|
* _.isObject(_.noop);
|
|
* // => true
|
|
*
|
|
* _.isObject(null);
|
|
* // => false
|
|
*/
|
|
function isObject(value) {
|
|
var type = typeof value;
|
|
return !!value && (type == 'object' || type == 'function');
|
|
}
|
|
|
|
/**
|
|
* Checks if `value` is object-like. A value is object-like if it's not `null`
|
|
* and has a `typeof` result of "object".
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.0.0
|
|
* @category Lang
|
|
* @param {*} value The value to check.
|
|
* @returns {boolean} Returns `true` if `value` is object-like, else `false`.
|
|
* @example
|
|
*
|
|
* _.isObjectLike({});
|
|
* // => true
|
|
*
|
|
* _.isObjectLike([1, 2, 3]);
|
|
* // => true
|
|
*
|
|
* _.isObjectLike(_.noop);
|
|
* // => false
|
|
*
|
|
* _.isObjectLike(null);
|
|
* // => false
|
|
*/
|
|
function isObjectLike(value) {
|
|
return !!value && typeof value == 'object';
|
|
}
|
|
|
|
/**
|
|
* Creates an array of the own enumerable property names of `object`.
|
|
*
|
|
* **Note:** Non-object values are coerced to objects. See the
|
|
* [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
|
|
* for more details.
|
|
*
|
|
* @static
|
|
* @since 0.1.0
|
|
* @memberOf _
|
|
* @category Object
|
|
* @param {Object} object The object to query.
|
|
* @returns {Array} Returns the array of property names.
|
|
* @example
|
|
*
|
|
* function Foo() {
|
|
* this.a = 1;
|
|
* this.b = 2;
|
|
* }
|
|
*
|
|
* Foo.prototype.c = 3;
|
|
*
|
|
* _.keys(new Foo);
|
|
* // => ['a', 'b'] (iteration order is not guaranteed)
|
|
*
|
|
* _.keys('hi');
|
|
* // => ['0', '1']
|
|
*/
|
|
function keys(object) {
|
|
return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
|
|
}
|
|
|
|
/**
|
|
* This method returns a new empty array.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.13.0
|
|
* @category Util
|
|
* @returns {Array} Returns the new empty array.
|
|
* @example
|
|
*
|
|
* var arrays = _.times(2, _.stubArray);
|
|
*
|
|
* console.log(arrays);
|
|
* // => [[], []]
|
|
*
|
|
* console.log(arrays[0] === arrays[1]);
|
|
* // => false
|
|
*/
|
|
function stubArray() {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* This method returns `false`.
|
|
*
|
|
* @static
|
|
* @memberOf _
|
|
* @since 4.13.0
|
|
* @category Util
|
|
* @returns {boolean} Returns `false`.
|
|
* @example
|
|
*
|
|
* _.times(2, _.stubFalse);
|
|
* // => [false, false]
|
|
*/
|
|
function stubFalse() {
|
|
return false;
|
|
}
|
|
|
|
module.exports = cloneDeep;
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{}],48:[function(require,module,exports){
|
|
var support = require('webrtcsupport');
|
|
|
|
|
|
function GainController(stream) {
|
|
this.support = support.webAudio && support.mediaStream;
|
|
|
|
// set our starting value
|
|
this.gain = 1;
|
|
|
|
if (this.support) {
|
|
var context = this.context = new support.AudioContext();
|
|
this.microphone = context.createMediaStreamSource(stream);
|
|
this.gainFilter = context.createGain();
|
|
this.destination = context.createMediaStreamDestination();
|
|
this.outputStream = this.destination.stream;
|
|
this.microphone.connect(this.gainFilter);
|
|
this.gainFilter.connect(this.destination);
|
|
stream.addTrack(this.outputStream.getAudioTracks()[0]);
|
|
stream.removeTrack(stream.getAudioTracks()[0]);
|
|
}
|
|
this.stream = stream;
|
|
}
|
|
|
|
// setting
|
|
GainController.prototype.setGain = function (val) {
|
|
// check for support
|
|
if (!this.support) return;
|
|
this.gainFilter.gain.value = val;
|
|
this.gain = val;
|
|
};
|
|
|
|
GainController.prototype.getGain = function () {
|
|
return this.gain;
|
|
};
|
|
|
|
GainController.prototype.off = function () {
|
|
return this.setGain(0);
|
|
};
|
|
|
|
GainController.prototype.on = function () {
|
|
this.setGain(1);
|
|
};
|
|
|
|
|
|
module.exports = GainController;
|
|
|
|
},{"webrtcsupport":49}],49:[function(require,module,exports){
|
|
// created by @HenrikJoreteg
|
|
var prefix;
|
|
var version;
|
|
|
|
if (window.mozRTCPeerConnection || navigator.mozGetUserMedia) {
|
|
prefix = 'moz';
|
|
version = navigator.userAgent.match(/Firefox/) && parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
|
} else if (window.webkitRTCPeerConnection || navigator.webkitGetUserMedia) {
|
|
prefix = 'webkit';
|
|
version = navigator.userAgent.match(/Chrom(e|ium)/) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
|
|
}
|
|
|
|
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:' &&
|
|
((prefix === 'webkit' && version >= 26) ||
|
|
(prefix === 'moz' && version >= 33))
|
|
var AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
var videoEl = document.createElement('video');
|
|
var supportVp8 = videoEl && videoEl.canPlayType && videoEl.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,
|
|
browserVersion: version,
|
|
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
|
|
};
|
|
|
|
},{}],50:[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;
|
|
|
|
},{}],51:[function(require,module,exports){
|
|
|
|
/**
|
|
* HOP ref.
|
|
*/
|
|
|
|
var has = Object.prototype.hasOwnProperty;
|
|
|
|
/**
|
|
* Return own keys in `obj`.
|
|
*
|
|
* @param {Object} obj
|
|
* @return {Array}
|
|
* @api public
|
|
*/
|
|
|
|
exports.keys = Object.keys || function(obj){
|
|
var keys = [];
|
|
for (var key in obj) {
|
|
if (has.call(obj, key)) {
|
|
keys.push(key);
|
|
}
|
|
}
|
|
return keys;
|
|
};
|
|
|
|
/**
|
|
* Return own values in `obj`.
|
|
*
|
|
* @param {Object} obj
|
|
* @return {Array}
|
|
* @api public
|
|
*/
|
|
|
|
exports.values = function(obj){
|
|
var vals = [];
|
|
for (var key in obj) {
|
|
if (has.call(obj, key)) {
|
|
vals.push(obj[key]);
|
|
}
|
|
}
|
|
return vals;
|
|
};
|
|
|
|
/**
|
|
* Merge `b` into `a`.
|
|
*
|
|
* @param {Object} a
|
|
* @param {Object} b
|
|
* @return {Object} a
|
|
* @api public
|
|
*/
|
|
|
|
exports.merge = function(a, b){
|
|
for (var key in b) {
|
|
if (has.call(b, key)) {
|
|
a[key] = b[key];
|
|
}
|
|
}
|
|
return a;
|
|
};
|
|
|
|
/**
|
|
* Return length of `obj`.
|
|
*
|
|
* @param {Object} obj
|
|
* @return {Number}
|
|
* @api public
|
|
*/
|
|
|
|
exports.length = function(obj){
|
|
return exports.keys(obj).length;
|
|
};
|
|
|
|
/**
|
|
* Check if `obj` is empty.
|
|
*
|
|
* @param {Object} obj
|
|
* @return {Boolean}
|
|
* @api public
|
|
*/
|
|
|
|
exports.isEmpty = function(obj){
|
|
return 0 == exports.length(obj);
|
|
};
|
|
},{}],52:[function(require,module,exports){
|
|
(function (global){
|
|
/**
|
|
* JSON parse.
|
|
*
|
|
* @see Based on jQuery#parseJSON (MIT) and JSON2
|
|
* @api private
|
|
*/
|
|
|
|
var rvalidchars = /^[\],:{}\s]*$/;
|
|
var rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
|
|
var rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
|
|
var rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g;
|
|
var rtrimLeft = /^\s+/;
|
|
var rtrimRight = /\s+$/;
|
|
|
|
module.exports = function parsejson(data) {
|
|
if ('string' != typeof data || !data) {
|
|
return null;
|
|
}
|
|
|
|
data = data.replace(rtrimLeft, '').replace(rtrimRight, '');
|
|
|
|
// Attempt to parse using the native JSON parser first
|
|
if (global.JSON && JSON.parse) {
|
|
return JSON.parse(data);
|
|
}
|
|
|
|
if (rvalidchars.test(data.replace(rvalidescape, '@')
|
|
.replace(rvalidtokens, ']')
|
|
.replace(rvalidbraces, ''))) {
|
|
return (new Function('return ' + data))();
|
|
}
|
|
};
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{}],53:[function(require,module,exports){
|
|
/**
|
|
* Compiles a querystring
|
|
* Returns string representation of the object
|
|
*
|
|
* @param {Object}
|
|
* @api private
|
|
*/
|
|
|
|
exports.encode = function (obj) {
|
|
var str = '';
|
|
|
|
for (var i in obj) {
|
|
if (obj.hasOwnProperty(i)) {
|
|
if (str.length) str += '&';
|
|
str += encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]);
|
|
}
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* Parses a simple querystring into an object
|
|
*
|
|
* @param {String} qs
|
|
* @api private
|
|
*/
|
|
|
|
exports.decode = function(qs){
|
|
var qry = {};
|
|
var pairs = qs.split('&');
|
|
for (var i = 0, l = pairs.length; i < l; i++) {
|
|
var pair = pairs[i].split('=');
|
|
qry[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
|
|
}
|
|
return qry;
|
|
};
|
|
|
|
},{}],54:[function(require,module,exports){
|
|
/**
|
|
* Parses an URI
|
|
*
|
|
* @author Steven Levithan <stevenlevithan.com> (MIT license)
|
|
* @api private
|
|
*/
|
|
|
|
var re = /^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/;
|
|
|
|
var parts = [
|
|
'source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host'
|
|
, 'port', 'relative', 'path', 'directory', 'file', 'query', 'anchor'
|
|
];
|
|
|
|
module.exports = function parseuri(str) {
|
|
var m = re.exec(str || '')
|
|
, uri = {}
|
|
, i = 14;
|
|
|
|
while (i--) {
|
|
uri[parts[i]] = m[i] || '';
|
|
}
|
|
|
|
return uri;
|
|
};
|
|
|
|
},{}],55:[function(require,module,exports){
|
|
// shim for using process in browser
|
|
var process = module.exports = {};
|
|
|
|
// cached from whatever global is present so that test runners that stub it
|
|
// don't break things. But we need to wrap it in a try catch in case it is
|
|
// wrapped in strict mode code which doesn't define any globals. It's inside a
|
|
// function because try/catches deoptimize in certain engines.
|
|
|
|
var cachedSetTimeout;
|
|
var cachedClearTimeout;
|
|
|
|
function defaultSetTimout() {
|
|
throw new Error('setTimeout has not been defined');
|
|
}
|
|
function defaultClearTimeout () {
|
|
throw new Error('clearTimeout has not been defined');
|
|
}
|
|
(function () {
|
|
try {
|
|
if (typeof setTimeout === 'function') {
|
|
cachedSetTimeout = setTimeout;
|
|
} else {
|
|
cachedSetTimeout = defaultSetTimout;
|
|
}
|
|
} catch (e) {
|
|
cachedSetTimeout = defaultSetTimout;
|
|
}
|
|
try {
|
|
if (typeof clearTimeout === 'function') {
|
|
cachedClearTimeout = clearTimeout;
|
|
} else {
|
|
cachedClearTimeout = defaultClearTimeout;
|
|
}
|
|
} catch (e) {
|
|
cachedClearTimeout = defaultClearTimeout;
|
|
}
|
|
} ())
|
|
function runTimeout(fun) {
|
|
if (cachedSetTimeout === setTimeout) {
|
|
//normal enviroments in sane situations
|
|
return setTimeout(fun, 0);
|
|
}
|
|
// if setTimeout wasn't available but was latter defined
|
|
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
|
|
cachedSetTimeout = setTimeout;
|
|
return setTimeout(fun, 0);
|
|
}
|
|
try {
|
|
// when when somebody has screwed with setTimeout but no I.E. maddness
|
|
return cachedSetTimeout(fun, 0);
|
|
} catch(e){
|
|
try {
|
|
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
|
|
return cachedSetTimeout.call(null, fun, 0);
|
|
} catch(e){
|
|
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
|
|
return cachedSetTimeout.call(this, fun, 0);
|
|
}
|
|
}
|
|
|
|
|
|
}
|
|
function runClearTimeout(marker) {
|
|
if (cachedClearTimeout === clearTimeout) {
|
|
//normal enviroments in sane situations
|
|
return clearTimeout(marker);
|
|
}
|
|
// if clearTimeout wasn't available but was latter defined
|
|
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
|
|
cachedClearTimeout = clearTimeout;
|
|
return clearTimeout(marker);
|
|
}
|
|
try {
|
|
// when when somebody has screwed with setTimeout but no I.E. maddness
|
|
return cachedClearTimeout(marker);
|
|
} catch (e){
|
|
try {
|
|
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
|
|
return cachedClearTimeout.call(null, marker);
|
|
} catch (e){
|
|
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
|
|
// Some versions of I.E. have different rules for clearTimeout vs setTimeout
|
|
return cachedClearTimeout.call(this, marker);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|
|
var queue = [];
|
|
var draining = false;
|
|
var currentQueue;
|
|
var queueIndex = -1;
|
|
|
|
function cleanUpNextTick() {
|
|
if (!draining || !currentQueue) {
|
|
return;
|
|
}
|
|
draining = false;
|
|
if (currentQueue.length) {
|
|
queue = currentQueue.concat(queue);
|
|
} else {
|
|
queueIndex = -1;
|
|
}
|
|
if (queue.length) {
|
|
drainQueue();
|
|
}
|
|
}
|
|
|
|
function drainQueue() {
|
|
if (draining) {
|
|
return;
|
|
}
|
|
var timeout = runTimeout(cleanUpNextTick);
|
|
draining = true;
|
|
|
|
var len = queue.length;
|
|
while(len) {
|
|
currentQueue = queue;
|
|
queue = [];
|
|
while (++queueIndex < len) {
|
|
if (currentQueue) {
|
|
currentQueue[queueIndex].run();
|
|
}
|
|
}
|
|
queueIndex = -1;
|
|
len = queue.length;
|
|
}
|
|
currentQueue = null;
|
|
draining = false;
|
|
runClearTimeout(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) {
|
|
runTimeout(drainQueue);
|
|
}
|
|
};
|
|
|
|
// 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');
|
|
};
|
|
|
|
process.cwd = function () { return '/' };
|
|
process.chdir = function (dir) {
|
|
throw new Error('process.chdir is not supported');
|
|
};
|
|
process.umask = function() { return 0; };
|
|
|
|
},{}],56:[function(require,module,exports){
|
|
var util = require('util');
|
|
var SJJ = require('sdp-jingle-json');
|
|
var WildEmitter = require('wildemitter');
|
|
var Peerconn = require('traceablepeerconnection');
|
|
var adapter = require('webrtc-adapter');
|
|
var cloneDeep = require('lodash.clonedeep');
|
|
|
|
function PeerConnection(config, constraints) {
|
|
var self = this;
|
|
var item;
|
|
WildEmitter.call(this);
|
|
|
|
config = config || {};
|
|
config.iceServers = config.iceServers || [];
|
|
|
|
var detectedBrowser = adapter.browserDetails.browser;
|
|
|
|
// make sure this only gets enabled in Google Chrome
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
this.enableChromeNativeSimulcast = false;
|
|
if (constraints && constraints.optional &&
|
|
detectedBrowser === 'chrome' &&
|
|
navigator.appVersion.match(/Chromium\//) === null) {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.enableChromeNativeSimulcast) {
|
|
self.enableChromeNativeSimulcast = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
this.enableMultiStreamHacks = false;
|
|
if (constraints && constraints.optional &&
|
|
detectedBrowser === 'chrome') {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.enableMultiStreamHacks) {
|
|
self.enableMultiStreamHacks = true;
|
|
}
|
|
});
|
|
}
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
this.restrictBandwidth = 0;
|
|
if (constraints && constraints.optional) {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.andyetRestrictBandwidth) {
|
|
self.restrictBandwidth = constraint.andyetRestrictBandwidth;
|
|
}
|
|
});
|
|
}
|
|
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
// bundle up ice candidates, only works for jingle mode
|
|
// number > 0 is the delay to wait for additional candidates
|
|
// ~20ms seems good
|
|
this.batchIceCandidates = 0;
|
|
if (constraints && constraints.optional) {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.andyetBatchIce) {
|
|
self.batchIceCandidates = constraint.andyetBatchIce;
|
|
}
|
|
});
|
|
}
|
|
this.batchedIceCandidates = [];
|
|
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
// this attemps to strip out candidates with an already known foundation
|
|
// and type -- i.e. those which are gathered via the same TURN server
|
|
// but different transports (TURN udp, tcp and tls respectively)
|
|
if (constraints && constraints.optional && detectedBrowser === 'chrome') {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.andyetFasterICE) {
|
|
self.eliminateDuplicateCandidates = constraint.andyetFasterICE;
|
|
}
|
|
});
|
|
}
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
// when using a server such as the jitsi videobridge we don't need to signal
|
|
// our candidates
|
|
if (constraints && constraints.optional) {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.andyetDontSignalCandidates) {
|
|
self.dontSignalCandidates = constraint.andyetDontSignalCandidates;
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
this.assumeSetLocalSuccess = false;
|
|
if (constraints && constraints.optional) {
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.andyetAssumeSetLocalSuccess) {
|
|
self.assumeSetLocalSuccess = constraint.andyetAssumeSetLocalSuccess;
|
|
}
|
|
});
|
|
}
|
|
|
|
// EXPERIMENTAL FLAG, might get removed without notice
|
|
// working around https://bugzilla.mozilla.org/show_bug.cgi?id=1087551
|
|
// pass in a timeout for this
|
|
if (detectedBrowser === 'firefox') {
|
|
if (constraints && constraints.optional) {
|
|
this.wtFirefox = 0;
|
|
constraints.optional.forEach(function (constraint) {
|
|
if (constraint.andyetFirefoxMakesMeSad) {
|
|
self.wtFirefox = constraint.andyetFirefoxMakesMeSad;
|
|
if (self.wtFirefox > 0) {
|
|
self.firefoxcandidatebuffer = [];
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
this.pc = new Peerconn(config, constraints);
|
|
|
|
this.getLocalStreams = this.pc.getLocalStreams.bind(this.pc);
|
|
this.getRemoteStreams = this.pc.getRemoteStreams.bind(this.pc);
|
|
this.addStream = this.pc.addStream.bind(this.pc);
|
|
this.removeStream = this.pc.removeStream.bind(this.pc);
|
|
|
|
// proxy events
|
|
this.pc.on('*', function () {
|
|
self.emit.apply(self, arguments);
|
|
});
|
|
|
|
// proxy some events directly
|
|
this.pc.onremovestream = this.emit.bind(this, 'removeStream');
|
|
this.pc.onaddstream = this.emit.bind(this, 'addStream');
|
|
this.pc.onnegotiationneeded = this.emit.bind(this, 'negotiationNeeded');
|
|
this.pc.oniceconnectionstatechange = this.emit.bind(this, 'iceConnectionStateChange');
|
|
this.pc.onsignalingstatechange = this.emit.bind(this, 'signalingStateChange');
|
|
|
|
// handle ice candidate and data channel events
|
|
this.pc.onicecandidate = this._onIce.bind(this);
|
|
this.pc.ondatachannel = this._onDataChannel.bind(this);
|
|
|
|
this.localDescription = {
|
|
contents: []
|
|
};
|
|
this.remoteDescription = {
|
|
contents: []
|
|
};
|
|
|
|
this.config = {
|
|
debug: false,
|
|
sid: '',
|
|
isInitiator: true,
|
|
sdpSessionID: Date.now(),
|
|
useJingle: false
|
|
};
|
|
|
|
this.iceCredentials = {
|
|
local: {},
|
|
remote: {}
|
|
};
|
|
|
|
// apply our config
|
|
for (item in config) {
|
|
this.config[item] = config[item];
|
|
}
|
|
|
|
if (this.config.debug) {
|
|
this.on('*', function () {
|
|
var logger = config.logger || console;
|
|
logger.log('PeerConnection event:', arguments);
|
|
});
|
|
}
|
|
this.hadLocalStunCandidate = false;
|
|
this.hadRemoteStunCandidate = false;
|
|
this.hadLocalRelayCandidate = false;
|
|
this.hadRemoteRelayCandidate = false;
|
|
|
|
this.hadLocalIPv6Candidate = false;
|
|
this.hadRemoteIPv6Candidate = false;
|
|
|
|
// keeping references for all our data channels
|
|
// so they dont get garbage collected
|
|
// can be removed once the following bugs have been fixed
|
|
// https://crbug.com/405545
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=964092
|
|
// to be filed for opera
|
|
this._remoteDataChannels = [];
|
|
this._localDataChannels = [];
|
|
|
|
this._candidateBuffer = [];
|
|
}
|
|
|
|
util.inherits(PeerConnection, WildEmitter);
|
|
|
|
Object.defineProperty(PeerConnection.prototype, 'signalingState', {
|
|
get: function () {
|
|
return this.pc.signalingState;
|
|
}
|
|
});
|
|
Object.defineProperty(PeerConnection.prototype, 'iceConnectionState', {
|
|
get: function () {
|
|
return this.pc.iceConnectionState;
|
|
}
|
|
});
|
|
|
|
PeerConnection.prototype._role = function () {
|
|
return this.isInitiator ? 'initiator' : 'responder';
|
|
};
|
|
|
|
// Add a stream to the peer connection object
|
|
PeerConnection.prototype.addStream = function (stream) {
|
|
this.localStream = stream;
|
|
this.pc.addStream(stream);
|
|
};
|
|
|
|
// helper function to check if a remote candidate is a stun/relay
|
|
// candidate or an ipv6 candidate
|
|
PeerConnection.prototype._checkLocalCandidate = function (candidate) {
|
|
var cand = SJJ.toCandidateJSON(candidate);
|
|
if (cand.type == 'srflx') {
|
|
this.hadLocalStunCandidate = true;
|
|
} else if (cand.type == 'relay') {
|
|
this.hadLocalRelayCandidate = true;
|
|
}
|
|
if (cand.ip.indexOf(':') != -1) {
|
|
this.hadLocalIPv6Candidate = true;
|
|
}
|
|
};
|
|
|
|
// helper function to check if a remote candidate is a stun/relay
|
|
// candidate or an ipv6 candidate
|
|
PeerConnection.prototype._checkRemoteCandidate = function (candidate) {
|
|
var cand = SJJ.toCandidateJSON(candidate);
|
|
if (cand.type == 'srflx') {
|
|
this.hadRemoteStunCandidate = true;
|
|
} else if (cand.type == 'relay') {
|
|
this.hadRemoteRelayCandidate = true;
|
|
}
|
|
if (cand.ip.indexOf(':') != -1) {
|
|
this.hadRemoteIPv6Candidate = true;
|
|
}
|
|
};
|
|
|
|
|
|
// Init and add ice candidate object with correct constructor
|
|
PeerConnection.prototype.processIce = function (update, cb) {
|
|
cb = cb || function () {};
|
|
var self = this;
|
|
|
|
// ignore any added ice candidates to avoid errors. why does the
|
|
// spec not do this?
|
|
if (this.pc.signalingState === 'closed') return cb();
|
|
|
|
if (update.contents || (update.jingle && update.jingle.contents)) {
|
|
var contentNames = this.remoteDescription.contents.map(function (c) { return c.name; });
|
|
var contents = update.contents || update.jingle.contents;
|
|
|
|
contents.forEach(function (content) {
|
|
var transport = content.transport || {};
|
|
var candidates = transport.candidates || [];
|
|
var mline = contentNames.indexOf(content.name);
|
|
var mid = content.name;
|
|
var remoteContent = self.remoteDescription.contents.find(function (c) {
|
|
return c.name === content.name;
|
|
});
|
|
|
|
// process candidates as a callback, in case we need to
|
|
// update ufrag and pwd with offer/answer
|
|
var processCandidates = function () {
|
|
candidates.forEach(
|
|
function (candidate) {
|
|
var iceCandidate = SJJ.toCandidateSDP(candidate) + '\r\n';
|
|
self.pc.addIceCandidate(
|
|
new RTCIceCandidate({
|
|
candidate: iceCandidate,
|
|
sdpMLineIndex: mline,
|
|
sdpMid: mid
|
|
}), function () {
|
|
// well, this success callback is pretty meaningless
|
|
},
|
|
function (err) {
|
|
self.emit('error', err);
|
|
}
|
|
);
|
|
self._checkRemoteCandidate(iceCandidate);
|
|
});
|
|
cb();
|
|
};
|
|
|
|
if (self.iceCredentials.remote[content.name] && transport.ufrag &&
|
|
self.iceCredentials.remote[content.name].ufrag !== transport.ufrag) {
|
|
if (remoteContent) {
|
|
remoteContent.transport.ufrag = transport.ufrag;
|
|
remoteContent.transport.pwd = transport.pwd;
|
|
var offer = {
|
|
type: 'offer',
|
|
jingle: self.remoteDescription
|
|
};
|
|
offer.sdp = SJJ.toSessionSDP(offer.jingle, {
|
|
sid: self.config.sdpSessionID,
|
|
role: self._role(),
|
|
direction: 'incoming'
|
|
});
|
|
self.pc.setRemoteDescription(new RTCSessionDescription(offer),
|
|
function () {
|
|
processCandidates();
|
|
},
|
|
function (err) {
|
|
self.emit('error', err);
|
|
}
|
|
);
|
|
} else {
|
|
self.emit('error', 'ice restart failed to find matching content');
|
|
}
|
|
} else {
|
|
processCandidates();
|
|
}
|
|
});
|
|
} else {
|
|
// working around https://code.google.com/p/webrtc/issues/detail?id=3669
|
|
if (update.candidate && update.candidate.candidate.indexOf('a=') !== 0) {
|
|
update.candidate.candidate = 'a=' + update.candidate.candidate;
|
|
}
|
|
|
|
if (this.wtFirefox && this.firefoxcandidatebuffer !== null) {
|
|
// we cant add this yet due to https://bugzilla.mozilla.org/show_bug.cgi?id=1087551
|
|
if (this.pc.localDescription && this.pc.localDescription.type === 'offer') {
|
|
this.firefoxcandidatebuffer.push(update.candidate);
|
|
return cb();
|
|
}
|
|
}
|
|
|
|
self.pc.addIceCandidate(
|
|
new RTCIceCandidate(update.candidate),
|
|
function () { },
|
|
function (err) {
|
|
self.emit('error', err);
|
|
}
|
|
);
|
|
self._checkRemoteCandidate(update.candidate.candidate);
|
|
cb();
|
|
}
|
|
};
|
|
|
|
// Generate and emit an offer with the given constraints
|
|
PeerConnection.prototype.offer = function (constraints, cb) {
|
|
var self = this;
|
|
var hasConstraints = arguments.length === 2;
|
|
var mediaConstraints = hasConstraints && constraints ? constraints : {
|
|
offerToReceiveAudio: 1,
|
|
offerToReceiveVideo: 1
|
|
};
|
|
cb = hasConstraints ? cb : constraints;
|
|
cb = cb || function () {};
|
|
|
|
if (this.pc.signalingState === 'closed') return cb('Already closed');
|
|
|
|
// Actually generate the offer
|
|
this.pc.createOffer(
|
|
function (offer) {
|
|
// does not work for jingle, but jingle.js doesn't need
|
|
// this hack...
|
|
var expandedOffer = {
|
|
type: 'offer',
|
|
sdp: offer.sdp
|
|
};
|
|
if (self.assumeSetLocalSuccess) {
|
|
self.emit('offer', expandedOffer);
|
|
cb(null, expandedOffer);
|
|
}
|
|
self._candidateBuffer = [];
|
|
self.pc.setLocalDescription(offer,
|
|
function () {
|
|
var jingle;
|
|
if (self.config.useJingle) {
|
|
jingle = SJJ.toSessionJSON(offer.sdp, {
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
jingle.sid = self.config.sid;
|
|
self.localDescription = jingle;
|
|
|
|
// Save ICE credentials
|
|
jingle.contents.forEach(function (content) {
|
|
var transport = content.transport || {};
|
|
if (transport.ufrag) {
|
|
self.iceCredentials.local[content.name] = {
|
|
ufrag: transport.ufrag,
|
|
pwd: transport.pwd
|
|
};
|
|
}
|
|
});
|
|
|
|
expandedOffer.jingle = jingle;
|
|
}
|
|
expandedOffer.sdp.split('\r\n').forEach(function (line) {
|
|
if (line.indexOf('a=candidate:') === 0) {
|
|
self._checkLocalCandidate(line);
|
|
}
|
|
});
|
|
|
|
if (!self.assumeSetLocalSuccess) {
|
|
self.emit('offer', expandedOffer);
|
|
cb(null, expandedOffer);
|
|
}
|
|
},
|
|
function (err) {
|
|
self.emit('error', err);
|
|
cb(err);
|
|
}
|
|
);
|
|
},
|
|
function (err) {
|
|
self.emit('error', err);
|
|
cb(err);
|
|
},
|
|
mediaConstraints
|
|
);
|
|
};
|
|
|
|
|
|
// Process an incoming offer so that ICE may proceed before deciding
|
|
// to answer the request.
|
|
PeerConnection.prototype.handleOffer = function (offer, cb) {
|
|
cb = cb || function () {};
|
|
var self = this;
|
|
offer.type = 'offer';
|
|
if (offer.jingle) {
|
|
if (this.enableChromeNativeSimulcast) {
|
|
offer.jingle.contents.forEach(function (content) {
|
|
if (content.name === 'video') {
|
|
content.application.googConferenceFlag = true;
|
|
}
|
|
|
|
});
|
|
}
|
|
if (this.enableMultiStreamHacks) {
|
|
// add a mixed video stream as first stream
|
|
offer.jingle.contents.forEach(function (content) {
|
|
if (content.name === 'video') {
|
|
var sources = content.application.sources || [];
|
|
if (sources.length === 0 || sources[0].ssrc !== "3735928559") {
|
|
sources.unshift({
|
|
ssrc: "3735928559", // 0xdeadbeef
|
|
parameters: [
|
|
{
|
|
key: "cname",
|
|
value: "deadbeef"
|
|
},
|
|
{
|
|
key: "msid",
|
|
value: "mixyourfecintothis please"
|
|
}
|
|
]
|
|
});
|
|
content.application.sources = sources;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
if (self.restrictBandwidth > 0) {
|
|
if (offer.jingle.contents.length >= 2 && offer.jingle.contents[1].name === 'video') {
|
|
var content = offer.jingle.contents[1];
|
|
var hasBw = content.application && content.application.bandwidth && content.application.bandwidth.bandwidth;
|
|
if (!hasBw) {
|
|
offer.jingle.contents[1].application.bandwidth = { type: 'AS', bandwidth: self.restrictBandwidth.toString() };
|
|
offer.sdp = SJJ.toSessionSDP(offer.jingle, {
|
|
sid: self.config.sdpSessionID,
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Save ICE credentials
|
|
offer.jingle.contents.forEach(function (content) {
|
|
var transport = content.transport || {};
|
|
if (transport.ufrag) {
|
|
self.iceCredentials.remote[content.name] = {
|
|
ufrag: transport.ufrag,
|
|
pwd: transport.pwd
|
|
};
|
|
}
|
|
});
|
|
offer.sdp = SJJ.toSessionSDP(offer.jingle, {
|
|
sid: self.config.sdpSessionID,
|
|
role: self._role(),
|
|
direction: 'incoming'
|
|
});
|
|
self.remoteDescription = offer.jingle;
|
|
}
|
|
offer.sdp.split('\r\n').forEach(function (line) {
|
|
if (line.indexOf('a=candidate:') === 0) {
|
|
self._checkRemoteCandidate(line);
|
|
}
|
|
});
|
|
self.pc.setRemoteDescription(new RTCSessionDescription(offer),
|
|
function () {
|
|
cb();
|
|
},
|
|
cb
|
|
);
|
|
};
|
|
|
|
// Answer an offer with audio only
|
|
PeerConnection.prototype.answerAudioOnly = function (cb) {
|
|
var mediaConstraints = {
|
|
mandatory: {
|
|
OfferToReceiveAudio: true,
|
|
OfferToReceiveVideo: false
|
|
}
|
|
};
|
|
this._answer(mediaConstraints, cb);
|
|
};
|
|
|
|
// Answer an offer without offering to recieve
|
|
PeerConnection.prototype.answerBroadcastOnly = function (cb) {
|
|
var mediaConstraints = {
|
|
mandatory: {
|
|
OfferToReceiveAudio: false,
|
|
OfferToReceiveVideo: false
|
|
}
|
|
};
|
|
this._answer(mediaConstraints, cb);
|
|
};
|
|
|
|
// Answer an offer with given constraints default is audio/video
|
|
PeerConnection.prototype.answer = function (constraints, cb) {
|
|
var hasConstraints = arguments.length === 2;
|
|
var callback = hasConstraints ? cb : constraints;
|
|
var mediaConstraints = hasConstraints && constraints ? constraints : {
|
|
mandatory: {
|
|
OfferToReceiveAudio: true,
|
|
OfferToReceiveVideo: true
|
|
}
|
|
};
|
|
|
|
this._answer(mediaConstraints, callback);
|
|
};
|
|
|
|
// Process an answer
|
|
PeerConnection.prototype.handleAnswer = function (answer, cb) {
|
|
cb = cb || function () {};
|
|
var self = this;
|
|
if (answer.jingle) {
|
|
answer.sdp = SJJ.toSessionSDP(answer.jingle, {
|
|
sid: self.config.sdpSessionID,
|
|
role: self._role(),
|
|
direction: 'incoming'
|
|
});
|
|
self.remoteDescription = answer.jingle;
|
|
|
|
// Save ICE credentials
|
|
answer.jingle.contents.forEach(function (content) {
|
|
var transport = content.transport || {};
|
|
if (transport.ufrag) {
|
|
self.iceCredentials.remote[content.name] = {
|
|
ufrag: transport.ufrag,
|
|
pwd: transport.pwd
|
|
};
|
|
}
|
|
});
|
|
}
|
|
answer.sdp.split('\r\n').forEach(function (line) {
|
|
if (line.indexOf('a=candidate:') === 0) {
|
|
self._checkRemoteCandidate(line);
|
|
}
|
|
});
|
|
self.pc.setRemoteDescription(
|
|
new RTCSessionDescription(answer),
|
|
function () {
|
|
if (self.wtFirefox) {
|
|
window.setTimeout(function () {
|
|
self.firefoxcandidatebuffer.forEach(function (candidate) {
|
|
// add candidates later
|
|
self.pc.addIceCandidate(
|
|
new RTCIceCandidate(candidate),
|
|
function () { },
|
|
function (err) {
|
|
self.emit('error', err);
|
|
}
|
|
);
|
|
self._checkRemoteCandidate(candidate.candidate);
|
|
});
|
|
self.firefoxcandidatebuffer = null;
|
|
}, self.wtFirefox);
|
|
}
|
|
cb(null);
|
|
},
|
|
cb
|
|
);
|
|
};
|
|
|
|
// Close the peer connection
|
|
PeerConnection.prototype.close = function () {
|
|
this.pc.close();
|
|
|
|
this._localDataChannels = [];
|
|
this._remoteDataChannels = [];
|
|
|
|
this.emit('close');
|
|
};
|
|
|
|
// Internal code sharing for various types of answer methods
|
|
PeerConnection.prototype._answer = function (constraints, cb) {
|
|
cb = cb || function () {};
|
|
var self = this;
|
|
if (!this.pc.remoteDescription) {
|
|
// the old API is used, call handleOffer
|
|
throw new Error('remoteDescription not set');
|
|
}
|
|
|
|
if (this.pc.signalingState === 'closed') return cb('Already closed');
|
|
|
|
self.pc.createAnswer(
|
|
function (answer) {
|
|
var sim = [];
|
|
if (self.enableChromeNativeSimulcast) {
|
|
// native simulcast part 1: add another SSRC
|
|
answer.jingle = SJJ.toSessionJSON(answer.sdp, {
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
if (answer.jingle.contents.length >= 2 && answer.jingle.contents[1].name === 'video') {
|
|
var groups = answer.jingle.contents[1].application.sourceGroups || [];
|
|
var hasSim = false;
|
|
groups.forEach(function (group) {
|
|
if (group.semantics == 'SIM') hasSim = true;
|
|
});
|
|
if (!hasSim &&
|
|
answer.jingle.contents[1].application.sources.length) {
|
|
var newssrc = JSON.parse(JSON.stringify(answer.jingle.contents[1].application.sources[0]));
|
|
newssrc.ssrc = '' + Math.floor(Math.random() * 0xffffffff); // FIXME: look for conflicts
|
|
answer.jingle.contents[1].application.sources.push(newssrc);
|
|
|
|
sim.push(answer.jingle.contents[1].application.sources[0].ssrc);
|
|
sim.push(newssrc.ssrc);
|
|
groups.push({
|
|
semantics: 'SIM',
|
|
sources: sim
|
|
});
|
|
|
|
// also create an RTX one for the SIM one
|
|
var rtxssrc = JSON.parse(JSON.stringify(newssrc));
|
|
rtxssrc.ssrc = '' + Math.floor(Math.random() * 0xffffffff); // FIXME: look for conflicts
|
|
answer.jingle.contents[1].application.sources.push(rtxssrc);
|
|
groups.push({
|
|
semantics: 'FID',
|
|
sources: [newssrc.ssrc, rtxssrc.ssrc]
|
|
});
|
|
|
|
answer.jingle.contents[1].application.sourceGroups = groups;
|
|
answer.sdp = SJJ.toSessionSDP(answer.jingle, {
|
|
sid: self.config.sdpSessionID,
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
}
|
|
}
|
|
}
|
|
var expandedAnswer = {
|
|
type: 'answer',
|
|
sdp: answer.sdp
|
|
};
|
|
if (self.assumeSetLocalSuccess) {
|
|
// not safe to do when doing simulcast mangling
|
|
var copy = cloneDeep(expandedAnswer);
|
|
self.emit('answer', copy);
|
|
cb(null, copy);
|
|
}
|
|
self._candidateBuffer = [];
|
|
self.pc.setLocalDescription(answer,
|
|
function () {
|
|
if (self.config.useJingle) {
|
|
var jingle = SJJ.toSessionJSON(answer.sdp, {
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
jingle.sid = self.config.sid;
|
|
self.localDescription = jingle;
|
|
expandedAnswer.jingle = jingle;
|
|
}
|
|
if (self.enableChromeNativeSimulcast) {
|
|
// native simulcast part 2:
|
|
// signal multiple tracks to the receiver
|
|
// for anything in the SIM group
|
|
if (!expandedAnswer.jingle) {
|
|
expandedAnswer.jingle = SJJ.toSessionJSON(answer.sdp, {
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
}
|
|
expandedAnswer.jingle.contents[1].application.sources.forEach(function (source, idx) {
|
|
// the floor idx/2 is a hack that relies on a particular order
|
|
// of groups, alternating between sim and rtx
|
|
source.parameters = source.parameters.map(function (parameter) {
|
|
if (parameter.key === 'msid') {
|
|
parameter.value += '-' + Math.floor(idx / 2);
|
|
}
|
|
return parameter;
|
|
});
|
|
});
|
|
expandedAnswer.sdp = SJJ.toSessionSDP(expandedAnswer.jingle, {
|
|
sid: self.sdpSessionID,
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
}
|
|
expandedAnswer.sdp.split('\r\n').forEach(function (line) {
|
|
if (line.indexOf('a=candidate:') === 0) {
|
|
self._checkLocalCandidate(line);
|
|
}
|
|
});
|
|
if (!self.assumeSetLocalSuccess) {
|
|
var copy = cloneDeep(expandedAnswer);
|
|
self.emit('answer', copy);
|
|
cb(null, copy);
|
|
}
|
|
},
|
|
function (err) {
|
|
self.emit('error', err);
|
|
cb(err);
|
|
}
|
|
);
|
|
},
|
|
function (err) {
|
|
self.emit('error', err);
|
|
cb(err);
|
|
},
|
|
constraints
|
|
);
|
|
};
|
|
|
|
// Internal method for emitting ice candidates on our peer object
|
|
PeerConnection.prototype._onIce = function (event) {
|
|
var self = this;
|
|
if (event.candidate) {
|
|
if (this.dontSignalCandidates) return;
|
|
var ice = event.candidate;
|
|
|
|
var expandedCandidate = {
|
|
candidate: {
|
|
candidate: ice.candidate,
|
|
sdpMid: ice.sdpMid,
|
|
sdpMLineIndex: ice.sdpMLineIndex
|
|
}
|
|
};
|
|
this._checkLocalCandidate(ice.candidate);
|
|
|
|
var cand = SJJ.toCandidateJSON(ice.candidate);
|
|
|
|
var already;
|
|
var idx;
|
|
if (this.eliminateDuplicateCandidates && cand.type === 'relay') {
|
|
// drop candidates with same foundation, component
|
|
// take local type pref into account so we don't ignore udp
|
|
// ones when we know about a TCP one. unlikely but...
|
|
already = this._candidateBuffer.filter(
|
|
function (c) {
|
|
return c.type === 'relay';
|
|
}).map(function (c) {
|
|
return c.foundation + ':' + c.component;
|
|
}
|
|
);
|
|
idx = already.indexOf(cand.foundation + ':' + cand.component);
|
|
// remember: local type pref of udp is 0, tcp 1, tls 2
|
|
if (idx > -1 && ((cand.priority >> 24) >= (already[idx].priority >> 24))) {
|
|
// drop it, same foundation with higher (worse) type pref
|
|
return;
|
|
}
|
|
}
|
|
if (this.config.bundlePolicy === 'max-bundle') {
|
|
// drop candidates which are duplicate for audio/video/data
|
|
// duplicate means same host/port but different sdpMid
|
|
already = this._candidateBuffer.filter(
|
|
function (c) {
|
|
return cand.type === c.type;
|
|
}).map(function (cand) {
|
|
return cand.address + ':' + cand.port;
|
|
}
|
|
);
|
|
idx = already.indexOf(cand.address + ':' + cand.port);
|
|
if (idx > -1) return;
|
|
}
|
|
// also drop rtcp candidates since we know the peer supports RTCP-MUX
|
|
// this is a workaround until browsers implement this natively
|
|
if (this.config.rtcpMuxPolicy === 'require' && cand.component === '2') {
|
|
return;
|
|
}
|
|
this._candidateBuffer.push(cand);
|
|
|
|
if (self.config.useJingle) {
|
|
if (!ice.sdpMid) { // firefox doesn't set this
|
|
if (self.pc.remoteDescription && self.pc.remoteDescription.type === 'offer') {
|
|
// preserve name from remote
|
|
ice.sdpMid = self.remoteDescription.contents[ice.sdpMLineIndex].name;
|
|
} else {
|
|
ice.sdpMid = self.localDescription.contents[ice.sdpMLineIndex].name;
|
|
}
|
|
}
|
|
if (!self.iceCredentials.local[ice.sdpMid]) {
|
|
var jingle = SJJ.toSessionJSON(self.pc.localDescription.sdp, {
|
|
role: self._role(),
|
|
direction: 'outgoing'
|
|
});
|
|
jingle.contents.forEach(function (content) {
|
|
var transport = content.transport || {};
|
|
if (transport.ufrag) {
|
|
self.iceCredentials.local[content.name] = {
|
|
ufrag: transport.ufrag,
|
|
pwd: transport.pwd
|
|
};
|
|
}
|
|
});
|
|
}
|
|
expandedCandidate.jingle = {
|
|
contents: [{
|
|
name: ice.sdpMid,
|
|
creator: self._role(),
|
|
transport: {
|
|
transportType: 'iceUdp',
|
|
ufrag: self.iceCredentials.local[ice.sdpMid].ufrag,
|
|
pwd: self.iceCredentials.local[ice.sdpMid].pwd,
|
|
candidates: [
|
|
cand
|
|
]
|
|
}
|
|
}]
|
|
};
|
|
if (self.batchIceCandidates > 0) {
|
|
if (self.batchedIceCandidates.length === 0) {
|
|
window.setTimeout(function () {
|
|
var contents = {};
|
|
self.batchedIceCandidates.forEach(function (content) {
|
|
content = content.contents[0];
|
|
if (!contents[content.name]) contents[content.name] = content;
|
|
contents[content.name].transport.candidates.push(content.transport.candidates[0]);
|
|
});
|
|
var newCand = {
|
|
jingle: {
|
|
contents: []
|
|
}
|
|
};
|
|
Object.keys(contents).forEach(function (name) {
|
|
newCand.jingle.contents.push(contents[name]);
|
|
});
|
|
self.batchedIceCandidates = [];
|
|
self.emit('ice', newCand);
|
|
}, self.batchIceCandidates);
|
|
}
|
|
self.batchedIceCandidates.push(expandedCandidate.jingle);
|
|
return;
|
|
}
|
|
|
|
}
|
|
this.emit('ice', expandedCandidate);
|
|
} else {
|
|
this.emit('endOfCandidates');
|
|
}
|
|
};
|
|
|
|
// Internal method for processing a new data channel being added by the
|
|
// other peer.
|
|
PeerConnection.prototype._onDataChannel = function (event) {
|
|
// make sure we keep a reference so this doesn't get garbage collected
|
|
var channel = event.channel;
|
|
this._remoteDataChannels.push(channel);
|
|
|
|
this.emit('addChannel', channel);
|
|
};
|
|
|
|
// Create a data channel spec reference:
|
|
// http://dev.w3.org/2011/webrtc/editor/webrtc.html#idl-def-RTCDataChannelInit
|
|
PeerConnection.prototype.createDataChannel = function (name, opts) {
|
|
var channel = this.pc.createDataChannel(name, opts);
|
|
|
|
// make sure we keep a reference so this doesn't get garbage collected
|
|
this._localDataChannels.push(channel);
|
|
|
|
return channel;
|
|
};
|
|
|
|
PeerConnection.prototype.getStats = function (cb) {
|
|
this.pc.getStats(null,
|
|
function (res) {
|
|
cb(null, res);
|
|
},
|
|
function (err) {
|
|
cb(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
module.exports = PeerConnection;
|
|
|
|
},{"lodash.clonedeep":47,"sdp-jingle-json":57,"traceablepeerconnection":76,"util":89,"webrtc-adapter":90,"wildemitter":100}],57:[function(require,module,exports){
|
|
var toSDP = require('./lib/tosdp');
|
|
var toJSON = require('./lib/tojson');
|
|
|
|
|
|
// Converstion from JSON to SDP
|
|
|
|
exports.toIncomingSDPOffer = function (session) {
|
|
return toSDP.toSessionSDP(session, {
|
|
role: 'responder',
|
|
direction: 'incoming'
|
|
});
|
|
};
|
|
exports.toOutgoingSDPOffer = function (session) {
|
|
return toSDP.toSessionSDP(session, {
|
|
role: 'initiator',
|
|
direction: 'outgoing'
|
|
});
|
|
};
|
|
exports.toIncomingSDPAnswer = function (session) {
|
|
return toSDP.toSessionSDP(session, {
|
|
role: 'initiator',
|
|
direction: 'incoming'
|
|
});
|
|
};
|
|
exports.toOutgoingSDPAnswer = function (session) {
|
|
return toSDP.toSessionSDP(session, {
|
|
role: 'responder',
|
|
direction: 'outgoing'
|
|
});
|
|
};
|
|
exports.toIncomingMediaSDPOffer = function (media) {
|
|
return toSDP.toMediaSDP(media, {
|
|
role: 'responder',
|
|
direction: 'incoming'
|
|
});
|
|
};
|
|
exports.toOutgoingMediaSDPOffer = function (media) {
|
|
return toSDP.toMediaSDP(media, {
|
|
role: 'initiator',
|
|
direction: 'outgoing'
|
|
});
|
|
};
|
|
exports.toIncomingMediaSDPAnswer = function (media) {
|
|
return toSDP.toMediaSDP(media, {
|
|
role: 'initiator',
|
|
direction: 'incoming'
|
|
});
|
|
};
|
|
exports.toOutgoingMediaSDPAnswer = function (media) {
|
|
return toSDP.toMediaSDP(media, {
|
|
role: 'responder',
|
|
direction: 'outgoing'
|
|
});
|
|
};
|
|
exports.toCandidateSDP = toSDP.toCandidateSDP;
|
|
exports.toMediaSDP = toSDP.toMediaSDP;
|
|
exports.toSessionSDP = toSDP.toSessionSDP;
|
|
|
|
|
|
// Conversion from SDP to JSON
|
|
|
|
exports.toIncomingJSONOffer = function (sdp, creators) {
|
|
return toJSON.toSessionJSON(sdp, {
|
|
role: 'responder',
|
|
direction: 'incoming',
|
|
creators: creators
|
|
});
|
|
};
|
|
exports.toOutgoingJSONOffer = function (sdp, creators) {
|
|
return toJSON.toSessionJSON(sdp, {
|
|
role: 'initiator',
|
|
direction: 'outgoing',
|
|
creators: creators
|
|
});
|
|
};
|
|
exports.toIncomingJSONAnswer = function (sdp, creators) {
|
|
return toJSON.toSessionJSON(sdp, {
|
|
role: 'initiator',
|
|
direction: 'incoming',
|
|
creators: creators
|
|
});
|
|
};
|
|
exports.toOutgoingJSONAnswer = function (sdp, creators) {
|
|
return toJSON.toSessionJSON(sdp, {
|
|
role: 'responder',
|
|
direction: 'outgoing',
|
|
creators: creators
|
|
});
|
|
};
|
|
exports.toIncomingMediaJSONOffer = function (sdp, creator) {
|
|
return toJSON.toMediaJSON(sdp, {
|
|
role: 'responder',
|
|
direction: 'incoming',
|
|
creator: creator
|
|
});
|
|
};
|
|
exports.toOutgoingMediaJSONOffer = function (sdp, creator) {
|
|
return toJSON.toMediaJSON(sdp, {
|
|
role: 'initiator',
|
|
direction: 'outgoing',
|
|
creator: creator
|
|
});
|
|
};
|
|
exports.toIncomingMediaJSONAnswer = function (sdp, creator) {
|
|
return toJSON.toMediaJSON(sdp, {
|
|
role: 'initiator',
|
|
direction: 'incoming',
|
|
creator: creator
|
|
});
|
|
};
|
|
exports.toOutgoingMediaJSONAnswer = function (sdp, creator) {
|
|
return toJSON.toMediaJSON(sdp, {
|
|
role: 'responder',
|
|
direction: 'outgoing',
|
|
creator: creator
|
|
});
|
|
};
|
|
exports.toCandidateJSON = toJSON.toCandidateJSON;
|
|
exports.toMediaJSON = toJSON.toMediaJSON;
|
|
exports.toSessionJSON = toJSON.toSessionJSON;
|
|
|
|
},{"./lib/tojson":60,"./lib/tosdp":61}],58:[function(require,module,exports){
|
|
exports.lines = function (sdp) {
|
|
return sdp.split('\r\n').filter(function (line) {
|
|
return line.length > 0;
|
|
});
|
|
};
|
|
|
|
exports.findLine = function (prefix, mediaLines, sessionLines) {
|
|
var prefixLength = prefix.length;
|
|
for (var i = 0; i < mediaLines.length; i++) {
|
|
if (mediaLines[i].substr(0, prefixLength) === prefix) {
|
|
return mediaLines[i];
|
|
}
|
|
}
|
|
// Continue searching in parent session section
|
|
if (!sessionLines) {
|
|
return false;
|
|
}
|
|
|
|
for (var j = 0; j < sessionLines.length; j++) {
|
|
if (sessionLines[j].substr(0, prefixLength) === prefix) {
|
|
return sessionLines[j];
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
exports.findLines = function (prefix, mediaLines, sessionLines) {
|
|
var results = [];
|
|
var prefixLength = prefix.length;
|
|
for (var i = 0; i < mediaLines.length; i++) {
|
|
if (mediaLines[i].substr(0, prefixLength) === prefix) {
|
|
results.push(mediaLines[i]);
|
|
}
|
|
}
|
|
if (results.length || !sessionLines) {
|
|
return results;
|
|
}
|
|
for (var j = 0; j < sessionLines.length; j++) {
|
|
if (sessionLines[j].substr(0, prefixLength) === prefix) {
|
|
results.push(sessionLines[j]);
|
|
}
|
|
}
|
|
return results;
|
|
};
|
|
|
|
exports.mline = function (line) {
|
|
var parts = line.substr(2).split(' ');
|
|
var parsed = {
|
|
media: parts[0],
|
|
port: parts[1],
|
|
proto: parts[2],
|
|
formats: []
|
|
};
|
|
for (var i = 3; i < parts.length; i++) {
|
|
if (parts[i]) {
|
|
parsed.formats.push(parts[i]);
|
|
}
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
exports.rtpmap = function (line) {
|
|
var parts = line.substr(9).split(' ');
|
|
var parsed = {
|
|
id: parts.shift()
|
|
};
|
|
|
|
parts = parts[0].split('/');
|
|
|
|
parsed.name = parts[0];
|
|
parsed.clockrate = parts[1];
|
|
parsed.channels = parts.length == 3 ? parts[2] : '1';
|
|
return parsed;
|
|
};
|
|
|
|
exports.sctpmap = function (line) {
|
|
// based on -05 draft
|
|
var parts = line.substr(10).split(' ');
|
|
var parsed = {
|
|
number: parts.shift(),
|
|
protocol: parts.shift(),
|
|
streams: parts.shift()
|
|
};
|
|
return parsed;
|
|
};
|
|
|
|
|
|
exports.fmtp = function (line) {
|
|
var kv, key, value;
|
|
var parts = line.substr(line.indexOf(' ') + 1).split(';');
|
|
var parsed = [];
|
|
for (var i = 0; i < parts.length; i++) {
|
|
kv = parts[i].split('=');
|
|
key = kv[0].trim();
|
|
value = kv[1];
|
|
if (key && value) {
|
|
parsed.push({key: key, value: value});
|
|
} else if (key) {
|
|
parsed.push({key: '', value: key});
|
|
}
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
exports.crypto = function (line) {
|
|
var parts = line.substr(9).split(' ');
|
|
var parsed = {
|
|
tag: parts[0],
|
|
cipherSuite: parts[1],
|
|
keyParams: parts[2],
|
|
sessionParams: parts.slice(3).join(' ')
|
|
};
|
|
return parsed;
|
|
};
|
|
|
|
exports.fingerprint = function (line) {
|
|
var parts = line.substr(14).split(' ');
|
|
return {
|
|
hash: parts[0],
|
|
value: parts[1]
|
|
};
|
|
};
|
|
|
|
exports.extmap = function (line) {
|
|
var parts = line.substr(9).split(' ');
|
|
var parsed = {};
|
|
|
|
var idpart = parts.shift();
|
|
var sp = idpart.indexOf('/');
|
|
if (sp >= 0) {
|
|
parsed.id = idpart.substr(0, sp);
|
|
parsed.senders = idpart.substr(sp + 1);
|
|
} else {
|
|
parsed.id = idpart;
|
|
parsed.senders = 'sendrecv';
|
|
}
|
|
|
|
parsed.uri = parts.shift() || '';
|
|
|
|
return parsed;
|
|
};
|
|
|
|
exports.rtcpfb = function (line) {
|
|
var parts = line.substr(10).split(' ');
|
|
var parsed = {};
|
|
parsed.id = parts.shift();
|
|
parsed.type = parts.shift();
|
|
if (parsed.type === 'trr-int') {
|
|
parsed.value = parts.shift();
|
|
} else {
|
|
parsed.subtype = parts.shift() || '';
|
|
}
|
|
parsed.parameters = parts;
|
|
return parsed;
|
|
};
|
|
|
|
exports.candidate = function (line) {
|
|
var parts;
|
|
if (line.indexOf('a=candidate:') === 0) {
|
|
parts = line.substring(12).split(' ');
|
|
} else { // no a=candidate
|
|
parts = line.substring(10).split(' ');
|
|
}
|
|
|
|
var candidate = {
|
|
foundation: parts[0],
|
|
component: parts[1],
|
|
protocol: parts[2].toLowerCase(),
|
|
priority: parts[3],
|
|
ip: parts[4],
|
|
port: parts[5],
|
|
// skip parts[6] == 'typ'
|
|
type: parts[7],
|
|
generation: '0'
|
|
};
|
|
|
|
for (var i = 8; i < parts.length; i += 2) {
|
|
if (parts[i] === 'raddr') {
|
|
candidate.relAddr = parts[i + 1];
|
|
} else if (parts[i] === 'rport') {
|
|
candidate.relPort = parts[i + 1];
|
|
} else if (parts[i] === 'generation') {
|
|
candidate.generation = parts[i + 1];
|
|
} else if (parts[i] === 'tcptype') {
|
|
candidate.tcpType = parts[i + 1];
|
|
}
|
|
}
|
|
|
|
candidate.network = '1';
|
|
|
|
return candidate;
|
|
};
|
|
|
|
exports.sourceGroups = function (lines) {
|
|
var parsed = [];
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var parts = lines[i].substr(13).split(' ');
|
|
parsed.push({
|
|
semantics: parts.shift(),
|
|
sources: parts
|
|
});
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
exports.sources = function (lines) {
|
|
// http://tools.ietf.org/html/rfc5576
|
|
var parsed = [];
|
|
var sources = {};
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var parts = lines[i].substr(7).split(' ');
|
|
var ssrc = parts.shift();
|
|
|
|
if (!sources[ssrc]) {
|
|
var source = {
|
|
ssrc: ssrc,
|
|
parameters: []
|
|
};
|
|
parsed.push(source);
|
|
|
|
// Keep an index
|
|
sources[ssrc] = source;
|
|
}
|
|
|
|
parts = parts.join(' ').split(':');
|
|
var attribute = parts.shift();
|
|
var value = parts.join(':') || null;
|
|
|
|
sources[ssrc].parameters.push({
|
|
key: attribute,
|
|
value: value
|
|
});
|
|
}
|
|
|
|
return parsed;
|
|
};
|
|
|
|
exports.groups = function (lines) {
|
|
// http://tools.ietf.org/html/rfc5888
|
|
var parsed = [];
|
|
var parts;
|
|
for (var i = 0; i < lines.length; i++) {
|
|
parts = lines[i].substr(8).split(' ');
|
|
parsed.push({
|
|
semantics: parts.shift(),
|
|
contents: parts
|
|
});
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
exports.bandwidth = function (line) {
|
|
var parts = line.substr(2).split(':');
|
|
var parsed = {};
|
|
parsed.type = parts.shift();
|
|
parsed.bandwidth = parts.shift();
|
|
return parsed;
|
|
};
|
|
|
|
exports.msid = function (line) {
|
|
var data = line.substr(7);
|
|
var parts = data.split(' ');
|
|
return {
|
|
msid: data,
|
|
mslabel: parts[0],
|
|
label: parts[1]
|
|
};
|
|
};
|
|
|
|
},{}],59:[function(require,module,exports){
|
|
module.exports = {
|
|
initiator: {
|
|
incoming: {
|
|
initiator: 'recvonly',
|
|
responder: 'sendonly',
|
|
both: 'sendrecv',
|
|
none: 'inactive',
|
|
recvonly: 'initiator',
|
|
sendonly: 'responder',
|
|
sendrecv: 'both',
|
|
inactive: 'none'
|
|
},
|
|
outgoing: {
|
|
initiator: 'sendonly',
|
|
responder: 'recvonly',
|
|
both: 'sendrecv',
|
|
none: 'inactive',
|
|
recvonly: 'responder',
|
|
sendonly: 'initiator',
|
|
sendrecv: 'both',
|
|
inactive: 'none'
|
|
}
|
|
},
|
|
responder: {
|
|
incoming: {
|
|
initiator: 'sendonly',
|
|
responder: 'recvonly',
|
|
both: 'sendrecv',
|
|
none: 'inactive',
|
|
recvonly: 'responder',
|
|
sendonly: 'initiator',
|
|
sendrecv: 'both',
|
|
inactive: 'none'
|
|
},
|
|
outgoing: {
|
|
initiator: 'recvonly',
|
|
responder: 'sendonly',
|
|
both: 'sendrecv',
|
|
none: 'inactive',
|
|
recvonly: 'initiator',
|
|
sendonly: 'responder',
|
|
sendrecv: 'both',
|
|
inactive: 'none'
|
|
}
|
|
}
|
|
};
|
|
|
|
},{}],60:[function(require,module,exports){
|
|
var SENDERS = require('./senders');
|
|
var parsers = require('./parsers');
|
|
var idCounter = Math.random();
|
|
|
|
|
|
exports._setIdCounter = function (counter) {
|
|
idCounter = counter;
|
|
};
|
|
|
|
exports.toSessionJSON = function (sdp, opts) {
|
|
var i;
|
|
var creators = opts.creators || [];
|
|
var role = opts.role || 'initiator';
|
|
var direction = opts.direction || 'outgoing';
|
|
|
|
|
|
// Divide the SDP into session and media sections.
|
|
var media = sdp.split('\r\nm=');
|
|
for (i = 1; i < media.length; i++) {
|
|
media[i] = 'm=' + media[i];
|
|
if (i !== media.length - 1) {
|
|
media[i] += '\r\n';
|
|
}
|
|
}
|
|
var session = media.shift() + '\r\n';
|
|
var sessionLines = parsers.lines(session);
|
|
var parsed = {};
|
|
|
|
var contents = [];
|
|
for (i = 0; i < media.length; i++) {
|
|
contents.push(exports.toMediaJSON(media[i], session, {
|
|
role: role,
|
|
direction: direction,
|
|
creator: creators[i] || 'initiator'
|
|
}));
|
|
}
|
|
parsed.contents = contents;
|
|
|
|
var groupLines = parsers.findLines('a=group:', sessionLines);
|
|
if (groupLines.length) {
|
|
parsed.groups = parsers.groups(groupLines);
|
|
}
|
|
|
|
return parsed;
|
|
};
|
|
|
|
exports.toMediaJSON = function (media, session, opts) {
|
|
var creator = opts.creator || 'initiator';
|
|
var role = opts.role || 'initiator';
|
|
var direction = opts.direction || 'outgoing';
|
|
|
|
var lines = parsers.lines(media);
|
|
var sessionLines = parsers.lines(session);
|
|
var mline = parsers.mline(lines[0]);
|
|
|
|
var content = {
|
|
creator: creator,
|
|
name: mline.media,
|
|
application: {
|
|
applicationType: 'rtp',
|
|
media: mline.media,
|
|
payloads: [],
|
|
encryption: [],
|
|
feedback: [],
|
|
headerExtensions: []
|
|
},
|
|
transport: {
|
|
transportType: 'iceUdp',
|
|
candidates: [],
|
|
fingerprints: []
|
|
}
|
|
};
|
|
if (mline.media == 'application') {
|
|
// FIXME: the description is most likely to be independent
|
|
// of the SDP and should be processed by other parts of the library
|
|
content.application = {
|
|
applicationType: 'datachannel'
|
|
};
|
|
content.transport.sctp = [];
|
|
}
|
|
var desc = content.application;
|
|
var trans = content.transport;
|
|
|
|
// If we have a mid, use that for the content name instead.
|
|
var mid = parsers.findLine('a=mid:', lines);
|
|
if (mid) {
|
|
content.name = mid.substr(6);
|
|
}
|
|
|
|
if (parsers.findLine('a=sendrecv', lines, sessionLines)) {
|
|
content.senders = 'both';
|
|
} else if (parsers.findLine('a=sendonly', lines, sessionLines)) {
|
|
content.senders = SENDERS[role][direction].sendonly;
|
|
} else if (parsers.findLine('a=recvonly', lines, sessionLines)) {
|
|
content.senders = SENDERS[role][direction].recvonly;
|
|
} else if (parsers.findLine('a=inactive', lines, sessionLines)) {
|
|
content.senders = 'none';
|
|
}
|
|
|
|
if (desc.applicationType == 'rtp') {
|
|
var bandwidth = parsers.findLine('b=', lines);
|
|
if (bandwidth) {
|
|
desc.bandwidth = parsers.bandwidth(bandwidth);
|
|
}
|
|
|
|
var ssrc = parsers.findLine('a=ssrc:', lines);
|
|
if (ssrc) {
|
|
desc.ssrc = ssrc.substr(7).split(' ')[0];
|
|
}
|
|
|
|
var rtpmapLines = parsers.findLines('a=rtpmap:', lines);
|
|
rtpmapLines.forEach(function (line) {
|
|
var payload = parsers.rtpmap(line);
|
|
payload.parameters = [];
|
|
payload.feedback = [];
|
|
|
|
var fmtpLines = parsers.findLines('a=fmtp:' + payload.id, lines);
|
|
// There should only be one fmtp line per payload
|
|
fmtpLines.forEach(function (line) {
|
|
payload.parameters = parsers.fmtp(line);
|
|
});
|
|
|
|
var fbLines = parsers.findLines('a=rtcp-fb:' + payload.id, lines);
|
|
fbLines.forEach(function (line) {
|
|
payload.feedback.push(parsers.rtcpfb(line));
|
|
});
|
|
|
|
desc.payloads.push(payload);
|
|
});
|
|
|
|
var cryptoLines = parsers.findLines('a=crypto:', lines, sessionLines);
|
|
cryptoLines.forEach(function (line) {
|
|
desc.encryption.push(parsers.crypto(line));
|
|
});
|
|
|
|
if (parsers.findLine('a=rtcp-mux', lines)) {
|
|
desc.mux = true;
|
|
}
|
|
|
|
var fbLines = parsers.findLines('a=rtcp-fb:*', lines);
|
|
fbLines.forEach(function (line) {
|
|
desc.feedback.push(parsers.rtcpfb(line));
|
|
});
|
|
|
|
var extLines = parsers.findLines('a=extmap:', lines);
|
|
extLines.forEach(function (line) {
|
|
var ext = parsers.extmap(line);
|
|
|
|
ext.senders = SENDERS[role][direction][ext.senders];
|
|
|
|
desc.headerExtensions.push(ext);
|
|
});
|
|
|
|
var ssrcGroupLines = parsers.findLines('a=ssrc-group:', lines);
|
|
desc.sourceGroups = parsers.sourceGroups(ssrcGroupLines || []);
|
|
|
|
var ssrcLines = parsers.findLines('a=ssrc:', lines);
|
|
var sources = desc.sources = parsers.sources(ssrcLines || []);
|
|
|
|
var msidLine = parsers.findLine('a=msid:', lines);
|
|
if (msidLine) {
|
|
var msid = parsers.msid(msidLine);
|
|
['msid', 'mslabel', 'label'].forEach(function (key) {
|
|
for (var i = 0; i < sources.length; i++) {
|
|
var found = false;
|
|
for (var j = 0; j < sources[i].parameters.length; j++) {
|
|
if (sources[i].parameters[j].key === key) {
|
|
found = true;
|
|
}
|
|
}
|
|
if (!found) {
|
|
sources[i].parameters.push({ key: key, value: msid[key] });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
if (parsers.findLine('a=x-google-flag:conference', lines, sessionLines)) {
|
|
desc.googConferenceFlag = true;
|
|
}
|
|
}
|
|
|
|
// transport specific attributes
|
|
var fingerprintLines = parsers.findLines('a=fingerprint:', lines, sessionLines);
|
|
var setup = parsers.findLine('a=setup:', lines, sessionLines);
|
|
fingerprintLines.forEach(function (line) {
|
|
var fp = parsers.fingerprint(line);
|
|
if (setup) {
|
|
fp.setup = setup.substr(8);
|
|
}
|
|
trans.fingerprints.push(fp);
|
|
});
|
|
|
|
var ufragLine = parsers.findLine('a=ice-ufrag:', lines, sessionLines);
|
|
var pwdLine = parsers.findLine('a=ice-pwd:', lines, sessionLines);
|
|
if (ufragLine && pwdLine) {
|
|
trans.ufrag = ufragLine.substr(12);
|
|
trans.pwd = pwdLine.substr(10);
|
|
trans.candidates = [];
|
|
|
|
var candidateLines = parsers.findLines('a=candidate:', lines, sessionLines);
|
|
candidateLines.forEach(function (line) {
|
|
trans.candidates.push(exports.toCandidateJSON(line));
|
|
});
|
|
}
|
|
|
|
if (desc.applicationType == 'datachannel') {
|
|
var sctpmapLines = parsers.findLines('a=sctpmap:', lines);
|
|
sctpmapLines.forEach(function (line) {
|
|
var sctp = parsers.sctpmap(line);
|
|
trans.sctp.push(sctp);
|
|
});
|
|
}
|
|
|
|
return content;
|
|
};
|
|
|
|
exports.toCandidateJSON = function (line) {
|
|
var candidate = parsers.candidate(line.split('\r\n')[0]);
|
|
candidate.id = (idCounter++).toString(36).substr(0, 12);
|
|
return candidate;
|
|
};
|
|
|
|
},{"./parsers":58,"./senders":59}],61:[function(require,module,exports){
|
|
var SENDERS = require('./senders');
|
|
|
|
|
|
exports.toSessionSDP = function (session, opts) {
|
|
var role = opts.role || 'initiator';
|
|
var direction = opts.direction || 'outgoing';
|
|
var sid = opts.sid || session.sid || Date.now();
|
|
var time = opts.time || Date.now();
|
|
|
|
var sdp = [
|
|
'v=0',
|
|
'o=- ' + sid + ' ' + time + ' IN IP4 0.0.0.0',
|
|
's=-',
|
|
't=0 0'
|
|
];
|
|
|
|
var contents = session.contents || [];
|
|
var hasSources = false;
|
|
contents.forEach(function (content) {
|
|
if (content.application.sources &&
|
|
content.application.sources.length) {
|
|
hasSources = true;
|
|
}
|
|
});
|
|
|
|
if (hasSources) {
|
|
sdp.push('a=msid-semantic: WMS *');
|
|
}
|
|
|
|
var groups = session.groups || [];
|
|
groups.forEach(function (group) {
|
|
sdp.push('a=group:' + group.semantics + ' ' + group.contents.join(' '));
|
|
});
|
|
|
|
|
|
contents.forEach(function (content) {
|
|
sdp.push(exports.toMediaSDP(content, opts));
|
|
});
|
|
|
|
return sdp.join('\r\n') + '\r\n';
|
|
};
|
|
|
|
exports.toMediaSDP = function (content, opts) {
|
|
var sdp = [];
|
|
|
|
var role = opts.role || 'initiator';
|
|
var direction = opts.direction || 'outgoing';
|
|
|
|
var desc = content.application;
|
|
var transport = content.transport;
|
|
var payloads = desc.payloads || [];
|
|
var fingerprints = (transport && transport.fingerprints) || [];
|
|
|
|
var mline = [];
|
|
if (desc.applicationType == 'datachannel') {
|
|
mline.push('application');
|
|
mline.push('1');
|
|
mline.push('DTLS/SCTP');
|
|
if (transport.sctp) {
|
|
transport.sctp.forEach(function (map) {
|
|
mline.push(map.number);
|
|
});
|
|
}
|
|
} else {
|
|
mline.push(desc.media);
|
|
mline.push('1');
|
|
if (fingerprints.length > 0) {
|
|
mline.push('UDP/TLS/RTP/SAVPF');
|
|
} else if (desc.encryption && desc.encryption.length > 0) {
|
|
mline.push('RTP/SAVPF');
|
|
} else {
|
|
mline.push('RTP/AVPF');
|
|
}
|
|
payloads.forEach(function (payload) {
|
|
mline.push(payload.id);
|
|
});
|
|
}
|
|
|
|
|
|
sdp.push('m=' + mline.join(' '));
|
|
|
|
sdp.push('c=IN IP4 0.0.0.0');
|
|
if (desc.bandwidth && desc.bandwidth.type && desc.bandwidth.bandwidth) {
|
|
sdp.push('b=' + desc.bandwidth.type + ':' + desc.bandwidth.bandwidth);
|
|
}
|
|
if (desc.applicationType == 'rtp') {
|
|
sdp.push('a=rtcp:1 IN IP4 0.0.0.0');
|
|
}
|
|
|
|
if (transport) {
|
|
if (transport.ufrag) {
|
|
sdp.push('a=ice-ufrag:' + transport.ufrag);
|
|
}
|
|
if (transport.pwd) {
|
|
sdp.push('a=ice-pwd:' + transport.pwd);
|
|
}
|
|
|
|
var pushedSetup = false;
|
|
fingerprints.forEach(function (fingerprint) {
|
|
sdp.push('a=fingerprint:' + fingerprint.hash + ' ' + fingerprint.value);
|
|
if (fingerprint.setup && !pushedSetup) {
|
|
sdp.push('a=setup:' + fingerprint.setup);
|
|
}
|
|
});
|
|
|
|
if (transport.sctp) {
|
|
transport.sctp.forEach(function (map) {
|
|
sdp.push('a=sctpmap:' + map.number + ' ' + map.protocol + ' ' + map.streams);
|
|
});
|
|
}
|
|
}
|
|
|
|
if (desc.applicationType == 'rtp') {
|
|
sdp.push('a=' + (SENDERS[role][direction][content.senders] || 'sendrecv'));
|
|
}
|
|
sdp.push('a=mid:' + content.name);
|
|
|
|
if (desc.sources && desc.sources.length) {
|
|
(desc.sources[0].parameters || []).forEach(function (param) {
|
|
if (param.key === 'msid') {
|
|
sdp.push('a=msid:' + param.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (desc.mux) {
|
|
sdp.push('a=rtcp-mux');
|
|
}
|
|
|
|
var encryption = desc.encryption || [];
|
|
encryption.forEach(function (crypto) {
|
|
sdp.push('a=crypto:' + crypto.tag + ' ' + crypto.cipherSuite + ' ' + crypto.keyParams + (crypto.sessionParams ? ' ' + crypto.sessionParams : ''));
|
|
});
|
|
if (desc.googConferenceFlag) {
|
|
sdp.push('a=x-google-flag:conference');
|
|
}
|
|
|
|
payloads.forEach(function (payload) {
|
|
var rtpmap = 'a=rtpmap:' + payload.id + ' ' + payload.name + '/' + payload.clockrate;
|
|
if (payload.channels && payload.channels != '1') {
|
|
rtpmap += '/' + payload.channels;
|
|
}
|
|
sdp.push(rtpmap);
|
|
|
|
if (payload.parameters && payload.parameters.length) {
|
|
var fmtp = ['a=fmtp:' + payload.id];
|
|
var parameters = [];
|
|
payload.parameters.forEach(function (param) {
|
|
parameters.push((param.key ? param.key + '=' : '') + param.value);
|
|
});
|
|
fmtp.push(parameters.join(';'));
|
|
sdp.push(fmtp.join(' '));
|
|
}
|
|
|
|
if (payload.feedback) {
|
|
payload.feedback.forEach(function (fb) {
|
|
if (fb.type === 'trr-int') {
|
|
sdp.push('a=rtcp-fb:' + payload.id + ' trr-int ' + (fb.value ? fb.value : '0'));
|
|
} else {
|
|
sdp.push('a=rtcp-fb:' + payload.id + ' ' + fb.type + (fb.subtype ? ' ' + fb.subtype : ''));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (desc.feedback) {
|
|
desc.feedback.forEach(function (fb) {
|
|
if (fb.type === 'trr-int') {
|
|
sdp.push('a=rtcp-fb:* trr-int ' + (fb.value ? fb.value : '0'));
|
|
} else {
|
|
sdp.push('a=rtcp-fb:* ' + fb.type + (fb.subtype ? ' ' + fb.subtype : ''));
|
|
}
|
|
});
|
|
}
|
|
|
|
var hdrExts = desc.headerExtensions || [];
|
|
hdrExts.forEach(function (hdr) {
|
|
sdp.push('a=extmap:' + hdr.id + (hdr.senders ? '/' + SENDERS[role][direction][hdr.senders] : '') + ' ' + hdr.uri);
|
|
});
|
|
|
|
var ssrcGroups = desc.sourceGroups || [];
|
|
ssrcGroups.forEach(function (ssrcGroup) {
|
|
sdp.push('a=ssrc-group:' + ssrcGroup.semantics + ' ' + ssrcGroup.sources.join(' '));
|
|
});
|
|
|
|
var ssrcs = desc.sources || [];
|
|
ssrcs.forEach(function (ssrc) {
|
|
for (var i = 0; i < ssrc.parameters.length; i++) {
|
|
var param = ssrc.parameters[i];
|
|
sdp.push('a=ssrc:' + (ssrc.ssrc || desc.ssrc) + ' ' + param.key + (param.value ? (':' + param.value) : ''));
|
|
}
|
|
});
|
|
|
|
var candidates = transport.candidates || [];
|
|
candidates.forEach(function (candidate) {
|
|
sdp.push(exports.toCandidateSDP(candidate));
|
|
});
|
|
|
|
return sdp.join('\r\n');
|
|
};
|
|
|
|
exports.toCandidateSDP = function (candidate) {
|
|
var sdp = [];
|
|
|
|
sdp.push(candidate.foundation);
|
|
sdp.push(candidate.component);
|
|
sdp.push(candidate.protocol.toUpperCase());
|
|
sdp.push(candidate.priority);
|
|
sdp.push(candidate.ip);
|
|
sdp.push(candidate.port);
|
|
|
|
var type = candidate.type;
|
|
sdp.push('typ');
|
|
sdp.push(type);
|
|
if (type === 'srflx' || type === 'prflx' || type === 'relay') {
|
|
if (candidate.relAddr && candidate.relPort) {
|
|
sdp.push('raddr');
|
|
sdp.push(candidate.relAddr);
|
|
sdp.push('rport');
|
|
sdp.push(candidate.relPort);
|
|
}
|
|
}
|
|
if (candidate.tcpType && candidate.protocol.toUpperCase() == 'TCP') {
|
|
sdp.push('tcptype');
|
|
sdp.push(candidate.tcpType);
|
|
}
|
|
|
|
sdp.push('generation');
|
|
sdp.push(candidate.generation || '0');
|
|
|
|
// FIXME: apparently this is wrong per spec
|
|
// but then, we need this when actually putting this into
|
|
// SDP so it's going to stay.
|
|
// decision needs to be revisited when browsers dont
|
|
// accept this any longer
|
|
return 'a=candidate:' + sdp.join(' ');
|
|
};
|
|
|
|
},{"./senders":59}],62:[function(require,module,exports){
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
// SDP helpers.
|
|
var SDPUtils = {};
|
|
|
|
// Generate an alphanumeric identifier for cname or mids.
|
|
// TODO: use UUIDs instead? https://gist.github.com/jed/982883
|
|
SDPUtils.generateIdentifier = function() {
|
|
return Math.random().toString(36).substr(2, 10);
|
|
};
|
|
|
|
// The RTCP CNAME used by all peerconnections from the same JS.
|
|
SDPUtils.localCName = SDPUtils.generateIdentifier();
|
|
|
|
// Splits SDP into lines, dealing with both CRLF and LF.
|
|
SDPUtils.splitLines = function(blob) {
|
|
return blob.trim().split('\n').map(function(line) {
|
|
return line.trim();
|
|
});
|
|
};
|
|
// Splits SDP into sessionpart and mediasections. Ensures CRLF.
|
|
SDPUtils.splitSections = function(blob) {
|
|
var parts = blob.split('\nm=');
|
|
return parts.map(function(part, index) {
|
|
return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
|
|
});
|
|
};
|
|
|
|
// Returns lines that start with a certain prefix.
|
|
SDPUtils.matchPrefix = function(blob, prefix) {
|
|
return SDPUtils.splitLines(blob).filter(function(line) {
|
|
return line.indexOf(prefix) === 0;
|
|
});
|
|
};
|
|
|
|
// Parses an ICE candidate line. Sample input:
|
|
// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
|
|
// rport 55996"
|
|
SDPUtils.parseCandidate = function(line) {
|
|
var parts;
|
|
// Parse both variants.
|
|
if (line.indexOf('a=candidate:') === 0) {
|
|
parts = line.substring(12).split(' ');
|
|
} else {
|
|
parts = line.substring(10).split(' ');
|
|
}
|
|
|
|
var candidate = {
|
|
foundation: parts[0],
|
|
component: parts[1],
|
|
protocol: parts[2].toLowerCase(),
|
|
priority: parseInt(parts[3], 10),
|
|
ip: parts[4],
|
|
port: parseInt(parts[5], 10),
|
|
// skip parts[6] == 'typ'
|
|
type: parts[7]
|
|
};
|
|
|
|
for (var i = 8; i < parts.length; i += 2) {
|
|
switch (parts[i]) {
|
|
case 'raddr':
|
|
candidate.relatedAddress = parts[i + 1];
|
|
break;
|
|
case 'rport':
|
|
candidate.relatedPort = parseInt(parts[i + 1], 10);
|
|
break;
|
|
case 'tcptype':
|
|
candidate.tcpType = parts[i + 1];
|
|
break;
|
|
default: // Unknown extensions are silently ignored.
|
|
break;
|
|
}
|
|
}
|
|
return candidate;
|
|
};
|
|
|
|
// Translates a candidate object into SDP candidate attribute.
|
|
SDPUtils.writeCandidate = function(candidate) {
|
|
var sdp = [];
|
|
sdp.push(candidate.foundation);
|
|
sdp.push(candidate.component);
|
|
sdp.push(candidate.protocol.toUpperCase());
|
|
sdp.push(candidate.priority);
|
|
sdp.push(candidate.ip);
|
|
sdp.push(candidate.port);
|
|
|
|
var type = candidate.type;
|
|
sdp.push('typ');
|
|
sdp.push(type);
|
|
if (type !== 'host' && candidate.relatedAddress &&
|
|
candidate.relatedPort) {
|
|
sdp.push('raddr');
|
|
sdp.push(candidate.relatedAddress); // was: relAddr
|
|
sdp.push('rport');
|
|
sdp.push(candidate.relatedPort); // was: relPort
|
|
}
|
|
if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
|
|
sdp.push('tcptype');
|
|
sdp.push(candidate.tcpType);
|
|
}
|
|
return 'candidate:' + sdp.join(' ');
|
|
};
|
|
|
|
// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
|
|
// a=rtpmap:111 opus/48000/2
|
|
SDPUtils.parseRtpMap = function(line) {
|
|
var parts = line.substr(9).split(' ');
|
|
var parsed = {
|
|
payloadType: parseInt(parts.shift(), 10) // was: id
|
|
};
|
|
|
|
parts = parts[0].split('/');
|
|
|
|
parsed.name = parts[0];
|
|
parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
|
|
// was: channels
|
|
parsed.numChannels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
|
|
return parsed;
|
|
};
|
|
|
|
// Generate an a=rtpmap line from RTCRtpCodecCapability or
|
|
// RTCRtpCodecParameters.
|
|
SDPUtils.writeRtpMap = function(codec) {
|
|
var pt = codec.payloadType;
|
|
if (codec.preferredPayloadType !== undefined) {
|
|
pt = codec.preferredPayloadType;
|
|
}
|
|
return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
|
|
(codec.numChannels !== 1 ? '/' + codec.numChannels : '') + '\r\n';
|
|
};
|
|
|
|
// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
|
|
// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
|
|
SDPUtils.parseExtmap = function(line) {
|
|
var parts = line.substr(9).split(' ');
|
|
return {
|
|
id: parseInt(parts[0], 10),
|
|
uri: parts[1]
|
|
};
|
|
};
|
|
|
|
// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
|
|
// RTCRtpHeaderExtension.
|
|
SDPUtils.writeExtmap = function(headerExtension) {
|
|
return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
|
|
' ' + headerExtension.uri + '\r\n';
|
|
};
|
|
|
|
// Parses an ftmp line, returns dictionary. Sample input:
|
|
// a=fmtp:96 vbr=on;cng=on
|
|
// Also deals with vbr=on; cng=on
|
|
SDPUtils.parseFmtp = function(line) {
|
|
var parsed = {};
|
|
var kv;
|
|
var parts = line.substr(line.indexOf(' ') + 1).split(';');
|
|
for (var j = 0; j < parts.length; j++) {
|
|
kv = parts[j].trim().split('=');
|
|
parsed[kv[0].trim()] = kv[1];
|
|
}
|
|
return parsed;
|
|
};
|
|
|
|
// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
|
|
SDPUtils.writeFmtp = function(codec) {
|
|
var line = '';
|
|
var pt = codec.payloadType;
|
|
if (codec.preferredPayloadType !== undefined) {
|
|
pt = codec.preferredPayloadType;
|
|
}
|
|
if (codec.parameters && Object.keys(codec.parameters).length) {
|
|
var params = [];
|
|
Object.keys(codec.parameters).forEach(function(param) {
|
|
params.push(param + '=' + codec.parameters[param]);
|
|
});
|
|
line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
|
|
}
|
|
return line;
|
|
};
|
|
|
|
// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
|
|
// a=rtcp-fb:98 nack rpsi
|
|
SDPUtils.parseRtcpFb = function(line) {
|
|
var parts = line.substr(line.indexOf(' ') + 1).split(' ');
|
|
return {
|
|
type: parts.shift(),
|
|
parameter: parts.join(' ')
|
|
};
|
|
};
|
|
// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
|
|
SDPUtils.writeRtcpFb = function(codec) {
|
|
var lines = '';
|
|
var pt = codec.payloadType;
|
|
if (codec.preferredPayloadType !== undefined) {
|
|
pt = codec.preferredPayloadType;
|
|
}
|
|
if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
|
|
// FIXME: special handling for trr-int?
|
|
codec.rtcpFeedback.forEach(function(fb) {
|
|
lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
|
|
(fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
|
|
'\r\n';
|
|
});
|
|
}
|
|
return lines;
|
|
};
|
|
|
|
// Parses an RFC 5576 ssrc media attribute. Sample input:
|
|
// a=ssrc:3735928559 cname:something
|
|
SDPUtils.parseSsrcMedia = function(line) {
|
|
var sp = line.indexOf(' ');
|
|
var parts = {
|
|
ssrc: parseInt(line.substr(7, sp - 7), 10)
|
|
};
|
|
var colon = line.indexOf(':', sp);
|
|
if (colon > -1) {
|
|
parts.attribute = line.substr(sp + 1, colon - sp - 1);
|
|
parts.value = line.substr(colon + 1);
|
|
} else {
|
|
parts.attribute = line.substr(sp + 1);
|
|
}
|
|
return parts;
|
|
};
|
|
|
|
// Extracts DTLS parameters from SDP media section or sessionpart.
|
|
// FIXME: for consistency with other functions this should only
|
|
// get the fingerprint line as input. See also getIceParameters.
|
|
SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
|
|
var lines = SDPUtils.splitLines(mediaSection);
|
|
// Search in session part, too.
|
|
lines = lines.concat(SDPUtils.splitLines(sessionpart));
|
|
var fpLine = lines.filter(function(line) {
|
|
return line.indexOf('a=fingerprint:') === 0;
|
|
})[0].substr(14);
|
|
// Note: a=setup line is ignored since we use the 'auto' role.
|
|
var dtlsParameters = {
|
|
role: 'auto',
|
|
fingerprints: [{
|
|
algorithm: fpLine.split(' ')[0],
|
|
value: fpLine.split(' ')[1]
|
|
}]
|
|
};
|
|
return dtlsParameters;
|
|
};
|
|
|
|
// Serializes DTLS parameters to SDP.
|
|
SDPUtils.writeDtlsParameters = function(params, setupType) {
|
|
var sdp = 'a=setup:' + setupType + '\r\n';
|
|
params.fingerprints.forEach(function(fp) {
|
|
sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
|
|
});
|
|
return sdp;
|
|
};
|
|
// Parses ICE information from SDP media section or sessionpart.
|
|
// FIXME: for consistency with other functions this should only
|
|
// get the ice-ufrag and ice-pwd lines as input.
|
|
SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
|
|
var lines = SDPUtils.splitLines(mediaSection);
|
|
// Search in session part, too.
|
|
lines = lines.concat(SDPUtils.splitLines(sessionpart));
|
|
var iceParameters = {
|
|
usernameFragment: lines.filter(function(line) {
|
|
return line.indexOf('a=ice-ufrag:') === 0;
|
|
})[0].substr(12),
|
|
password: lines.filter(function(line) {
|
|
return line.indexOf('a=ice-pwd:') === 0;
|
|
})[0].substr(10)
|
|
};
|
|
return iceParameters;
|
|
};
|
|
|
|
// Serializes ICE parameters to SDP.
|
|
SDPUtils.writeIceParameters = function(params) {
|
|
return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
|
|
'a=ice-pwd:' + params.password + '\r\n';
|
|
};
|
|
|
|
// Parses the SDP media section and returns RTCRtpParameters.
|
|
SDPUtils.parseRtpParameters = function(mediaSection) {
|
|
var description = {
|
|
codecs: [],
|
|
headerExtensions: [],
|
|
fecMechanisms: [],
|
|
rtcp: []
|
|
};
|
|
var lines = SDPUtils.splitLines(mediaSection);
|
|
var mline = lines[0].split(' ');
|
|
for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
|
|
var pt = mline[i];
|
|
var rtpmapline = SDPUtils.matchPrefix(
|
|
mediaSection, 'a=rtpmap:' + pt + ' ')[0];
|
|
if (rtpmapline) {
|
|
var codec = SDPUtils.parseRtpMap(rtpmapline);
|
|
var fmtps = SDPUtils.matchPrefix(
|
|
mediaSection, 'a=fmtp:' + pt + ' ');
|
|
// Only the first a=fmtp:<pt> is considered.
|
|
codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
|
|
codec.rtcpFeedback = SDPUtils.matchPrefix(
|
|
mediaSection, 'a=rtcp-fb:' + pt + ' ')
|
|
.map(SDPUtils.parseRtcpFb);
|
|
description.codecs.push(codec);
|
|
// parse FEC mechanisms from rtpmap lines.
|
|
switch (codec.name.toUpperCase()) {
|
|
case 'RED':
|
|
case 'ULPFEC':
|
|
description.fecMechanisms.push(codec.name.toUpperCase());
|
|
break;
|
|
default: // only RED and ULPFEC are recognized as FEC mechanisms.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
|
|
description.headerExtensions.push(SDPUtils.parseExtmap(line));
|
|
});
|
|
// FIXME: parse rtcp.
|
|
return description;
|
|
};
|
|
|
|
// Generates parts of the SDP media section describing the capabilities /
|
|
// parameters.
|
|
SDPUtils.writeRtpDescription = function(kind, caps) {
|
|
var sdp = '';
|
|
|
|
// Build the mline.
|
|
sdp += 'm=' + kind + ' ';
|
|
sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
|
|
sdp += ' UDP/TLS/RTP/SAVPF ';
|
|
sdp += caps.codecs.map(function(codec) {
|
|
if (codec.preferredPayloadType !== undefined) {
|
|
return codec.preferredPayloadType;
|
|
}
|
|
return codec.payloadType;
|
|
}).join(' ') + '\r\n';
|
|
|
|
sdp += 'c=IN IP4 0.0.0.0\r\n';
|
|
sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';
|
|
|
|
// Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
|
|
caps.codecs.forEach(function(codec) {
|
|
sdp += SDPUtils.writeRtpMap(codec);
|
|
sdp += SDPUtils.writeFmtp(codec);
|
|
sdp += SDPUtils.writeRtcpFb(codec);
|
|
});
|
|
var maxptime = 0;
|
|
caps.codecs.forEach(function(codec) {
|
|
if (codec.maxptime > maxptime) {
|
|
maxptime = codec.maxptime;
|
|
}
|
|
});
|
|
if (maxptime > 0) {
|
|
sdp += 'a=maxptime:' + maxptime + '\r\n';
|
|
}
|
|
sdp += 'a=rtcp-mux\r\n';
|
|
|
|
caps.headerExtensions.forEach(function(extension) {
|
|
sdp += SDPUtils.writeExtmap(extension);
|
|
});
|
|
// FIXME: write fecMechanisms.
|
|
return sdp;
|
|
};
|
|
|
|
// Parses the SDP media section and returns an array of
|
|
// RTCRtpEncodingParameters.
|
|
SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
|
|
var encodingParameters = [];
|
|
var description = SDPUtils.parseRtpParameters(mediaSection);
|
|
var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
|
|
var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;
|
|
|
|
// filter a=ssrc:... cname:, ignore PlanB-msid
|
|
var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
|
|
.map(function(line) {
|
|
return SDPUtils.parseSsrcMedia(line);
|
|
})
|
|
.filter(function(parts) {
|
|
return parts.attribute === 'cname';
|
|
});
|
|
var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
|
|
var secondarySsrc;
|
|
|
|
var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
|
|
.map(function(line) {
|
|
var parts = line.split(' ');
|
|
parts.shift();
|
|
return parts.map(function(part) {
|
|
return parseInt(part, 10);
|
|
});
|
|
});
|
|
if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
|
|
secondarySsrc = flows[0][1];
|
|
}
|
|
|
|
description.codecs.forEach(function(codec) {
|
|
if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
|
|
var encParam = {
|
|
ssrc: primarySsrc,
|
|
codecPayloadType: parseInt(codec.parameters.apt, 10),
|
|
rtx: {
|
|
ssrc: secondarySsrc
|
|
}
|
|
};
|
|
encodingParameters.push(encParam);
|
|
if (hasRed) {
|
|
encParam = JSON.parse(JSON.stringify(encParam));
|
|
encParam.fec = {
|
|
ssrc: secondarySsrc,
|
|
mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
|
|
};
|
|
encodingParameters.push(encParam);
|
|
}
|
|
}
|
|
});
|
|
if (encodingParameters.length === 0 && primarySsrc) {
|
|
encodingParameters.push({
|
|
ssrc: primarySsrc
|
|
});
|
|
}
|
|
|
|
// we support both b=AS and b=TIAS but interpret AS as TIAS.
|
|
var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
|
|
if (bandwidth.length) {
|
|
if (bandwidth[0].indexOf('b=TIAS:') === 0) {
|
|
bandwidth = parseInt(bandwidth[0].substr(7), 10);
|
|
} else if (bandwidth[0].indexOf('b=AS:') === 0) {
|
|
bandwidth = parseInt(bandwidth[0].substr(5), 10);
|
|
}
|
|
encodingParameters.forEach(function(params) {
|
|
params.maxBitrate = bandwidth;
|
|
});
|
|
}
|
|
return encodingParameters;
|
|
};
|
|
|
|
SDPUtils.writeSessionBoilerplate = function() {
|
|
// FIXME: sess-id should be an NTP timestamp.
|
|
return 'v=0\r\n' +
|
|
'o=thisisadapterortc 8169639915646943137 2 IN IP4 127.0.0.1\r\n' +
|
|
's=-\r\n' +
|
|
't=0 0\r\n';
|
|
};
|
|
|
|
SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
|
|
var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
|
|
|
|
// Map ICE parameters (ufrag, pwd) to SDP.
|
|
sdp += SDPUtils.writeIceParameters(
|
|
transceiver.iceGatherer.getLocalParameters());
|
|
|
|
// Map DTLS parameters to SDP.
|
|
sdp += SDPUtils.writeDtlsParameters(
|
|
transceiver.dtlsTransport.getLocalParameters(),
|
|
type === 'offer' ? 'actpass' : 'active');
|
|
|
|
sdp += 'a=mid:' + transceiver.mid + '\r\n';
|
|
|
|
if (transceiver.rtpSender && transceiver.rtpReceiver) {
|
|
sdp += 'a=sendrecv\r\n';
|
|
} else if (transceiver.rtpSender) {
|
|
sdp += 'a=sendonly\r\n';
|
|
} else if (transceiver.rtpReceiver) {
|
|
sdp += 'a=recvonly\r\n';
|
|
} else {
|
|
sdp += 'a=inactive\r\n';
|
|
}
|
|
|
|
// FIXME: for RTX there might be multiple SSRCs. Not implemented in Edge yet.
|
|
if (transceiver.rtpSender) {
|
|
var msid = 'msid:' + stream.id + ' ' +
|
|
transceiver.rtpSender.track.id + '\r\n';
|
|
sdp += 'a=' + msid;
|
|
sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
|
|
' ' + msid;
|
|
if (transceiver.sendEncodingParameters[0].rtx) {
|
|
sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
|
|
' ' + msid;
|
|
sdp += 'a=ssrc-group:FID ' +
|
|
transceiver.sendEncodingParameters[0].ssrc + ' ' +
|
|
transceiver.sendEncodingParameters[0].rtx.ssrc +
|
|
'\r\n';
|
|
}
|
|
}
|
|
// FIXME: this should be written by writeRtpDescription.
|
|
sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
|
|
' cname:' + SDPUtils.localCName + '\r\n';
|
|
if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
|
|
sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
|
|
' cname:' + SDPUtils.localCName + '\r\n';
|
|
}
|
|
return sdp;
|
|
};
|
|
|
|
// Gets the direction from the mediaSection or the sessionpart.
|
|
SDPUtils.getDirection = function(mediaSection, sessionpart) {
|
|
// Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
|
|
var lines = SDPUtils.splitLines(mediaSection);
|
|
for (var i = 0; i < lines.length; i++) {
|
|
switch (lines[i]) {
|
|
case 'a=sendrecv':
|
|
case 'a=sendonly':
|
|
case 'a=recvonly':
|
|
case 'a=inactive':
|
|
return lines[i].substr(2);
|
|
default:
|
|
// FIXME: What should happen here?
|
|
}
|
|
}
|
|
if (sessionpart) {
|
|
return SDPUtils.getDirection(sessionpart);
|
|
}
|
|
return 'sendrecv';
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = SDPUtils;
|
|
|
|
},{}],63:[function(require,module,exports){
|
|
|
|
module.exports = require('./lib/');
|
|
|
|
},{"./lib/":64}],64:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var url = require('./url');
|
|
var parser = require('socket.io-parser');
|
|
var Manager = require('./manager');
|
|
var debug = require('debug')('socket.io-client');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = exports = lookup;
|
|
|
|
/**
|
|
* Managers cache.
|
|
*/
|
|
|
|
var cache = exports.managers = {};
|
|
|
|
/**
|
|
* Looks up an existing `Manager` for multiplexing.
|
|
* If the user summons:
|
|
*
|
|
* `io('http://localhost/a');`
|
|
* `io('http://localhost/b');`
|
|
*
|
|
* We reuse the existing instance based on same scheme/port/host,
|
|
* and we initialize sockets for each namespace.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function lookup(uri, opts) {
|
|
if (typeof uri == 'object') {
|
|
opts = uri;
|
|
uri = undefined;
|
|
}
|
|
|
|
opts = opts || {};
|
|
|
|
var parsed = url(uri);
|
|
var source = parsed.source;
|
|
var id = parsed.id;
|
|
var io;
|
|
|
|
if (opts.forceNew || opts['force new connection'] || false === opts.multiplex) {
|
|
debug('ignoring socket cache for %s', source);
|
|
io = Manager(source, opts);
|
|
} else {
|
|
if (!cache[id]) {
|
|
debug('new io instance for %s', source);
|
|
cache[id] = Manager(source, opts);
|
|
}
|
|
io = cache[id];
|
|
}
|
|
|
|
return io.socket(parsed.path);
|
|
}
|
|
|
|
/**
|
|
* Protocol version.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.protocol = parser.protocol;
|
|
|
|
/**
|
|
* `connect`.
|
|
*
|
|
* @param {String} uri
|
|
* @api public
|
|
*/
|
|
|
|
exports.connect = lookup;
|
|
|
|
/**
|
|
* Expose constructors for standalone build.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.Manager = require('./manager');
|
|
exports.Socket = require('./socket');
|
|
|
|
},{"./manager":65,"./socket":67,"./url":68,"debug":69,"socket.io-parser":71}],65:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var url = require('./url');
|
|
var eio = require('engine.io-client');
|
|
var Socket = require('./socket');
|
|
var Emitter = require('component-emitter');
|
|
var parser = require('socket.io-parser');
|
|
var on = require('./on');
|
|
var bind = require('component-bind');
|
|
var object = require('object-component');
|
|
var debug = require('debug')('socket.io-client:manager');
|
|
var indexOf = require('indexof');
|
|
var Backoff = require('backo2');
|
|
|
|
/**
|
|
* Module exports
|
|
*/
|
|
|
|
module.exports = Manager;
|
|
|
|
/**
|
|
* `Manager` constructor.
|
|
*
|
|
* @param {String} engine instance or engine uri/opts
|
|
* @param {Object} options
|
|
* @api public
|
|
*/
|
|
|
|
function Manager(uri, opts){
|
|
if (!(this instanceof Manager)) return new Manager(uri, opts);
|
|
if (uri && ('object' == typeof uri)) {
|
|
opts = uri;
|
|
uri = undefined;
|
|
}
|
|
opts = opts || {};
|
|
|
|
opts.path = opts.path || '/socket.io';
|
|
this.nsps = {};
|
|
this.subs = [];
|
|
this.opts = opts;
|
|
this.reconnection(opts.reconnection !== false);
|
|
this.reconnectionAttempts(opts.reconnectionAttempts || Infinity);
|
|
this.reconnectionDelay(opts.reconnectionDelay || 1000);
|
|
this.reconnectionDelayMax(opts.reconnectionDelayMax || 5000);
|
|
this.randomizationFactor(opts.randomizationFactor || 0.5);
|
|
this.backoff = new Backoff({
|
|
min: this.reconnectionDelay(),
|
|
max: this.reconnectionDelayMax(),
|
|
jitter: this.randomizationFactor()
|
|
});
|
|
this.timeout(null == opts.timeout ? 20000 : opts.timeout);
|
|
this.readyState = 'closed';
|
|
this.uri = uri;
|
|
this.connected = [];
|
|
this.encoding = false;
|
|
this.packetBuffer = [];
|
|
this.encoder = new parser.Encoder();
|
|
this.decoder = new parser.Decoder();
|
|
this.autoConnect = opts.autoConnect !== false;
|
|
if (this.autoConnect) this.open();
|
|
}
|
|
|
|
/**
|
|
* Propagate given event to sockets and emit on `this`
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.emitAll = function() {
|
|
this.emit.apply(this, arguments);
|
|
for (var nsp in this.nsps) {
|
|
this.nsps[nsp].emit.apply(this.nsps[nsp], arguments);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update `socket.id` of all sockets
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.updateSocketIds = function(){
|
|
for (var nsp in this.nsps) {
|
|
this.nsps[nsp].id = this.engine.id;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mix in `Emitter`.
|
|
*/
|
|
|
|
Emitter(Manager.prototype);
|
|
|
|
/**
|
|
* Sets the `reconnection` config.
|
|
*
|
|
* @param {Boolean} true/false if it should automatically reconnect
|
|
* @return {Manager} self or value
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.reconnection = function(v){
|
|
if (!arguments.length) return this._reconnection;
|
|
this._reconnection = !!v;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets the reconnection attempts config.
|
|
*
|
|
* @param {Number} max reconnection attempts before giving up
|
|
* @return {Manager} self or value
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.reconnectionAttempts = function(v){
|
|
if (!arguments.length) return this._reconnectionAttempts;
|
|
this._reconnectionAttempts = v;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets the delay between reconnections.
|
|
*
|
|
* @param {Number} delay
|
|
* @return {Manager} self or value
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.reconnectionDelay = function(v){
|
|
if (!arguments.length) return this._reconnectionDelay;
|
|
this._reconnectionDelay = v;
|
|
this.backoff && this.backoff.setMin(v);
|
|
return this;
|
|
};
|
|
|
|
Manager.prototype.randomizationFactor = function(v){
|
|
if (!arguments.length) return this._randomizationFactor;
|
|
this._randomizationFactor = v;
|
|
this.backoff && this.backoff.setJitter(v);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets the maximum delay between reconnections.
|
|
*
|
|
* @param {Number} delay
|
|
* @return {Manager} self or value
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.reconnectionDelayMax = function(v){
|
|
if (!arguments.length) return this._reconnectionDelayMax;
|
|
this._reconnectionDelayMax = v;
|
|
this.backoff && this.backoff.setMax(v);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sets the connection timeout. `false` to disable
|
|
*
|
|
* @return {Manager} self or value
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.timeout = function(v){
|
|
if (!arguments.length) return this._timeout;
|
|
this._timeout = v;
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Starts trying to reconnect if reconnection is enabled and we have not
|
|
* started reconnecting yet
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.maybeReconnectOnOpen = function() {
|
|
// Only try to reconnect if it's the first time we're connecting
|
|
if (!this.reconnecting && this._reconnection && this.backoff.attempts === 0) {
|
|
// keeps reconnection from firing twice for the same reconnection loop
|
|
this.reconnect();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the current transport `socket`.
|
|
*
|
|
* @param {Function} optional, callback
|
|
* @return {Manager} self
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.open =
|
|
Manager.prototype.connect = function(fn){
|
|
debug('readyState %s', this.readyState);
|
|
if (~this.readyState.indexOf('open')) return this;
|
|
|
|
debug('opening %s', this.uri);
|
|
this.engine = eio(this.uri, this.opts);
|
|
var socket = this.engine;
|
|
var self = this;
|
|
this.readyState = 'opening';
|
|
this.skipReconnect = false;
|
|
|
|
// emit `open`
|
|
var openSub = on(socket, 'open', function() {
|
|
self.onopen();
|
|
fn && fn();
|
|
});
|
|
|
|
// emit `connect_error`
|
|
var errorSub = on(socket, 'error', function(data){
|
|
debug('connect_error');
|
|
self.cleanup();
|
|
self.readyState = 'closed';
|
|
self.emitAll('connect_error', data);
|
|
if (fn) {
|
|
var err = new Error('Connection error');
|
|
err.data = data;
|
|
fn(err);
|
|
} else {
|
|
// Only do this if there is no fn to handle the error
|
|
self.maybeReconnectOnOpen();
|
|
}
|
|
});
|
|
|
|
// emit `connect_timeout`
|
|
if (false !== this._timeout) {
|
|
var timeout = this._timeout;
|
|
debug('connect attempt will timeout after %d', timeout);
|
|
|
|
// set timer
|
|
var timer = setTimeout(function(){
|
|
debug('connect attempt timed out after %d', timeout);
|
|
openSub.destroy();
|
|
socket.close();
|
|
socket.emit('error', 'timeout');
|
|
self.emitAll('connect_timeout', timeout);
|
|
}, timeout);
|
|
|
|
this.subs.push({
|
|
destroy: function(){
|
|
clearTimeout(timer);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.subs.push(openSub);
|
|
this.subs.push(errorSub);
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Called upon transport open.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.onopen = function(){
|
|
debug('open');
|
|
|
|
// clear old subs
|
|
this.cleanup();
|
|
|
|
// mark as open
|
|
this.readyState = 'open';
|
|
this.emit('open');
|
|
|
|
// add new subs
|
|
var socket = this.engine;
|
|
this.subs.push(on(socket, 'data', bind(this, 'ondata')));
|
|
this.subs.push(on(this.decoder, 'decoded', bind(this, 'ondecoded')));
|
|
this.subs.push(on(socket, 'error', bind(this, 'onerror')));
|
|
this.subs.push(on(socket, 'close', bind(this, 'onclose')));
|
|
};
|
|
|
|
/**
|
|
* Called with data.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.ondata = function(data){
|
|
this.decoder.add(data);
|
|
};
|
|
|
|
/**
|
|
* Called when parser fully decodes a packet.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.ondecoded = function(packet) {
|
|
this.emit('packet', packet);
|
|
};
|
|
|
|
/**
|
|
* Called upon socket error.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.onerror = function(err){
|
|
debug('error', err);
|
|
this.emitAll('error', err);
|
|
};
|
|
|
|
/**
|
|
* Creates a new socket for the given `nsp`.
|
|
*
|
|
* @return {Socket}
|
|
* @api public
|
|
*/
|
|
|
|
Manager.prototype.socket = function(nsp){
|
|
var socket = this.nsps[nsp];
|
|
if (!socket) {
|
|
socket = new Socket(this, nsp);
|
|
this.nsps[nsp] = socket;
|
|
var self = this;
|
|
socket.on('connect', function(){
|
|
socket.id = self.engine.id;
|
|
if (!~indexOf(self.connected, socket)) {
|
|
self.connected.push(socket);
|
|
}
|
|
});
|
|
}
|
|
return socket;
|
|
};
|
|
|
|
/**
|
|
* Called upon a socket close.
|
|
*
|
|
* @param {Socket} socket
|
|
*/
|
|
|
|
Manager.prototype.destroy = function(socket){
|
|
var index = indexOf(this.connected, socket);
|
|
if (~index) this.connected.splice(index, 1);
|
|
if (this.connected.length) return;
|
|
|
|
this.close();
|
|
};
|
|
|
|
/**
|
|
* Writes a packet.
|
|
*
|
|
* @param {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.packet = function(packet){
|
|
debug('writing packet %j', packet);
|
|
var self = this;
|
|
|
|
if (!self.encoding) {
|
|
// encode, then write to engine with result
|
|
self.encoding = true;
|
|
this.encoder.encode(packet, function(encodedPackets) {
|
|
for (var i = 0; i < encodedPackets.length; i++) {
|
|
self.engine.write(encodedPackets[i]);
|
|
}
|
|
self.encoding = false;
|
|
self.processPacketQueue();
|
|
});
|
|
} else { // add packet to the queue
|
|
self.packetBuffer.push(packet);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* If packet buffer is non-empty, begins encoding the
|
|
* next packet in line.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.processPacketQueue = function() {
|
|
if (this.packetBuffer.length > 0 && !this.encoding) {
|
|
var pack = this.packetBuffer.shift();
|
|
this.packet(pack);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Clean up transport subscriptions and packet buffer.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.cleanup = function(){
|
|
var sub;
|
|
while (sub = this.subs.shift()) sub.destroy();
|
|
|
|
this.packetBuffer = [];
|
|
this.encoding = false;
|
|
|
|
this.decoder.destroy();
|
|
};
|
|
|
|
/**
|
|
* Close the current socket.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.close =
|
|
Manager.prototype.disconnect = function(){
|
|
this.skipReconnect = true;
|
|
this.backoff.reset();
|
|
this.readyState = 'closed';
|
|
this.engine && this.engine.close();
|
|
};
|
|
|
|
/**
|
|
* Called upon engine close.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.onclose = function(reason){
|
|
debug('close');
|
|
this.cleanup();
|
|
this.backoff.reset();
|
|
this.readyState = 'closed';
|
|
this.emit('close', reason);
|
|
if (this._reconnection && !this.skipReconnect) {
|
|
this.reconnect();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Attempt a reconnection.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.reconnect = function(){
|
|
if (this.reconnecting || this.skipReconnect) return this;
|
|
|
|
var self = this;
|
|
|
|
if (this.backoff.attempts >= this._reconnectionAttempts) {
|
|
debug('reconnect failed');
|
|
this.backoff.reset();
|
|
this.emitAll('reconnect_failed');
|
|
this.reconnecting = false;
|
|
} else {
|
|
var delay = this.backoff.duration();
|
|
debug('will wait %dms before reconnect attempt', delay);
|
|
|
|
this.reconnecting = true;
|
|
var timer = setTimeout(function(){
|
|
if (self.skipReconnect) return;
|
|
|
|
debug('attempting reconnect');
|
|
self.emitAll('reconnect_attempt', self.backoff.attempts);
|
|
self.emitAll('reconnecting', self.backoff.attempts);
|
|
|
|
// check again for the case socket closed in above events
|
|
if (self.skipReconnect) return;
|
|
|
|
self.open(function(err){
|
|
if (err) {
|
|
debug('reconnect attempt error');
|
|
self.reconnecting = false;
|
|
self.reconnect();
|
|
self.emitAll('reconnect_error', err.data);
|
|
} else {
|
|
debug('reconnect success');
|
|
self.onreconnect();
|
|
}
|
|
});
|
|
}, delay);
|
|
|
|
this.subs.push({
|
|
destroy: function(){
|
|
clearTimeout(timer);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon successful reconnect.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Manager.prototype.onreconnect = function(){
|
|
var attempt = this.backoff.attempts;
|
|
this.reconnecting = false;
|
|
this.backoff.reset();
|
|
this.updateSocketIds();
|
|
this.emitAll('reconnect', attempt);
|
|
};
|
|
|
|
},{"./on":66,"./socket":67,"./url":68,"backo2":4,"component-bind":7,"component-emitter":8,"debug":69,"engine.io-client":10,"indexof":44,"object-component":51,"socket.io-parser":71}],66:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = on;
|
|
|
|
/**
|
|
* Helper for subscriptions.
|
|
*
|
|
* @param {Object|EventEmitter} obj with `Emitter` mixin or `EventEmitter`
|
|
* @param {String} event name
|
|
* @param {Function} callback
|
|
* @api public
|
|
*/
|
|
|
|
function on(obj, ev, fn) {
|
|
obj.on(ev, fn);
|
|
return {
|
|
destroy: function(){
|
|
obj.removeListener(ev, fn);
|
|
}
|
|
};
|
|
}
|
|
|
|
},{}],67:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var parser = require('socket.io-parser');
|
|
var Emitter = require('component-emitter');
|
|
var toArray = require('to-array');
|
|
var on = require('./on');
|
|
var bind = require('component-bind');
|
|
var debug = require('debug')('socket.io-client:socket');
|
|
var hasBin = require('has-binary');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = exports = Socket;
|
|
|
|
/**
|
|
* Internal events (blacklisted).
|
|
* These events can't be emitted by the user.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
var events = {
|
|
connect: 1,
|
|
connect_error: 1,
|
|
connect_timeout: 1,
|
|
disconnect: 1,
|
|
error: 1,
|
|
reconnect: 1,
|
|
reconnect_attempt: 1,
|
|
reconnect_failed: 1,
|
|
reconnect_error: 1,
|
|
reconnecting: 1
|
|
};
|
|
|
|
/**
|
|
* Shortcut to `Emitter#emit`.
|
|
*/
|
|
|
|
var emit = Emitter.prototype.emit;
|
|
|
|
/**
|
|
* `Socket` constructor.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function Socket(io, nsp){
|
|
this.io = io;
|
|
this.nsp = nsp;
|
|
this.json = this; // compat
|
|
this.ids = 0;
|
|
this.acks = {};
|
|
if (this.io.autoConnect) this.open();
|
|
this.receiveBuffer = [];
|
|
this.sendBuffer = [];
|
|
this.connected = false;
|
|
this.disconnected = true;
|
|
}
|
|
|
|
/**
|
|
* Mix in `Emitter`.
|
|
*/
|
|
|
|
Emitter(Socket.prototype);
|
|
|
|
/**
|
|
* Subscribe to open, close and packet events
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.subEvents = function() {
|
|
if (this.subs) return;
|
|
|
|
var io = this.io;
|
|
this.subs = [
|
|
on(io, 'open', bind(this, 'onopen')),
|
|
on(io, 'packet', bind(this, 'onpacket')),
|
|
on(io, 'close', bind(this, 'onclose'))
|
|
];
|
|
};
|
|
|
|
/**
|
|
* "Opens" the socket.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.open =
|
|
Socket.prototype.connect = function(){
|
|
if (this.connected) return this;
|
|
|
|
this.subEvents();
|
|
this.io.open(); // ensure open
|
|
if ('open' == this.io.readyState) this.onopen();
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sends a `message` event.
|
|
*
|
|
* @return {Socket} self
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.send = function(){
|
|
var args = toArray(arguments);
|
|
args.unshift('message');
|
|
this.emit.apply(this, args);
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Override `emit`.
|
|
* If the event is in `events`, it's emitted normally.
|
|
*
|
|
* @param {String} event name
|
|
* @return {Socket} self
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.emit = function(ev){
|
|
if (events.hasOwnProperty(ev)) {
|
|
emit.apply(this, arguments);
|
|
return this;
|
|
}
|
|
|
|
var args = toArray(arguments);
|
|
var parserType = parser.EVENT; // default
|
|
if (hasBin(args)) { parserType = parser.BINARY_EVENT; } // binary
|
|
var packet = { type: parserType, data: args };
|
|
|
|
// event ack callback
|
|
if ('function' == typeof args[args.length - 1]) {
|
|
debug('emitting packet with ack id %d', this.ids);
|
|
this.acks[this.ids] = args.pop();
|
|
packet.id = this.ids++;
|
|
}
|
|
|
|
if (this.connected) {
|
|
this.packet(packet);
|
|
} else {
|
|
this.sendBuffer.push(packet);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Sends a packet.
|
|
*
|
|
* @param {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.packet = function(packet){
|
|
packet.nsp = this.nsp;
|
|
this.io.packet(packet);
|
|
};
|
|
|
|
/**
|
|
* Called upon engine `open`.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onopen = function(){
|
|
debug('transport is open - connecting');
|
|
|
|
// write connect packet if necessary
|
|
if ('/' != this.nsp) {
|
|
this.packet({ type: parser.CONNECT });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon engine `close`.
|
|
*
|
|
* @param {String} reason
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onclose = function(reason){
|
|
debug('close (%s)', reason);
|
|
this.connected = false;
|
|
this.disconnected = true;
|
|
delete this.id;
|
|
this.emit('disconnect', reason);
|
|
};
|
|
|
|
/**
|
|
* Called with socket packet.
|
|
*
|
|
* @param {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onpacket = function(packet){
|
|
if (packet.nsp != this.nsp) return;
|
|
|
|
switch (packet.type) {
|
|
case parser.CONNECT:
|
|
this.onconnect();
|
|
break;
|
|
|
|
case parser.EVENT:
|
|
this.onevent(packet);
|
|
break;
|
|
|
|
case parser.BINARY_EVENT:
|
|
this.onevent(packet);
|
|
break;
|
|
|
|
case parser.ACK:
|
|
this.onack(packet);
|
|
break;
|
|
|
|
case parser.BINARY_ACK:
|
|
this.onack(packet);
|
|
break;
|
|
|
|
case parser.DISCONNECT:
|
|
this.ondisconnect();
|
|
break;
|
|
|
|
case parser.ERROR:
|
|
this.emit('error', packet.data);
|
|
break;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Called upon a server event.
|
|
*
|
|
* @param {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onevent = function(packet){
|
|
var args = packet.data || [];
|
|
debug('emitting event %j', args);
|
|
|
|
if (null != packet.id) {
|
|
debug('attaching ack callback to event');
|
|
args.push(this.ack(packet.id));
|
|
}
|
|
|
|
if (this.connected) {
|
|
emit.apply(this, args);
|
|
} else {
|
|
this.receiveBuffer.push(args);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Produces an ack callback to emit with an event.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.ack = function(id){
|
|
var self = this;
|
|
var sent = false;
|
|
return function(){
|
|
// prevent double callbacks
|
|
if (sent) return;
|
|
sent = true;
|
|
var args = toArray(arguments);
|
|
debug('sending ack %j', args);
|
|
|
|
var type = hasBin(args) ? parser.BINARY_ACK : parser.ACK;
|
|
self.packet({
|
|
type: type,
|
|
id: id,
|
|
data: args
|
|
});
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Called upon a server acknowlegement.
|
|
*
|
|
* @param {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onack = function(packet){
|
|
debug('calling ack %s with %j', packet.id, packet.data);
|
|
var fn = this.acks[packet.id];
|
|
fn.apply(this, packet.data);
|
|
delete this.acks[packet.id];
|
|
};
|
|
|
|
/**
|
|
* Called upon server connect.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.onconnect = function(){
|
|
this.connected = true;
|
|
this.disconnected = false;
|
|
this.emit('connect');
|
|
this.emitBuffered();
|
|
};
|
|
|
|
/**
|
|
* Emit buffered events (received and emitted).
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.emitBuffered = function(){
|
|
var i;
|
|
for (i = 0; i < this.receiveBuffer.length; i++) {
|
|
emit.apply(this, this.receiveBuffer[i]);
|
|
}
|
|
this.receiveBuffer = [];
|
|
|
|
for (i = 0; i < this.sendBuffer.length; i++) {
|
|
this.packet(this.sendBuffer[i]);
|
|
}
|
|
this.sendBuffer = [];
|
|
};
|
|
|
|
/**
|
|
* Called upon server disconnect.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
Socket.prototype.ondisconnect = function(){
|
|
debug('server disconnect (%s)', this.nsp);
|
|
this.destroy();
|
|
this.onclose('io server disconnect');
|
|
};
|
|
|
|
/**
|
|
* Called upon forced client/server side disconnections,
|
|
* this method ensures the manager stops tracking us and
|
|
* that reconnections don't get triggered for this.
|
|
*
|
|
* @api private.
|
|
*/
|
|
|
|
Socket.prototype.destroy = function(){
|
|
if (this.subs) {
|
|
// clean subscriptions to avoid reconnections
|
|
for (var i = 0; i < this.subs.length; i++) {
|
|
this.subs[i].destroy();
|
|
}
|
|
this.subs = null;
|
|
}
|
|
|
|
this.io.destroy(this);
|
|
};
|
|
|
|
/**
|
|
* Disconnects the socket manually.
|
|
*
|
|
* @return {Socket} self
|
|
* @api public
|
|
*/
|
|
|
|
Socket.prototype.close =
|
|
Socket.prototype.disconnect = function(){
|
|
if (this.connected) {
|
|
debug('performing disconnect (%s)', this.nsp);
|
|
this.packet({ type: parser.DISCONNECT });
|
|
}
|
|
|
|
// remove socket from pool
|
|
this.destroy();
|
|
|
|
if (this.connected) {
|
|
// fire events
|
|
this.onclose('io client disconnect');
|
|
}
|
|
return this;
|
|
};
|
|
|
|
},{"./on":66,"component-bind":7,"component-emitter":8,"debug":69,"has-binary":41,"socket.io-parser":71,"to-array":75}],68:[function(require,module,exports){
|
|
(function (global){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var parseuri = require('parseuri');
|
|
var debug = require('debug')('socket.io-client:url');
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = url;
|
|
|
|
/**
|
|
* URL parser.
|
|
*
|
|
* @param {String} url
|
|
* @param {Object} An object meant to mimic window.location.
|
|
* Defaults to window.location.
|
|
* @api public
|
|
*/
|
|
|
|
function url(uri, loc){
|
|
var obj = uri;
|
|
|
|
// default to window.location
|
|
var loc = loc || global.location;
|
|
if (null == uri) uri = loc.protocol + '//' + loc.host;
|
|
|
|
// relative path support
|
|
if ('string' == typeof uri) {
|
|
if ('/' == uri.charAt(0)) {
|
|
if ('/' == uri.charAt(1)) {
|
|
uri = loc.protocol + uri;
|
|
} else {
|
|
uri = loc.hostname + uri;
|
|
}
|
|
}
|
|
|
|
if (!/^(https?|wss?):\/\//.test(uri)) {
|
|
debug('protocol-less url %s', uri);
|
|
if ('undefined' != typeof loc) {
|
|
uri = loc.protocol + '//' + uri;
|
|
} else {
|
|
uri = 'https://' + uri;
|
|
}
|
|
}
|
|
|
|
// parse
|
|
debug('parse %s', uri);
|
|
obj = parseuri(uri);
|
|
}
|
|
|
|
// make sure we treat `localhost:80` and `localhost` equally
|
|
if (!obj.port) {
|
|
if (/^(http|ws)$/.test(obj.protocol)) {
|
|
obj.port = '80';
|
|
}
|
|
else if (/^(http|ws)s$/.test(obj.protocol)) {
|
|
obj.port = '443';
|
|
}
|
|
}
|
|
|
|
obj.path = obj.path || '/';
|
|
|
|
// define unique id
|
|
obj.id = obj.protocol + '://' + obj.host + ':' + obj.port;
|
|
// define href
|
|
obj.href = obj.protocol + '://' + obj.host + (loc && loc.port == obj.port ? '' : (':' + obj.port));
|
|
|
|
return obj;
|
|
}
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"debug":69,"parseuri":54}],69:[function(require,module,exports){
|
|
|
|
/**
|
|
* Expose `debug()` as the module.
|
|
*/
|
|
|
|
module.exports = debug;
|
|
|
|
/**
|
|
* Create a debugger with the given `name`.
|
|
*
|
|
* @param {String} name
|
|
* @return {Type}
|
|
* @api public
|
|
*/
|
|
|
|
function debug(name) {
|
|
if (!debug.enabled(name)) return function(){};
|
|
|
|
return function(fmt){
|
|
fmt = coerce(fmt);
|
|
|
|
var curr = new Date;
|
|
var ms = curr - (debug[name] || curr);
|
|
debug[name] = curr;
|
|
|
|
fmt = name
|
|
+ ' '
|
|
+ fmt
|
|
+ ' +' + debug.humanize(ms);
|
|
|
|
// This hackery is required for IE8
|
|
// where `console.log` doesn't have 'apply'
|
|
window.console
|
|
&& console.log
|
|
&& Function.prototype.apply.call(console.log, console, arguments);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The currently active debug mode names.
|
|
*/
|
|
|
|
debug.names = [];
|
|
debug.skips = [];
|
|
|
|
/**
|
|
* Enables a debug mode by name. This can include modes
|
|
* separated by a colon and wildcards.
|
|
*
|
|
* @param {String} name
|
|
* @api public
|
|
*/
|
|
|
|
debug.enable = function(name) {
|
|
try {
|
|
localStorage.debug = name;
|
|
} catch(e){}
|
|
|
|
var split = (name || '').split(/[\s,]+/)
|
|
, len = split.length;
|
|
|
|
for (var i = 0; i < len; i++) {
|
|
name = split[i].replace('*', '.*?');
|
|
if (name[0] === '-') {
|
|
debug.skips.push(new RegExp('^' + name.substr(1) + '$'));
|
|
}
|
|
else {
|
|
debug.names.push(new RegExp('^' + name + '$'));
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Disable debug output.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
debug.disable = function(){
|
|
debug.enable('');
|
|
};
|
|
|
|
/**
|
|
* Humanize the given `ms`.
|
|
*
|
|
* @param {Number} m
|
|
* @return {String}
|
|
* @api private
|
|
*/
|
|
|
|
debug.humanize = function(ms) {
|
|
var sec = 1000
|
|
, min = 60 * 1000
|
|
, hour = 60 * min;
|
|
|
|
if (ms >= hour) return (ms / hour).toFixed(1) + 'h';
|
|
if (ms >= min) return (ms / min).toFixed(1) + 'm';
|
|
if (ms >= sec) return (ms / sec | 0) + 's';
|
|
return ms + 'ms';
|
|
};
|
|
|
|
/**
|
|
* Returns true if the given mode name is enabled, false otherwise.
|
|
*
|
|
* @param {String} name
|
|
* @return {Boolean}
|
|
* @api public
|
|
*/
|
|
|
|
debug.enabled = function(name) {
|
|
for (var i = 0, len = debug.skips.length; i < len; i++) {
|
|
if (debug.skips[i].test(name)) {
|
|
return false;
|
|
}
|
|
}
|
|
for (var i = 0, len = debug.names.length; i < len; i++) {
|
|
if (debug.names[i].test(name)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Coerce `val`.
|
|
*/
|
|
|
|
function coerce(val) {
|
|
if (val instanceof Error) return val.stack || val.message;
|
|
return val;
|
|
}
|
|
|
|
// persist
|
|
|
|
try {
|
|
if (window.localStorage) debug.enable(localStorage.debug);
|
|
} catch(e){}
|
|
|
|
},{}],70:[function(require,module,exports){
|
|
(function (global){
|
|
/*global Blob,File*/
|
|
|
|
/**
|
|
* Module requirements
|
|
*/
|
|
|
|
var isArray = require('isarray');
|
|
var isBuf = require('./is-buffer');
|
|
|
|
/**
|
|
* Replaces every Buffer | ArrayBuffer in packet with a numbered placeholder.
|
|
* Anything with blobs or files should be fed through removeBlobs before coming
|
|
* here.
|
|
*
|
|
* @param {Object} packet - socket.io event packet
|
|
* @return {Object} with deconstructed packet and list of buffers
|
|
* @api public
|
|
*/
|
|
|
|
exports.deconstructPacket = function(packet){
|
|
var buffers = [];
|
|
var packetData = packet.data;
|
|
|
|
function _deconstructPacket(data) {
|
|
if (!data) return data;
|
|
|
|
if (isBuf(data)) {
|
|
var placeholder = { _placeholder: true, num: buffers.length };
|
|
buffers.push(data);
|
|
return placeholder;
|
|
} else if (isArray(data)) {
|
|
var newData = new Array(data.length);
|
|
for (var i = 0; i < data.length; i++) {
|
|
newData[i] = _deconstructPacket(data[i]);
|
|
}
|
|
return newData;
|
|
} else if ('object' == typeof data && !(data instanceof Date)) {
|
|
var newData = {};
|
|
for (var key in data) {
|
|
newData[key] = _deconstructPacket(data[key]);
|
|
}
|
|
return newData;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
var pack = packet;
|
|
pack.data = _deconstructPacket(packetData);
|
|
pack.attachments = buffers.length; // number of binary 'attachments'
|
|
return {packet: pack, buffers: buffers};
|
|
};
|
|
|
|
/**
|
|
* Reconstructs a binary packet from its placeholder packet and buffers
|
|
*
|
|
* @param {Object} packet - event packet with placeholders
|
|
* @param {Array} buffers - binary buffers to put in placeholder positions
|
|
* @return {Object} reconstructed packet
|
|
* @api public
|
|
*/
|
|
|
|
exports.reconstructPacket = function(packet, buffers) {
|
|
var curPlaceHolder = 0;
|
|
|
|
function _reconstructPacket(data) {
|
|
if (data && data._placeholder) {
|
|
var buf = buffers[data.num]; // appropriate buffer (should be natural order anyway)
|
|
return buf;
|
|
} else if (isArray(data)) {
|
|
for (var i = 0; i < data.length; i++) {
|
|
data[i] = _reconstructPacket(data[i]);
|
|
}
|
|
return data;
|
|
} else if (data && 'object' == typeof data) {
|
|
for (var key in data) {
|
|
data[key] = _reconstructPacket(data[key]);
|
|
}
|
|
return data;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
packet.data = _reconstructPacket(packet.data);
|
|
packet.attachments = undefined; // no longer useful
|
|
return packet;
|
|
};
|
|
|
|
/**
|
|
* Asynchronously removes Blobs or Files from data via
|
|
* FileReader's readAsArrayBuffer method. Used before encoding
|
|
* data as msgpack. Calls callback with the blobless data.
|
|
*
|
|
* @param {Object} data
|
|
* @param {Function} callback
|
|
* @api private
|
|
*/
|
|
|
|
exports.removeBlobs = function(data, callback) {
|
|
function _removeBlobs(obj, curKey, containingObject) {
|
|
if (!obj) return obj;
|
|
|
|
// convert any blob
|
|
if ((global.Blob && obj instanceof Blob) ||
|
|
(global.File && obj instanceof File)) {
|
|
pendingBlobs++;
|
|
|
|
// async filereader
|
|
var fileReader = new FileReader();
|
|
fileReader.onload = function() { // this.result == arraybuffer
|
|
if (containingObject) {
|
|
containingObject[curKey] = this.result;
|
|
}
|
|
else {
|
|
bloblessData = this.result;
|
|
}
|
|
|
|
// if nothing pending its callback time
|
|
if(! --pendingBlobs) {
|
|
callback(bloblessData);
|
|
}
|
|
};
|
|
|
|
fileReader.readAsArrayBuffer(obj); // blob -> arraybuffer
|
|
} else if (isArray(obj)) { // handle array
|
|
for (var i = 0; i < obj.length; i++) {
|
|
_removeBlobs(obj[i], i, obj);
|
|
}
|
|
} else if (obj && 'object' == typeof obj && !isBuf(obj)) { // and object
|
|
for (var key in obj) {
|
|
_removeBlobs(obj[key], key, obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
var pendingBlobs = 0;
|
|
var bloblessData = data;
|
|
_removeBlobs(bloblessData);
|
|
if (!pendingBlobs) {
|
|
callback(bloblessData);
|
|
}
|
|
};
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./is-buffer":72,"isarray":74}],71:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var debug = require('debug')('socket.io-parser');
|
|
var json = require('json3');
|
|
var isArray = require('isarray');
|
|
var Emitter = require('component-emitter');
|
|
var binary = require('./binary');
|
|
var isBuf = require('./is-buffer');
|
|
|
|
/**
|
|
* Protocol version.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.protocol = 4;
|
|
|
|
/**
|
|
* Packet types.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.types = [
|
|
'CONNECT',
|
|
'DISCONNECT',
|
|
'EVENT',
|
|
'BINARY_EVENT',
|
|
'ACK',
|
|
'BINARY_ACK',
|
|
'ERROR'
|
|
];
|
|
|
|
/**
|
|
* Packet type `connect`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.CONNECT = 0;
|
|
|
|
/**
|
|
* Packet type `disconnect`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.DISCONNECT = 1;
|
|
|
|
/**
|
|
* Packet type `event`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.EVENT = 2;
|
|
|
|
/**
|
|
* Packet type `ack`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.ACK = 3;
|
|
|
|
/**
|
|
* Packet type `error`.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.ERROR = 4;
|
|
|
|
/**
|
|
* Packet type 'binary event'
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.BINARY_EVENT = 5;
|
|
|
|
/**
|
|
* Packet type `binary ack`. For acks with binary arguments.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.BINARY_ACK = 6;
|
|
|
|
/**
|
|
* Encoder constructor.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.Encoder = Encoder;
|
|
|
|
/**
|
|
* Decoder constructor.
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
exports.Decoder = Decoder;
|
|
|
|
/**
|
|
* A socket.io Encoder instance
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
function Encoder() {}
|
|
|
|
/**
|
|
* Encode a packet as a single string if non-binary, or as a
|
|
* buffer sequence, depending on packet type.
|
|
*
|
|
* @param {Object} obj - packet object
|
|
* @param {Function} callback - function to handle encodings (likely engine.write)
|
|
* @return Calls callback with Array of encodings
|
|
* @api public
|
|
*/
|
|
|
|
Encoder.prototype.encode = function(obj, callback){
|
|
debug('encoding packet %j', obj);
|
|
|
|
if (exports.BINARY_EVENT == obj.type || exports.BINARY_ACK == obj.type) {
|
|
encodeAsBinary(obj, callback);
|
|
}
|
|
else {
|
|
var encoding = encodeAsString(obj);
|
|
callback([encoding]);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Encode packet as string.
|
|
*
|
|
* @param {Object} packet
|
|
* @return {String} encoded
|
|
* @api private
|
|
*/
|
|
|
|
function encodeAsString(obj) {
|
|
var str = '';
|
|
var nsp = false;
|
|
|
|
// first is type
|
|
str += obj.type;
|
|
|
|
// attachments if we have them
|
|
if (exports.BINARY_EVENT == obj.type || exports.BINARY_ACK == obj.type) {
|
|
str += obj.attachments;
|
|
str += '-';
|
|
}
|
|
|
|
// if we have a namespace other than `/`
|
|
// we append it followed by a comma `,`
|
|
if (obj.nsp && '/' != obj.nsp) {
|
|
nsp = true;
|
|
str += obj.nsp;
|
|
}
|
|
|
|
// immediately followed by the id
|
|
if (null != obj.id) {
|
|
if (nsp) {
|
|
str += ',';
|
|
nsp = false;
|
|
}
|
|
str += obj.id;
|
|
}
|
|
|
|
// json data
|
|
if (null != obj.data) {
|
|
if (nsp) str += ',';
|
|
str += json.stringify(obj.data);
|
|
}
|
|
|
|
debug('encoded %j as %s', obj, str);
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* Encode packet as 'buffer sequence' by removing blobs, and
|
|
* deconstructing packet into object with placeholders and
|
|
* a list of buffers.
|
|
*
|
|
* @param {Object} packet
|
|
* @return {Buffer} encoded
|
|
* @api private
|
|
*/
|
|
|
|
function encodeAsBinary(obj, callback) {
|
|
|
|
function writeEncoding(bloblessData) {
|
|
var deconstruction = binary.deconstructPacket(bloblessData);
|
|
var pack = encodeAsString(deconstruction.packet);
|
|
var buffers = deconstruction.buffers;
|
|
|
|
buffers.unshift(pack); // add packet info to beginning of data list
|
|
callback(buffers); // write all the buffers
|
|
}
|
|
|
|
binary.removeBlobs(obj, writeEncoding);
|
|
}
|
|
|
|
/**
|
|
* A socket.io Decoder instance
|
|
*
|
|
* @return {Object} decoder
|
|
* @api public
|
|
*/
|
|
|
|
function Decoder() {
|
|
this.reconstructor = null;
|
|
}
|
|
|
|
/**
|
|
* Mix in `Emitter` with Decoder.
|
|
*/
|
|
|
|
Emitter(Decoder.prototype);
|
|
|
|
/**
|
|
* Decodes an ecoded packet string into packet JSON.
|
|
*
|
|
* @param {String} obj - encoded packet
|
|
* @return {Object} packet
|
|
* @api public
|
|
*/
|
|
|
|
Decoder.prototype.add = function(obj) {
|
|
var packet;
|
|
if ('string' == typeof obj) {
|
|
packet = decodeString(obj);
|
|
if (exports.BINARY_EVENT == packet.type || exports.BINARY_ACK == packet.type) { // binary packet's json
|
|
this.reconstructor = new BinaryReconstructor(packet);
|
|
|
|
// no attachments, labeled binary but no binary data to follow
|
|
if (this.reconstructor.reconPack.attachments === 0) {
|
|
this.emit('decoded', packet);
|
|
}
|
|
} else { // non-binary full packet
|
|
this.emit('decoded', packet);
|
|
}
|
|
}
|
|
else if (isBuf(obj) || obj.base64) { // raw binary data
|
|
if (!this.reconstructor) {
|
|
throw new Error('got binary data when not reconstructing a packet');
|
|
} else {
|
|
packet = this.reconstructor.takeBinaryData(obj);
|
|
if (packet) { // received final buffer
|
|
this.reconstructor = null;
|
|
this.emit('decoded', packet);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
throw new Error('Unknown type: ' + obj);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Decode a packet String (JSON data)
|
|
*
|
|
* @param {String} str
|
|
* @return {Object} packet
|
|
* @api private
|
|
*/
|
|
|
|
function decodeString(str) {
|
|
var p = {};
|
|
var i = 0;
|
|
|
|
// look up type
|
|
p.type = Number(str.charAt(0));
|
|
if (null == exports.types[p.type]) return error();
|
|
|
|
// look up attachments if type binary
|
|
if (exports.BINARY_EVENT == p.type || exports.BINARY_ACK == p.type) {
|
|
var buf = '';
|
|
while (str.charAt(++i) != '-') {
|
|
buf += str.charAt(i);
|
|
if (i == str.length) break;
|
|
}
|
|
if (buf != Number(buf) || str.charAt(i) != '-') {
|
|
throw new Error('Illegal attachments');
|
|
}
|
|
p.attachments = Number(buf);
|
|
}
|
|
|
|
// look up namespace (if any)
|
|
if ('/' == str.charAt(i + 1)) {
|
|
p.nsp = '';
|
|
while (++i) {
|
|
var c = str.charAt(i);
|
|
if (',' == c) break;
|
|
p.nsp += c;
|
|
if (i == str.length) break;
|
|
}
|
|
} else {
|
|
p.nsp = '/';
|
|
}
|
|
|
|
// look up id
|
|
var next = str.charAt(i + 1);
|
|
if ('' !== next && Number(next) == next) {
|
|
p.id = '';
|
|
while (++i) {
|
|
var c = str.charAt(i);
|
|
if (null == c || Number(c) != c) {
|
|
--i;
|
|
break;
|
|
}
|
|
p.id += str.charAt(i);
|
|
if (i == str.length) break;
|
|
}
|
|
p.id = Number(p.id);
|
|
}
|
|
|
|
// look up json data
|
|
if (str.charAt(++i)) {
|
|
try {
|
|
p.data = json.parse(str.substr(i));
|
|
} catch(e){
|
|
return error();
|
|
}
|
|
}
|
|
|
|
debug('decoded %s as %j', str, p);
|
|
return p;
|
|
}
|
|
|
|
/**
|
|
* Deallocates a parser's resources
|
|
*
|
|
* @api public
|
|
*/
|
|
|
|
Decoder.prototype.destroy = function() {
|
|
if (this.reconstructor) {
|
|
this.reconstructor.finishedReconstruction();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A manager of a binary event's 'buffer sequence'. Should
|
|
* be constructed whenever a packet of type BINARY_EVENT is
|
|
* decoded.
|
|
*
|
|
* @param {Object} packet
|
|
* @return {BinaryReconstructor} initialized reconstructor
|
|
* @api private
|
|
*/
|
|
|
|
function BinaryReconstructor(packet) {
|
|
this.reconPack = packet;
|
|
this.buffers = [];
|
|
}
|
|
|
|
/**
|
|
* Method to be called when binary data received from connection
|
|
* after a BINARY_EVENT packet.
|
|
*
|
|
* @param {Buffer | ArrayBuffer} binData - the raw binary data received
|
|
* @return {null | Object} returns null if more binary data is expected or
|
|
* a reconstructed packet object if all buffers have been received.
|
|
* @api private
|
|
*/
|
|
|
|
BinaryReconstructor.prototype.takeBinaryData = function(binData) {
|
|
this.buffers.push(binData);
|
|
if (this.buffers.length == this.reconPack.attachments) { // done with buffer list
|
|
var packet = binary.reconstructPacket(this.reconPack, this.buffers);
|
|
this.finishedReconstruction();
|
|
return packet;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Cleans up binary packet reconstruction variables.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
BinaryReconstructor.prototype.finishedReconstruction = function() {
|
|
this.reconPack = null;
|
|
this.buffers = [];
|
|
};
|
|
|
|
function error(data){
|
|
return {
|
|
type: exports.ERROR,
|
|
data: 'parser error'
|
|
};
|
|
}
|
|
|
|
},{"./binary":70,"./is-buffer":72,"component-emitter":8,"debug":73,"isarray":74,"json3":45}],72:[function(require,module,exports){
|
|
(function (global){
|
|
|
|
module.exports = isBuf;
|
|
|
|
/**
|
|
* Returns true if obj is a buffer or an arraybuffer.
|
|
*
|
|
* @api private
|
|
*/
|
|
|
|
function isBuf(obj) {
|
|
return (global.Buffer && global.Buffer.isBuffer(obj)) ||
|
|
(global.ArrayBuffer && obj instanceof ArrayBuffer);
|
|
}
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{}],73:[function(require,module,exports){
|
|
arguments[4][69][0].apply(exports,arguments)
|
|
},{"dup":69}],74:[function(require,module,exports){
|
|
arguments[4][42][0].apply(exports,arguments)
|
|
},{"dup":42}],75:[function(require,module,exports){
|
|
module.exports = toArray
|
|
|
|
function toArray(list, index) {
|
|
var array = []
|
|
|
|
index = index || 0
|
|
|
|
for (var i = index || 0; i < list.length; i++) {
|
|
array[i - index] = list[i]
|
|
}
|
|
|
|
return array
|
|
}
|
|
|
|
},{}],76:[function(require,module,exports){
|
|
// based on https://github.com/ESTOS/strophe.jingle/
|
|
// adds wildemitter support
|
|
var util = require('util');
|
|
var adapter = require('webrtc-adapter'); // jshint ignore:line
|
|
var WildEmitter = require('wildemitter');
|
|
|
|
function dumpSDP(description) {
|
|
return {
|
|
type: description.type,
|
|
sdp: description.sdp
|
|
};
|
|
}
|
|
|
|
function dumpStream(stream) {
|
|
var info = {
|
|
label: stream.id,
|
|
};
|
|
if (stream.getAudioTracks().length) {
|
|
info.audio = stream.getAudioTracks().map(function (track) {
|
|
return track.id;
|
|
});
|
|
}
|
|
if (stream.getVideoTracks().length) {
|
|
info.video = stream.getVideoTracks().map(function (track) {
|
|
return track.id;
|
|
});
|
|
}
|
|
return info;
|
|
}
|
|
|
|
function TraceablePeerConnection(config, constraints) {
|
|
var self = this;
|
|
WildEmitter.call(this);
|
|
|
|
this.peerconnection = new window.RTCPeerConnection(config, constraints);
|
|
|
|
this.trace = function (what, info) {
|
|
self.emit('PeerConnectionTrace', {
|
|
time: new Date(),
|
|
type: what,
|
|
value: info || ""
|
|
});
|
|
};
|
|
|
|
this.onicecandidate = null;
|
|
this.peerconnection.onicecandidate = function (event) {
|
|
self.trace('onicecandidate', event.candidate);
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(event);
|
|
}
|
|
};
|
|
this.onaddstream = null;
|
|
this.peerconnection.onaddstream = function (event) {
|
|
self.trace('onaddstream', dumpStream(event.stream));
|
|
if (self.onaddstream !== null) {
|
|
self.onaddstream(event);
|
|
}
|
|
};
|
|
this.onremovestream = null;
|
|
this.peerconnection.onremovestream = function (event) {
|
|
self.trace('onremovestream', dumpStream(event.stream));
|
|
if (self.onremovestream !== null) {
|
|
self.onremovestream(event);
|
|
}
|
|
};
|
|
this.onsignalingstatechange = null;
|
|
this.peerconnection.onsignalingstatechange = function (event) {
|
|
self.trace('onsignalingstatechange', self.signalingState);
|
|
if (self.onsignalingstatechange !== null) {
|
|
self.onsignalingstatechange(event);
|
|
}
|
|
};
|
|
this.oniceconnectionstatechange = null;
|
|
this.peerconnection.oniceconnectionstatechange = function (event) {
|
|
self.trace('oniceconnectionstatechange', self.iceConnectionState);
|
|
if (self.oniceconnectionstatechange !== null) {
|
|
self.oniceconnectionstatechange(event);
|
|
}
|
|
};
|
|
this.onnegotiationneeded = null;
|
|
this.peerconnection.onnegotiationneeded = function (event) {
|
|
self.trace('onnegotiationneeded');
|
|
if (self.onnegotiationneeded !== null) {
|
|
self.onnegotiationneeded(event);
|
|
}
|
|
};
|
|
self.ondatachannel = null;
|
|
this.peerconnection.ondatachannel = function (event) {
|
|
self.trace('ondatachannel', event);
|
|
if (self.ondatachannel !== null) {
|
|
self.ondatachannel(event);
|
|
}
|
|
};
|
|
this.getLocalStreams = this.peerconnection.getLocalStreams.bind(this.peerconnection);
|
|
this.getRemoteStreams = this.peerconnection.getRemoteStreams.bind(this.peerconnection);
|
|
}
|
|
|
|
util.inherits(TraceablePeerConnection, WildEmitter);
|
|
|
|
['signalingState', 'iceConnectionState', 'localDescription', 'remoteDescription'].forEach(function (prop) {
|
|
Object.defineProperty(TraceablePeerConnection.prototype, prop, {
|
|
get: function () {
|
|
return this.peerconnection[prop];
|
|
}
|
|
});
|
|
});
|
|
|
|
TraceablePeerConnection.prototype.addStream = function (stream) {
|
|
this.trace('addStream', dumpStream(stream));
|
|
this.peerconnection.addStream(stream);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.removeStream = function (stream) {
|
|
this.trace('removeStream', dumpStream(stream));
|
|
this.peerconnection.removeStream(stream);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.createDataChannel = function (label, opts) {
|
|
this.trace('createDataChannel', label, opts);
|
|
return this.peerconnection.createDataChannel(label, opts);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.setLocalDescription = function (description, successCallback, failureCallback) {
|
|
var self = this;
|
|
this.trace('setLocalDescription', dumpSDP(description));
|
|
return this.peerconnection.setLocalDescription(
|
|
description
|
|
).then(
|
|
function () {
|
|
self.trace('setLocalDescriptionOnSuccess');
|
|
if (successCallback) successCallback();
|
|
},
|
|
function (err) {
|
|
self.trace('setLocalDescriptionOnFailure', err);
|
|
if (failureCallback) failureCallback(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.setRemoteDescription = function (description, successCallback, failureCallback) {
|
|
var self = this;
|
|
this.trace('setRemoteDescription', dumpSDP(description));
|
|
return this.peerconnection.setRemoteDescription(
|
|
description
|
|
).then(
|
|
function () {
|
|
self.trace('setRemoteDescriptionOnSuccess');
|
|
if (successCallback) successCallback();
|
|
},
|
|
function (err) {
|
|
self.trace('setRemoteDescriptionOnFailure', err);
|
|
if (failureCallback) failureCallback(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.close = function () {
|
|
this.trace('stopXXX');
|
|
console.trace('Stop Trace');
|
|
if (this.peerconnection.signalingState != 'closed') {
|
|
this.peerconnection.close();
|
|
}
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.createOffer = function (successCallback, failureCallback, constraints) {
|
|
var self = this;
|
|
this.trace('createOffer', constraints);
|
|
return this.peerconnection.createOffer(
|
|
constraints
|
|
).then(
|
|
function (offer) {
|
|
self.trace('createOfferOnSuccess', dumpSDP(offer));
|
|
if (successCallback) successCallback(offer);
|
|
},
|
|
function (err) {
|
|
self.trace('createOfferOnFailure', err);
|
|
if (failureCallback) failureCallback(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.createAnswer = function (successCallback, failureCallback, constraints) {
|
|
var self = this;
|
|
this.trace('createAnswer', constraints);
|
|
return this.peerconnection.createAnswer(
|
|
constraints
|
|
).then(
|
|
function (answer) {
|
|
self.trace('createAnswerOnSuccess', dumpSDP(answer));
|
|
if (successCallback) successCallback(answer);
|
|
},
|
|
function (err) {
|
|
self.trace('createAnswerOnFailure', err);
|
|
if (failureCallback) failureCallback(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.addIceCandidate = function (candidate, successCallback, failureCallback) {
|
|
var self = this;
|
|
this.trace('addIceCandidate', candidate);
|
|
return this.peerconnection.addIceCandidate(
|
|
candidate
|
|
).then(
|
|
function () {
|
|
//self.trace('addIceCandidateOnSuccess');
|
|
if (successCallback) successCallback();
|
|
},
|
|
function (err) {
|
|
self.trace('addIceCandidateOnFailure', err);
|
|
if (failureCallback) failureCallback(err);
|
|
}
|
|
);
|
|
};
|
|
|
|
TraceablePeerConnection.prototype.getStats = function () {
|
|
this.peerconnection.getStats.apply(this.peerconnection, arguments);
|
|
};
|
|
|
|
module.exports = TraceablePeerConnection;
|
|
|
|
},{"util":89,"webrtc-adapter":77,"wildemitter":100}],77:[function(require,module,exports){
|
|
arguments[4][30][0].apply(exports,arguments)
|
|
},{"./chrome/chrome_shim":78,"./edge/edge_shim":80,"./firefox/firefox_shim":82,"./safari/safari_shim":84,"./utils":85,"dup":30}],78:[function(require,module,exports){
|
|
arguments[4][31][0].apply(exports,arguments)
|
|
},{"../utils.js":85,"./getusermedia":79,"dup":31}],79:[function(require,module,exports){
|
|
arguments[4][32][0].apply(exports,arguments)
|
|
},{"../utils.js":85,"dup":32}],80:[function(require,module,exports){
|
|
arguments[4][33][0].apply(exports,arguments)
|
|
},{"../utils":85,"./getusermedia":81,"dup":33,"sdp":62}],81:[function(require,module,exports){
|
|
arguments[4][34][0].apply(exports,arguments)
|
|
},{"dup":34}],82:[function(require,module,exports){
|
|
arguments[4][35][0].apply(exports,arguments)
|
|
},{"../utils":85,"./getusermedia":83,"dup":35}],83:[function(require,module,exports){
|
|
arguments[4][36][0].apply(exports,arguments)
|
|
},{"../utils":85,"dup":36}],84:[function(require,module,exports){
|
|
arguments[4][37][0].apply(exports,arguments)
|
|
},{"dup":37}],85:[function(require,module,exports){
|
|
arguments[4][38][0].apply(exports,arguments)
|
|
},{"dup":38}],86:[function(require,module,exports){
|
|
(function (global){
|
|
/*! https://mths.be/utf8js v2.0.0 by @mathias */
|
|
;(function(root) {
|
|
|
|
// Detect free variables `exports`
|
|
var freeExports = typeof exports == 'object' && exports;
|
|
|
|
// Detect free variable `module`
|
|
var freeModule = typeof module == 'object' && module &&
|
|
module.exports == freeExports && module;
|
|
|
|
// Detect free variable `global`, from Node.js or Browserified code,
|
|
// and use it as `root`
|
|
var freeGlobal = typeof global == 'object' && global;
|
|
if (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal) {
|
|
root = freeGlobal;
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
var stringFromCharCode = String.fromCharCode;
|
|
|
|
// Taken from https://mths.be/punycode
|
|
function ucs2decode(string) {
|
|
var output = [];
|
|
var counter = 0;
|
|
var length = string.length;
|
|
var value;
|
|
var extra;
|
|
while (counter < length) {
|
|
value = string.charCodeAt(counter++);
|
|
if (value >= 0xD800 && value <= 0xDBFF && counter < length) {
|
|
// high surrogate, and there is a next character
|
|
extra = string.charCodeAt(counter++);
|
|
if ((extra & 0xFC00) == 0xDC00) { // low surrogate
|
|
output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000);
|
|
} else {
|
|
// unmatched surrogate; only append this code unit, in case the next
|
|
// code unit is the high surrogate of a surrogate pair
|
|
output.push(value);
|
|
counter--;
|
|
}
|
|
} else {
|
|
output.push(value);
|
|
}
|
|
}
|
|
return output;
|
|
}
|
|
|
|
// Taken from https://mths.be/punycode
|
|
function ucs2encode(array) {
|
|
var length = array.length;
|
|
var index = -1;
|
|
var value;
|
|
var output = '';
|
|
while (++index < length) {
|
|
value = array[index];
|
|
if (value > 0xFFFF) {
|
|
value -= 0x10000;
|
|
output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800);
|
|
value = 0xDC00 | value & 0x3FF;
|
|
}
|
|
output += stringFromCharCode(value);
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function checkScalarValue(codePoint) {
|
|
if (codePoint >= 0xD800 && codePoint <= 0xDFFF) {
|
|
throw Error(
|
|
'Lone surrogate U+' + codePoint.toString(16).toUpperCase() +
|
|
' is not a scalar value'
|
|
);
|
|
}
|
|
}
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
function createByte(codePoint, shift) {
|
|
return stringFromCharCode(((codePoint >> shift) & 0x3F) | 0x80);
|
|
}
|
|
|
|
function encodeCodePoint(codePoint) {
|
|
if ((codePoint & 0xFFFFFF80) == 0) { // 1-byte sequence
|
|
return stringFromCharCode(codePoint);
|
|
}
|
|
var symbol = '';
|
|
if ((codePoint & 0xFFFFF800) == 0) { // 2-byte sequence
|
|
symbol = stringFromCharCode(((codePoint >> 6) & 0x1F) | 0xC0);
|
|
}
|
|
else if ((codePoint & 0xFFFF0000) == 0) { // 3-byte sequence
|
|
checkScalarValue(codePoint);
|
|
symbol = stringFromCharCode(((codePoint >> 12) & 0x0F) | 0xE0);
|
|
symbol += createByte(codePoint, 6);
|
|
}
|
|
else if ((codePoint & 0xFFE00000) == 0) { // 4-byte sequence
|
|
symbol = stringFromCharCode(((codePoint >> 18) & 0x07) | 0xF0);
|
|
symbol += createByte(codePoint, 12);
|
|
symbol += createByte(codePoint, 6);
|
|
}
|
|
symbol += stringFromCharCode((codePoint & 0x3F) | 0x80);
|
|
return symbol;
|
|
}
|
|
|
|
function utf8encode(string) {
|
|
var codePoints = ucs2decode(string);
|
|
var length = codePoints.length;
|
|
var index = -1;
|
|
var codePoint;
|
|
var byteString = '';
|
|
while (++index < length) {
|
|
codePoint = codePoints[index];
|
|
byteString += encodeCodePoint(codePoint);
|
|
}
|
|
return byteString;
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
function readContinuationByte() {
|
|
if (byteIndex >= byteCount) {
|
|
throw Error('Invalid byte index');
|
|
}
|
|
|
|
var continuationByte = byteArray[byteIndex] & 0xFF;
|
|
byteIndex++;
|
|
|
|
if ((continuationByte & 0xC0) == 0x80) {
|
|
return continuationByte & 0x3F;
|
|
}
|
|
|
|
// If we end up here, it’s not a continuation byte
|
|
throw Error('Invalid continuation byte');
|
|
}
|
|
|
|
function decodeSymbol() {
|
|
var byte1;
|
|
var byte2;
|
|
var byte3;
|
|
var byte4;
|
|
var codePoint;
|
|
|
|
if (byteIndex > byteCount) {
|
|
throw Error('Invalid byte index');
|
|
}
|
|
|
|
if (byteIndex == byteCount) {
|
|
return false;
|
|
}
|
|
|
|
// Read first byte
|
|
byte1 = byteArray[byteIndex] & 0xFF;
|
|
byteIndex++;
|
|
|
|
// 1-byte sequence (no continuation bytes)
|
|
if ((byte1 & 0x80) == 0) {
|
|
return byte1;
|
|
}
|
|
|
|
// 2-byte sequence
|
|
if ((byte1 & 0xE0) == 0xC0) {
|
|
var byte2 = readContinuationByte();
|
|
codePoint = ((byte1 & 0x1F) << 6) | byte2;
|
|
if (codePoint >= 0x80) {
|
|
return codePoint;
|
|
} else {
|
|
throw Error('Invalid continuation byte');
|
|
}
|
|
}
|
|
|
|
// 3-byte sequence (may include unpaired surrogates)
|
|
if ((byte1 & 0xF0) == 0xE0) {
|
|
byte2 = readContinuationByte();
|
|
byte3 = readContinuationByte();
|
|
codePoint = ((byte1 & 0x0F) << 12) | (byte2 << 6) | byte3;
|
|
if (codePoint >= 0x0800) {
|
|
checkScalarValue(codePoint);
|
|
return codePoint;
|
|
} else {
|
|
throw Error('Invalid continuation byte');
|
|
}
|
|
}
|
|
|
|
// 4-byte sequence
|
|
if ((byte1 & 0xF8) == 0xF0) {
|
|
byte2 = readContinuationByte();
|
|
byte3 = readContinuationByte();
|
|
byte4 = readContinuationByte();
|
|
codePoint = ((byte1 & 0x0F) << 0x12) | (byte2 << 0x0C) |
|
|
(byte3 << 0x06) | byte4;
|
|
if (codePoint >= 0x010000 && codePoint <= 0x10FFFF) {
|
|
return codePoint;
|
|
}
|
|
}
|
|
|
|
throw Error('Invalid UTF-8 detected');
|
|
}
|
|
|
|
var byteArray;
|
|
var byteCount;
|
|
var byteIndex;
|
|
function utf8decode(byteString) {
|
|
byteArray = ucs2decode(byteString);
|
|
byteCount = byteArray.length;
|
|
byteIndex = 0;
|
|
var codePoints = [];
|
|
var tmp;
|
|
while ((tmp = decodeSymbol()) !== false) {
|
|
codePoints.push(tmp);
|
|
}
|
|
return ucs2encode(codePoints);
|
|
}
|
|
|
|
/*--------------------------------------------------------------------------*/
|
|
|
|
var utf8 = {
|
|
'version': '2.0.0',
|
|
'encode': utf8encode,
|
|
'decode': utf8decode
|
|
};
|
|
|
|
// Some AMD build optimizers, like r.js, check for specific condition patterns
|
|
// like the following:
|
|
if (
|
|
typeof define == 'function' &&
|
|
typeof define.amd == 'object' &&
|
|
define.amd
|
|
) {
|
|
define(function() {
|
|
return utf8;
|
|
});
|
|
} else if (freeExports && !freeExports.nodeType) {
|
|
if (freeModule) { // in Node.js or RingoJS v0.8.0+
|
|
freeModule.exports = utf8;
|
|
} else { // in Narwhal or RingoJS v0.7.0-
|
|
var object = {};
|
|
var hasOwnProperty = object.hasOwnProperty;
|
|
for (var key in utf8) {
|
|
hasOwnProperty.call(utf8, key) && (freeExports[key] = utf8[key]);
|
|
}
|
|
}
|
|
} else { // in Rhino or a web browser
|
|
root.utf8 = utf8;
|
|
}
|
|
|
|
}(this));
|
|
|
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{}],87:[function(require,module,exports){
|
|
if (typeof Object.create === 'function') {
|
|
// implementation from standard node.js 'util' module
|
|
module.exports = function inherits(ctor, superCtor) {
|
|
ctor.super_ = superCtor
|
|
ctor.prototype = Object.create(superCtor.prototype, {
|
|
constructor: {
|
|
value: ctor,
|
|
enumerable: false,
|
|
writable: true,
|
|
configurable: true
|
|
}
|
|
});
|
|
};
|
|
} else {
|
|
// old school shim for old browsers
|
|
module.exports = function inherits(ctor, superCtor) {
|
|
ctor.super_ = superCtor
|
|
var TempCtor = function () {}
|
|
TempCtor.prototype = superCtor.prototype
|
|
ctor.prototype = new TempCtor()
|
|
ctor.prototype.constructor = ctor
|
|
}
|
|
}
|
|
|
|
},{}],88:[function(require,module,exports){
|
|
module.exports = function isBuffer(arg) {
|
|
return arg && typeof arg === 'object'
|
|
&& typeof arg.copy === 'function'
|
|
&& typeof arg.fill === 'function'
|
|
&& typeof arg.readUInt8 === 'function';
|
|
}
|
|
},{}],89:[function(require,module,exports){
|
|
(function (process,global){
|
|
// Copyright Joyent, Inc. and other Node contributors.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a
|
|
// copy of this software and associated documentation files (the
|
|
// "Software"), to deal in the Software without restriction, including
|
|
// without limitation the rights to use, copy, modify, merge, publish,
|
|
// distribute, sublicense, and/or sell copies of the Software, and to permit
|
|
// persons to whom the Software is furnished to do so, subject to the
|
|
// following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included
|
|
// in all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
|
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
|
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
// USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
var formatRegExp = /%[sdj%]/g;
|
|
exports.format = function(f) {
|
|
if (!isString(f)) {
|
|
var objects = [];
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
objects.push(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':
|
|
try {
|
|
return JSON.stringify(args[i++]);
|
|
} catch (_) {
|
|
return '[Circular]';
|
|
}
|
|
default:
|
|
return x;
|
|
}
|
|
});
|
|
for (var x = args[i]; i < len; x = args[++i]) {
|
|
if (isNull(x) || !isObject(x)) {
|
|
str += ' ' + x;
|
|
} else {
|
|
str += ' ' + inspect(x);
|
|
}
|
|
}
|
|
return str;
|
|
};
|
|
|
|
|
|
// Mark that a method should not be used.
|
|
// Returns a modified function which warns once by default.
|
|
// If --no-deprecation is set, then it is a no-op.
|
|
exports.deprecate = function(fn, msg) {
|
|
// Allow for deprecating things in the process of starting up.
|
|
if (isUndefined(global.process)) {
|
|
return function() {
|
|
return exports.deprecate(fn, msg).apply(this, arguments);
|
|
};
|
|
}
|
|
|
|
if (process.noDeprecation === true) {
|
|
return fn;
|
|
}
|
|
|
|
var warned = false;
|
|
function deprecated() {
|
|
if (!warned) {
|
|
if (process.throwDeprecation) {
|
|
throw new Error(msg);
|
|
} else if (process.traceDeprecation) {
|
|
console.trace(msg);
|
|
} else {
|
|
console.error(msg);
|
|
}
|
|
warned = true;
|
|
}
|
|
return fn.apply(this, arguments);
|
|
}
|
|
|
|
return deprecated;
|
|
};
|
|
|
|
|
|
var debugs = {};
|
|
var debugEnviron;
|
|
exports.debuglog = function(set) {
|
|
if (isUndefined(debugEnviron))
|
|
debugEnviron = process.env.NODE_DEBUG || '';
|
|
set = set.toUpperCase();
|
|
if (!debugs[set]) {
|
|
if (new RegExp('\\b' + set + '\\b', 'i').test(debugEnviron)) {
|
|
var pid = process.pid;
|
|
debugs[set] = function() {
|
|
var msg = exports.format.apply(exports, arguments);
|
|
console.error('%s %d: %s', set, pid, msg);
|
|
};
|
|
} else {
|
|
debugs[set] = function() {};
|
|
}
|
|
}
|
|
return debugs[set];
|
|
};
|
|
|
|
|
|
/**
|
|
* Echos the value of a value. Trys to print the value out
|
|
* in the best way possible given the different types.
|
|
*
|
|
* @param {Object} obj The object to print out.
|
|
* @param {Object} opts Optional options object that alters the output.
|
|
*/
|
|
/* legacy: obj, showHidden, depth, colors*/
|
|
function inspect(obj, opts) {
|
|
// default options
|
|
var ctx = {
|
|
seen: [],
|
|
stylize: stylizeNoColor
|
|
};
|
|
// legacy...
|
|
if (arguments.length >= 3) ctx.depth = arguments[2];
|
|
if (arguments.length >= 4) ctx.colors = arguments[3];
|
|
if (isBoolean(opts)) {
|
|
// legacy...
|
|
ctx.showHidden = opts;
|
|
} else if (opts) {
|
|
// got an "options" object
|
|
exports._extend(ctx, opts);
|
|
}
|
|
// set default options
|
|
if (isUndefined(ctx.showHidden)) ctx.showHidden = false;
|
|
if (isUndefined(ctx.depth)) ctx.depth = 2;
|
|
if (isUndefined(ctx.colors)) ctx.colors = false;
|
|
if (isUndefined(ctx.customInspect)) ctx.customInspect = true;
|
|
if (ctx.colors) ctx.stylize = stylizeWithColor;
|
|
return formatValue(ctx, obj, ctx.depth);
|
|
}
|
|
exports.inspect = inspect;
|
|
|
|
|
|
// http://en.wikipedia.org/wiki/ANSI_escape_code#graphics
|
|
inspect.colors = {
|
|
'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]
|
|
};
|
|
|
|
// Don't use 'blue' not visible on cmd.exe
|
|
inspect.styles = {
|
|
'special': 'cyan',
|
|
'number': 'yellow',
|
|
'boolean': 'yellow',
|
|
'undefined': 'grey',
|
|
'null': 'bold',
|
|
'string': 'green',
|
|
'date': 'magenta',
|
|
// "name": intentionally not styling
|
|
'regexp': 'red'
|
|
};
|
|
|
|
|
|
function stylizeWithColor(str, styleType) {
|
|
var style = inspect.styles[styleType];
|
|
|
|
if (style) {
|
|
return '\u001b[' + inspect.colors[style][0] + 'm' + str +
|
|
'\u001b[' + inspect.colors[style][1] + 'm';
|
|
} else {
|
|
return str;
|
|
}
|
|
}
|
|
|
|
|
|
function stylizeNoColor(str, styleType) {
|
|
return str;
|
|
}
|
|
|
|
|
|
function arrayToHash(array) {
|
|
var hash = {};
|
|
|
|
array.forEach(function(val, idx) {
|
|
hash[val] = true;
|
|
});
|
|
|
|
return hash;
|
|
}
|
|
|
|
|
|
function formatValue(ctx, value, recurseTimes) {
|
|
// Provide a hook for user-specified inspect functions.
|
|
// Check that value is an object with an inspect function on it
|
|
if (ctx.customInspect &&
|
|
value &&
|
|
isFunction(value.inspect) &&
|
|
// Filter out the util module, it's inspect function is special
|
|
value.inspect !== exports.inspect &&
|
|
// Also filter out any prototype objects using the circular check.
|
|
!(value.constructor && value.constructor.prototype === value)) {
|
|
var ret = value.inspect(recurseTimes, ctx);
|
|
if (!isString(ret)) {
|
|
ret = formatValue(ctx, ret, recurseTimes);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
// Primitive types cannot have properties
|
|
var primitive = formatPrimitive(ctx, value);
|
|
if (primitive) {
|
|
return primitive;
|
|
}
|
|
|
|
// Look up the keys of the object.
|
|
var keys = Object.keys(value);
|
|
var visibleKeys = arrayToHash(keys);
|
|
|
|
if (ctx.showHidden) {
|
|
keys = Object.getOwnPropertyNames(value);
|
|
}
|
|
|
|
// IE doesn't make error fields non-enumerable
|
|
// http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
|
|
if (isError(value)
|
|
&& (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
|
|
return formatError(value);
|
|
}
|
|
|
|
// Some type of object without properties can be shortcutted.
|
|
if (keys.length === 0) {
|
|
if (isFunction(value)) {
|
|
var name = value.name ? ': ' + value.name : '';
|
|
return ctx.stylize('[Function' + name + ']', 'special');
|
|
}
|
|
if (isRegExp(value)) {
|
|
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
|
|
}
|
|
if (isDate(value)) {
|
|
return ctx.stylize(Date.prototype.toString.call(value), 'date');
|
|
}
|
|
if (isError(value)) {
|
|
return formatError(value);
|
|
}
|
|
}
|
|
|
|
var base = '', array = false, braces = ['{', '}'];
|
|
|
|
// Make Array say that they are Array
|
|
if (isArray(value)) {
|
|
array = true;
|
|
braces = ['[', ']'];
|
|
}
|
|
|
|
// Make functions say that they are functions
|
|
if (isFunction(value)) {
|
|
var n = value.name ? ': ' + value.name : '';
|
|
base = ' [Function' + n + ']';
|
|
}
|
|
|
|
// Make RegExps say that they are RegExps
|
|
if (isRegExp(value)) {
|
|
base = ' ' + RegExp.prototype.toString.call(value);
|
|
}
|
|
|
|
// Make dates with properties first say the date
|
|
if (isDate(value)) {
|
|
base = ' ' + Date.prototype.toUTCString.call(value);
|
|
}
|
|
|
|
// Make error with message first say the error
|
|
if (isError(value)) {
|
|
base = ' ' + formatError(value);
|
|
}
|
|
|
|
if (keys.length === 0 && (!array || value.length == 0)) {
|
|
return braces[0] + base + braces[1];
|
|
}
|
|
|
|
if (recurseTimes < 0) {
|
|
if (isRegExp(value)) {
|
|
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
|
|
} else {
|
|
return ctx.stylize('[Object]', 'special');
|
|
}
|
|
}
|
|
|
|
ctx.seen.push(value);
|
|
|
|
var output;
|
|
if (array) {
|
|
output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
|
|
} else {
|
|
output = keys.map(function(key) {
|
|
return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
|
|
});
|
|
}
|
|
|
|
ctx.seen.pop();
|
|
|
|
return reduceToSingleString(output, base, braces);
|
|
}
|
|
|
|
|
|
function formatPrimitive(ctx, value) {
|
|
if (isUndefined(value))
|
|
return ctx.stylize('undefined', 'undefined');
|
|
if (isString(value)) {
|
|
var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
|
|
.replace(/'/g, "\\'")
|
|
.replace(/\\"/g, '"') + '\'';
|
|
return ctx.stylize(simple, 'string');
|
|
}
|
|
if (isNumber(value))
|
|
return ctx.stylize('' + value, 'number');
|
|
if (isBoolean(value))
|
|
return ctx.stylize('' + value, 'boolean');
|
|
// For some reason typeof null is "object", so special case here.
|
|
if (isNull(value))
|
|
return ctx.stylize('null', 'null');
|
|
}
|
|
|
|
|
|
function formatError(value) {
|
|
return '[' + Error.prototype.toString.call(value) + ']';
|
|
}
|
|
|
|
|
|
function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
|
|
var output = [];
|
|
for (var i = 0, l = value.length; i < l; ++i) {
|
|
if (hasOwnProperty(value, String(i))) {
|
|
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
|
|
String(i), true));
|
|
} else {
|
|
output.push('');
|
|
}
|
|
}
|
|
keys.forEach(function(key) {
|
|
if (!key.match(/^\d+$/)) {
|
|
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
|
|
key, true));
|
|
}
|
|
});
|
|
return output;
|
|
}
|
|
|
|
|
|
function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
|
|
var name, str, desc;
|
|
desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
|
|
if (desc.get) {
|
|
if (desc.set) {
|
|
str = ctx.stylize('[Getter/Setter]', 'special');
|
|
} else {
|
|
str = ctx.stylize('[Getter]', 'special');
|
|
}
|
|
} else {
|
|
if (desc.set) {
|
|
str = ctx.stylize('[Setter]', 'special');
|
|
}
|
|
}
|
|
if (!hasOwnProperty(visibleKeys, key)) {
|
|
name = '[' + key + ']';
|
|
}
|
|
if (!str) {
|
|
if (ctx.seen.indexOf(desc.value) < 0) {
|
|
if (isNull(recurseTimes)) {
|
|
str = formatValue(ctx, desc.value, null);
|
|
} else {
|
|
str = formatValue(ctx, desc.value, recurseTimes - 1);
|
|
}
|
|
if (str.indexOf('\n') > -1) {
|
|
if (array) {
|
|
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 = ctx.stylize('[Circular]', 'special');
|
|
}
|
|
}
|
|
if (isUndefined(name)) {
|
|
if (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 = ctx.stylize(name, 'name');
|
|
} else {
|
|
name = name.replace(/'/g, "\\'")
|
|
.replace(/\\"/g, '"')
|
|
.replace(/(^"|"$)/g, "'");
|
|
name = ctx.stylize(name, 'string');
|
|
}
|
|
}
|
|
|
|
return name + ': ' + str;
|
|
}
|
|
|
|
|
|
function reduceToSingleString(output, base, braces) {
|
|
var numLinesEst = 0;
|
|
var length = output.reduce(function(prev, cur) {
|
|
numLinesEst++;
|
|
if (cur.indexOf('\n') >= 0) numLinesEst++;
|
|
return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
|
|
}, 0);
|
|
|
|
if (length > 60) {
|
|
return braces[0] +
|
|
(base === '' ? '' : base + '\n ') +
|
|
' ' +
|
|
output.join(',\n ') +
|
|
' ' +
|
|
braces[1];
|
|
}
|
|
|
|
return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
|
|
}
|
|
|
|
|
|
// NOTE: These type checking functions intentionally don't use `instanceof`
|
|
// because it is fragile and can be easily faked with `Object.create()`.
|
|
function isArray(ar) {
|
|
return Array.isArray(ar);
|
|
}
|
|
exports.isArray = isArray;
|
|
|
|
function isBoolean(arg) {
|
|
return typeof arg === 'boolean';
|
|
}
|
|
exports.isBoolean = isBoolean;
|
|
|
|
function isNull(arg) {
|
|
return arg === null;
|
|
}
|
|
exports.isNull = isNull;
|
|
|
|
function isNullOrUndefined(arg) {
|
|
return arg == null;
|
|
}
|
|
exports.isNullOrUndefined = isNullOrUndefined;
|
|
|
|
function isNumber(arg) {
|
|
return typeof arg === 'number';
|
|
}
|
|
exports.isNumber = isNumber;
|
|
|
|
function isString(arg) {
|
|
return typeof arg === 'string';
|
|
}
|
|
exports.isString = isString;
|
|
|
|
function isSymbol(arg) {
|
|
return typeof arg === 'symbol';
|
|
}
|
|
exports.isSymbol = isSymbol;
|
|
|
|
function isUndefined(arg) {
|
|
return arg === void 0;
|
|
}
|
|
exports.isUndefined = isUndefined;
|
|
|
|
function isRegExp(re) {
|
|
return isObject(re) && objectToString(re) === '[object RegExp]';
|
|
}
|
|
exports.isRegExp = isRegExp;
|
|
|
|
function isObject(arg) {
|
|
return typeof arg === 'object' && arg !== null;
|
|
}
|
|
exports.isObject = isObject;
|
|
|
|
function isDate(d) {
|
|
return isObject(d) && objectToString(d) === '[object Date]';
|
|
}
|
|
exports.isDate = isDate;
|
|
|
|
function isError(e) {
|
|
return isObject(e) &&
|
|
(objectToString(e) === '[object Error]' || e instanceof Error);
|
|
}
|
|
exports.isError = isError;
|
|
|
|
function isFunction(arg) {
|
|
return typeof arg === 'function';
|
|
}
|
|
exports.isFunction = isFunction;
|
|
|
|
function isPrimitive(arg) {
|
|
return arg === null ||
|
|
typeof arg === 'boolean' ||
|
|
typeof arg === 'number' ||
|
|
typeof arg === 'string' ||
|
|
typeof arg === 'symbol' || // ES6 symbol
|
|
typeof arg === 'undefined';
|
|
}
|
|
exports.isPrimitive = isPrimitive;
|
|
|
|
exports.isBuffer = require('./support/isBuffer');
|
|
|
|
function objectToString(o) {
|
|
return Object.prototype.toString.call(o);
|
|
}
|
|
|
|
|
|
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(' ');
|
|
}
|
|
|
|
|
|
// log is just a thin wrapper to console.log that prepends a timestamp
|
|
exports.log = function() {
|
|
console.log('%s - %s', timestamp(), exports.format.apply(exports, arguments));
|
|
};
|
|
|
|
|
|
/**
|
|
* Inherit the prototype methods from one constructor into another.
|
|
*
|
|
* The Function.prototype.inherits from lang.js rewritten as a standalone
|
|
* function (not on Function.prototype). NOTE: If this file is to be loaded
|
|
* during bootstrapping this function needs to be rewritten using some native
|
|
* functions as prototype setup using normal JavaScript does not work as
|
|
* expected during bootstrapping (see mirror.js in r114903).
|
|
*
|
|
* @param {function} ctor Constructor function which needs to inherit the
|
|
* prototype.
|
|
* @param {function} superCtor Constructor function to inherit prototype from.
|
|
*/
|
|
exports.inherits = require('inherits');
|
|
|
|
exports._extend = function(origin, add) {
|
|
// Don't do anything if add isn't an object
|
|
if (!add || !isObject(add)) return origin;
|
|
|
|
var keys = Object.keys(add);
|
|
var i = keys.length;
|
|
while (i--) {
|
|
origin[keys[i]] = add[keys[i]];
|
|
}
|
|
return origin;
|
|
};
|
|
|
|
function hasOwnProperty(obj, prop) {
|
|
return Object.prototype.hasOwnProperty.call(obj, prop);
|
|
}
|
|
|
|
}).call(this,require('_process'),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
|
},{"./support/isBuffer":88,"_process":55,"inherits":87}],90:[function(require,module,exports){
|
|
arguments[4][30][0].apply(exports,arguments)
|
|
},{"./chrome/chrome_shim":91,"./edge/edge_shim":93,"./firefox/firefox_shim":95,"./safari/safari_shim":97,"./utils":98,"dup":30}],91:[function(require,module,exports){
|
|
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
var logging = require('../utils.js').log;
|
|
var browserDetails = require('../utils.js').browserDetails;
|
|
|
|
var chromeShim = {
|
|
shimMediaStream: function() {
|
|
window.MediaStream = window.MediaStream || window.webkitMediaStream;
|
|
},
|
|
|
|
shimOnTrack: function() {
|
|
if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
|
|
window.RTCPeerConnection.prototype)) {
|
|
Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
|
|
get: function() {
|
|
return this._ontrack;
|
|
},
|
|
set: function(f) {
|
|
var self = this;
|
|
if (this._ontrack) {
|
|
this.removeEventListener('track', this._ontrack);
|
|
this.removeEventListener('addstream', this._ontrackpoly);
|
|
}
|
|
this.addEventListener('track', this._ontrack = f);
|
|
this.addEventListener('addstream', this._ontrackpoly = function(e) {
|
|
// onaddstream does not fire when a track is added to an existing
|
|
// stream. But stream.onaddtrack is implemented so we use that.
|
|
e.stream.addEventListener('addtrack', function(te) {
|
|
var event = new Event('track');
|
|
event.track = te.track;
|
|
event.receiver = {track: te.track};
|
|
event.streams = [e.stream];
|
|
self.dispatchEvent(event);
|
|
});
|
|
e.stream.getTracks().forEach(function(track) {
|
|
var event = new Event('track');
|
|
event.track = track;
|
|
event.receiver = {track: track};
|
|
event.streams = [e.stream];
|
|
this.dispatchEvent(event);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
shimSourceObject: function() {
|
|
if (typeof window === 'object') {
|
|
if (window.HTMLMediaElement &&
|
|
!('srcObject' in window.HTMLMediaElement.prototype)) {
|
|
// Shim the srcObject property, once, when HTMLMediaElement is found.
|
|
Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
|
|
get: function() {
|
|
return this._srcObject;
|
|
},
|
|
set: function(stream) {
|
|
var self = this;
|
|
// Use _srcObject as a private property for this shim
|
|
this._srcObject = stream;
|
|
if (this.src) {
|
|
URL.revokeObjectURL(this.src);
|
|
}
|
|
|
|
if (!stream) {
|
|
this.src = '';
|
|
return;
|
|
}
|
|
this.src = URL.createObjectURL(stream);
|
|
// We need to recreate the blob url when a track is added or
|
|
// removed. Doing it manually since we want to avoid a recursion.
|
|
stream.addEventListener('addtrack', function() {
|
|
if (self.src) {
|
|
URL.revokeObjectURL(self.src);
|
|
}
|
|
self.src = URL.createObjectURL(stream);
|
|
});
|
|
stream.addEventListener('removetrack', function() {
|
|
if (self.src) {
|
|
URL.revokeObjectURL(self.src);
|
|
}
|
|
self.src = URL.createObjectURL(stream);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
shimPeerConnection: function() {
|
|
// The RTCPeerConnection object.
|
|
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
|
|
// Translate iceTransportPolicy to iceTransports,
|
|
// see https://code.google.com/p/webrtc/issues/detail?id=4869
|
|
logging('PeerConnection');
|
|
if (pcConfig && pcConfig.iceTransportPolicy) {
|
|
pcConfig.iceTransports = pcConfig.iceTransportPolicy;
|
|
}
|
|
|
|
var pc = new webkitRTCPeerConnection(pcConfig, pcConstraints);
|
|
var origGetStats = pc.getStats.bind(pc);
|
|
pc.getStats = function(selector, successCallback, errorCallback) {
|
|
var self = this;
|
|
var args = arguments;
|
|
|
|
// If selector is a function then we are in the old style stats so just
|
|
// pass back the original getStats format to avoid breaking old users.
|
|
if (arguments.length > 0 && typeof selector === 'function') {
|
|
return origGetStats(selector, successCallback);
|
|
}
|
|
|
|
var fixChromeStats_ = function(response) {
|
|
var standardReport = {};
|
|
var reports = response.result();
|
|
reports.forEach(function(report) {
|
|
var standardStats = {
|
|
id: report.id,
|
|
timestamp: report.timestamp,
|
|
type: report.type
|
|
};
|
|
report.names().forEach(function(name) {
|
|
standardStats[name] = report.stat(name);
|
|
});
|
|
standardReport[standardStats.id] = standardStats;
|
|
});
|
|
|
|
return standardReport;
|
|
};
|
|
|
|
// shim getStats with maplike support
|
|
var makeMapStats = function(stats, legacyStats) {
|
|
var map = new Map(Object.keys(stats).map(function(key) {
|
|
return[key, stats[key]];
|
|
}));
|
|
legacyStats = legacyStats || stats;
|
|
Object.keys(legacyStats).forEach(function(key) {
|
|
map[key] = legacyStats[key];
|
|
});
|
|
return map;
|
|
};
|
|
|
|
if (arguments.length >= 2) {
|
|
var successCallbackWrapper_ = function(response) {
|
|
args[1](makeMapStats(fixChromeStats_(response)));
|
|
};
|
|
|
|
return origGetStats.apply(this, [successCallbackWrapper_,
|
|
arguments[0]]);
|
|
}
|
|
|
|
// promise-support
|
|
return new Promise(function(resolve, reject) {
|
|
if (args.length === 1 && typeof selector === 'object') {
|
|
origGetStats.apply(self, [
|
|
function(response) {
|
|
resolve(makeMapStats(fixChromeStats_(response)));
|
|
}, reject]);
|
|
} else {
|
|
// Preserve legacy chrome stats only on legacy access of stats obj
|
|
origGetStats.apply(self, [
|
|
function(response) {
|
|
resolve(makeMapStats(fixChromeStats_(response),
|
|
response.result()));
|
|
}, reject]);
|
|
}
|
|
}).then(successCallback, errorCallback);
|
|
};
|
|
|
|
return pc;
|
|
};
|
|
window.RTCPeerConnection.prototype = webkitRTCPeerConnection.prototype;
|
|
|
|
// wrap static methods. Currently just generateCertificate.
|
|
if (webkitRTCPeerConnection.generateCertificate) {
|
|
Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
|
|
get: function() {
|
|
return webkitRTCPeerConnection.generateCertificate;
|
|
}
|
|
});
|
|
}
|
|
|
|
['createOffer', 'createAnswer'].forEach(function(method) {
|
|
var nativeMethod = webkitRTCPeerConnection.prototype[method];
|
|
webkitRTCPeerConnection.prototype[method] = function() {
|
|
var self = this;
|
|
if (arguments.length < 1 || (arguments.length === 1 &&
|
|
typeof arguments[0] === 'object')) {
|
|
var opts = arguments.length === 1 ? arguments[0] : undefined;
|
|
return new Promise(function(resolve, reject) {
|
|
nativeMethod.apply(self, [resolve, reject, opts]);
|
|
});
|
|
}
|
|
return nativeMethod.apply(this, arguments);
|
|
};
|
|
});
|
|
|
|
// add promise support -- natively available in Chrome 51
|
|
if (browserDetails.version < 51) {
|
|
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
|
|
.forEach(function(method) {
|
|
var nativeMethod = webkitRTCPeerConnection.prototype[method];
|
|
webkitRTCPeerConnection.prototype[method] = function() {
|
|
var args = arguments;
|
|
var self = this;
|
|
var promise = new Promise(function(resolve, reject) {
|
|
nativeMethod.apply(self, [args[0], resolve, reject]);
|
|
});
|
|
if (args.length < 2) {
|
|
return promise;
|
|
}
|
|
return promise.then(function() {
|
|
args[1].apply(null, []);
|
|
},
|
|
function(err) {
|
|
if (args.length >= 3) {
|
|
args[2].apply(null, [err]);
|
|
}
|
|
});
|
|
};
|
|
});
|
|
}
|
|
|
|
// shim implicit creation of RTCSessionDescription/RTCIceCandidate
|
|
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
|
|
.forEach(function(method) {
|
|
var nativeMethod = webkitRTCPeerConnection.prototype[method];
|
|
webkitRTCPeerConnection.prototype[method] = function() {
|
|
arguments[0] = new ((method === 'addIceCandidate') ?
|
|
RTCIceCandidate : RTCSessionDescription)(arguments[0]);
|
|
return nativeMethod.apply(this, arguments);
|
|
};
|
|
});
|
|
|
|
// support for addIceCandidate(null or undefined)
|
|
var nativeAddIceCandidate =
|
|
RTCPeerConnection.prototype.addIceCandidate;
|
|
RTCPeerConnection.prototype.addIceCandidate = function() {
|
|
if (!arguments[0]) {
|
|
if (arguments[1]) {
|
|
arguments[1].apply(null);
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
return nativeAddIceCandidate.apply(this, arguments);
|
|
};
|
|
}
|
|
};
|
|
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimMediaStream: chromeShim.shimMediaStream,
|
|
shimOnTrack: chromeShim.shimOnTrack,
|
|
shimSourceObject: chromeShim.shimSourceObject,
|
|
shimPeerConnection: chromeShim.shimPeerConnection,
|
|
shimGetUserMedia: require('./getusermedia')
|
|
};
|
|
|
|
},{"../utils.js":98,"./getusermedia":92}],92:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
var logging = require('../utils.js').log;
|
|
|
|
// Expose public methods.
|
|
module.exports = function() {
|
|
var constraintsToChrome_ = function(c) {
|
|
if (typeof c !== 'object' || c.mandatory || c.optional) {
|
|
return c;
|
|
}
|
|
var cc = {};
|
|
Object.keys(c).forEach(function(key) {
|
|
if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
|
|
return;
|
|
}
|
|
var r = (typeof c[key] === 'object') ? c[key] : {ideal: c[key]};
|
|
if (r.exact !== undefined && typeof r.exact === 'number') {
|
|
r.min = r.max = r.exact;
|
|
}
|
|
var oldname_ = function(prefix, name) {
|
|
if (prefix) {
|
|
return prefix + name.charAt(0).toUpperCase() + name.slice(1);
|
|
}
|
|
return (name === 'deviceId') ? 'sourceId' : name;
|
|
};
|
|
if (r.ideal !== undefined) {
|
|
cc.optional = cc.optional || [];
|
|
var oc = {};
|
|
if (typeof r.ideal === 'number') {
|
|
oc[oldname_('min', key)] = r.ideal;
|
|
cc.optional.push(oc);
|
|
oc = {};
|
|
oc[oldname_('max', key)] = r.ideal;
|
|
cc.optional.push(oc);
|
|
} else {
|
|
oc[oldname_('', key)] = r.ideal;
|
|
cc.optional.push(oc);
|
|
}
|
|
}
|
|
if (r.exact !== undefined && typeof r.exact !== 'number') {
|
|
cc.mandatory = cc.mandatory || {};
|
|
cc.mandatory[oldname_('', key)] = r.exact;
|
|
} else {
|
|
['min', 'max'].forEach(function(mix) {
|
|
if (r[mix] !== undefined) {
|
|
cc.mandatory = cc.mandatory || {};
|
|
cc.mandatory[oldname_(mix, key)] = r[mix];
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (c.advanced) {
|
|
cc.optional = (cc.optional || []).concat(c.advanced);
|
|
}
|
|
return cc;
|
|
};
|
|
|
|
var shimConstraints_ = function(constraints, func) {
|
|
constraints = JSON.parse(JSON.stringify(constraints));
|
|
if (constraints && constraints.audio) {
|
|
constraints.audio = constraintsToChrome_(constraints.audio);
|
|
}
|
|
if (constraints && typeof constraints.video === 'object') {
|
|
// Shim facingMode for mobile, where it defaults to "user".
|
|
var face = constraints.video.facingMode;
|
|
face = face && ((typeof face === 'object') ? face : {ideal: face});
|
|
|
|
if ((face && (face.exact === 'user' || face.exact === 'environment' ||
|
|
face.ideal === 'user' || face.ideal === 'environment')) &&
|
|
!(navigator.mediaDevices.getSupportedConstraints &&
|
|
navigator.mediaDevices.getSupportedConstraints().facingMode)) {
|
|
delete constraints.video.facingMode;
|
|
if (face.exact === 'environment' || face.ideal === 'environment') {
|
|
// Look for "back" in label, or use last cam (typically back cam).
|
|
return navigator.mediaDevices.enumerateDevices()
|
|
.then(function(devices) {
|
|
devices = devices.filter(function(d) {
|
|
return d.kind === 'videoinput';
|
|
});
|
|
var back = devices.find(function(d) {
|
|
return d.label.toLowerCase().indexOf('back') !== -1;
|
|
}) || (devices.length && devices[devices.length - 1]);
|
|
if (back) {
|
|
constraints.video.deviceId = face.exact ? {exact: back.deviceId} :
|
|
{ideal: back.deviceId};
|
|
}
|
|
constraints.video = constraintsToChrome_(constraints.video);
|
|
logging('chrome: ' + JSON.stringify(constraints));
|
|
return func(constraints);
|
|
});
|
|
}
|
|
}
|
|
constraints.video = constraintsToChrome_(constraints.video);
|
|
}
|
|
logging('chrome: ' + JSON.stringify(constraints));
|
|
return func(constraints);
|
|
};
|
|
|
|
var shimError_ = function(e) {
|
|
return {
|
|
name: {
|
|
PermissionDeniedError: 'NotAllowedError',
|
|
ConstraintNotSatisfiedError: 'OverconstrainedError'
|
|
}[e.name] || e.name,
|
|
message: e.message,
|
|
constraint: e.constraintName,
|
|
toString: function() {
|
|
return this.name + (this.message && ': ') + this.message;
|
|
}
|
|
};
|
|
};
|
|
|
|
var getUserMedia_ = function(constraints, onSuccess, onError) {
|
|
shimConstraints_(constraints, function(c) {
|
|
navigator.webkitGetUserMedia(c, onSuccess, function(e) {
|
|
onError(shimError_(e));
|
|
});
|
|
});
|
|
};
|
|
|
|
navigator.getUserMedia = getUserMedia_;
|
|
|
|
// Returns the result of getUserMedia as a Promise.
|
|
var getUserMediaPromise_ = function(constraints) {
|
|
return new Promise(function(resolve, reject) {
|
|
navigator.getUserMedia(constraints, resolve, reject);
|
|
});
|
|
};
|
|
|
|
if (!navigator.mediaDevices) {
|
|
navigator.mediaDevices = {
|
|
getUserMedia: getUserMediaPromise_,
|
|
enumerateDevices: function() {
|
|
return new Promise(function(resolve) {
|
|
var kinds = {audio: 'audioinput', video: 'videoinput'};
|
|
return MediaStreamTrack.getSources(function(devices) {
|
|
resolve(devices.map(function(device) {
|
|
return {label: device.label,
|
|
kind: kinds[device.kind],
|
|
deviceId: device.id,
|
|
groupId: ''};
|
|
}));
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
// A shim for getUserMedia method on the mediaDevices object.
|
|
// TODO(KaptenJansson) remove once implemented in Chrome stable.
|
|
if (!navigator.mediaDevices.getUserMedia) {
|
|
navigator.mediaDevices.getUserMedia = function(constraints) {
|
|
return getUserMediaPromise_(constraints);
|
|
};
|
|
} else {
|
|
// Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
|
|
// function which returns a Promise, it does not accept spec-style
|
|
// constraints.
|
|
var origGetUserMedia = navigator.mediaDevices.getUserMedia.
|
|
bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.getUserMedia = function(cs) {
|
|
return shimConstraints_(cs, function(c) {
|
|
return origGetUserMedia(c).then(function(stream) {
|
|
if (c.audio && !stream.getAudioTracks().length ||
|
|
c.video && !stream.getVideoTracks().length) {
|
|
stream.getTracks().forEach(function(track) {
|
|
track.stop();
|
|
});
|
|
throw new DOMException('', 'NotFoundError');
|
|
}
|
|
return stream;
|
|
}, function(e) {
|
|
return Promise.reject(shimError_(e));
|
|
});
|
|
});
|
|
};
|
|
}
|
|
|
|
// Dummy devicechange event methods.
|
|
// TODO(KaptenJansson) remove once implemented in Chrome stable.
|
|
if (typeof navigator.mediaDevices.addEventListener === 'undefined') {
|
|
navigator.mediaDevices.addEventListener = function() {
|
|
logging('Dummy mediaDevices.addEventListener called.');
|
|
};
|
|
}
|
|
if (typeof navigator.mediaDevices.removeEventListener === 'undefined') {
|
|
navigator.mediaDevices.removeEventListener = function() {
|
|
logging('Dummy mediaDevices.removeEventListener called.');
|
|
};
|
|
}
|
|
};
|
|
|
|
},{"../utils.js":98}],93:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var SDPUtils = require('sdp');
|
|
var browserDetails = require('../utils').browserDetails;
|
|
|
|
var edgeShim = {
|
|
shimPeerConnection: function() {
|
|
if (window.RTCIceGatherer) {
|
|
// ORTC defines an RTCIceCandidate object but no constructor.
|
|
// Not implemented in Edge.
|
|
if (!window.RTCIceCandidate) {
|
|
window.RTCIceCandidate = function(args) {
|
|
return args;
|
|
};
|
|
}
|
|
// ORTC does not have a session description object but
|
|
// other browsers (i.e. Chrome) that will support both PC and ORTC
|
|
// in the future might have this defined already.
|
|
if (!window.RTCSessionDescription) {
|
|
window.RTCSessionDescription = function(args) {
|
|
return args;
|
|
};
|
|
}
|
|
// this adds an additional event listener to MediaStrackTrack that signals
|
|
// when a tracks enabled property was changed.
|
|
var origMSTEnabled = Object.getOwnPropertyDescriptor(
|
|
MediaStreamTrack.prototype, 'enabled');
|
|
Object.defineProperty(MediaStreamTrack.prototype, 'enabled', {
|
|
set: function(value) {
|
|
origMSTEnabled.set.call(this, value);
|
|
var ev = new Event('enabled');
|
|
ev.enabled = value;
|
|
this.dispatchEvent(ev);
|
|
}
|
|
});
|
|
}
|
|
|
|
window.RTCPeerConnection = function(config) {
|
|
var self = this;
|
|
|
|
var _eventTarget = document.createDocumentFragment();
|
|
['addEventListener', 'removeEventListener', 'dispatchEvent']
|
|
.forEach(function(method) {
|
|
self[method] = _eventTarget[method].bind(_eventTarget);
|
|
});
|
|
|
|
this.onicecandidate = null;
|
|
this.onaddstream = null;
|
|
this.ontrack = null;
|
|
this.onremovestream = null;
|
|
this.onsignalingstatechange = null;
|
|
this.oniceconnectionstatechange = null;
|
|
this.onnegotiationneeded = null;
|
|
this.ondatachannel = null;
|
|
|
|
this.localStreams = [];
|
|
this.remoteStreams = [];
|
|
this.getLocalStreams = function() {
|
|
return self.localStreams;
|
|
};
|
|
this.getRemoteStreams = function() {
|
|
return self.remoteStreams;
|
|
};
|
|
|
|
this.localDescription = new RTCSessionDescription({
|
|
type: '',
|
|
sdp: ''
|
|
});
|
|
this.remoteDescription = new RTCSessionDescription({
|
|
type: '',
|
|
sdp: ''
|
|
});
|
|
this.signalingState = 'stable';
|
|
this.iceConnectionState = 'new';
|
|
this.iceGatheringState = 'new';
|
|
|
|
this.iceOptions = {
|
|
gatherPolicy: 'all',
|
|
iceServers: []
|
|
};
|
|
if (config && config.iceTransportPolicy) {
|
|
switch (config.iceTransportPolicy) {
|
|
case 'all':
|
|
case 'relay':
|
|
this.iceOptions.gatherPolicy = config.iceTransportPolicy;
|
|
break;
|
|
case 'none':
|
|
// FIXME: remove once implementation and spec have added this.
|
|
throw new TypeError('iceTransportPolicy "none" not supported');
|
|
default:
|
|
// don't set iceTransportPolicy.
|
|
break;
|
|
}
|
|
}
|
|
this.usingBundle = config && config.bundlePolicy === 'max-bundle';
|
|
|
|
if (config && config.iceServers) {
|
|
// Edge does not like
|
|
// 1) stun:
|
|
// 2) turn: that does not have all of turn:host:port?transport=udp
|
|
// 3) turn: with ipv6 addresses
|
|
var iceServers = JSON.parse(JSON.stringify(config.iceServers));
|
|
this.iceOptions.iceServers = iceServers.filter(function(server) {
|
|
if (server && server.urls) {
|
|
var urls = server.urls;
|
|
if (typeof urls === 'string') {
|
|
urls = [urls];
|
|
}
|
|
urls = urls.filter(function(url) {
|
|
return (url.indexOf('turn:') === 0 &&
|
|
url.indexOf('transport=udp') !== -1 &&
|
|
url.indexOf('turn:[') === -1) ||
|
|
(url.indexOf('stun:') === 0 &&
|
|
browserDetails.version >= 14393);
|
|
})[0];
|
|
return !!urls;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
this._config = config;
|
|
|
|
// per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
|
|
// everything that is needed to describe a SDP m-line.
|
|
this.transceivers = [];
|
|
|
|
// since the iceGatherer is currently created in createOffer but we
|
|
// must not emit candidates until after setLocalDescription we buffer
|
|
// them in this array.
|
|
this._localIceCandidatesBuffer = [];
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype._emitBufferedCandidates = function() {
|
|
var self = this;
|
|
var sections = SDPUtils.splitSections(self.localDescription.sdp);
|
|
// FIXME: need to apply ice candidates in a way which is async but
|
|
// in-order
|
|
this._localIceCandidatesBuffer.forEach(function(event) {
|
|
var end = !event.candidate || Object.keys(event.candidate).length === 0;
|
|
if (end) {
|
|
for (var j = 1; j < sections.length; j++) {
|
|
if (sections[j].indexOf('\r\na=end-of-candidates\r\n') === -1) {
|
|
sections[j] += 'a=end-of-candidates\r\n';
|
|
}
|
|
}
|
|
} else if (event.candidate.candidate.indexOf('typ endOfCandidates')
|
|
=== -1) {
|
|
sections[event.candidate.sdpMLineIndex + 1] +=
|
|
'a=' + event.candidate.candidate + '\r\n';
|
|
}
|
|
self.localDescription.sdp = sections.join('');
|
|
self.dispatchEvent(event);
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(event);
|
|
}
|
|
if (!event.candidate && self.iceGatheringState !== 'complete') {
|
|
var complete = self.transceivers.every(function(transceiver) {
|
|
return transceiver.iceGatherer &&
|
|
transceiver.iceGatherer.state === 'completed';
|
|
});
|
|
if (complete) {
|
|
self.iceGatheringState = 'complete';
|
|
}
|
|
}
|
|
});
|
|
this._localIceCandidatesBuffer = [];
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getConfiguration = function() {
|
|
return this._config;
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.addStream = function(stream) {
|
|
// Clone is necessary for local demos mostly, attaching directly
|
|
// to two different senders does not work (build 10547).
|
|
var clonedStream = stream.clone();
|
|
stream.getTracks().forEach(function(track, idx) {
|
|
var clonedTrack = clonedStream.getTracks()[idx];
|
|
track.addEventListener('enabled', function(event) {
|
|
clonedTrack.enabled = event.enabled;
|
|
});
|
|
});
|
|
this.localStreams.push(clonedStream);
|
|
this._maybeFireNegotiationNeeded();
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.removeStream = function(stream) {
|
|
var idx = this.localStreams.indexOf(stream);
|
|
if (idx > -1) {
|
|
this.localStreams.splice(idx, 1);
|
|
this._maybeFireNegotiationNeeded();
|
|
}
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getSenders = function() {
|
|
return this.transceivers.filter(function(transceiver) {
|
|
return !!transceiver.rtpSender;
|
|
})
|
|
.map(function(transceiver) {
|
|
return transceiver.rtpSender;
|
|
});
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getReceivers = function() {
|
|
return this.transceivers.filter(function(transceiver) {
|
|
return !!transceiver.rtpReceiver;
|
|
})
|
|
.map(function(transceiver) {
|
|
return transceiver.rtpReceiver;
|
|
});
|
|
};
|
|
|
|
// Determines the intersection of local and remote capabilities.
|
|
window.RTCPeerConnection.prototype._getCommonCapabilities =
|
|
function(localCapabilities, remoteCapabilities) {
|
|
var commonCapabilities = {
|
|
codecs: [],
|
|
headerExtensions: [],
|
|
fecMechanisms: []
|
|
};
|
|
localCapabilities.codecs.forEach(function(lCodec) {
|
|
for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
|
|
var rCodec = remoteCapabilities.codecs[i];
|
|
if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
|
|
lCodec.clockRate === rCodec.clockRate) {
|
|
// number of channels is the highest common number of channels
|
|
rCodec.numChannels = Math.min(lCodec.numChannels,
|
|
rCodec.numChannels);
|
|
// push rCodec so we reply with offerer payload type
|
|
commonCapabilities.codecs.push(rCodec);
|
|
|
|
// determine common feedback mechanisms
|
|
rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
|
|
for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
|
|
if (lCodec.rtcpFeedback[j].type === fb.type &&
|
|
lCodec.rtcpFeedback[j].parameter === fb.parameter) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
// FIXME: also need to determine .parameters
|
|
// see https://github.com/openpeer/ortc/issues/569
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
localCapabilities.headerExtensions
|
|
.forEach(function(lHeaderExtension) {
|
|
for (var i = 0; i < remoteCapabilities.headerExtensions.length;
|
|
i++) {
|
|
var rHeaderExtension = remoteCapabilities.headerExtensions[i];
|
|
if (lHeaderExtension.uri === rHeaderExtension.uri) {
|
|
commonCapabilities.headerExtensions.push(rHeaderExtension);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
// FIXME: fecMechanisms
|
|
return commonCapabilities;
|
|
};
|
|
|
|
// Create ICE gatherer, ICE transport and DTLS transport.
|
|
window.RTCPeerConnection.prototype._createIceAndDtlsTransports =
|
|
function(mid, sdpMLineIndex) {
|
|
var self = this;
|
|
var iceGatherer = new RTCIceGatherer(self.iceOptions);
|
|
var iceTransport = new RTCIceTransport(iceGatherer);
|
|
iceGatherer.onlocalcandidate = function(evt) {
|
|
var event = new Event('icecandidate');
|
|
event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};
|
|
|
|
var cand = evt.candidate;
|
|
var end = !cand || Object.keys(cand).length === 0;
|
|
// Edge emits an empty object for RTCIceCandidateComplete‥
|
|
if (end) {
|
|
// polyfill since RTCIceGatherer.state is not implemented in
|
|
// Edge 10547 yet.
|
|
if (iceGatherer.state === undefined) {
|
|
iceGatherer.state = 'completed';
|
|
}
|
|
|
|
// Emit a candidate with type endOfCandidates to make the samples
|
|
// work. Edge requires addIceCandidate with this empty candidate
|
|
// to start checking. The real solution is to signal
|
|
// end-of-candidates to the other side when getting the null
|
|
// candidate but some apps (like the samples) don't do that.
|
|
event.candidate.candidate =
|
|
'candidate:1 1 udp 1 0.0.0.0 9 typ endOfCandidates';
|
|
} else {
|
|
// RTCIceCandidate doesn't have a component, needs to be added
|
|
cand.component = iceTransport.component === 'RTCP' ? 2 : 1;
|
|
event.candidate.candidate = SDPUtils.writeCandidate(cand);
|
|
}
|
|
|
|
// update local description.
|
|
var sections = SDPUtils.splitSections(self.localDescription.sdp);
|
|
if (event.candidate.candidate.indexOf('typ endOfCandidates')
|
|
=== -1) {
|
|
sections[event.candidate.sdpMLineIndex + 1] +=
|
|
'a=' + event.candidate.candidate + '\r\n';
|
|
} else {
|
|
sections[event.candidate.sdpMLineIndex + 1] +=
|
|
'a=end-of-candidates\r\n';
|
|
}
|
|
self.localDescription.sdp = sections.join('');
|
|
|
|
var complete = self.transceivers.every(function(transceiver) {
|
|
return transceiver.iceGatherer &&
|
|
transceiver.iceGatherer.state === 'completed';
|
|
});
|
|
|
|
// Emit candidate if localDescription is set.
|
|
// Also emits null candidate when all gatherers are complete.
|
|
switch (self.iceGatheringState) {
|
|
case 'new':
|
|
self._localIceCandidatesBuffer.push(event);
|
|
if (end && complete) {
|
|
self._localIceCandidatesBuffer.push(
|
|
new Event('icecandidate'));
|
|
}
|
|
break;
|
|
case 'gathering':
|
|
self._emitBufferedCandidates();
|
|
self.dispatchEvent(event);
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(event);
|
|
}
|
|
if (complete) {
|
|
self.dispatchEvent(new Event('icecandidate'));
|
|
if (self.onicecandidate !== null) {
|
|
self.onicecandidate(new Event('icecandidate'));
|
|
}
|
|
self.iceGatheringState = 'complete';
|
|
}
|
|
break;
|
|
case 'complete':
|
|
// should not happen... currently!
|
|
break;
|
|
default: // no-op.
|
|
break;
|
|
}
|
|
};
|
|
iceTransport.onicestatechange = function() {
|
|
self._updateConnectionState();
|
|
};
|
|
|
|
var dtlsTransport = new RTCDtlsTransport(iceTransport);
|
|
dtlsTransport.ondtlsstatechange = function() {
|
|
self._updateConnectionState();
|
|
};
|
|
dtlsTransport.onerror = function() {
|
|
// onerror does not set state to failed by itself.
|
|
dtlsTransport.state = 'failed';
|
|
self._updateConnectionState();
|
|
};
|
|
|
|
return {
|
|
iceGatherer: iceGatherer,
|
|
iceTransport: iceTransport,
|
|
dtlsTransport: dtlsTransport
|
|
};
|
|
};
|
|
|
|
// Start the RTP Sender and Receiver for a transceiver.
|
|
window.RTCPeerConnection.prototype._transceive = function(transceiver,
|
|
send, recv) {
|
|
var params = this._getCommonCapabilities(transceiver.localCapabilities,
|
|
transceiver.remoteCapabilities);
|
|
if (send && transceiver.rtpSender) {
|
|
params.encodings = transceiver.sendEncodingParameters;
|
|
params.rtcp = {
|
|
cname: SDPUtils.localCName
|
|
};
|
|
if (transceiver.recvEncodingParameters.length) {
|
|
params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
|
|
}
|
|
transceiver.rtpSender.send(params);
|
|
}
|
|
if (recv && transceiver.rtpReceiver) {
|
|
// remove RTX field in Edge 14942
|
|
if (transceiver.kind === 'video'
|
|
&& transceiver.recvEncodingParameters) {
|
|
transceiver.recvEncodingParameters.forEach(function(p) {
|
|
delete p.rtx;
|
|
});
|
|
}
|
|
params.encodings = transceiver.recvEncodingParameters;
|
|
params.rtcp = {
|
|
cname: transceiver.cname
|
|
};
|
|
if (transceiver.sendEncodingParameters.length) {
|
|
params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
|
|
}
|
|
transceiver.rtpReceiver.receive(params);
|
|
}
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.setLocalDescription =
|
|
function(description) {
|
|
var self = this;
|
|
var sections;
|
|
var sessionpart;
|
|
if (description.type === 'offer') {
|
|
// FIXME: What was the purpose of this empty if statement?
|
|
// if (!this._pendingOffer) {
|
|
// } else {
|
|
if (this._pendingOffer) {
|
|
// VERY limited support for SDP munging. Limited to:
|
|
// * changing the order of codecs
|
|
sections = SDPUtils.splitSections(description.sdp);
|
|
sessionpart = sections.shift();
|
|
sections.forEach(function(mediaSection, sdpMLineIndex) {
|
|
var caps = SDPUtils.parseRtpParameters(mediaSection);
|
|
self._pendingOffer[sdpMLineIndex].localCapabilities = caps;
|
|
});
|
|
this.transceivers = this._pendingOffer;
|
|
delete this._pendingOffer;
|
|
}
|
|
} else if (description.type === 'answer') {
|
|
sections = SDPUtils.splitSections(self.remoteDescription.sdp);
|
|
sessionpart = sections.shift();
|
|
var isIceLite = SDPUtils.matchPrefix(sessionpart,
|
|
'a=ice-lite').length > 0;
|
|
sections.forEach(function(mediaSection, sdpMLineIndex) {
|
|
var transceiver = self.transceivers[sdpMLineIndex];
|
|
var iceGatherer = transceiver.iceGatherer;
|
|
var iceTransport = transceiver.iceTransport;
|
|
var dtlsTransport = transceiver.dtlsTransport;
|
|
var localCapabilities = transceiver.localCapabilities;
|
|
var remoteCapabilities = transceiver.remoteCapabilities;
|
|
|
|
var rejected = mediaSection.split('\n', 1)[0]
|
|
.split(' ', 2)[1] === '0';
|
|
|
|
if (!rejected && !transceiver.isDatachannel) {
|
|
var remoteIceParameters = SDPUtils.getIceParameters(
|
|
mediaSection, sessionpart);
|
|
if (isIceLite) {
|
|
var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
|
|
.map(function(cand) {
|
|
return SDPUtils.parseCandidate(cand);
|
|
})
|
|
.filter(function(cand) {
|
|
return cand.component === '1';
|
|
});
|
|
// ice-lite only includes host candidates in the SDP so we can
|
|
// use setRemoteCandidates (which implies an
|
|
// RTCIceCandidateComplete)
|
|
if (cands.length) {
|
|
iceTransport.setRemoteCandidates(cands);
|
|
}
|
|
}
|
|
var remoteDtlsParameters = SDPUtils.getDtlsParameters(
|
|
mediaSection, sessionpart);
|
|
if (isIceLite) {
|
|
remoteDtlsParameters.role = 'server';
|
|
}
|
|
|
|
if (!self.usingBundle || sdpMLineIndex === 0) {
|
|
iceTransport.start(iceGatherer, remoteIceParameters,
|
|
isIceLite ? 'controlling' : 'controlled');
|
|
dtlsTransport.start(remoteDtlsParameters);
|
|
}
|
|
|
|
// Calculate intersection of capabilities.
|
|
var params = self._getCommonCapabilities(localCapabilities,
|
|
remoteCapabilities);
|
|
|
|
// Start the RTCRtpSender. The RTCRtpReceiver for this
|
|
// transceiver has already been started in setRemoteDescription.
|
|
self._transceive(transceiver,
|
|
params.codecs.length > 0,
|
|
false);
|
|
}
|
|
});
|
|
}
|
|
|
|
this.localDescription = {
|
|
type: description.type,
|
|
sdp: description.sdp
|
|
};
|
|
switch (description.type) {
|
|
case 'offer':
|
|
this._updateSignalingState('have-local-offer');
|
|
break;
|
|
case 'answer':
|
|
this._updateSignalingState('stable');
|
|
break;
|
|
default:
|
|
throw new TypeError('unsupported type "' + description.type +
|
|
'"');
|
|
}
|
|
|
|
// If a success callback was provided, emit ICE candidates after it
|
|
// has been executed. Otherwise, emit callback after the Promise is
|
|
// resolved.
|
|
var hasCallback = arguments.length > 1 &&
|
|
typeof arguments[1] === 'function';
|
|
if (hasCallback) {
|
|
var cb = arguments[1];
|
|
window.setTimeout(function() {
|
|
cb();
|
|
if (self.iceGatheringState === 'new') {
|
|
self.iceGatheringState = 'gathering';
|
|
}
|
|
self._emitBufferedCandidates();
|
|
}, 0);
|
|
}
|
|
var p = Promise.resolve();
|
|
p.then(function() {
|
|
if (!hasCallback) {
|
|
if (self.iceGatheringState === 'new') {
|
|
self.iceGatheringState = 'gathering';
|
|
}
|
|
// Usually candidates will be emitted earlier.
|
|
window.setTimeout(self._emitBufferedCandidates.bind(self), 500);
|
|
}
|
|
});
|
|
return p;
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.setRemoteDescription =
|
|
function(description) {
|
|
var self = this;
|
|
var stream = new MediaStream();
|
|
var receiverList = [];
|
|
var sections = SDPUtils.splitSections(description.sdp);
|
|
var sessionpart = sections.shift();
|
|
var isIceLite = SDPUtils.matchPrefix(sessionpart,
|
|
'a=ice-lite').length > 0;
|
|
this.usingBundle = SDPUtils.matchPrefix(sessionpart,
|
|
'a=group:BUNDLE ').length > 0;
|
|
sections.forEach(function(mediaSection, sdpMLineIndex) {
|
|
var lines = SDPUtils.splitLines(mediaSection);
|
|
var mline = lines[0].substr(2).split(' ');
|
|
var kind = mline[0];
|
|
var rejected = mline[1] === '0';
|
|
var direction = SDPUtils.getDirection(mediaSection, sessionpart);
|
|
|
|
var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:');
|
|
if (mid.length) {
|
|
mid = mid[0].substr(6);
|
|
} else {
|
|
mid = SDPUtils.generateIdentifier();
|
|
}
|
|
|
|
// Reject datachannels which are not implemented yet.
|
|
if (kind === 'application' && mline[2] === 'DTLS/SCTP') {
|
|
self.transceivers[sdpMLineIndex] = {
|
|
mid: mid,
|
|
isDatachannel: true
|
|
};
|
|
return;
|
|
}
|
|
|
|
var transceiver;
|
|
var iceGatherer;
|
|
var iceTransport;
|
|
var dtlsTransport;
|
|
var rtpSender;
|
|
var rtpReceiver;
|
|
var sendEncodingParameters;
|
|
var recvEncodingParameters;
|
|
var localCapabilities;
|
|
|
|
var track;
|
|
// FIXME: ensure the mediaSection has rtcp-mux set.
|
|
var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
|
|
var remoteIceParameters;
|
|
var remoteDtlsParameters;
|
|
if (!rejected) {
|
|
remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
|
|
sessionpart);
|
|
remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
|
|
sessionpart);
|
|
remoteDtlsParameters.role = 'client';
|
|
}
|
|
recvEncodingParameters =
|
|
SDPUtils.parseRtpEncodingParameters(mediaSection);
|
|
|
|
var cname;
|
|
// Gets the first SSRC. Note that with RTX there might be multiple
|
|
// SSRCs.
|
|
var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
|
|
.map(function(line) {
|
|
return SDPUtils.parseSsrcMedia(line);
|
|
})
|
|
.filter(function(obj) {
|
|
return obj.attribute === 'cname';
|
|
})[0];
|
|
if (remoteSsrc) {
|
|
cname = remoteSsrc.value;
|
|
}
|
|
|
|
var isComplete = SDPUtils.matchPrefix(mediaSection,
|
|
'a=end-of-candidates', sessionpart).length > 0;
|
|
var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
|
|
.map(function(cand) {
|
|
return SDPUtils.parseCandidate(cand);
|
|
})
|
|
.filter(function(cand) {
|
|
return cand.component === '1';
|
|
});
|
|
if (description.type === 'offer' && !rejected) {
|
|
var transports = self.usingBundle && sdpMLineIndex > 0 ? {
|
|
iceGatherer: self.transceivers[0].iceGatherer,
|
|
iceTransport: self.transceivers[0].iceTransport,
|
|
dtlsTransport: self.transceivers[0].dtlsTransport
|
|
} : self._createIceAndDtlsTransports(mid, sdpMLineIndex);
|
|
|
|
if (isComplete) {
|
|
transports.iceTransport.setRemoteCandidates(cands);
|
|
}
|
|
|
|
localCapabilities = RTCRtpReceiver.getCapabilities(kind);
|
|
|
|
// filter RTX until additional stuff needed for RTX is implemented
|
|
// in adapter.js
|
|
localCapabilities.codecs = localCapabilities.codecs.filter(
|
|
function(codec) {
|
|
return codec.name !== 'rtx';
|
|
});
|
|
|
|
sendEncodingParameters = [{
|
|
ssrc: (2 * sdpMLineIndex + 2) * 1001
|
|
}];
|
|
|
|
rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
|
|
|
|
track = rtpReceiver.track;
|
|
receiverList.push([track, rtpReceiver]);
|
|
// FIXME: not correct when there are multiple streams but that is
|
|
// not currently supported in this shim.
|
|
stream.addTrack(track);
|
|
|
|
// FIXME: look at direction.
|
|
if (self.localStreams.length > 0 &&
|
|
self.localStreams[0].getTracks().length >= sdpMLineIndex) {
|
|
var localTrack;
|
|
if (kind === 'audio') {
|
|
localTrack = self.localStreams[0].getAudioTracks()[0];
|
|
} else if (kind === 'video') {
|
|
localTrack = self.localStreams[0].getVideoTracks()[0];
|
|
}
|
|
if (localTrack) {
|
|
rtpSender = new RTCRtpSender(localTrack,
|
|
transports.dtlsTransport);
|
|
}
|
|
}
|
|
|
|
self.transceivers[sdpMLineIndex] = {
|
|
iceGatherer: transports.iceGatherer,
|
|
iceTransport: transports.iceTransport,
|
|
dtlsTransport: transports.dtlsTransport,
|
|
localCapabilities: localCapabilities,
|
|
remoteCapabilities: remoteCapabilities,
|
|
rtpSender: rtpSender,
|
|
rtpReceiver: rtpReceiver,
|
|
kind: kind,
|
|
mid: mid,
|
|
cname: cname,
|
|
sendEncodingParameters: sendEncodingParameters,
|
|
recvEncodingParameters: recvEncodingParameters
|
|
};
|
|
// Start the RTCRtpReceiver now. The RTPSender is started in
|
|
// setLocalDescription.
|
|
self._transceive(self.transceivers[sdpMLineIndex],
|
|
false,
|
|
direction === 'sendrecv' || direction === 'sendonly');
|
|
} else if (description.type === 'answer' && !rejected) {
|
|
transceiver = self.transceivers[sdpMLineIndex];
|
|
iceGatherer = transceiver.iceGatherer;
|
|
iceTransport = transceiver.iceTransport;
|
|
dtlsTransport = transceiver.dtlsTransport;
|
|
rtpSender = transceiver.rtpSender;
|
|
rtpReceiver = transceiver.rtpReceiver;
|
|
sendEncodingParameters = transceiver.sendEncodingParameters;
|
|
localCapabilities = transceiver.localCapabilities;
|
|
|
|
self.transceivers[sdpMLineIndex].recvEncodingParameters =
|
|
recvEncodingParameters;
|
|
self.transceivers[sdpMLineIndex].remoteCapabilities =
|
|
remoteCapabilities;
|
|
self.transceivers[sdpMLineIndex].cname = cname;
|
|
|
|
if ((isIceLite || isComplete) && cands.length) {
|
|
iceTransport.setRemoteCandidates(cands);
|
|
}
|
|
if (!self.usingBundle || sdpMLineIndex === 0) {
|
|
iceTransport.start(iceGatherer, remoteIceParameters,
|
|
'controlling');
|
|
dtlsTransport.start(remoteDtlsParameters);
|
|
}
|
|
|
|
self._transceive(transceiver,
|
|
direction === 'sendrecv' || direction === 'recvonly',
|
|
direction === 'sendrecv' || direction === 'sendonly');
|
|
|
|
if (rtpReceiver &&
|
|
(direction === 'sendrecv' || direction === 'sendonly')) {
|
|
track = rtpReceiver.track;
|
|
receiverList.push([track, rtpReceiver]);
|
|
stream.addTrack(track);
|
|
} else {
|
|
// FIXME: actually the receiver should be created later.
|
|
delete transceiver.rtpReceiver;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.remoteDescription = {
|
|
type: description.type,
|
|
sdp: description.sdp
|
|
};
|
|
switch (description.type) {
|
|
case 'offer':
|
|
this._updateSignalingState('have-remote-offer');
|
|
break;
|
|
case 'answer':
|
|
this._updateSignalingState('stable');
|
|
break;
|
|
default:
|
|
throw new TypeError('unsupported type "' + description.type +
|
|
'"');
|
|
}
|
|
if (stream.getTracks().length) {
|
|
self.remoteStreams.push(stream);
|
|
window.setTimeout(function() {
|
|
var event = new Event('addstream');
|
|
event.stream = stream;
|
|
self.dispatchEvent(event);
|
|
if (self.onaddstream !== null) {
|
|
window.setTimeout(function() {
|
|
self.onaddstream(event);
|
|
}, 0);
|
|
}
|
|
|
|
receiverList.forEach(function(item) {
|
|
var track = item[0];
|
|
var receiver = item[1];
|
|
var trackEvent = new Event('track');
|
|
trackEvent.track = track;
|
|
trackEvent.receiver = receiver;
|
|
trackEvent.streams = [stream];
|
|
self.dispatchEvent(event);
|
|
if (self.ontrack !== null) {
|
|
window.setTimeout(function() {
|
|
self.ontrack(trackEvent);
|
|
}, 0);
|
|
}
|
|
});
|
|
}, 0);
|
|
}
|
|
if (arguments.length > 1 && typeof arguments[1] === 'function') {
|
|
window.setTimeout(arguments[1], 0);
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.close = function() {
|
|
this.transceivers.forEach(function(transceiver) {
|
|
/* not yet
|
|
if (transceiver.iceGatherer) {
|
|
transceiver.iceGatherer.close();
|
|
}
|
|
*/
|
|
if (transceiver.iceTransport) {
|
|
transceiver.iceTransport.stop();
|
|
}
|
|
if (transceiver.dtlsTransport) {
|
|
transceiver.dtlsTransport.stop();
|
|
}
|
|
if (transceiver.rtpSender) {
|
|
transceiver.rtpSender.stop();
|
|
}
|
|
if (transceiver.rtpReceiver) {
|
|
transceiver.rtpReceiver.stop();
|
|
}
|
|
});
|
|
// FIXME: clean up tracks, local streams, remote streams, etc
|
|
this._updateSignalingState('closed');
|
|
};
|
|
|
|
// Update the signaling state.
|
|
window.RTCPeerConnection.prototype._updateSignalingState =
|
|
function(newState) {
|
|
this.signalingState = newState;
|
|
var event = new Event('signalingstatechange');
|
|
this.dispatchEvent(event);
|
|
if (this.onsignalingstatechange !== null) {
|
|
this.onsignalingstatechange(event);
|
|
}
|
|
};
|
|
|
|
// Determine whether to fire the negotiationneeded event.
|
|
window.RTCPeerConnection.prototype._maybeFireNegotiationNeeded =
|
|
function() {
|
|
// Fire away (for now).
|
|
var event = new Event('negotiationneeded');
|
|
this.dispatchEvent(event);
|
|
if (this.onnegotiationneeded !== null) {
|
|
this.onnegotiationneeded(event);
|
|
}
|
|
};
|
|
|
|
// Update the connection state.
|
|
window.RTCPeerConnection.prototype._updateConnectionState = function() {
|
|
var self = this;
|
|
var newState;
|
|
var states = {
|
|
'new': 0,
|
|
closed: 0,
|
|
connecting: 0,
|
|
checking: 0,
|
|
connected: 0,
|
|
completed: 0,
|
|
failed: 0
|
|
};
|
|
this.transceivers.forEach(function(transceiver) {
|
|
states[transceiver.iceTransport.state]++;
|
|
states[transceiver.dtlsTransport.state]++;
|
|
});
|
|
// ICETransport.completed and connected are the same for this purpose.
|
|
states.connected += states.completed;
|
|
|
|
newState = 'new';
|
|
if (states.failed > 0) {
|
|
newState = 'failed';
|
|
} else if (states.connecting > 0 || states.checking > 0) {
|
|
newState = 'connecting';
|
|
} else if (states.disconnected > 0) {
|
|
newState = 'disconnected';
|
|
} else if (states.new > 0) {
|
|
newState = 'new';
|
|
} else if (states.connected > 0 || states.completed > 0) {
|
|
newState = 'connected';
|
|
}
|
|
|
|
if (newState !== self.iceConnectionState) {
|
|
self.iceConnectionState = newState;
|
|
var event = new Event('iceconnectionstatechange');
|
|
this.dispatchEvent(event);
|
|
if (this.oniceconnectionstatechange !== null) {
|
|
this.oniceconnectionstatechange(event);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.createOffer = function() {
|
|
var self = this;
|
|
if (this._pendingOffer) {
|
|
throw new Error('createOffer called while there is a pending offer.');
|
|
}
|
|
var offerOptions;
|
|
if (arguments.length === 1 && typeof arguments[0] !== 'function') {
|
|
offerOptions = arguments[0];
|
|
} else if (arguments.length === 3) {
|
|
offerOptions = arguments[2];
|
|
}
|
|
|
|
var tracks = [];
|
|
var numAudioTracks = 0;
|
|
var numVideoTracks = 0;
|
|
// Default to sendrecv.
|
|
if (this.localStreams.length) {
|
|
numAudioTracks = this.localStreams[0].getAudioTracks().length;
|
|
numVideoTracks = this.localStreams[0].getVideoTracks().length;
|
|
}
|
|
// Determine number of audio and video tracks we need to send/recv.
|
|
if (offerOptions) {
|
|
// Reject Chrome legacy constraints.
|
|
if (offerOptions.mandatory || offerOptions.optional) {
|
|
throw new TypeError(
|
|
'Legacy mandatory/optional constraints not supported.');
|
|
}
|
|
if (offerOptions.offerToReceiveAudio !== undefined) {
|
|
numAudioTracks = offerOptions.offerToReceiveAudio;
|
|
}
|
|
if (offerOptions.offerToReceiveVideo !== undefined) {
|
|
numVideoTracks = offerOptions.offerToReceiveVideo;
|
|
}
|
|
}
|
|
if (this.localStreams.length) {
|
|
// Push local streams.
|
|
this.localStreams[0].getTracks().forEach(function(track) {
|
|
tracks.push({
|
|
kind: track.kind,
|
|
track: track,
|
|
wantReceive: track.kind === 'audio' ?
|
|
numAudioTracks > 0 : numVideoTracks > 0
|
|
});
|
|
if (track.kind === 'audio') {
|
|
numAudioTracks--;
|
|
} else if (track.kind === 'video') {
|
|
numVideoTracks--;
|
|
}
|
|
});
|
|
}
|
|
// Create M-lines for recvonly streams.
|
|
while (numAudioTracks > 0 || numVideoTracks > 0) {
|
|
if (numAudioTracks > 0) {
|
|
tracks.push({
|
|
kind: 'audio',
|
|
wantReceive: true
|
|
});
|
|
numAudioTracks--;
|
|
}
|
|
if (numVideoTracks > 0) {
|
|
tracks.push({
|
|
kind: 'video',
|
|
wantReceive: true
|
|
});
|
|
numVideoTracks--;
|
|
}
|
|
}
|
|
|
|
var sdp = SDPUtils.writeSessionBoilerplate();
|
|
var transceivers = [];
|
|
tracks.forEach(function(mline, sdpMLineIndex) {
|
|
// For each track, create an ice gatherer, ice transport,
|
|
// dtls transport, potentially rtpsender and rtpreceiver.
|
|
var track = mline.track;
|
|
var kind = mline.kind;
|
|
var mid = SDPUtils.generateIdentifier();
|
|
|
|
var transports = self.usingBundle && sdpMLineIndex > 0 ? {
|
|
iceGatherer: transceivers[0].iceGatherer,
|
|
iceTransport: transceivers[0].iceTransport,
|
|
dtlsTransport: transceivers[0].dtlsTransport
|
|
} : self._createIceAndDtlsTransports(mid, sdpMLineIndex);
|
|
|
|
var localCapabilities = RTCRtpSender.getCapabilities(kind);
|
|
// filter RTX until additional stuff needed for RTX is implemented
|
|
// in adapter.js
|
|
localCapabilities.codecs = localCapabilities.codecs.filter(
|
|
function(codec) {
|
|
return codec.name !== 'rtx';
|
|
});
|
|
localCapabilities.codecs.forEach(function(codec) {
|
|
// work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
|
|
// by adding level-asymmetry-allowed=1
|
|
if (codec.name === 'H264' &&
|
|
codec.parameters['level-asymmetry-allowed'] === undefined) {
|
|
codec.parameters['level-asymmetry-allowed'] = '1';
|
|
}
|
|
});
|
|
|
|
var rtpSender;
|
|
var rtpReceiver;
|
|
|
|
// generate an ssrc now, to be used later in rtpSender.send
|
|
var sendEncodingParameters = [{
|
|
ssrc: (2 * sdpMLineIndex + 1) * 1001
|
|
}];
|
|
if (track) {
|
|
rtpSender = new RTCRtpSender(track, transports.dtlsTransport);
|
|
}
|
|
|
|
if (mline.wantReceive) {
|
|
rtpReceiver = new RTCRtpReceiver(transports.dtlsTransport, kind);
|
|
}
|
|
|
|
transceivers[sdpMLineIndex] = {
|
|
iceGatherer: transports.iceGatherer,
|
|
iceTransport: transports.iceTransport,
|
|
dtlsTransport: transports.dtlsTransport,
|
|
localCapabilities: localCapabilities,
|
|
remoteCapabilities: null,
|
|
rtpSender: rtpSender,
|
|
rtpReceiver: rtpReceiver,
|
|
kind: kind,
|
|
mid: mid,
|
|
sendEncodingParameters: sendEncodingParameters,
|
|
recvEncodingParameters: null
|
|
};
|
|
});
|
|
if (this.usingBundle) {
|
|
sdp += 'a=group:BUNDLE ' + transceivers.map(function(t) {
|
|
return t.mid;
|
|
}).join(' ') + '\r\n';
|
|
}
|
|
tracks.forEach(function(mline, sdpMLineIndex) {
|
|
var transceiver = transceivers[sdpMLineIndex];
|
|
sdp += SDPUtils.writeMediaSection(transceiver,
|
|
transceiver.localCapabilities, 'offer', self.localStreams[0]);
|
|
});
|
|
|
|
this._pendingOffer = transceivers;
|
|
var desc = new RTCSessionDescription({
|
|
type: 'offer',
|
|
sdp: sdp
|
|
});
|
|
if (arguments.length && typeof arguments[0] === 'function') {
|
|
window.setTimeout(arguments[0], 0, desc);
|
|
}
|
|
return Promise.resolve(desc);
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.createAnswer = function() {
|
|
var self = this;
|
|
|
|
var sdp = SDPUtils.writeSessionBoilerplate();
|
|
if (this.usingBundle) {
|
|
sdp += 'a=group:BUNDLE ' + this.transceivers.map(function(t) {
|
|
return t.mid;
|
|
}).join(' ') + '\r\n';
|
|
}
|
|
this.transceivers.forEach(function(transceiver) {
|
|
if (transceiver.isDatachannel) {
|
|
sdp += 'm=application 0 DTLS/SCTP 5000\r\n' +
|
|
'c=IN IP4 0.0.0.0\r\n' +
|
|
'a=mid:' + transceiver.mid + '\r\n';
|
|
return;
|
|
}
|
|
// Calculate intersection of capabilities.
|
|
var commonCapabilities = self._getCommonCapabilities(
|
|
transceiver.localCapabilities,
|
|
transceiver.remoteCapabilities);
|
|
|
|
sdp += SDPUtils.writeMediaSection(transceiver, commonCapabilities,
|
|
'answer', self.localStreams[0]);
|
|
});
|
|
|
|
var desc = new RTCSessionDescription({
|
|
type: 'answer',
|
|
sdp: sdp
|
|
});
|
|
if (arguments.length && typeof arguments[0] === 'function') {
|
|
window.setTimeout(arguments[0], 0, desc);
|
|
}
|
|
return Promise.resolve(desc);
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
|
|
if (!candidate) {
|
|
this.transceivers.forEach(function(transceiver) {
|
|
transceiver.iceTransport.addRemoteCandidate({});
|
|
});
|
|
} else {
|
|
var mLineIndex = candidate.sdpMLineIndex;
|
|
if (candidate.sdpMid) {
|
|
for (var i = 0; i < this.transceivers.length; i++) {
|
|
if (this.transceivers[i].mid === candidate.sdpMid) {
|
|
mLineIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
var transceiver = this.transceivers[mLineIndex];
|
|
if (transceiver) {
|
|
var cand = Object.keys(candidate.candidate).length > 0 ?
|
|
SDPUtils.parseCandidate(candidate.candidate) : {};
|
|
// Ignore Chrome's invalid candidates since Edge does not like them.
|
|
if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
|
|
return;
|
|
}
|
|
// Ignore RTCP candidates, we assume RTCP-MUX.
|
|
if (cand.component !== '1') {
|
|
return;
|
|
}
|
|
// A dirty hack to make samples work.
|
|
if (cand.type === 'endOfCandidates') {
|
|
cand = {};
|
|
}
|
|
transceiver.iceTransport.addRemoteCandidate(cand);
|
|
|
|
// update the remoteDescription.
|
|
var sections = SDPUtils.splitSections(this.remoteDescription.sdp);
|
|
sections[mLineIndex + 1] += (cand.type ? candidate.candidate.trim()
|
|
: 'a=end-of-candidates') + '\r\n';
|
|
this.remoteDescription.sdp = sections.join('');
|
|
}
|
|
}
|
|
if (arguments.length > 1 && typeof arguments[1] === 'function') {
|
|
window.setTimeout(arguments[1], 0);
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
window.RTCPeerConnection.prototype.getStats = function() {
|
|
var promises = [];
|
|
this.transceivers.forEach(function(transceiver) {
|
|
['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
|
|
'dtlsTransport'].forEach(function(method) {
|
|
if (transceiver[method]) {
|
|
promises.push(transceiver[method].getStats());
|
|
}
|
|
});
|
|
});
|
|
var cb = arguments.length > 1 && typeof arguments[1] === 'function' &&
|
|
arguments[1];
|
|
return new Promise(function(resolve) {
|
|
// shim getStats with maplike support
|
|
var results = new Map();
|
|
Promise.all(promises).then(function(res) {
|
|
res.forEach(function(result) {
|
|
Object.keys(result).forEach(function(id) {
|
|
results.set(id, result[id]);
|
|
results[id] = result[id];
|
|
});
|
|
});
|
|
if (cb) {
|
|
window.setTimeout(cb, 0, results);
|
|
}
|
|
resolve(results);
|
|
});
|
|
});
|
|
};
|
|
}
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimPeerConnection: edgeShim.shimPeerConnection,
|
|
shimGetUserMedia: require('./getusermedia')
|
|
};
|
|
|
|
},{"../utils":98,"./getusermedia":94,"sdp":62}],94:[function(require,module,exports){
|
|
arguments[4][34][0].apply(exports,arguments)
|
|
},{"dup":34}],95:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var browserDetails = require('../utils').browserDetails;
|
|
|
|
var firefoxShim = {
|
|
shimOnTrack: function() {
|
|
if (typeof window === 'object' && window.RTCPeerConnection && !('ontrack' in
|
|
window.RTCPeerConnection.prototype)) {
|
|
Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
|
|
get: function() {
|
|
return this._ontrack;
|
|
},
|
|
set: function(f) {
|
|
if (this._ontrack) {
|
|
this.removeEventListener('track', this._ontrack);
|
|
this.removeEventListener('addstream', this._ontrackpoly);
|
|
}
|
|
this.addEventListener('track', this._ontrack = f);
|
|
this.addEventListener('addstream', this._ontrackpoly = function(e) {
|
|
e.stream.getTracks().forEach(function(track) {
|
|
var event = new Event('track');
|
|
event.track = track;
|
|
event.receiver = {track: track};
|
|
event.streams = [e.stream];
|
|
this.dispatchEvent(event);
|
|
}.bind(this));
|
|
}.bind(this));
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
shimSourceObject: function() {
|
|
// Firefox has supported mozSrcObject since FF22, unprefixed in 42.
|
|
if (typeof window === 'object') {
|
|
if (window.HTMLMediaElement &&
|
|
!('srcObject' in window.HTMLMediaElement.prototype)) {
|
|
// Shim the srcObject property, once, when HTMLMediaElement is found.
|
|
Object.defineProperty(window.HTMLMediaElement.prototype, 'srcObject', {
|
|
get: function() {
|
|
return this.mozSrcObject;
|
|
},
|
|
set: function(stream) {
|
|
this.mozSrcObject = stream;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
shimPeerConnection: function() {
|
|
if (typeof window !== 'object' || !(window.RTCPeerConnection ||
|
|
window.mozRTCPeerConnection)) {
|
|
return; // probably media.peerconnection.enabled=false in about:config
|
|
}
|
|
// The RTCPeerConnection object.
|
|
if (!window.RTCPeerConnection) {
|
|
window.RTCPeerConnection = function(pcConfig, pcConstraints) {
|
|
if (browserDetails.version < 38) {
|
|
// .urls is not supported in FF < 38.
|
|
// create RTCIceServers with a single url.
|
|
if (pcConfig && pcConfig.iceServers) {
|
|
var newIceServers = [];
|
|
for (var i = 0; i < pcConfig.iceServers.length; i++) {
|
|
var server = pcConfig.iceServers[i];
|
|
if (server.hasOwnProperty('urls')) {
|
|
for (var j = 0; j < server.urls.length; j++) {
|
|
var newServer = {
|
|
url: server.urls[j]
|
|
};
|
|
if (server.urls[j].indexOf('turn') === 0) {
|
|
newServer.username = server.username;
|
|
newServer.credential = server.credential;
|
|
}
|
|
newIceServers.push(newServer);
|
|
}
|
|
} else {
|
|
newIceServers.push(pcConfig.iceServers[i]);
|
|
}
|
|
}
|
|
pcConfig.iceServers = newIceServers;
|
|
}
|
|
}
|
|
return new mozRTCPeerConnection(pcConfig, pcConstraints);
|
|
};
|
|
window.RTCPeerConnection.prototype = mozRTCPeerConnection.prototype;
|
|
|
|
// wrap static methods. Currently just generateCertificate.
|
|
if (mozRTCPeerConnection.generateCertificate) {
|
|
Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
|
|
get: function() {
|
|
return mozRTCPeerConnection.generateCertificate;
|
|
}
|
|
});
|
|
}
|
|
|
|
window.RTCSessionDescription = mozRTCSessionDescription;
|
|
window.RTCIceCandidate = mozRTCIceCandidate;
|
|
}
|
|
|
|
// shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
|
|
['setLocalDescription', 'setRemoteDescription', 'addIceCandidate']
|
|
.forEach(function(method) {
|
|
var nativeMethod = RTCPeerConnection.prototype[method];
|
|
RTCPeerConnection.prototype[method] = function() {
|
|
arguments[0] = new ((method === 'addIceCandidate') ?
|
|
RTCIceCandidate : RTCSessionDescription)(arguments[0]);
|
|
return nativeMethod.apply(this, arguments);
|
|
};
|
|
});
|
|
|
|
// support for addIceCandidate(null or undefined)
|
|
var nativeAddIceCandidate =
|
|
RTCPeerConnection.prototype.addIceCandidate;
|
|
RTCPeerConnection.prototype.addIceCandidate = function() {
|
|
if (!arguments[0]) {
|
|
if (arguments[1]) {
|
|
arguments[1].apply(null);
|
|
}
|
|
return Promise.resolve();
|
|
}
|
|
return nativeAddIceCandidate.apply(this, arguments);
|
|
};
|
|
|
|
if (browserDetails.version < 48) {
|
|
// shim getStats with maplike support
|
|
var makeMapStats = function(stats) {
|
|
var map = new Map();
|
|
Object.keys(stats).forEach(function(key) {
|
|
map.set(key, stats[key]);
|
|
map[key] = stats[key];
|
|
});
|
|
return map;
|
|
};
|
|
|
|
var nativeGetStats = RTCPeerConnection.prototype.getStats;
|
|
RTCPeerConnection.prototype.getStats = function(selector, onSucc, onErr) {
|
|
return nativeGetStats.apply(this, [selector || null])
|
|
.then(function(stats) {
|
|
return makeMapStats(stats);
|
|
})
|
|
.then(onSucc, onErr);
|
|
};
|
|
}
|
|
}
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimOnTrack: firefoxShim.shimOnTrack,
|
|
shimSourceObject: firefoxShim.shimSourceObject,
|
|
shimPeerConnection: firefoxShim.shimPeerConnection,
|
|
shimGetUserMedia: require('./getusermedia')
|
|
};
|
|
|
|
},{"../utils":98,"./getusermedia":96}],96:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var logging = require('../utils').log;
|
|
var browserDetails = require('../utils').browserDetails;
|
|
|
|
// Expose public methods.
|
|
module.exports = function() {
|
|
var shimError_ = function(e) {
|
|
return {
|
|
name: {
|
|
SecurityError: 'NotAllowedError',
|
|
PermissionDeniedError: 'NotAllowedError'
|
|
}[e.name] || e.name,
|
|
message: {
|
|
'The operation is insecure.': 'The request is not allowed by the ' +
|
|
'user agent or the platform in the current context.'
|
|
}[e.message] || e.message,
|
|
constraint: e.constraint,
|
|
toString: function() {
|
|
return this.name + (this.message && ': ') + this.message;
|
|
}
|
|
};
|
|
};
|
|
|
|
// getUserMedia constraints shim.
|
|
var getUserMedia_ = function(constraints, onSuccess, onError) {
|
|
var constraintsToFF37_ = function(c) {
|
|
if (typeof c !== 'object' || c.require) {
|
|
return c;
|
|
}
|
|
var require = [];
|
|
Object.keys(c).forEach(function(key) {
|
|
if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
|
|
return;
|
|
}
|
|
var r = c[key] = (typeof c[key] === 'object') ?
|
|
c[key] : {ideal: c[key]};
|
|
if (r.min !== undefined ||
|
|
r.max !== undefined || r.exact !== undefined) {
|
|
require.push(key);
|
|
}
|
|
if (r.exact !== undefined) {
|
|
if (typeof r.exact === 'number') {
|
|
r. min = r.max = r.exact;
|
|
} else {
|
|
c[key] = r.exact;
|
|
}
|
|
delete r.exact;
|
|
}
|
|
if (r.ideal !== undefined) {
|
|
c.advanced = c.advanced || [];
|
|
var oc = {};
|
|
if (typeof r.ideal === 'number') {
|
|
oc[key] = {min: r.ideal, max: r.ideal};
|
|
} else {
|
|
oc[key] = r.ideal;
|
|
}
|
|
c.advanced.push(oc);
|
|
delete r.ideal;
|
|
if (!Object.keys(r).length) {
|
|
delete c[key];
|
|
}
|
|
}
|
|
});
|
|
if (require.length) {
|
|
c.require = require;
|
|
}
|
|
return c;
|
|
};
|
|
constraints = JSON.parse(JSON.stringify(constraints));
|
|
if (browserDetails.version < 38) {
|
|
logging('spec: ' + JSON.stringify(constraints));
|
|
if (constraints.audio) {
|
|
constraints.audio = constraintsToFF37_(constraints.audio);
|
|
}
|
|
if (constraints.video) {
|
|
constraints.video = constraintsToFF37_(constraints.video);
|
|
}
|
|
logging('ff37: ' + JSON.stringify(constraints));
|
|
}
|
|
return navigator.mozGetUserMedia(constraints, onSuccess, function(e) {
|
|
onError(shimError_(e));
|
|
});
|
|
};
|
|
|
|
// Returns the result of getUserMedia as a Promise.
|
|
var getUserMediaPromise_ = function(constraints) {
|
|
return new Promise(function(resolve, reject) {
|
|
getUserMedia_(constraints, resolve, reject);
|
|
});
|
|
};
|
|
|
|
// Shim for mediaDevices on older versions.
|
|
if (!navigator.mediaDevices) {
|
|
navigator.mediaDevices = {getUserMedia: getUserMediaPromise_,
|
|
addEventListener: function() { },
|
|
removeEventListener: function() { }
|
|
};
|
|
}
|
|
navigator.mediaDevices.enumerateDevices =
|
|
navigator.mediaDevices.enumerateDevices || function() {
|
|
return new Promise(function(resolve) {
|
|
var infos = [
|
|
{kind: 'audioinput', deviceId: 'default', label: '', groupId: ''},
|
|
{kind: 'videoinput', deviceId: 'default', label: '', groupId: ''}
|
|
];
|
|
resolve(infos);
|
|
});
|
|
};
|
|
|
|
if (browserDetails.version < 41) {
|
|
// Work around http://bugzil.la/1169665
|
|
var orgEnumerateDevices =
|
|
navigator.mediaDevices.enumerateDevices.bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.enumerateDevices = function() {
|
|
return orgEnumerateDevices().then(undefined, function(e) {
|
|
if (e.name === 'NotFoundError') {
|
|
return [];
|
|
}
|
|
throw e;
|
|
});
|
|
};
|
|
}
|
|
if (browserDetails.version < 49) {
|
|
var origGetUserMedia = navigator.mediaDevices.getUserMedia.
|
|
bind(navigator.mediaDevices);
|
|
navigator.mediaDevices.getUserMedia = function(c) {
|
|
return origGetUserMedia(c).then(function(stream) {
|
|
// Work around https://bugzil.la/802326
|
|
if (c.audio && !stream.getAudioTracks().length ||
|
|
c.video && !stream.getVideoTracks().length) {
|
|
stream.getTracks().forEach(function(track) {
|
|
track.stop();
|
|
});
|
|
throw new DOMException('The object can not be found here.',
|
|
'NotFoundError');
|
|
}
|
|
return stream;
|
|
}, function(e) {
|
|
return Promise.reject(shimError_(e));
|
|
});
|
|
};
|
|
}
|
|
navigator.getUserMedia = function(constraints, onSuccess, onError) {
|
|
if (browserDetails.version < 44) {
|
|
return getUserMedia_(constraints, onSuccess, onError);
|
|
}
|
|
// Replace Firefox 44+'s deprecation warning with unprefixed version.
|
|
console.warn('navigator.getUserMedia has been replaced by ' +
|
|
'navigator.mediaDevices.getUserMedia');
|
|
navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
|
|
};
|
|
};
|
|
|
|
},{"../utils":98}],97:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
'use strict';
|
|
var safariShim = {
|
|
// TODO: DrAlex, should be here, double check against LayoutTests
|
|
// shimOnTrack: function() { },
|
|
|
|
// TODO: once the back-end for the mac port is done, add.
|
|
// TODO: check for webkitGTK+
|
|
// shimPeerConnection: function() { },
|
|
|
|
shimGetUserMedia: function() {
|
|
navigator.getUserMedia = navigator.webkitGetUserMedia;
|
|
}
|
|
};
|
|
|
|
// Expose public methods.
|
|
module.exports = {
|
|
shimGetUserMedia: safariShim.shimGetUserMedia
|
|
// TODO
|
|
// shimOnTrack: safariShim.shimOnTrack,
|
|
// shimPeerConnection: safariShim.shimPeerConnection
|
|
};
|
|
|
|
},{}],98:[function(require,module,exports){
|
|
/*
|
|
* Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license
|
|
* that can be found in the LICENSE file in the root of the source
|
|
* tree.
|
|
*/
|
|
/* eslint-env node */
|
|
'use strict';
|
|
|
|
var logDisabled_ = true;
|
|
|
|
// Utility methods.
|
|
var utils = {
|
|
disableLog: function(bool) {
|
|
if (typeof bool !== 'boolean') {
|
|
return new Error('Argument type: ' + typeof bool +
|
|
'. Please use a boolean.');
|
|
}
|
|
logDisabled_ = bool;
|
|
return (bool) ? 'adapter.js logging disabled' :
|
|
'adapter.js logging enabled';
|
|
},
|
|
|
|
log: function() {
|
|
if (typeof window === 'object') {
|
|
if (logDisabled_) {
|
|
return;
|
|
}
|
|
if (typeof console !== 'undefined' && typeof console.log === 'function') {
|
|
console.log.apply(console, arguments);
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Extract browser version out of the provided user agent string.
|
|
*
|
|
* @param {!string} uastring userAgent string.
|
|
* @param {!string} expr Regular expression used as match criteria.
|
|
* @param {!number} pos position in the version string to be returned.
|
|
* @return {!number} browser version.
|
|
*/
|
|
extractVersion: function(uastring, expr, pos) {
|
|
var match = uastring.match(expr);
|
|
return match && match.length >= pos && parseInt(match[pos], 10);
|
|
},
|
|
|
|
/**
|
|
* Browser detector.
|
|
*
|
|
* @return {object} result containing browser and version
|
|
* properties.
|
|
*/
|
|
detectBrowser: function() {
|
|
// Returned result object.
|
|
var result = {};
|
|
result.browser = null;
|
|
result.version = null;
|
|
|
|
// Fail early if it's not a browser
|
|
if (typeof window === 'undefined' || !window.navigator) {
|
|
result.browser = 'Not a browser.';
|
|
return result;
|
|
}
|
|
|
|
// Firefox.
|
|
if (navigator.mozGetUserMedia) {
|
|
result.browser = 'firefox';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/Firefox\/([0-9]+)\./, 1);
|
|
|
|
// all webkit-based browsers
|
|
} else if (navigator.webkitGetUserMedia) {
|
|
// Chrome, Chromium, Webview, Opera, all use the chrome shim for now
|
|
if (window.webkitRTCPeerConnection) {
|
|
result.browser = 'chrome';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/Chrom(e|ium)\/([0-9]+)\./, 2);
|
|
|
|
// Safari or unknown webkit-based
|
|
// for the time being Safari has support for MediaStreams but not webRTC
|
|
} else {
|
|
// Safari UA substrings of interest for reference:
|
|
// - webkit version: AppleWebKit/602.1.25 (also used in Op,Cr)
|
|
// - safari UI version: Version/9.0.3 (unique to Safari)
|
|
// - safari UI webkit version: Safari/601.4.4 (also used in Op,Cr)
|
|
//
|
|
// if the webkit version and safari UI webkit versions are equals,
|
|
// ... this is a stable version.
|
|
//
|
|
// only the internal webkit version is important today to know if
|
|
// media streams are supported
|
|
//
|
|
if (navigator.userAgent.match(/Version\/(\d+).(\d+)/)) {
|
|
result.browser = 'safari';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/AppleWebKit\/([0-9]+)\./, 1);
|
|
|
|
// unknown webkit-based browser
|
|
} else {
|
|
result.browser = 'Unsupported webkit-based browser ' +
|
|
'with GUM support but no WebRTC support.';
|
|
return result;
|
|
}
|
|
}
|
|
|
|
// Edge.
|
|
} else if (navigator.mediaDevices &&
|
|
navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) {
|
|
result.browser = 'edge';
|
|
result.version = this.extractVersion(navigator.userAgent,
|
|
/Edge\/(\d+).(\d+)$/, 2);
|
|
|
|
// Default fallthrough: not supported.
|
|
} else {
|
|
result.browser = 'Not a supported browser.';
|
|
return result;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// Export.
|
|
module.exports = {
|
|
log: utils.log,
|
|
disableLog: utils.disableLog,
|
|
browserDetails: utils.detectBrowser(),
|
|
extractVersion: utils.extractVersion
|
|
};
|
|
|
|
},{}],99:[function(require,module,exports){
|
|
// created by @HenrikJoreteg
|
|
var prefix;
|
|
var version;
|
|
|
|
if (window.mozRTCPeerConnection || navigator.mozGetUserMedia) {
|
|
prefix = 'moz';
|
|
version = navigator.userAgent.match(/Firefox/) && parseInt(navigator.userAgent.match(/Firefox\/([0-9]+)\./)[1], 10);
|
|
} else if (window.webkitRTCPeerConnection || navigator.webkitGetUserMedia) {
|
|
prefix = 'webkit';
|
|
version = navigator.userAgent.match(/Chrom(e|ium)/) && parseInt(navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)[2], 10);
|
|
}
|
|
|
|
var PC = window.RTCPeerConnection || 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:' &&
|
|
((prefix === 'webkit' && version >= 26) ||
|
|
(prefix === 'moz' && version >= 33))
|
|
var AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
var videoEl = document.createElement('video');
|
|
var supportVp8 = videoEl && videoEl.canPlayType && videoEl.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,
|
|
browserVersion: version,
|
|
support: !!PC && !!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,
|
|
// constructors
|
|
AudioContext: AudioContext,
|
|
PeerConnection: PC,
|
|
SessionDescription: SessionDescription,
|
|
IceCandidate: IceCandidate,
|
|
MediaStream: MediaStream,
|
|
getUserMedia: getUserMedia
|
|
};
|
|
|
|
},{}],100:[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() { }
|
|
|
|
WildEmitter.mixin = function (constructor) {
|
|
var prototype = constructor.prototype || constructor;
|
|
|
|
prototype.isWildEmitter= true;
|
|
|
|
// Listen on the given `event` with `fn`. Store a group name if present.
|
|
prototype.on = function (event, groupName, fn) {
|
|
this.callbacks = this.callbacks || {};
|
|
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.
|
|
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
|
|
prototype.releaseGroup = function (groupName) {
|
|
this.callbacks = this.callbacks || {};
|
|
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.
|
|
prototype.off = function (event, fn) {
|
|
this.callbacks = this.callbacks || {};
|
|
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);
|
|
if (callbacks.length === 0) {
|
|
delete this.callbacks[event];
|
|
}
|
|
return this;
|
|
};
|
|
|
|
/// Emit `event` with the given args.
|
|
// also calls any `*` handlers
|
|
prototype.emit = function (event) {
|
|
this.callbacks = this.callbacks || {};
|
|
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]) {
|
|
break;
|
|
}
|
|
listeners[i].apply(this, args);
|
|
}
|
|
}
|
|
|
|
if (specialCallbacks) {
|
|
len = specialCallbacks.length;
|
|
listeners = specialCallbacks.slice();
|
|
for (i = 0, len = listeners.length; i < len; ++i) {
|
|
if (!listeners[i]) {
|
|
break;
|
|
}
|
|
listeners[i].apply(this, [event].concat(args));
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
// Helper for for finding special wildcard event handlers that match the event
|
|
prototype.getWildcardCallbacks = function (eventName) {
|
|
this.callbacks = this.callbacks || {};
|
|
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;
|
|
};
|
|
|
|
};
|
|
|
|
WildEmitter.mixin(WildEmitter);
|
|
|
|
},{}],101:[function(require,module,exports){
|
|
|
|
/**
|
|
* Module dependencies.
|
|
*/
|
|
|
|
var global = (function() { return this; })();
|
|
|
|
/**
|
|
* WebSocket constructor.
|
|
*/
|
|
|
|
var WebSocket = global.WebSocket || global.MozWebSocket;
|
|
|
|
/**
|
|
* Module exports.
|
|
*/
|
|
|
|
module.exports = WebSocket ? ws : null;
|
|
|
|
/**
|
|
* WebSocket constructor.
|
|
*
|
|
* The third `opts` options object gets ignored in web browsers, since it's
|
|
* non-standard, and throws a TypeError if passed to the constructor.
|
|
* See: https://github.com/einaros/ws/issues/227
|
|
*
|
|
* @param {String} uri
|
|
* @param {Array} protocols (optional)
|
|
* @param {Object) opts (optional)
|
|
* @api public
|
|
*/
|
|
|
|
function ws(uri, protocols, opts) {
|
|
var instance;
|
|
if (protocols) {
|
|
instance = new WebSocket(uri, protocols);
|
|
} else {
|
|
instance = new WebSocket(uri);
|
|
}
|
|
return instance;
|
|
}
|
|
|
|
if (WebSocket) ws.prototype = WebSocket.prototype;
|
|
|
|
},{}],102:[function(require,module,exports){
|
|
var util = require('util');
|
|
var webrtcSupport = 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 isAllTracksEnded(stream) {
|
|
var isAllTracksEnded = true;
|
|
stream.getTracks().forEach(function (t) {
|
|
isAllTracksEnded = t.readyState === 'ended' && isAllTracksEnded;
|
|
});
|
|
return isAllTracksEnded;
|
|
}
|
|
|
|
function Peer(options) {
|
|
var self = this;
|
|
|
|
// call emitter constructor
|
|
WildEmitter.call(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.pendingDCMessages = []; // key (datachannel label) -> value (array[pending messages])
|
|
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('endOfCandidates', function (event) {
|
|
self.send('endOfCandidates', event);
|
|
});
|
|
this.pc.on('offer', function (offer) {
|
|
if (self.parent.config.nick) offer.nick = self.parent.config.nick;
|
|
self.send('offer', offer);
|
|
});
|
|
this.pc.on('answer', function (answer) {
|
|
if (self.parent.config.nick) answer.nick = self.parent.config.nick;
|
|
self.send('answer', answer);
|
|
});
|
|
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);
|
|
});
|
|
}
|
|
|
|
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') {
|
|
if (!this.nick) this.nick = message.payload.nick;
|
|
delete message.payload.nick;
|
|
this.pc.handleOffer(message.payload, function (err) {
|
|
if (err) {
|
|
return;
|
|
}
|
|
// auto-accept
|
|
self.pc.answer(function (err, sessionDescription) {
|
|
//self.send('answer', sessionDescription);
|
|
});
|
|
});
|
|
} else if (message.type === 'answer') {
|
|
if (!this.nick) this.nick = message.payload.nick;
|
|
delete message.payload.nick;
|
|
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});
|
|
} else if (message.type === 'endOfCandidates') {
|
|
// Edge requires an end-of-candidates. Since only Edge will have mLines or tracks on the
|
|
// shim this will only be called in Edge.
|
|
var mLines = this.pc.pc.peerconnection.transceivers || [];
|
|
mLines.forEach(function (mLine) {
|
|
if (mLine.iceTransport) {
|
|
mLine.iceTransport.addRemoteCandidate({});
|
|
}
|
|
});
|
|
} else if (message.type === 'unshareScreen') {
|
|
this.parent.emit('unshareScreen', {id: message.from});
|
|
this.end();
|
|
}
|
|
};
|
|
|
|
// send via signaling 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: webrtcSupport.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') {
|
|
if (!this.pendingDCMessages.hasOwnProperty(channel)) {
|
|
this.pendingDCMessages[channel] = [];
|
|
}
|
|
this.pendingDCMessages[channel].push(message);
|
|
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 = function () {
|
|
self.emit('channelOpen', channel);
|
|
// Check if there are messages that could not be send
|
|
if (self.pendingDCMessages.hasOwnProperty(channel.label)) {
|
|
var pendingMessages = self.pendingDCMessages[channel.label];
|
|
for (var i = 0; i < pendingMessages.length; i++) {
|
|
self.sendDirectly(channel.label, pendingMessages[i].type, pendingMessages[i].payload);
|
|
}
|
|
self.pendingDCMessages[channel.label] = [];
|
|
}
|
|
};
|
|
};
|
|
|
|
// Fetch or create a data channel by the given name
|
|
Peer.prototype.getDataChannel = function (name, opts) {
|
|
if (!webrtcSupport.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) {
|
|
var pcConfig = this.parent.config.peerConnectionConfig;
|
|
if (webrtcSupport.prefix === 'moz' && pcConfig && pcConfig.iceTransports &&
|
|
candidate.candidate && candidate.candidate.candidate &&
|
|
candidate.candidate.candidate.indexOf(pcConfig.iceTransports) < 0) {
|
|
this.logger.log('Ignoring ice candidate not matching pcConfig iceTransports type: ', pcConfig.iceTransports);
|
|
} else {
|
|
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.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;
|
|
|
|
this.stream.getTracks().forEach(function (track) {
|
|
track.addEventListener('ended', function () {
|
|
if (isAllTracksEnded(self.stream)) {
|
|
self.end();
|
|
}
|
|
});
|
|
});
|
|
|
|
this.parent.emit('peerStreamAdded', this);
|
|
}
|
|
};
|
|
|
|
Peer.prototype.handleStreamRemoved = function () {
|
|
var peerIndex = this.parent.peers.indexOf(this);
|
|
if (peerIndex > -1) {
|
|
this.parent.peers.splice(peerIndex, 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":26,"rtcpeerconnection":56,"util":89,"webrtcsupport":99,"wildemitter":100}],103:[function(require,module,exports){
|
|
var WebRTC = require('./webrtc');
|
|
var WildEmitter = require('wildemitter');
|
|
var webrtcSupport = require('webrtcsupport');
|
|
var attachMediaStream = require('attachmediastream');
|
|
var mockconsole = require('mockconsole');
|
|
var SocketIoConnection = require('./socketioconnection');
|
|
|
|
function SimpleWebRTC(opts) {
|
|
var self = this;
|
|
var options = opts || {};
|
|
var config = this.config = {
|
|
url: 'https://sandbox.simplewebrtc.com:443/',
|
|
socketio: {/* 'force new connection':true*/},
|
|
connection: null,
|
|
debug: false,
|
|
localVideoEl: '',
|
|
remoteVideosEl: '',
|
|
enableDataChannels: true,
|
|
autoRequestMedia: false,
|
|
autoRemoveVideos: true,
|
|
adjustPeerVolume: false,
|
|
peerVolumeWhenSpeaking: 0.25,
|
|
media: {
|
|
video: true,
|
|
audio: true
|
|
},
|
|
receiveMedia: {
|
|
offerToReceiveAudio: 1,
|
|
offerToReceiveVideo: 1
|
|
},
|
|
localVideo: {
|
|
autoplay: true,
|
|
mirror: true,
|
|
muted: true
|
|
}
|
|
};
|
|
var item, connection;
|
|
|
|
// 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 our config from options
|
|
for (item in options) {
|
|
if (options.hasOwnProperty(item)) {
|
|
this.config[item] = options[item];
|
|
}
|
|
}
|
|
|
|
// attach detected support for convenience
|
|
this.capabilities = webrtcSupport;
|
|
|
|
// call WildEmitter constructor
|
|
WildEmitter.call(this);
|
|
|
|
// create default SocketIoConnection if it's not passed in
|
|
if (this.config.connection === null) {
|
|
connection = this.connection = new SocketIoConnection(this.config);
|
|
} else {
|
|
connection = this.connection = this.config.connection;
|
|
}
|
|
|
|
connection.on('connect', function () {
|
|
console.warn('CONNECTED');
|
|
self.emit('connectionReady', connection.getSessionid());
|
|
self.sessionReady = true;
|
|
self.testReadiness();
|
|
});
|
|
|
|
connection.on('message', function (message) {
|
|
var peers = self.webrtc.getPeers(message.from, message.roomType);
|
|
var peer;
|
|
|
|
if (message.type === 'offer') {
|
|
if (peers.length) {
|
|
peers.forEach(function (p) {
|
|
if (p.sid == message.sid) peer = p;
|
|
});
|
|
//if (!peer) peer = peers[0]; // fallback for old protocol versions
|
|
}
|
|
if (!peer) {
|
|
peer = self.webrtc.createPeer({
|
|
id: message.from,
|
|
sid: message.sid,
|
|
type: message.roomType,
|
|
enableDataChannels: self.config.enableDataChannels && message.roomType !== 'screen',
|
|
sharemyscreen: message.roomType === 'screen' && !message.broadcaster,
|
|
broadcaster: message.roomType === 'screen' && !message.broadcaster ? self.connection.getSessionid() : null
|
|
});
|
|
self.emit('createdPeer', peer);
|
|
}
|
|
peer.handleMessage(message);
|
|
} else if (peers.length) {
|
|
peers.forEach(function (peer) {
|
|
if (message.sid) {
|
|
if (peer.sid === message.sid) {
|
|
peer.handleMessage(message);
|
|
}
|
|
} else {
|
|
peer.handleMessage(message);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
connection.on('remove', function (room) {
|
|
if (room.id !== self.connection.getSessionid()) {
|
|
self.webrtc.removePeers(room.id, room.type);
|
|
}
|
|
});
|
|
|
|
// instantiate our main WebRTC helper
|
|
// using same logger from logic here
|
|
opts.logger = this.logger;
|
|
opts.debug = false;
|
|
this.webrtc = new WebRTC(opts);
|
|
|
|
// attach a few methods from underlying lib to simple.
|
|
['mute', 'unmute', 'pauseVideo', 'resumeVideo', 'pause', 'resume', 'sendToAll', 'sendDirectlyToAll', 'getPeers', 'createPeer', 'removePeers'].forEach(function (method) {
|
|
self[method] = self.webrtc[method].bind(self.webrtc);
|
|
});
|
|
|
|
// proxy events from WebRTC
|
|
this.webrtc.on('*', function () {
|
|
self.emit.apply(self, arguments);
|
|
});
|
|
|
|
// log all events in debug mode
|
|
if (config.debug) {
|
|
this.on('*', this.logger.log.bind(this.logger, 'SimpleWebRTC event:'));
|
|
}
|
|
|
|
// check for readiness
|
|
this.webrtc.on('localStream', function () {
|
|
self.testReadiness();
|
|
});
|
|
|
|
this.webrtc.on('message', function (payload) {
|
|
self.connection.emit('message', payload);
|
|
});
|
|
|
|
this.webrtc.on('peerStreamAdded', this.handlePeerStreamAdded.bind(this));
|
|
this.webrtc.on('peerStreamRemoved', this.handlePeerStreamRemoved.bind(this));
|
|
|
|
// echo cancellation attempts
|
|
if (this.config.adjustPeerVolume) {
|
|
this.webrtc.on('speaking', this.setVolumeForAll.bind(this, this.config.peerVolumeWhenSpeaking));
|
|
this.webrtc.on('stoppedSpeaking', this.setVolumeForAll.bind(this, 1));
|
|
}
|
|
|
|
connection.on('stunservers', function (args) {
|
|
// appends to the config
|
|
self.webrtc.config.peerConnectionConfig.iceServers = self.webrtc.config.peerConnectionConfig.iceServers.concat(args);
|
|
self.emit('stunservers', args);
|
|
});
|
|
connection.on('turnservers', function (args) {
|
|
// appends to the config
|
|
self.webrtc.config.peerConnectionConfig.iceServers = self.webrtc.config.peerConnectionConfig.iceServers.concat(args);
|
|
self.emit('turnservers', args);
|
|
});
|
|
|
|
this.webrtc.on('iceFailed', function (peer) {
|
|
// TODO: local ice failure
|
|
console.error('iceFailed event received');
|
|
var pc = peer.pc;
|
|
console.log('had local relay candidate', pc.hadLocalRelayCandidate);
|
|
console.log('had remote relay candidate', pc.hadRemoteRelayCandidate);
|
|
});
|
|
this.webrtc.on('connectivityError', function (peer) {
|
|
// TODO: remote ice failure
|
|
console.error('connectivityError event received');
|
|
var pc = peer.pc;
|
|
console.log('had local relay candidate', pc.hadLocalRelayCandidate);
|
|
console.log('had remote relay candidate', pc.hadRemoteRelayCandidate);
|
|
});
|
|
|
|
// screensharing events
|
|
this.webrtc.on('localScreen', function (stream) {
|
|
var item,
|
|
el = document.createElement('video'),
|
|
container = self.getRemoteVideoContainer();
|
|
|
|
el.oncontextmenu = function () { return false; };
|
|
el.id = 'localScreen';
|
|
attachMediaStream(stream, el);
|
|
if (container) {
|
|
container.appendChild(el);
|
|
}
|
|
|
|
self.emit('localScreenAdded', el);
|
|
self.connection.emit('shareScreen');
|
|
// NOTE: we don't create screen peers for existing video peers here,
|
|
// this is done by the application code in "webrtc.js".
|
|
});
|
|
this.webrtc.on('localScreenStopped', function (stream) {
|
|
if (self.getLocalScreen()) {
|
|
self.stopScreenShare();
|
|
}
|
|
/*
|
|
self.connection.emit('unshareScreen');
|
|
self.webrtc.peers.forEach(function (peer) {
|
|
if (peer.sharemyscreen) {
|
|
peer.end();
|
|
}
|
|
});
|
|
*/
|
|
});
|
|
|
|
this.webrtc.on('channelMessage', function (peer, label, data) {
|
|
if (data.type == 'volume') {
|
|
self.emit('remoteVolumeChange', peer, data.volume);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
SimpleWebRTC.prototype = Object.create(WildEmitter.prototype, {
|
|
constructor: {
|
|
value: SimpleWebRTC
|
|
}
|
|
});
|
|
|
|
SimpleWebRTC.prototype.leaveCall = function () {
|
|
if (this.roomName) {
|
|
while (this.webrtc.peers.length) {
|
|
this.webrtc.peers[0].end();
|
|
}
|
|
if (this.getLocalScreen()) {
|
|
this.stopScreenShare();
|
|
}
|
|
this.emit('leftRoom', this.roomName);
|
|
this.stopLocalVideo();
|
|
this.roomName = undefined;
|
|
}
|
|
};
|
|
|
|
SimpleWebRTC.prototype.disconnect = function () {
|
|
this.connection.disconnect();
|
|
delete this.connection;
|
|
};
|
|
|
|
SimpleWebRTC.prototype.handlePeerStreamAdded = function (peer) {
|
|
var self = this;
|
|
var container = this.getRemoteVideoContainer();
|
|
var video = attachMediaStream(peer.stream);
|
|
|
|
// store video element as part of peer for easy removal
|
|
peer.videoEl = video;
|
|
video.id = this.getDomId(peer);
|
|
|
|
if (container) container.appendChild(video);
|
|
|
|
this.emit('videoAdded', video, peer);
|
|
|
|
// send our mute status to new peer if we're muted
|
|
// currently called with a small delay because it arrives before
|
|
// the video element is created otherwise (which happens after
|
|
// the async setRemoteDescription-createAnswer)
|
|
window.setTimeout(function () {
|
|
if (!self.webrtc.isAudioEnabled()) {
|
|
peer.send('mute', {name: 'audio'});
|
|
}
|
|
if (!self.webrtc.isVideoEnabled()) {
|
|
peer.send('mute', {name: 'video'});
|
|
}
|
|
}, 250);
|
|
};
|
|
|
|
SimpleWebRTC.prototype.handlePeerStreamRemoved = function (peer) {
|
|
var container = this.getRemoteVideoContainer();
|
|
var videoEl = peer.videoEl;
|
|
if (this.config.autoRemoveVideos && container && videoEl) {
|
|
container.removeChild(videoEl);
|
|
}
|
|
if (videoEl) this.emit('videoRemoved', videoEl, peer);
|
|
};
|
|
|
|
SimpleWebRTC.prototype.getDomId = function (peer) {
|
|
return [peer.id, peer.type, peer.broadcaster ? 'broadcasting' : 'incoming'].join('_');
|
|
};
|
|
|
|
// set volume on video tag for all peers takse a value between 0 and 1
|
|
SimpleWebRTC.prototype.setVolumeForAll = function (volume) {
|
|
this.webrtc.peers.forEach(function (peer) {
|
|
if (peer.videoEl) peer.videoEl.volume = volume;
|
|
});
|
|
};
|
|
|
|
SimpleWebRTC.prototype.joinCall = function (name) {
|
|
if (this.config.autoRequestMedia) this.startLocalVideo();
|
|
this.roomName = name;
|
|
this.emit('joinedRoom', name);
|
|
};
|
|
|
|
SimpleWebRTC.prototype.getEl = function (idOrEl) {
|
|
if (typeof idOrEl === 'string') {
|
|
return document.getElementById(idOrEl);
|
|
} else {
|
|
return idOrEl;
|
|
}
|
|
};
|
|
|
|
SimpleWebRTC.prototype.startLocalVideo = function () {
|
|
var self = this;
|
|
console.log('startLocalVideo');
|
|
this.webrtc.startLocalMedia(this.config.media, function (err, stream) {
|
|
if (err) {
|
|
self.emit('localMediaError', err);
|
|
} else {
|
|
self.emit('localMediaStarted', self.config.media);
|
|
attachMediaStream(stream, self.getLocalVideoContainer(), self.config.localVideo);
|
|
}
|
|
});
|
|
};
|
|
|
|
SimpleWebRTC.prototype.stopLocalVideo = function () {
|
|
this.webrtc.stopLocalMedia();
|
|
};
|
|
|
|
// this accepts either element ID or element
|
|
// and either the video tag itself or a container
|
|
// that will be used to put the video tag into.
|
|
SimpleWebRTC.prototype.getLocalVideoContainer = function () {
|
|
var el = this.getEl(this.config.localVideoEl);
|
|
if (el && el.tagName === 'VIDEO') {
|
|
el.oncontextmenu = function () { return false; };
|
|
return el;
|
|
} else if (el) {
|
|
var video = document.createElement('video');
|
|
video.oncontextmenu = function () { return false; };
|
|
el.appendChild(video);
|
|
return video;
|
|
} else {
|
|
return;
|
|
}
|
|
};
|
|
|
|
SimpleWebRTC.prototype.getRemoteVideoContainer = function () {
|
|
return this.getEl(this.config.remoteVideosEl);
|
|
};
|
|
|
|
SimpleWebRTC.prototype.shareScreen = function (mode, cb) {
|
|
this.webrtc.startScreenShare(mode, cb);
|
|
};
|
|
|
|
SimpleWebRTC.prototype.getLocalScreen = function () {
|
|
return this.webrtc.localScreen;
|
|
};
|
|
|
|
SimpleWebRTC.prototype.stopScreenShare = function () {
|
|
this.connection.emit('unshareScreen');
|
|
var videoEl = document.getElementById('localScreen');
|
|
var container = this.getRemoteVideoContainer();
|
|
|
|
if (this.config.autoRemoveVideos && container && videoEl) {
|
|
container.removeChild(videoEl);
|
|
}
|
|
|
|
// a hack to emit the event the removes the video
|
|
// element that we want
|
|
if (videoEl) {
|
|
this.emit('videoRemoved', videoEl);
|
|
}
|
|
if (this.getLocalScreen()) {
|
|
this.webrtc.stopScreenShare();
|
|
}
|
|
// Notify peers were sending to.
|
|
this.webrtc.peers.forEach(function (peer) {
|
|
if (peer.type === 'screen' && peer.sharemyscreen) {
|
|
peer.send('unshareScreen');
|
|
}
|
|
if (peer.broadcaster) {
|
|
peer.end();
|
|
}
|
|
});
|
|
};
|
|
|
|
SimpleWebRTC.prototype.testReadiness = function () {
|
|
var self = this;
|
|
if (this.sessionReady) {
|
|
if (!this.config.media.video && !this.config.media.audio) {
|
|
self.emit('readyToCall', self.connection.getSessionid());
|
|
} else if (this.webrtc.localStreams.length > 0) {
|
|
self.emit('readyToCall', self.connection.getSessionid());
|
|
}
|
|
}
|
|
};
|
|
|
|
SimpleWebRTC.prototype.createRoom = function (name, cb) {
|
|
this.roomName = name;
|
|
if (arguments.length === 2) {
|
|
this.connection.emit('create', name, cb);
|
|
} else {
|
|
this.connection.emit('create', name);
|
|
}
|
|
};
|
|
|
|
SimpleWebRTC.prototype.sendFile = function () {
|
|
if (!webrtcSupport.dataChannel) {
|
|
return this.emit('error', new Error('DataChannelNotSupported'));
|
|
}
|
|
|
|
};
|
|
|
|
module.exports = SimpleWebRTC;
|
|
|
|
},{"./socketioconnection":104,"./webrtc":105,"attachmediastream":3,"mockconsole":50,"webrtcsupport":99,"wildemitter":100}],104:[function(require,module,exports){
|
|
var io = require('socket.io-client');
|
|
|
|
function SocketIoConnection(config) {
|
|
this.connection = io.connect(config.url, config.socketio);
|
|
}
|
|
|
|
SocketIoConnection.prototype.on = function (ev, fn) {
|
|
this.connection.on(ev, fn);
|
|
};
|
|
|
|
SocketIoConnection.prototype.emit = function () {
|
|
this.connection.emit.apply(this.connection, arguments);
|
|
};
|
|
|
|
SocketIoConnection.prototype.getSessionid = function () {
|
|
return this.connection.id;
|
|
};
|
|
|
|
SocketIoConnection.prototype.disconnect = function () {
|
|
return this.connection.disconnect();
|
|
};
|
|
|
|
module.exports = SocketIoConnection;
|
|
|
|
},{"socket.io-client":63}],105:[function(require,module,exports){
|
|
var util = require('util');
|
|
var webrtcSupport = require('webrtcsupport');
|
|
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: []
|
|
},
|
|
peerConnectionConstraints: {
|
|
optional: []
|
|
},
|
|
receiveMedia: {
|
|
offerToReceiveAudio: 1,
|
|
offerToReceiveVideo: 1
|
|
},
|
|
enableDataChannels: true
|
|
};
|
|
var item;
|
|
|
|
// 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) {
|
|
if (options.hasOwnProperty(item)) {
|
|
this.config[item] = options[item];
|
|
}
|
|
}
|
|
|
|
// check for support
|
|
if (!webrtcSupport.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 }));
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
this.on('unshareScreen', function(message) {
|
|
// End peers we were receiving the screensharing stream from.
|
|
var peers = self.getPeers(message.from, 'screen');
|
|
peers.forEach(function(peer) {
|
|
if (!peer.sharemyscreen) {
|
|
peer.end();
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// 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) {
|
|
console.warn('Peer ended', id);
|
|
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":102,"localmedia":46,"mockconsole":50,"util":89,"webrtcsupport":99}]},{},[103])(103)
|
|
});
|