mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-11-11 21:58:41 +02:00
--HG-- rename : static/shared/README.md => browser/components/loop/content/shared/README.md rename : static/shared/css/common.css => browser/components/loop/content/shared/css/common.css rename : static/shared/css/conversation.css => browser/components/loop/content/shared/css/conversation.css rename : static/shared/css/panel.css => browser/components/loop/content/shared/css/panel.css rename : static/shared/css/readme.html => browser/components/loop/content/shared/css/readme.html rename : static/shared/img/icon_32.png => browser/components/loop/content/shared/img/icon_32.png rename : static/shared/img/icon_64.png => browser/components/loop/content/shared/img/icon_64.png rename : static/shared/js/models.js => browser/components/loop/content/shared/js/models.js rename : static/shared/js/views.js => browser/components/loop/content/shared/js/views.js rename : static/shared/libs/backbone-1.1.2.js => browser/components/loop/content/shared/libs/backbone-1.1.2.js rename : static/shared/libs/jquery-2.1.0.js => browser/components/loop/content/shared/libs/jquery-2.1.0.js rename : static/shared/libs/lodash-2.4.1.js => browser/components/loop/content/shared/libs/lodash-2.4.1.js rename : static/shared/libs/sdk.js => browser/components/loop/content/shared/libs/sdk.js rename : static/shared/libs/webl10n-20130617.js => browser/components/loop/content/shared/libs/webl10n-20130617.js rename : static/css/webapp.css => browser/components/loop/standalone/content/css/webapp.css rename : static/index.html => browser/components/loop/standalone/content/index.html rename : static/js/webapp.js => browser/components/loop/standalone/content/js/webapp.js rename : static/l10n/data.ini => browser/components/loop/standalone/content/l10n/data.ini rename : test/webapp_test.js => browser/components/loop/test/standalone/webapp_test.js extra : transplant_source : %DA%D9%3A%E9%C6%E0d%13%84%C1%BEps%C8b%F09o%D7m
15256 lines
488 KiB
JavaScript
15256 lines
488 KiB
JavaScript
/**
|
|
* @license OpenTok JavaScript Library v2.0.18.1
|
|
* http://www.tokbox.com/
|
|
*
|
|
* Copyright (c) 2013 TokBox, Inc.
|
|
*
|
|
* Date: March 18 06:20:17 2014
|
|
*/
|
|
|
|
(function(window) {
|
|
if (!window.OT) window.OT = {};
|
|
|
|
OT.properties = {
|
|
version: "v2.0.18.1", // The current version (eg. v2.0.4) (This is replaced by gradle)
|
|
build: "7333ca3", // The current build hash (This is replaced by gradle)
|
|
|
|
debug: "false", // Whether or not to turn on debug logging by default
|
|
websiteURL: "http://www.tokbox.com", // The URL of the tokbox website
|
|
|
|
cdnURL: "http://static.opentok.com", // The URL of the CDN
|
|
loggingURL: "http://hlg.tokbox.com/prod", // The URL to use for logging
|
|
apiURL: "http://anvil.opentok.com", // The anvil API URL
|
|
|
|
messagingProtocol: "wss", // What protocol to use when connecting to the rumor web socket
|
|
messagingPort: 443, // What port to use when connection to the rumor web socket
|
|
|
|
supportSSL: "true", // If this environment supports SSL
|
|
cdnURLSSL: "https://static.opentok.com", // The CDN to use if we're using SSL
|
|
loggingURLSSL: "https://hlg.tokbox.com/prod", // The loggging URL to use if we're using SSL
|
|
apiURLSSL: "https://anvil.opentok.com" // The anvil API URL to use if we're using SSL
|
|
};
|
|
|
|
})(window);
|
|
/**
|
|
* @license Common JS Helpers on OpenTok 0.1.0 628a0b9 master
|
|
* http://www.tokbox.com/
|
|
*
|
|
* Copyright (c) 2014 TokBox, Inc.
|
|
*
|
|
* Date: January 08 04:13:37 2014
|
|
*
|
|
*/
|
|
|
|
// OT Helper Methods
|
|
//
|
|
// helpers.js <- the root file
|
|
// helpers/lib/{helper topic}.js <- specialised helpers for specific tasks/topics
|
|
// (i.e. video, dom, etc)
|
|
//
|
|
// @example Getting a DOM element by it's id
|
|
// var element = OTHelpers('domId');
|
|
//
|
|
// @example Testing for web socket support
|
|
// if (OT.supportsWebSockets()) {
|
|
// // do some stuff with websockets
|
|
// }
|
|
//
|
|
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
!(function(window, undefined) {
|
|
|
|
|
|
var OTHelpers = function(domId) {
|
|
return document.getElementById(domId);
|
|
};
|
|
|
|
var previousOTHelpers = window.OTHelpers;
|
|
|
|
window.OTHelpers = OTHelpers;
|
|
|
|
OTHelpers.noConflict = function() {
|
|
OTHelpers.noConflict = function() {
|
|
return OTHelpers;
|
|
};
|
|
window.OTHelpers = previousOTHelpers;
|
|
return OTHelpers;
|
|
};
|
|
|
|
|
|
OTHelpers.isEmpty = function(obj) {
|
|
if (obj === null || obj === undefined) return true;
|
|
if (Array.isArray(obj) || typeof(obj) === 'string') return obj.length === 0;
|
|
|
|
// Objects without enumerable owned properties are empty.
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
OTHelpers.isNone = function(obj) {
|
|
return obj === undefined || obj === null;
|
|
};
|
|
|
|
OTHelpers.isObject = function(obj) {
|
|
return obj === Object(obj);
|
|
};
|
|
|
|
|
|
OTHelpers.isFunction = function(obj) {
|
|
return typeof obj === 'function';
|
|
};
|
|
|
|
// Extend a target object with the properties from one or
|
|
// more source objects
|
|
//
|
|
// @example:
|
|
// dest = OTHelpers.extend(dest, source1, source2, source3);
|
|
//
|
|
OTHelpers.extend = function(/* dest, source1[, source2, ..., , sourceN]*/) {
|
|
var sources = Array.prototype.slice.call(arguments),
|
|
dest = sources.shift();
|
|
|
|
sources.forEach(function(source) {
|
|
for (var key in source) {
|
|
dest[key] = source[key];
|
|
}
|
|
});
|
|
|
|
return dest;
|
|
};
|
|
|
|
// Ensures that the target object contains certain defaults.
|
|
//
|
|
// @example
|
|
// var options = OTHelpers.defaults(options, {
|
|
// loading: true // loading by default
|
|
// });
|
|
//
|
|
OTHelpers.defaults = function(/* dest, defaults1[, defaults2, ..., , defaultsN]*/) {
|
|
var sources = Array.prototype.slice.call(arguments),
|
|
dest = sources.shift();
|
|
|
|
sources.forEach(function(source) {
|
|
for (var key in source) {
|
|
if (dest[key] === void 0) dest[key] = source[key];
|
|
}
|
|
});
|
|
|
|
return dest;
|
|
};
|
|
|
|
//
|
|
OTHelpers.clone = function(obj) {
|
|
if (!OTHelpers.isObject(obj)) return obj;
|
|
return Array.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj);
|
|
};
|
|
|
|
|
|
|
|
// Handy do nothing function
|
|
OTHelpers.noop = function() {};
|
|
|
|
// Returns true if the client supports WebSockets, false otherwise.
|
|
OTHelpers.supportsWebSockets = function() {
|
|
return 'WebSocket' in window;
|
|
};
|
|
|
|
// Returns the number of millisceonds since the the UNIX epoch, this is functionally
|
|
// equivalent to executing new Date().getTime().
|
|
//
|
|
// Where available, we use 'performance.now' which is more accurate and reliable,
|
|
// otherwise we default to new Date().getTime().
|
|
OTHelpers.now = (function() {
|
|
var performance = window.performance || {},
|
|
navigationStart,
|
|
now = performance.now ||
|
|
performance.mozNow ||
|
|
performance.msNow ||
|
|
performance.oNow ||
|
|
performance.webkitNow;
|
|
|
|
if (now) {
|
|
now = now.bind(performance);
|
|
navigationStart = performance.timing.navigationStart;
|
|
|
|
return function() { return navigationStart + now(); };
|
|
}
|
|
else {
|
|
return function() { return new Date().getTime(); };
|
|
}
|
|
})();
|
|
|
|
OTHelpers.browser = function() {
|
|
var userAgent = window.navigator.userAgent.toLowerCase(),
|
|
navigatorVendor,
|
|
browser = 'Unknown';
|
|
|
|
if (userAgent.indexOf('firefox') > -1) {
|
|
browser = 'Firefox';
|
|
}
|
|
if (userAgent.indexOf('opera') > -1) {
|
|
browser = 'Opera';
|
|
}
|
|
else if (userAgent.indexOf("msie") > -1) {
|
|
browser = "IE";
|
|
}
|
|
else if (userAgent.indexOf("chrome") > -1) {
|
|
browser = "Chrome";
|
|
}
|
|
|
|
if ((navigatorVendor = window.navigator.vendor) && navigatorVendor.toLowerCase().indexOf("apple") > -1) {
|
|
browser = "Safari";
|
|
}
|
|
|
|
userAgent = null;
|
|
OTHelpers.browser = function() { return browser; };
|
|
return browser;
|
|
};
|
|
|
|
|
|
OTHelpers.canDefineProperty = true;
|
|
|
|
try {
|
|
Object.defineProperty({}, 'x', {});
|
|
} catch (err) {
|
|
OTHelpers.canDefineProperty = false;
|
|
}
|
|
|
|
// A helper for defining a number of getters at once.
|
|
//
|
|
// @example: from inside an object
|
|
// OTHelpers.defineGetters(this, {
|
|
// apiKey: function() { return _apiKey; },
|
|
// token: function() { return _token; },
|
|
// connected: function() { return this.is('connected'); },
|
|
// connection: function() { return _socket && _socket.id ? this.connections.get(_socket.id) : null; },
|
|
// capabilities: function() { return _socket.capabilities; },
|
|
// sessionId: function() { return _sessionId; },
|
|
// id: function() { return _sessionId; }
|
|
// });
|
|
//
|
|
OTHelpers.defineGetters = function(self, getters, enumerable) {
|
|
var propsDefinition = {};
|
|
|
|
if (enumerable === void 0) enumerable = false;
|
|
|
|
for (var key in getters) {
|
|
propsDefinition[key] = {
|
|
get: getters[key],
|
|
enumerable: enumerable
|
|
};
|
|
}
|
|
|
|
Object.defineProperties(self, propsDefinition);
|
|
};
|
|
|
|
|
|
// Polyfill Object.create for IE8
|
|
//
|
|
// See https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create
|
|
//
|
|
if (!Object.create) {
|
|
Object.create = function (o) {
|
|
if (arguments.length > 1) {
|
|
throw new Error('Object.create implementation only accepts the first parameter.');
|
|
}
|
|
function F() {}
|
|
F.prototype = o;
|
|
return new F();
|
|
};
|
|
}
|
|
|
|
OTHelpers.setCookie = function(key, value) {
|
|
try {
|
|
localStorage.setItem(key, value);
|
|
} catch (err) {
|
|
// Store in browser cookie
|
|
var date = new Date();
|
|
date.setTime(date.getTime()+(365*24*60*60*1000));
|
|
var expires = "; expires="+date.toGMTString();
|
|
document.cookie = key+"="+value+expires+"; path=/";
|
|
}
|
|
};
|
|
|
|
OTHelpers.getCookie = function(key) {
|
|
var value;
|
|
|
|
try {
|
|
value = localStorage.getItem("opentok_client_id");
|
|
return value;
|
|
} catch (err) {
|
|
// Check browser cookies
|
|
var nameEQ = key + "=";
|
|
var ca = document.cookie.split(';');
|
|
for(var i=0;i < ca.length;i++) {
|
|
var c = ca[i];
|
|
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
|
if (c.indexOf(nameEQ) === 0) {
|
|
value = c.substring(nameEQ.length,c.length);
|
|
}
|
|
}
|
|
|
|
if (value) {
|
|
return value;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
|
|
/// Stolen from Underscore, this needs replacing
|
|
|
|
|
|
// Invert the keys and values of an object. The values must be serializable.
|
|
OTHelpers.invert = function(obj) {
|
|
var result = {};
|
|
for (var key in obj) if (obj.hasOwnProperty(key)) result[obj[key]] = key;
|
|
return result;
|
|
};
|
|
|
|
|
|
// List of HTML entities for escaping.
|
|
var entityMap = {
|
|
escape: {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": ''',
|
|
'/': '/'
|
|
}
|
|
};
|
|
entityMap.unescape = OTHelpers.invert(entityMap.escape);
|
|
|
|
// Regexes containing the keys and values listed immediately above.
|
|
var entityRegexes = {
|
|
escape: new RegExp('[' + Object.keys(entityMap.escape).join('') + ']', 'g'),
|
|
unescape: new RegExp('(' + Object.keys(entityMap.unescape).join('|') + ')', 'g')
|
|
};
|
|
|
|
// Functions for escaping and unescaping strings to/from HTML interpolation.
|
|
['escape', 'unescape'].forEach(function(method) {
|
|
OTHelpers[method] = function(string) {
|
|
if (string === null || string === undefined) return '';
|
|
return ('' + string).replace(entityRegexes[method], function(match) {
|
|
return entityMap[method][match];
|
|
});
|
|
};
|
|
});
|
|
|
|
// By default, Underscore uses ERB-style template delimiters, change the
|
|
// following template settings to use alternative delimiters.
|
|
OTHelpers.templateSettings = {
|
|
evaluate : /<%([\s\S]+?)%>/g,
|
|
interpolate : /<%=([\s\S]+?)%>/g,
|
|
escape : /<%-([\s\S]+?)%>/g
|
|
};
|
|
|
|
// When customizing `templateSettings`, if you don't want to define an
|
|
// interpolation, evaluation or escaping regex, we need one that is
|
|
// guaranteed not to match.
|
|
var noMatch = /(.)^/;
|
|
|
|
// Certain characters need to be escaped so that they can be put into a
|
|
// string literal.
|
|
var escapes = {
|
|
"'": "'",
|
|
'\\': '\\',
|
|
'\r': 'r',
|
|
'\n': 'n',
|
|
'\t': 't',
|
|
'\u2028': 'u2028',
|
|
'\u2029': 'u2029'
|
|
};
|
|
|
|
var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
|
|
|
|
// JavaScript micro-templating, similar to John Resig's implementation.
|
|
// Underscore templating handles arbitrary delimiters, preserves whitespace,
|
|
// and correctly escapes quotes within interpolated code.
|
|
OTHelpers.template = function(text, data, settings) {
|
|
var render;
|
|
settings = OTHelpers.defaults({}, settings, OTHelpers.templateSettings);
|
|
|
|
// Combine delimiters into one regular expression via alternation.
|
|
var matcher = new RegExp([
|
|
(settings.escape || noMatch).source,
|
|
(settings.interpolate || noMatch).source,
|
|
(settings.evaluate || noMatch).source
|
|
].join('|') + '|$', 'g');
|
|
|
|
// Compile the template source, escaping string literals appropriately.
|
|
var index = 0;
|
|
var source = "__p+='";
|
|
text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
|
|
source += text.slice(index, offset)
|
|
.replace(escaper, function(match) { return '\\' + escapes[match]; });
|
|
|
|
if (escape) {
|
|
source += "'+\n((__t=(" + escape + "))==null?'':OTHelpers.escape(__t))+\n'";
|
|
}
|
|
if (interpolate) {
|
|
source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
|
|
}
|
|
if (evaluate) {
|
|
source += "';\n" + evaluate + "\n__p+='";
|
|
}
|
|
index = offset + match.length;
|
|
return match;
|
|
});
|
|
source += "';\n";
|
|
|
|
// If a variable is not specified, place data values in local scope.
|
|
if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
|
|
|
|
source = "var __t,__p='',__j=Array.prototype.join," +
|
|
"print=function(){__p+=__j.call(arguments,'');};\n" +
|
|
source + "return __p;\n";
|
|
|
|
|
|
try {
|
|
// evil is necessary for the new Function line
|
|
/*jshint evil:true */
|
|
render = new Function(settings.variable || 'obj', source);
|
|
} catch (e) {
|
|
e.source = source;
|
|
throw e;
|
|
}
|
|
|
|
if (data) return render(data);
|
|
var template = function(data) {
|
|
return render.call(this, data);
|
|
};
|
|
|
|
// Provide the compiled function source as a convenience for precompilation.
|
|
template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
|
|
|
|
return template;
|
|
};
|
|
|
|
})(window);
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../../helpers.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
OTHelpers.statable = function(self, possibleStates, initialState, stateChanged, stateChangedFailed) {
|
|
var previousState,
|
|
currentState = initialState;
|
|
|
|
var setState = function(state) {
|
|
if (currentState !== state) {
|
|
if (possibleStates.indexOf(state) === -1) {
|
|
if (stateChangedFailed && OTHelpers.isFunction(stateChangedFailed)) stateChangedFailed('invalidState', state);
|
|
return;
|
|
}
|
|
|
|
previousState = currentState;
|
|
currentState = state;
|
|
if (stateChanged && OTHelpers.isFunction(stateChanged)) stateChanged(state, previousState);
|
|
}
|
|
};
|
|
|
|
|
|
// Returns a number of states and returns true if the current state
|
|
// is any of them.
|
|
//
|
|
// @example
|
|
// if (this.is('connecting', 'connected')) {
|
|
// // do some stuff
|
|
// }
|
|
//
|
|
self.is = function (/* state0:String, state1:String, ..., stateN:String */) {
|
|
return Array.prototype.indexOf.call(arguments, currentState) !== -1;
|
|
};
|
|
|
|
|
|
// Returns a number of states and returns true if the current state
|
|
// is none of them.
|
|
//
|
|
// @example
|
|
// if (this.isNot('connecting', 'connected')) {
|
|
// // do some stuff
|
|
// }
|
|
//
|
|
self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) {
|
|
return Array.prototype.indexOf.call(arguments, currentState) === -1;
|
|
};
|
|
|
|
Object.defineProperties(self, {
|
|
state: {
|
|
get: function() { return currentState; }
|
|
},
|
|
|
|
previousState: {
|
|
get: function() { return previousState; }
|
|
}
|
|
});
|
|
|
|
return setState;
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
/*!
|
|
* This is a modified version of Robert Kieffer awesome uuid.js library.
|
|
* The only modifications we've made are to remove the Node.js specific
|
|
* parts of the code and the UUID version 1 generator (which we don't
|
|
* use). The original copyright notice is below.
|
|
*
|
|
* node-uuid/uuid.js
|
|
*
|
|
* Copyright (c) 2010 Robert Kieffer
|
|
* Dual licensed under the MIT and GPL licenses.
|
|
* Documentation and details at https://github.com/broofa/node-uuid
|
|
*/
|
|
// tb_require('../helpers.js')
|
|
|
|
/*global crypto:true, Uint32Array:true, Buffer:true */
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
// Unique ID creation requires a high quality random # generator, but
|
|
// Math.random() does not guarantee "cryptographic quality". So we feature
|
|
// detect for more robust APIs, normalizing each method to return 128-bits
|
|
// (16 bytes) of random data.
|
|
var mathRNG, whatwgRNG;
|
|
|
|
// Math.random()-based RNG. All platforms, very fast, unknown quality
|
|
var _rndBytes = new Array(16);
|
|
mathRNG = function() {
|
|
var r, b = _rndBytes, i = 0;
|
|
|
|
for (i = 0; i < 16; i++) {
|
|
if ((i & 0x03) === 0) r = Math.random() * 0x100000000;
|
|
b[i] = r >>> ((i & 0x03) << 3) & 0xff;
|
|
}
|
|
|
|
return b;
|
|
};
|
|
|
|
// WHATWG crypto-based RNG - http://wiki.whatwg.org/wiki/Crypto
|
|
// WebKit only (currently), moderately fast, high quality
|
|
if (window.crypto && crypto.getRandomValues) {
|
|
var _rnds = new Uint32Array(4);
|
|
whatwgRNG = function() {
|
|
crypto.getRandomValues(_rnds);
|
|
|
|
for (var c = 0 ; c < 16; c++) {
|
|
_rndBytes[c] = _rnds[c >> 2] >>> ((c & 0x03) * 8) & 0xff;
|
|
}
|
|
return _rndBytes;
|
|
};
|
|
}
|
|
|
|
// Select RNG with best quality
|
|
var _rng = whatwgRNG || mathRNG;
|
|
|
|
// Buffer class to use
|
|
var BufferClass = typeof(Buffer) == 'function' ? Buffer : Array;
|
|
|
|
// Maps for number <-> hex string conversion
|
|
var _byteToHex = [];
|
|
var _hexToByte = {};
|
|
for (var i = 0; i < 256; i++) {
|
|
_byteToHex[i] = (i + 0x100).toString(16).substr(1);
|
|
_hexToByte[_byteToHex[i]] = i;
|
|
}
|
|
|
|
// **`parse()` - Parse a UUID into it's component bytes**
|
|
function parse(s, buf, offset) {
|
|
var i = (buf && offset) || 0, ii = 0;
|
|
|
|
buf = buf || [];
|
|
s.toLowerCase().replace(/[0-9a-f]{2}/g, function(oct) {
|
|
if (ii < 16) { // Don't overflow!
|
|
buf[i + ii++] = _hexToByte[oct];
|
|
}
|
|
});
|
|
|
|
// Zero out remaining bytes if string was short
|
|
while (ii < 16) {
|
|
buf[i + ii++] = 0;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
// **`unparse()` - Convert UUID byte array (ala parse()) into a string**
|
|
function unparse(buf, offset) {
|
|
var i = offset || 0, bth = _byteToHex;
|
|
return bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] + '-' +
|
|
bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]] +
|
|
bth[buf[i++]] + bth[buf[i++]];
|
|
}
|
|
|
|
// **`v4()` - Generate random UUID**
|
|
|
|
// See https://github.com/broofa/node-uuid for API details
|
|
function v4(options, buf, offset) {
|
|
// Deprecated - 'format' argument, as supported in v1.2
|
|
var i = buf && offset || 0;
|
|
|
|
if (typeof(options) == 'string') {
|
|
buf = options == 'binary' ? new BufferClass(16) : null;
|
|
options = null;
|
|
}
|
|
options = options || {};
|
|
|
|
var rnds = options.random || (options.rng || _rng)();
|
|
|
|
// Per 4.4, set bits for version and `clock_seq_hi_and_reserved`
|
|
rnds[6] = (rnds[6] & 0x0f) | 0x40;
|
|
rnds[8] = (rnds[8] & 0x3f) | 0x80;
|
|
|
|
// Copy bytes to buffer, if provided
|
|
if (buf) {
|
|
for (var ii = 0; ii < 16; ii++) {
|
|
buf[i + ii] = rnds[ii];
|
|
}
|
|
}
|
|
|
|
return buf || unparse(rnds);
|
|
}
|
|
|
|
// Export public API
|
|
var uuid = v4;
|
|
uuid.v4 = v4;
|
|
uuid.parse = parse;
|
|
uuid.unparse = unparse;
|
|
uuid.BufferClass = BufferClass;
|
|
|
|
// Export RNG options
|
|
uuid.mathRNG = mathRNG;
|
|
uuid.whatwgRNG = whatwgRNG;
|
|
|
|
OTHelpers.uuid = uuid;
|
|
|
|
}(window, window.OTHelpers));
|
|
// AJAX helpers
|
|
|
|
/*jshint browser:true, smarttabs:true*/
|
|
/*global ActiveXObject:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
// Shim up XML support for IE8
|
|
function shimXMLForIE8(xml) {
|
|
// https://developer.mozilla.org/en/DOM/Element.firstElementChild
|
|
Object.defineProperty(xml.prototype, "firstElementChild", {
|
|
"get" : function() {
|
|
var node = this;
|
|
node = node.firstChild;
|
|
while(node && node.nodeType != 1) node = node.nextSibling;
|
|
return node;
|
|
}
|
|
});
|
|
|
|
// https://developer.mozilla.org/En/DOM/Element.lastElementChild
|
|
Object.defineProperty(xml.prototype, "lastElementChild", {
|
|
"get" : function() {
|
|
var node = this;
|
|
node = node.lastChild;
|
|
while(node && node.nodeType != 1) node = node.previousSibling;
|
|
return node;
|
|
}
|
|
});
|
|
|
|
// https://developer.mozilla.org/En/DOM/Element.nextElementSibling
|
|
Object.defineProperty(xml.prototype, "nextElementSibling", {
|
|
"get" : function() {
|
|
var node = this;
|
|
while(!OTHelpers.isNone(node = node.nextSibling)) {
|
|
if(node.nodeType == 1) break;
|
|
}
|
|
|
|
return node;
|
|
}
|
|
});
|
|
|
|
// https://developer.mozilla.org/En/DOM/Element.previousElementSibling
|
|
Object.defineProperty(xml.prototype, "previousElementSibling", {
|
|
"get" : function() {
|
|
var node = this;
|
|
while(!OTHelpers.isNone(node = node.previousSibling)) {
|
|
if(node.nodeType == 1) break;
|
|
}
|
|
return node;
|
|
}
|
|
});
|
|
}
|
|
|
|
OTHelpers.parseXML = function(xmlString) {
|
|
var root, xml;
|
|
|
|
if (window.DOMParser) { // Standard IE9 + everyone else
|
|
xml = (new DOMParser()).parseFromString(xmlString, "text/xml");
|
|
} else { // <= IE8
|
|
xml = new ActiveXObject("Microsoft.XMLDOM");
|
|
xml.async = "false";
|
|
xml.loadXML(xmlString);
|
|
|
|
shimXMLForIE8(xml);
|
|
}
|
|
|
|
root = xml.documentElement;
|
|
|
|
if (!root || !root.nodeName || root.nodeName === "parsererror") {
|
|
// Badly formed XML
|
|
return null;
|
|
}
|
|
|
|
return xml;
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
OTHelpers.useLogHelpers = function(on){
|
|
|
|
// Log levels for OTLog.setLogLevel
|
|
on.DEBUG = 5;
|
|
on.LOG = 4;
|
|
on.INFO = 3;
|
|
on.WARN = 2;
|
|
on.ERROR = 1;
|
|
on.NONE = 0;
|
|
|
|
var _logLevel = on.NONE,
|
|
_logs = [];
|
|
|
|
// Generates a logging method for a particular method and log level.
|
|
//
|
|
// Attempts to handle the following cases:
|
|
// * the desired log method doesn't exist, call fallback (if available) instead
|
|
// * the console functionality isn't available because the developer tools (in IE)
|
|
// aren't open, call fallback (if available)
|
|
// * attempt to deal with weird IE hosted logging methods as best we can.
|
|
//
|
|
function generateLoggingMethod(method, level, fallback) {
|
|
return function() {
|
|
if (on.shouldLog(level)) {
|
|
var cons = window.console;
|
|
|
|
// In IE, window.console may not exist if the developer tools aren't open
|
|
if (cons && cons[method]) {
|
|
// the desired console method isn't a real object, which means
|
|
// that we can't use apply on it. We force it to be a real object
|
|
// using Function.bind, assuming that's available.
|
|
if (cons[method].apply || Function.prototype.bind) {
|
|
if (!cons[method].apply) {
|
|
cons[method] = Function.prototype.bind.call(cons[method], cons);
|
|
}
|
|
|
|
cons[method].apply(cons, arguments);
|
|
}
|
|
else {
|
|
// This isn't the same result as the above, but it's better
|
|
// than nothing.
|
|
cons[method](
|
|
Array.prototype.slice.apply(arguments).join(' ')
|
|
);
|
|
}
|
|
}
|
|
else if (fallback) {
|
|
fallback.apply(on, arguments);
|
|
}
|
|
|
|
appendToLogs(method, arguments);
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
on.log = generateLoggingMethod('log', on.LOG);
|
|
|
|
// Generate debug, info, warn, and error logging methods, these all fallback to on.log
|
|
on.debug = generateLoggingMethod('debug', on.DEBUG, on.log);
|
|
on.info = generateLoggingMethod('info', on.INFO, on.log);
|
|
on.warn = generateLoggingMethod('warn', on.WARN, on.log);
|
|
on.error = generateLoggingMethod('error', on.ERROR, on.log);
|
|
|
|
|
|
on.setLogLevel = function(level) {
|
|
_logLevel = typeof(level) === 'number' ? level : 0;
|
|
on.debug("TB.setLogLevel(" + _logLevel + ")");
|
|
return _logLevel;
|
|
};
|
|
|
|
on.getLogs = function() {
|
|
return _logs;
|
|
};
|
|
|
|
// Determine if the level is visible given the current logLevel.
|
|
on.shouldLog = function(level) {
|
|
return _logLevel >= level;
|
|
};
|
|
|
|
// Format the current time nicely for logging. Returns the current
|
|
// local time.
|
|
function formatDateStamp() {
|
|
var now = new Date();
|
|
return now.toLocaleTimeString() + now.getMilliseconds();
|
|
}
|
|
|
|
|
|
// Append +args+ to logs, along with the current log level and the a date stamp.
|
|
function appendToLogs(level, args) {
|
|
if (!args) return;
|
|
|
|
var message;
|
|
|
|
try {
|
|
message = JSON.stringify(args);
|
|
} catch(e) {
|
|
message = args.toString();
|
|
}
|
|
|
|
if (message.length <= 2) return;
|
|
|
|
_logs.push(
|
|
[level, formatDateStamp(), message]
|
|
);
|
|
}
|
|
|
|
};
|
|
|
|
OTHelpers.useLogHelpers(OTHelpers);
|
|
OTHelpers.setLogLevel(OTHelpers.ERROR);
|
|
|
|
})(window, window.OTHelpers);
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
OTHelpers.castToBoolean = function(value, defaultValue) {
|
|
if (value === undefined) return defaultValue;
|
|
return value === 'true' || value === true;
|
|
};
|
|
|
|
OTHelpers.roundFloat = function(value, places) {
|
|
return Number(value.toFixed(places));
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
// tb_require('../vendor/uuid.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
var timeouts = [],
|
|
messageName = "OTHelpers." + OTHelpers.uuid.v4() + ".zero-timeout";
|
|
|
|
var handleMessage = function(event) {
|
|
// XXX In chrome windows, event.source is not defined, so the full check
|
|
// doesn't work. I'm not even convinced they need the window check anyway.
|
|
// if (event.source == window && event.data == messageName) {
|
|
if (event.data == messageName) {
|
|
event.stopPropagation();
|
|
if (timeouts.length > 0) {
|
|
var args = timeouts.shift(),
|
|
fn = args.shift();
|
|
|
|
fn.apply(null, args);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener("message", handleMessage, true);
|
|
|
|
// Calls the function +fn+ asynchronously with the current execution.
|
|
// This is most commonly used to execute something straight after
|
|
// the current function.
|
|
//
|
|
// Any arguments in addition to +fn+ will be passed to +fn+ when it's
|
|
// called.
|
|
//
|
|
// You would use this inplace of setTimeout(fn, 0) type constructs. callAsync
|
|
// is preferable as it executes in a much more predictable time window,
|
|
// unlike setTimeout which could execute anywhere from 2ms to several thousand
|
|
// depending on the browser/context.
|
|
//
|
|
OTHelpers.callAsync = function (/* fn, [arg1, arg2, ..., argN] */) {
|
|
timeouts.push(Array.prototype.slice.call(arguments));
|
|
window.postMessage(messageName, "*");
|
|
};
|
|
|
|
|
|
// Wraps +handler+ in a function that will execute it asynchronously
|
|
// so that it doesn't interfere with it's exceution context if it raises
|
|
// an exception.
|
|
OTHelpers.createAsyncHandler = function(handler) {
|
|
return function() {
|
|
var args = Array.prototype.slice.call(arguments);
|
|
|
|
OTHelpers.callAsync(function() {
|
|
handler.apply(null, args);
|
|
});
|
|
};
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
/*jshint browser:true, smarttabs:true*/
|
|
/*global jasmine:true*/
|
|
|
|
// tb_require('../../helpers.js')
|
|
// tb_require('../callbacks.js')
|
|
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
/**
|
|
* This base class defines the <code>addEventListener()</code> and <code>removeEventListner()</code> methods of objects
|
|
* that can dispatch events.
|
|
*
|
|
* @class EventDispatcher
|
|
*/
|
|
OTHelpers.eventing = function(self, syncronous) {
|
|
var _events = {};
|
|
|
|
|
|
// Call the defaultAction, passing args
|
|
function executeDefaultAction(defaultAction, args) {
|
|
if (!defaultAction) return;
|
|
|
|
defaultAction.apply(null, args.slice());
|
|
}
|
|
|
|
// Execute each handler in +listeners+ with +args+.
|
|
//
|
|
// Each handler will be executed async. On completion the defaultAction
|
|
// handler will be executed with the args.
|
|
//
|
|
// @param [Array] listeners
|
|
// An array of functions to execute. Each will be passed args.
|
|
//
|
|
// @param [Array] args
|
|
// An array of arguments to execute each function in +listeners+ with.
|
|
//
|
|
// @param [String] name
|
|
// The name of this event.
|
|
//
|
|
// @param [Function, Null, Undefined] defaultAction
|
|
// An optional function to execute after every other handler. This will execute even
|
|
// if +listeners+ is empty. +defaultAction+ will be passed args as a normal
|
|
// handler would.
|
|
//
|
|
// @return Undefined
|
|
//
|
|
function executeListenersAsyncronously(name, args, defaultAction) {
|
|
var listeners = _events[name];
|
|
if (!listeners || listeners.length === 0) return;
|
|
|
|
var listenerAcks = listeners.length;
|
|
|
|
listeners.forEach(function(listener) { // , index
|
|
function filterHandlerAndContext(_listener) {
|
|
return _listener.context === listener.context && _listener.handler === listener.handler;
|
|
}
|
|
|
|
// We run this asynchronously so that it doesn't interfere with execution if an error happens
|
|
OTHelpers.callAsync(function() {
|
|
try {
|
|
// have to check if the listener has not been removed
|
|
if (_events[name] && _events[name].some(filterHandlerAndContext)) {
|
|
(listener.closure || listener.handler).apply(listener.context || null, args);
|
|
}
|
|
}
|
|
finally {
|
|
listenerAcks--;
|
|
|
|
if (listenerAcks === 0) {
|
|
executeDefaultAction(defaultAction, args);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
// This is identical to executeListenersAsyncronously except that handlers will
|
|
// be executed syncronously.
|
|
//
|
|
// On completion the defaultAction handler will be executed with the args.
|
|
//
|
|
// @param [Array] listeners
|
|
// An array of functions to execute. Each will be passed args.
|
|
//
|
|
// @param [Array] args
|
|
// An array of arguments to execute each function in +listeners+ with.
|
|
//
|
|
// @param [String] name
|
|
// The name of this event.
|
|
//
|
|
// @param [Function, Null, Undefined] defaultAction
|
|
// An optional function to execute after every other handler. This will execute even
|
|
// if +listeners+ is empty. +defaultAction+ will be passed args as a normal
|
|
// handler would.
|
|
//
|
|
// @return Undefined
|
|
//
|
|
function executeListenersSyncronously(name, args) { // defaultAction is not used
|
|
var listeners = _events[name];
|
|
if (!listeners || listeners.length === 0) return;
|
|
|
|
listeners.forEach(function(listener) { // index
|
|
(listener.closure || listener.handler).apply(listener.context || null, args);
|
|
});
|
|
}
|
|
|
|
var executeListeners = syncronous === true ? executeListenersSyncronously : executeListenersAsyncronously;
|
|
|
|
|
|
var removeAllListenersNamed = function (eventName, context) {
|
|
if (_events[eventName]) {
|
|
if (context) {
|
|
// We are removing by context, get only events that don't
|
|
// match that context
|
|
_events[eventName] = _events[eventName].filter(function(listener){
|
|
return listener.context !== context;
|
|
});
|
|
}
|
|
else {
|
|
delete _events[eventName];
|
|
}
|
|
}
|
|
};
|
|
|
|
var addListeners = function (eventNames, handler, context, closure) {
|
|
var listener = {handler: handler};
|
|
if (context) listener.context = context;
|
|
if (closure) listener.closure = closure;
|
|
|
|
eventNames.forEach(function(name) {
|
|
if (!_events[name]) _events[name] = [];
|
|
_events[name].push(listener);
|
|
});
|
|
}.bind(self);
|
|
|
|
|
|
var removeListeners = function (eventNames, handler, context) {
|
|
function filterHandlerAndContext(listener) {
|
|
return !(listener.handler === handler && listener.context === context);
|
|
}
|
|
|
|
eventNames.forEach(function(name) {
|
|
if (_events[name]) {
|
|
_events[name] = _events[name].filter(filterHandlerAndContext);
|
|
if (_events[name].length === 0) delete _events[name];
|
|
}
|
|
});
|
|
}.bind(self);
|
|
|
|
|
|
// Execute any listeners bound to the +event+ Event.
|
|
//
|
|
// Each handler will be executed async. On completion the defaultAction
|
|
// handler will be executed with the args.
|
|
//
|
|
// @param [Event] event
|
|
// An Event object.
|
|
//
|
|
// @param [Function, Null, Undefined] defaultAction
|
|
// An optional function to execute after every other handler. This will execute even
|
|
// if there are listeners bound to this event. +defaultAction+ will be passed
|
|
// args as a normal handler would.
|
|
//
|
|
// @return this
|
|
//
|
|
self.dispatchEvent = function(event, defaultAction) {
|
|
if (!event.type) {
|
|
OTHelpers.error("OTHelpers.Eventing.dispatchEvent: Event has no type");
|
|
OTHelpers.error(event);
|
|
|
|
throw new Error("OTHelpers.Eventing.dispatchEvent: Event has no type");
|
|
}
|
|
|
|
if (!event.target) {
|
|
event.target = this;
|
|
}
|
|
|
|
if (!_events[event.type] || _events[event.type].length === 0) {
|
|
executeDefaultAction(defaultAction, [event]);
|
|
return;
|
|
}
|
|
|
|
executeListeners(event.type, [event], defaultAction);
|
|
|
|
return this;
|
|
};
|
|
|
|
// Execute each handler for the event called +name+.
|
|
//
|
|
// Each handler will be executed async, and any exceptions that they throw will
|
|
// be caught and logged
|
|
//
|
|
// How to pass these?
|
|
// * defaultAction
|
|
//
|
|
// @example
|
|
// foo.on('bar', function(name, message) {
|
|
// alert("Hello " + name + ": " + message);
|
|
// });
|
|
//
|
|
// foo.trigger('OpenTok', 'asdf'); // -> Hello OpenTok: asdf
|
|
//
|
|
//
|
|
// @param [String] eventName
|
|
// The name of this event.
|
|
//
|
|
// @param [Array] arguments
|
|
// Any additional arguments beyond +eventName+ will be passed to the handlers.
|
|
//
|
|
// @return this
|
|
//
|
|
self.trigger = function(eventName) {
|
|
if (!_events[eventName] || _events[eventName].length === 0) {
|
|
return;
|
|
}
|
|
|
|
var args = Array.prototype.slice.call(arguments);
|
|
|
|
// Remove the eventName arg
|
|
args.shift();
|
|
|
|
executeListeners(eventName, args);
|
|
|
|
return this;
|
|
};
|
|
|
|
self.on = function(eventNames, handler_or_context, context) {
|
|
if (typeof(eventNames) === "string" && handler_or_context) {
|
|
addListeners(eventNames.split(' '), handler_or_context, context);
|
|
}
|
|
else {
|
|
for (var name in eventNames) {
|
|
if (eventNames.hasOwnProperty(name)) {
|
|
addListeners([name], eventNames[name], handler_or_context);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
self.off = function(eventNames, handler_or_context, context) {
|
|
if (typeof(eventNames) === "string") {
|
|
if (handler_or_context && OTHelpers.isFunction(handler_or_context)) {
|
|
removeListeners(eventNames.split(' '), handler_or_context, context);
|
|
}
|
|
else {
|
|
eventNames.split(' ').forEach(function(name) {
|
|
removeAllListenersNamed(name, handler_or_context);
|
|
}, this);
|
|
}
|
|
}
|
|
else if (!eventNames) {
|
|
// remove all bound events
|
|
_events = {};
|
|
}
|
|
else {
|
|
for (var name in eventNames) {
|
|
if (eventNames.hasOwnProperty(name)) {
|
|
removeListeners([name], eventNames[name], handler_or_context);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
self.once = function(eventNames, handler, context) {
|
|
var names = eventNames.split(' '),
|
|
fun = function() {
|
|
var result = handler.apply(context || null, arguments);
|
|
removeListeners(names, handler, context);
|
|
|
|
return result;
|
|
}.bind(this);
|
|
|
|
addListeners(names, handler, context, fun);
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* This method registers a method as an event listener for a specific event.
|
|
* <p>
|
|
*
|
|
* <p>
|
|
* If a handler is not registered for an event, the event is ignored locally. If the event listener function does not exist,
|
|
* the event is ignored locally.
|
|
* </p>
|
|
* <p>
|
|
* Throws an exception if the <code>listener</code> name is invalid.
|
|
* </p>
|
|
*
|
|
* @param {String} type The string identifying the type of event.
|
|
*
|
|
* @param {Function} listener The function to be invoked when the object dispatches the event.
|
|
*
|
|
* @memberOf EventDispatcher
|
|
* @method #addEventListener
|
|
* @see <a href="#events">Events</a>
|
|
*/
|
|
// See 'on' for usage.
|
|
// @depreciated will become a private helper function in the future.
|
|
self.addEventListener = function(eventName, handler, context) {
|
|
addListeners([eventName], handler, context);
|
|
};
|
|
|
|
|
|
/**
|
|
* Removes an event listener for a specific event.
|
|
* <p>
|
|
*
|
|
* <p>
|
|
* Throws an exception if the <code>listener</code> name is invalid.
|
|
* </p>
|
|
*
|
|
* @param {String} type The string identifying the type of event.
|
|
*
|
|
* @param {Function} listener The event listener function to remove.
|
|
*
|
|
* @memberOf EventDispatcher
|
|
* @method #removeEventListener
|
|
* @see <a href="#events">Events</a>
|
|
*/
|
|
// See 'off' for usage.
|
|
// @depreciated will become a private helper function in the future.
|
|
self.removeEventListener = function(eventName, handler, context) {
|
|
removeListeners([eventName], handler, context);
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return self;
|
|
};
|
|
|
|
OTHelpers.eventing.Event = function() {
|
|
|
|
return function (type, cancelable) {
|
|
this.type = type;
|
|
this.cancelable = cancelable !== undefined ? cancelable : true;
|
|
|
|
var _defaultPrevented = false,
|
|
_target = null;
|
|
|
|
this.preventDefault = function() {
|
|
if (this.cancelable) {
|
|
_defaultPrevented = true;
|
|
} else {
|
|
OTHelpers.warn("Event.preventDefault :: Trying to preventDefault on an Event that isn't cancelable");
|
|
}
|
|
};
|
|
|
|
this.isDefaultPrevented = function() {
|
|
return _defaultPrevented;
|
|
};
|
|
|
|
if (OTHelpers.canDefineProperty) {
|
|
Object.defineProperty(this, 'target', {
|
|
set: function(target) {
|
|
_target = target;
|
|
},
|
|
|
|
get: function() {
|
|
return _target;
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
};
|
|
})(window, window.OTHelpers);
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
// tb_require('./callbacks.js')
|
|
|
|
// DOM helpers
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
// Returns true if the client supports element.classList
|
|
OTHelpers.supportsClassList = function() {
|
|
var hasSupport = typeof(document !== "undefined") && ("classList" in document.createElement("a"));
|
|
OTHelpers.supportsClassList = function() { return hasSupport; };
|
|
|
|
return hasSupport;
|
|
};
|
|
|
|
OTHelpers.removeElement = function(element) {
|
|
if (element && element.parentNode) {
|
|
element.parentNode.removeChild(element);
|
|
}
|
|
};
|
|
|
|
OTHelpers.removeElementById = function(elementId) {
|
|
this.removeElement(OTHelpers(elementId));
|
|
};
|
|
|
|
OTHelpers.removeElementsByType = function(parentElem, type) {
|
|
if (!parentElem) return;
|
|
|
|
var elements = parentElem.getElementsByTagName(type);
|
|
|
|
// elements is a "live" NodesList collection. Meaning that the collection
|
|
// itself will be mutated as we remove elements from the DOM. This means
|
|
// that "while there are still elements" is safer than "iterate over each
|
|
// element" as the collection length and the elements indices will be modified
|
|
// with each iteration.
|
|
while (elements.length) {
|
|
parentElem.removeChild(elements[0]);
|
|
}
|
|
};
|
|
|
|
OTHelpers.emptyElement = function(element) {
|
|
while (element.firstChild) {
|
|
element.removeChild(element.firstChild);
|
|
}
|
|
return element;
|
|
};
|
|
|
|
OTHelpers.createElement = function(nodeName, attributes, innerHTML) {
|
|
var element = document.createElement(nodeName);
|
|
|
|
if (attributes) {
|
|
for (var name in attributes) {
|
|
if (typeof(attributes[name]) === 'object') {
|
|
if (!element[name]) element[name] = {};
|
|
|
|
var subAttrs = attributes[name];
|
|
for (var n in subAttrs) {
|
|
element[name][n] = subAttrs[n];
|
|
}
|
|
}
|
|
else if (name === 'className') {
|
|
element.className = attributes[name];
|
|
}
|
|
else {
|
|
element.setAttribute(name, attributes[name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (innerHTML) {
|
|
element.innerHTML = innerHTML;
|
|
}
|
|
|
|
return element;
|
|
};
|
|
|
|
OTHelpers.createButton = function(innerHTML, attributes, events) {
|
|
var button = OTHelpers.createElement('button', attributes, innerHTML);
|
|
|
|
if (events) {
|
|
for (var name in events) {
|
|
if (events.hasOwnProperty(name)) {
|
|
OTHelpers.on(button, name, events[name]);
|
|
}
|
|
}
|
|
|
|
button._boundEvents = events;
|
|
}
|
|
|
|
return button;
|
|
};
|
|
|
|
// Helper function for adding event listeners to dom elements.
|
|
// WARNING: This doesn't preserve event types, your handler could be getting all kinds of different
|
|
// parameters depending on the browser. You also may have different scopes depending on the browser
|
|
// and bubbling and cancelable are not supported.
|
|
OTHelpers.on = function(element, eventName, handler) {
|
|
if (element.addEventListener) {
|
|
element.addEventListener(eventName, handler, false);
|
|
} else if (element.attachEvent) {
|
|
element.attachEvent("on" + eventName, handler);
|
|
} else {
|
|
var oldHandler = element["on"+eventName];
|
|
element["on"+eventName] = function() {
|
|
handler.apply(this, arguments);
|
|
if (oldHandler) oldHandler.apply(this, arguments);
|
|
};
|
|
}
|
|
};
|
|
|
|
// Helper function for removing event listeners from dom elements.
|
|
OTHelpers.off = function(element, eventName, handler) {
|
|
if (element.removeEventListener) {
|
|
element.removeEventListener (eventName, handler,false);
|
|
}
|
|
else if (element.detachEvent) {
|
|
element.detachEvent("on" + eventName, handler);
|
|
}
|
|
};
|
|
|
|
|
|
// Detects when an element is not part of the document flow because it or one of it's ancesters has display:none.
|
|
OTHelpers.isDisplayNone = function(element) {
|
|
if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return true;
|
|
if (element.parentNode && element.parentNode.style) return OTHelpers.isDisplayNone(element.parentNode);
|
|
return false;
|
|
};
|
|
|
|
OTHelpers.findElementWithDisplayNone = function(element) {
|
|
if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return element;
|
|
if (element.parentNode && element.parentNode.style) return OTHelpers.findElementWithDisplayNone(element.parentNode);
|
|
return null;
|
|
};
|
|
|
|
function objectHasProperties(obj) {
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// Allows an +onChange+ callback to be triggered when specific style properties
|
|
// of +element+ are notified. The callback accepts a single parameter, which is
|
|
// a hash where the keys are the style property that changed and the values are
|
|
// an array containing the old and new values ([oldValue, newValue]).
|
|
//
|
|
// Width and Height changes while the element is display: none will not be
|
|
// fired until such time as the element becomes visible again.
|
|
//
|
|
// This function returns the MutationObserver itself. Once you no longer wish
|
|
// to observe the element you should call disconnect on the observer.
|
|
//
|
|
// Observing changes:
|
|
// // observe changings to the width and height of object
|
|
// dimensionsObserver = OTHelpers.observeStyleChanges(object, ['width', 'height'], function(changeSet) {
|
|
// OT.debug("The new width and height are " + changeSet.width[1] + ',' + changeSet.height[1]);
|
|
// });
|
|
//
|
|
// Cleaning up
|
|
// // stop observing changes
|
|
// dimensionsObserver.disconnect();
|
|
// dimensionsObserver = null;
|
|
//
|
|
OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
|
|
var oldStyles = {};
|
|
|
|
var getStyle = function getStyle(style) {
|
|
switch (style) {
|
|
case 'width':
|
|
return OTHelpers.width(element);
|
|
|
|
case 'height':
|
|
return OTHelpers.height(element);
|
|
|
|
default:
|
|
return OTHelpers.css(element);
|
|
}
|
|
};
|
|
|
|
// get the inital values
|
|
stylesToObserve.forEach(function(style) {
|
|
oldStyles[style] = getStyle(style);
|
|
});
|
|
|
|
var observer = new MutationObserver(function(mutations) {
|
|
var changeSet = {};
|
|
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.attributeName !== 'style') return;
|
|
|
|
var isHidden = OTHelpers.isDisplayNone(element);
|
|
|
|
stylesToObserve.forEach(function(style) {
|
|
if(isHidden && (style == 'width' || style == 'height')) return;
|
|
|
|
var newValue = getStyle(style);
|
|
|
|
if (newValue !== oldStyles[style]) {
|
|
// OT.debug("CHANGED " + style + ": " + oldStyles[style] + " -> " + newValue);
|
|
|
|
changeSet[style] = [oldStyles[style], newValue];
|
|
oldStyles[style] = newValue;
|
|
}
|
|
});
|
|
});
|
|
|
|
if (objectHasProperties(changeSet)) {
|
|
// Do this after so as to help avoid infinite loops of mutations.
|
|
OTHelpers.callAsync(function() {
|
|
onChange.call(null, changeSet);
|
|
});
|
|
}
|
|
});
|
|
|
|
observer.observe(element, {
|
|
attributes:true,
|
|
attributeFilter: ['style'],
|
|
childList:false,
|
|
characterData:false,
|
|
subtree:false
|
|
});
|
|
|
|
return observer;
|
|
};
|
|
|
|
|
|
// trigger the +onChange+ callback whenever
|
|
// 1. +element+ is removed
|
|
// 2. or an immediate child of +element+ is removed.
|
|
//
|
|
// This function returns the MutationObserver itself. Once you no longer wish
|
|
// to observe the element you should call disconnect on the observer.
|
|
//
|
|
// Observing changes:
|
|
// // observe changings to the width and height of object
|
|
// nodeObserver = OTHelpers.observeNodeOrChildNodeRemoval(object, function(removedNodes) {
|
|
// OT.debug("Some child nodes were removed");
|
|
// removedNodes.forEach(function(node) {
|
|
// OT.debug(node);
|
|
// });
|
|
// });
|
|
//
|
|
// Cleaning up
|
|
// // stop observing changes
|
|
// nodeObserver.disconnect();
|
|
// nodeObserver = null;
|
|
//
|
|
OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) {
|
|
var observer = new MutationObserver(function(mutations) {
|
|
var removedNodes = [];
|
|
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.removedNodes.length) {
|
|
removedNodes = removedNodes.concat(Array.prototype.slice.call(mutation.removedNodes));
|
|
}
|
|
});
|
|
|
|
if (removedNodes.length) {
|
|
// Do this after so as to help avoid infinite loops of mutations.
|
|
OTHelpers.callAsync(function() {
|
|
onChange(removedNodes);
|
|
});
|
|
}
|
|
});
|
|
|
|
observer.observe(element, {
|
|
attributes:false,
|
|
childList:true,
|
|
characterData:false,
|
|
subtree:true
|
|
});
|
|
|
|
return observer;
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
// tb_require('./dom.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
OTHelpers.Modal = function(title, body) {
|
|
/*jshint multistr:true*/
|
|
var tmpl = "\
|
|
<header>\
|
|
<h1><%= title %></h1>\
|
|
</header>\
|
|
<div class='OT_dialog-body'>\
|
|
<%= body %>\
|
|
</div>\
|
|
";
|
|
|
|
this.el = OTHelpers.createElement("section", {
|
|
className: "OT_root OT_dialog OT_modal"
|
|
},
|
|
OTHelpers.template(tmpl, {
|
|
title: title,
|
|
body: body
|
|
})
|
|
);
|
|
|
|
// We're going to center the element in the window. We need to add the
|
|
// element to the body before we do, otherwise we can't calculate the dialog's
|
|
// widths and heights. This means we need to hide the element first.
|
|
this.el.style.display = 'none';
|
|
document.body.appendChild(this.el);
|
|
OTHelpers.centerElement(this.el);
|
|
OTHelpers.show(this.el);
|
|
|
|
this.close = function() {
|
|
OTHelpers.removeElement(this.el);
|
|
this.el = null;
|
|
return this;
|
|
};
|
|
};
|
|
|
|
// Custom alert dialog
|
|
OTHelpers.tbAlert = function(title, message) {
|
|
var modal = new OTHelpers.Modal(title, "<div>" + message + "</div>");
|
|
OTHelpers.addClass(modal.el, "OT_tbalert");
|
|
|
|
var closeBtn = OTHelpers.createElement("input", {
|
|
className: "OT_closeButton",
|
|
type: "button",
|
|
value: "close"
|
|
});
|
|
modal.el.appendChild(closeBtn);
|
|
|
|
closeBtn.onclick = function() {
|
|
if (modal) modal.close();
|
|
modal = null;
|
|
};
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
|
|
// DOM Attribute helpers helpers
|
|
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
// tb_require('./dom.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
OTHelpers.addClass = function(element, value) {
|
|
// Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
|
|
if (element.nodeType !== 1) {
|
|
return;
|
|
}
|
|
|
|
var classNames = value.trim().split(/\s+/),
|
|
i, l;
|
|
|
|
if (OTHelpers.supportsClassList()) {
|
|
for (i=0, l=classNames.length; i<l; ++i) {
|
|
element.classList.add(classNames[i]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// Here's our fallback to browsers that don't support element.classList
|
|
|
|
if (!element.className && classNames.length === 1) {
|
|
element.className = value;
|
|
}
|
|
else {
|
|
var setClass = " " + element.className + " ";
|
|
|
|
for (i=0, l=classNames.length; i<l; ++i) {
|
|
if ( !~setClass.indexOf( " " + classNames[i] + " ")) {
|
|
setClass += classNames[i] + " ";
|
|
}
|
|
}
|
|
|
|
element.className = setClass.trim();
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
OTHelpers.removeClass = function(element, value) {
|
|
if (!value) return;
|
|
|
|
// Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc
|
|
if (element.nodeType !== 1) {
|
|
return;
|
|
}
|
|
|
|
var newClasses = value.trim().split(/\s+/),
|
|
i, l;
|
|
|
|
if (OTHelpers.supportsClassList()) {
|
|
for (i=0, l=newClasses.length; i<l; ++i) {
|
|
element.classList.remove(newClasses[i]);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
var className = (" " + element.className + " ").replace(/[\s+]/, ' ');
|
|
|
|
for (i=0,l=newClasses.length; i<l; ++i) {
|
|
className = className.replace(' ' + newClasses[i] + ' ', ' ');
|
|
}
|
|
|
|
element.className = className.trim();
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Methods to calculate element widths and heights.
|
|
*/
|
|
|
|
var _width = function(element) {
|
|
if (element.offsetWidth > 0) {
|
|
return element.offsetWidth + 'px';
|
|
}
|
|
|
|
return OTHelpers.css(element, 'width');
|
|
},
|
|
|
|
_height = function(element) {
|
|
if (element.offsetHeight > 0) {
|
|
return element.offsetHeight + 'px';
|
|
}
|
|
|
|
return OTHelpers.css(element, 'height');
|
|
};
|
|
|
|
OTHelpers.width = function(element, newWidth) {
|
|
if (newWidth) {
|
|
OTHelpers.css(element, 'width', newWidth);
|
|
return this;
|
|
}
|
|
else {
|
|
if (OTHelpers.isDisplayNone(element)) {
|
|
// We can't get the width, probably since the element is hidden.
|
|
return OTHelpers.makeVisibleAndYield(element, function() {
|
|
return _width(element);
|
|
});
|
|
}
|
|
else {
|
|
return _width(element);
|
|
}
|
|
}
|
|
};
|
|
|
|
OTHelpers.height = function(element, newHeight) {
|
|
if (newHeight) {
|
|
OTHelpers.css(element, 'height', newHeight);
|
|
return this;
|
|
}
|
|
else {
|
|
if (OTHelpers.isDisplayNone(element)) {
|
|
// We can't get the height, probably since the element is hidden.
|
|
return OTHelpers.makeVisibleAndYield(element, function() {
|
|
return _height(element);
|
|
});
|
|
}
|
|
else {
|
|
return _height(element);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Centers +element+ within the window. You can pass through the width and height
|
|
// if you know it, if you don't they will be calculated for you.
|
|
OTHelpers.centerElement = function(element, width, height) {
|
|
if (!width) width = parseInt(OTHelpers.width(element), 10);
|
|
if (!height) height = parseInt(OTHelpers.height(element), 10);
|
|
|
|
var marginLeft = -0.5 * width + "px";
|
|
var marginTop = -0.5 * height + "px";
|
|
OTHelpers.css(element, "margin", marginTop + " 0 0 " + marginLeft);
|
|
OTHelpers.addClass(element, "OT_centered");
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
// CSS helpers helpers
|
|
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
// tb_require('./dom.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
var displayStateCache = {},
|
|
defaultDisplays = {};
|
|
|
|
var defaultDisplayValueForElement = function(element) {
|
|
if (defaultDisplays[element.ownerDocument] && defaultDisplays[element.ownerDocument][element.nodeName]) {
|
|
return defaultDisplays[element.ownerDocument][element.nodeName];
|
|
}
|
|
|
|
if (!defaultDisplays[element.ownerDocument]) defaultDisplays[element.ownerDocument] = {};
|
|
|
|
// We need to know what display value to use for this node. The easiest way
|
|
// is to actually create a node and read it out.
|
|
var testNode = element.ownerDocument.createElement(element.nodeName),
|
|
defaultDisplay;
|
|
|
|
element.ownerDocument.body.appendChild(testNode);
|
|
defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] = OTHelpers.css(testNode, 'display');
|
|
|
|
OTHelpers.removeElement(testNode);
|
|
testNode = null;
|
|
|
|
return defaultDisplay;
|
|
};
|
|
|
|
var isHidden = function(element) {
|
|
var computedStyle = element.ownerDocument.defaultView.getComputedStyle(element, null);
|
|
return computedStyle.getPropertyValue('display') === 'none';
|
|
};
|
|
|
|
OTHelpers.show = function(element) {
|
|
var display = element.style.display;
|
|
//,
|
|
// computedStyle = element.ownerDocument.defaultView.getComputedStyle(element, null),
|
|
// computedDisplay = computedStyle.getPropertyValue('display');
|
|
|
|
if (display === '' || display === 'none') {
|
|
element.style.display = displayStateCache[element] || '';
|
|
delete displayStateCache[element];
|
|
}
|
|
|
|
if (isHidden(element)) {
|
|
// It's still hidden so there's probably a stylesheet that declares this
|
|
// element as display:none;
|
|
displayStateCache[element] = 'none';
|
|
|
|
element.style.display = defaultDisplayValueForElement(element);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
OTHelpers.hide = function(element) {
|
|
if (element.style.display === 'none') return;
|
|
|
|
displayStateCache[element] = element.style.display;
|
|
element.style.display = 'none';
|
|
|
|
return this;
|
|
};
|
|
|
|
OTHelpers.css = function(element, nameOrHash, value) {
|
|
if (typeof(nameOrHash) !== 'string') {
|
|
var style = element.style;
|
|
|
|
for (var cssName in nameOrHash) {
|
|
style[cssName] = nameOrHash[cssName];
|
|
}
|
|
|
|
return this;
|
|
}
|
|
else if (value !== undefined) {
|
|
element.style[nameOrHash] = value;
|
|
return this;
|
|
}
|
|
else {
|
|
// Normalise vendor prefixes from the form MozTranform to -moz-transform
|
|
// except for ms extensions, which are weird...
|
|
var name = nameOrHash.replace( /([A-Z]|^ms)/g, "-$1" ).toLowerCase(),
|
|
computedStyle = element.ownerDocument.defaultView.getComputedStyle(element, null),
|
|
currentValue = computedStyle.getPropertyValue(name);
|
|
|
|
if (currentValue === '') {
|
|
currentValue = element.style[name];
|
|
}
|
|
|
|
return currentValue;
|
|
}
|
|
};
|
|
|
|
|
|
// Apply +styles+ to +element+ while executing +callback+, restoring the previous
|
|
// styles after the callback executes.
|
|
OTHelpers.applyCSS = function(element, styles, callback) {
|
|
var oldStyles = {},
|
|
name,
|
|
ret;
|
|
|
|
// Backup the old styles
|
|
for (name in styles) {
|
|
if (styles.hasOwnProperty(name)) {
|
|
// We intentionally read out of style here, instead of using the css
|
|
// helper. This is because the css helper uses querySelector and we
|
|
// only want to pull values out of the style (domeElement.style) hash.
|
|
oldStyles[name] = element.style[name];
|
|
|
|
OTHelpers.css(element, name, styles[name]);
|
|
}
|
|
}
|
|
|
|
ret = callback();
|
|
|
|
// Restore the old styles
|
|
for (name in styles) {
|
|
if (styles.hasOwnProperty(name)) {
|
|
OTHelpers.css(element, name, oldStyles[name] || '');
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
// Make +element+ visible while executing +callback+.
|
|
OTHelpers.makeVisibleAndYield = function(element, callback) {
|
|
// find whether it's the element or an ancester that's display none and
|
|
// then apply to whichever it is
|
|
var targetElement = OTHelpers.findElementWithDisplayNone(element);
|
|
if (!targetElement) return;
|
|
|
|
return OTHelpers.applyCSS(targetElement, {
|
|
display: "block",
|
|
visibility: "hidden"
|
|
},
|
|
callback
|
|
);
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
// AJAX helpers
|
|
|
|
/*jshint browser:true, smarttabs:true*/
|
|
|
|
// tb_require('../helpers.js')
|
|
// tb_require('./xml.js')
|
|
|
|
(function(window, OTHelpers, undefined) {
|
|
|
|
function formatPostData(data) { //, contentType
|
|
// If it's a string, we assume it's properly encoded
|
|
if (typeof(data) === 'string') return data;
|
|
|
|
var queryString = [];
|
|
|
|
for (var key in data) {
|
|
queryString.push(
|
|
encodeURIComponent(key) + '=' + encodeURIComponent(data[key])
|
|
);
|
|
}
|
|
|
|
return queryString.join('&').replace(/\+/g, "%20");
|
|
}
|
|
|
|
OTHelpers.getXML = function(url, options) {
|
|
var callerSuccessCb = options && options.success,
|
|
|
|
isValidXMLDocument = function(xmlDocument) {
|
|
var root;
|
|
|
|
if (!xmlDocument) {
|
|
// Badly formed XML
|
|
return false;
|
|
}
|
|
|
|
// If we got some XML back, attempt to infer if the XML is badly formed
|
|
root = xmlDocument.documentElement;
|
|
|
|
if (!root || !root.nodeName || root.nodeName === "parsererror") {
|
|
// Badly formed XML
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
|
|
onSuccess = function(event) {
|
|
var response = event.target.responseXML;
|
|
|
|
if (isValidXMLDocument(response)) {
|
|
if (callerSuccessCb) callerSuccessCb(response, event, event.target);
|
|
}
|
|
else if (options && options.error) {
|
|
options.error(event, event.target);
|
|
}
|
|
};
|
|
|
|
var extendedHeaders = OTHelpers.extend(options.headers || {}, {
|
|
'Content-Type': 'application/xml'
|
|
});
|
|
|
|
OTHelpers.get(url, OTHelpers.extend(options || {}, {
|
|
success: onSuccess,
|
|
headers: extendedHeaders
|
|
}));
|
|
};
|
|
|
|
OTHelpers.getJSON = function(url, options) {
|
|
var callerSuccessCb = options && options.success,
|
|
onSuccess = function(event) {
|
|
var response;
|
|
|
|
try {
|
|
response = JSON.parse(event.target.responseText);
|
|
} catch(e) {
|
|
// Badly formed JSON
|
|
if (options && options.error) options.error(event, event.target);
|
|
return;
|
|
}
|
|
|
|
if (callerSuccessCb) callerSuccessCb(response, event, event.target);
|
|
};
|
|
|
|
OTHelpers.get(url, OTHelpers.extend(options || {}, {
|
|
success: onSuccess,
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
}));
|
|
};
|
|
|
|
OTHelpers.get = function(url, options) {
|
|
var request = new XMLHttpRequest(),
|
|
_options = options || {};
|
|
|
|
bindToSuccessAndErrorCallbacks(request, _options.success, _options.error);
|
|
if (_options.process) request.addEventListener("progress", _options.progress, false);
|
|
if (_options.cancelled) request.addEventListener("abort", _options.cancelled, false);
|
|
|
|
|
|
request.open('GET', url, true);
|
|
|
|
if (!_options.headers) _options.headers = {};
|
|
|
|
for (var name in _options.headers) {
|
|
request.setRequestHeader(name, _options.headers[name]);
|
|
}
|
|
|
|
request.send();
|
|
};
|
|
|
|
OTHelpers.post = function(url, options) {
|
|
var request = new XMLHttpRequest(),
|
|
_options = options || {};
|
|
|
|
bindToSuccessAndErrorCallbacks(request, _options.success, _options.error);
|
|
|
|
if (_options.process) request.addEventListener("progress", _options.progress, false);
|
|
if (_options.cancelled) request.addEventListener("abort", _options.cancelled, false);
|
|
|
|
request.open('POST', url, true);
|
|
|
|
if (!_options.headers) _options.headers = {};
|
|
|
|
for (var name in _options.headers) {
|
|
request.setRequestHeader(name, _options.headers[name]);
|
|
}
|
|
|
|
request.send(formatPostData(_options.data));
|
|
};
|
|
|
|
OTHelpers.postFormData = function(url, data, options) {
|
|
if (!data) {
|
|
throw new Error("OTHelpers.postFormData must be passed a data options.");
|
|
}
|
|
|
|
var formData = new FormData();
|
|
|
|
for (var key in data) {
|
|
formData.append(key, data[key]);
|
|
}
|
|
|
|
OTHelpers.post(url, OTHelpers.extend(options || {}, {
|
|
data: formData
|
|
}));
|
|
};
|
|
|
|
|
|
// Make a GET request via JSONP.
|
|
//
|
|
// @example Make a request to 'http://tokbox.com:9999/session/foo' with success and error callbacks
|
|
//
|
|
// OTHelpers.getJSONP('http://tokbox.com:9999/session/foo', {
|
|
// success: successCallback,
|
|
// error: errorCallback
|
|
// });
|
|
//
|
|
OTHelpers.getJSONP = function(url, options) {
|
|
var _loadTimeout = 30000,
|
|
_script,
|
|
_head = document.head || document.getElementsByTagName('head')[0],
|
|
_waitUntilTimeout,
|
|
_urlWithCallback = url,
|
|
_options = OTHelpers.extend(options || {}, {
|
|
callbackParameter: 'callback'
|
|
}),
|
|
|
|
_clearTimeout = function() {
|
|
if (_waitUntilTimeout) {
|
|
clearTimeout(_waitUntilTimeout);
|
|
_waitUntilTimeout = null;
|
|
}
|
|
},
|
|
|
|
_cleanup = function() {
|
|
_clearTimeout();
|
|
|
|
if (_script) {
|
|
_script.onload = _script.onreadystatechange = null;
|
|
|
|
OTHelpers.removeElement( _script );
|
|
|
|
_script = undefined;
|
|
}
|
|
},
|
|
|
|
_onLoad = function() {
|
|
if (_script.readyState && !/loaded|complete/.test( _script.readyState )) {
|
|
// Yeah, we're not ready yet...
|
|
return;
|
|
}
|
|
|
|
_clearTimeout();
|
|
},
|
|
|
|
_onLoadTimeout = function() {
|
|
_cleanup();
|
|
|
|
OTHelpers.error("The JSONP request to " + _urlWithCallback + " timed out after " + _loadTimeout + "ms.");
|
|
if (_options.error) _options.error("The JSONP request to " + url + " timed out after " + _loadTimeout + "ms.", _urlWithCallback, _options);
|
|
},
|
|
|
|
_generateCallbackName = function() {
|
|
return 'jsonp_callback_' + (+new Date());
|
|
};
|
|
|
|
|
|
_options.callbackName = _generateCallbackName();
|
|
this.jsonp_callbacks[_options.callbackName] = function(response) {
|
|
_cleanup();
|
|
|
|
if (_options.success) _options.success(response);
|
|
};
|
|
|
|
// This doesn't handle URLs like "http://www.google.com?blah=1#something". The callback name
|
|
// will be incorrectly appended after the #.
|
|
_urlWithCallback += ((/\?/).test(_urlWithCallback) ? "&" : "?") + _options.callbackParameter + '=' + _options.callbackName;
|
|
|
|
_script = OTHelpers.createElement('script', {
|
|
async: 'async',
|
|
src: _urlWithCallback,
|
|
onload: _onLoad,
|
|
onreadystatechange: _onLoad
|
|
});
|
|
_head.appendChild(_script);
|
|
|
|
_waitUntilTimeout = setTimeout(function() { _onLoadTimeout(); }, _loadTimeout);
|
|
};
|
|
|
|
// Private helper method used (by OTHelpers.get and OTHelpers.post among others) to setup
|
|
// callbacks to correctly respond to success and error callbacks. This includes
|
|
// interpreting the responses HTTP status, which XmlHttpRequest seems to ignore
|
|
// by default.
|
|
var bindToSuccessAndErrorCallbacks = function(request, success, error) {
|
|
request.addEventListener("load", function(event) {
|
|
var status = event.target.status;
|
|
|
|
// We need to detect things that XMLHttpRequest considers a success,
|
|
// but we consider to be failures.
|
|
if ( status >= 200 && status < 300 || status === 304 ) {
|
|
if (success) success.apply(null, arguments);
|
|
}
|
|
else if (error) {
|
|
error(event);
|
|
}
|
|
}, false);
|
|
|
|
|
|
if (error) {
|
|
request.addEventListener("error", error, false);
|
|
}
|
|
};
|
|
|
|
})(window, window.OTHelpers);
|
|
!(function(window) {
|
|
|
|
if (!window.OT) window.OT = {};
|
|
|
|
// Bring OTHelpers in as OT.$
|
|
OT.$ = OTHelpers.noConflict();
|
|
|
|
// Allow events to be bound on OT
|
|
OT.$.eventing(OT);
|
|
|
|
// OT.$.Modal was OT.Modal before the great common-js-helpers move
|
|
OT.Modal = OT.$.Modal;
|
|
|
|
// Add logging methods
|
|
OT.$.useLogHelpers(OT);
|
|
var _debugHeaderLogged = false;
|
|
var _setLogLevel = OT.setLogLevel;
|
|
|
|
// On the first time log level is set to DEBUG (or higher) show version info.
|
|
OT.setLogLevel = function(level) {
|
|
// Set OT.$ to the same log level
|
|
OT.$.setLogLevel(level);
|
|
var retVal = _setLogLevel.call(OT, level);
|
|
if (OT.shouldLog(OT.DEBUG) && !_debugHeaderLogged) {
|
|
OT.debug("OpenTok JavaScript library " + OT.properties.version);
|
|
OT.debug("Release notes: " + OT.properties.websiteURL +"/opentok/webrtc/docs/js/release-notes.html");
|
|
OT.debug("Known issues: " + OT.properties.websiteURL + "/opentok/webrtc/docs/js/release-notes.html#knownIssues");
|
|
_debugHeaderLogged = true;
|
|
}
|
|
OT.debug("TB.setLogLevel(" + retVal + ")");
|
|
return retVal;
|
|
};
|
|
|
|
OT.setLogLevel(OT.properties.debug ? OT.DEBUG : OT.ERROR);
|
|
|
|
/**
|
|
* Sets the API log level.
|
|
* <p>
|
|
* Calling <code>TB.setLogLevel()</code> sets the log level for runtime log messages that are the OpenTok library generates.
|
|
* The default value for the log level is <code>TB.ERROR</code>.
|
|
* </p>
|
|
* <p>
|
|
* The OpenTok JavaScript library displays log messages in the debugger console (such as Firebug), if one exists.
|
|
* </p>
|
|
* <p>
|
|
* The following example logs the session ID to the console, by calling <code>TB.log()</code>. The code also logs
|
|
* an error message when it attempts to publish a stream before the Session object dispatches a
|
|
* <code>sessionConnected</code> event.
|
|
* </p>
|
|
* <pre>
|
|
* TB.setLogLevel(TB.LOG);
|
|
* session = TB.initSession(sessionId);
|
|
* TB.log(sessionId);
|
|
* publisher = TB.initPublisher(API_KEY, "publishContainer");
|
|
* session.publish(publisher);
|
|
* </pre>
|
|
*
|
|
* @param {Number} logLevel The degree of logging desired by the developer:
|
|
*
|
|
* <p>
|
|
* <ul>
|
|
* <li>
|
|
* <code>TB.NONE</code> — API logging is disabled.
|
|
* </li>
|
|
* <li>
|
|
* <code>TB.ERROR</code> — Logging of errors only.
|
|
* </li>
|
|
* <li>
|
|
* <code>TB.WARN</code> — Logging of warnings and errors.
|
|
* </li>
|
|
* <li>
|
|
* <code>TB.INFO</code> — Logging of other useful information, in addition to warnings and errors.
|
|
* </li>
|
|
* <li>
|
|
* <code>TB.LOG</code> — Logging of <code>TB.log()</code> messages, in addition to OpenTok info, warning,
|
|
* and error messages.
|
|
* </li>
|
|
* <li>
|
|
* <code>TB.DEBUG</code> — Fine-grained logging of all API actions, as well as <code>TB.log()</code> messages.
|
|
* </li>
|
|
* </ul>
|
|
* </p>
|
|
*
|
|
* @name TB.setLogLevel
|
|
* @memberof TB
|
|
* @function
|
|
* @see <a href="#log">TB.log()</a>
|
|
*/
|
|
|
|
/**
|
|
* Sends a string to the the debugger console (such as Firebug), if one exists. However, the function
|
|
* only logs to the console if you have set the log level to <code>TB.LOG</code> or <code>TB.DEBUG</code>,
|
|
* by calling <code>TB.setLogLevel(TB.LOG)</code> or <code>TB.setLogLevel(TB.DEBUG)</code>.
|
|
*
|
|
* @param {String} message The string to log.
|
|
*
|
|
* @name TB.log
|
|
* @memberof TB
|
|
* @function
|
|
* @see <a href="#setLogLevel">TB.setLogLevel()</a>
|
|
*/
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// IMPORTANT This file should be included straight after helpers.js
|
|
if (!window.OT) window.OT = {};
|
|
|
|
if (!OT.properties) {
|
|
throw new Error("OT.properties does not exist, please ensure that you include a valid properties file.");
|
|
}
|
|
|
|
// Consumes and overwrites OT.properties. Makes it better and stronger!
|
|
OT.properties = function(properties) {
|
|
var props = OT.$.clone(properties);
|
|
|
|
props.debug = properties.debug === 'true' || properties.debug === true;
|
|
props.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true;
|
|
|
|
if (props.supportSSL && (window.location.protocol.indexOf("https") >= 0 || window.location.protocol.indexOf("chrome-extension") >= 0)) {
|
|
props.assetURL = props.cdnURLSSL + "/webrtc/" + props.version;
|
|
props.loggingURL = props.loggingURLSSL;
|
|
props.apiURL = props.apiURLSSL;
|
|
} else {
|
|
props.assetURL = props.cdnURL + "/webrtc/" + props.version;
|
|
}
|
|
|
|
props.configURL = props.assetURL + "/js/dynamic_config.min.js";
|
|
props.cssURL = props.assetURL + "/css/ot.min.css";
|
|
|
|
return props;
|
|
}(OT.properties);
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
//--------------------------------------
|
|
// JS Dynamic Config
|
|
//--------------------------------------
|
|
|
|
|
|
OT.Config = (function() {
|
|
var _loaded = false,
|
|
_global = {},
|
|
_partners = {},
|
|
_script,
|
|
_head = document.head || document.getElementsByTagName('head')[0],
|
|
_loadTimer,
|
|
|
|
_clearTimeout = function() {
|
|
if (_loadTimer) {
|
|
clearTimeout(_loadTimer);
|
|
_loadTimer = null;
|
|
}
|
|
},
|
|
|
|
_cleanup = function() {
|
|
_clearTimeout();
|
|
|
|
if (_script) {
|
|
_script.onload = _script.onreadystatechange = null;
|
|
|
|
if ( _head && _script.parentNode ) {
|
|
_head.removeChild( _script );
|
|
}
|
|
|
|
_script = undefined;
|
|
}
|
|
},
|
|
|
|
_onLoad = function() {
|
|
// Only IE and Opera actually support readyState on Script elements.
|
|
if (_script.readyState && !/loaded|complete/.test( _script.readyState )) {
|
|
// Yeah, we're not ready yet...
|
|
return;
|
|
}
|
|
|
|
_clearTimeout();
|
|
|
|
if (!_loaded) {
|
|
// Our config script is loaded but there is not config (as
|
|
// replaceWith wasn't called). Something went wrong. Possibly
|
|
// the file we loaded wasn't actually a valid config file.
|
|
_this._onLoadTimeout();
|
|
}
|
|
},
|
|
|
|
_getModule = function(moduleName, apiKey) {
|
|
if (apiKey && _partners[apiKey] && _partners[apiKey][moduleName]) {
|
|
return _partners[apiKey][moduleName];
|
|
}
|
|
|
|
return _global[moduleName];
|
|
},
|
|
|
|
_this = {
|
|
// In ms
|
|
loadTimeout: 4000,
|
|
|
|
load: function(configUrl) {
|
|
if (!configUrl) throw new Error("You must pass a valid configUrl to Config.load");
|
|
|
|
_loaded = false;
|
|
|
|
setTimeout(function() {
|
|
_script = document.createElement( "script" );
|
|
_script.async = "async";
|
|
_script.src = configUrl;
|
|
_script.onload = _script.onreadystatechange = _onLoad.bind(this);
|
|
_head.appendChild(_script);
|
|
},1);
|
|
|
|
_loadTimer = setTimeout(function() {
|
|
_this._onLoadTimeout();
|
|
}, this.loadTimeout);
|
|
},
|
|
|
|
_onLoadTimeout: function() {
|
|
_cleanup();
|
|
|
|
OT.warn("TB DynamicConfig failed to load in " + _this.loadTimeout + " ms");
|
|
this.trigger('dynamicConfigLoadFailed');
|
|
},
|
|
|
|
isLoaded: function() {
|
|
return _loaded;
|
|
},
|
|
|
|
reset: function() {
|
|
_cleanup();
|
|
_loaded = false;
|
|
_global = {};
|
|
_partners = {};
|
|
},
|
|
|
|
// This is public so that the dynamic config file can load itself.
|
|
// Using it for other purposes is discouraged, but not forbidden.
|
|
replaceWith: function(config) {
|
|
_cleanup();
|
|
|
|
if (!config) config = {};
|
|
|
|
_global = config.global || {};
|
|
_partners = config.partners || {};
|
|
|
|
if (!_loaded) _loaded = true;
|
|
this.trigger('dynamicConfigChanged');
|
|
},
|
|
|
|
// @example Get the value that indicates whether exceptionLogging is enabled
|
|
// OT.Config.get('exceptionLogging', 'enabled');
|
|
//
|
|
// @example Get a key for a specific partner, fallback to the default if there is
|
|
// no key for that partner
|
|
// OT.Config.get('exceptionLogging', 'enabled', 'apiKey');
|
|
//
|
|
get: function(moduleName, key, apiKey) {
|
|
var module = _getModule(moduleName, apiKey);
|
|
return module ? module[key] : null;
|
|
}
|
|
};
|
|
|
|
OT.$.eventing(_this);
|
|
|
|
return _this;
|
|
})();
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
var defaultAspectRatio = 4.0/3.0;
|
|
|
|
// This code positions the video element so that we don't get any letterboxing.
|
|
// It will take into consideration aspect ratios other than 4/3 but only when
|
|
// the video element is first created. If the aspect ratio changes at a later point
|
|
// this calculation will become incorrect.
|
|
function fixAspectRatio(element, width, height, desiredAspectRatio, rotated) {
|
|
if (!width) width = parseInt(OT.$.width(element.parentNode), 10);
|
|
else width = parseInt(width, 10);
|
|
|
|
if (!height) height = parseInt(OT.$.height(element.parentNode), 10);
|
|
else height = parseInt(height, 10);
|
|
|
|
if (width === 0 || height === 0) return;
|
|
|
|
if (!desiredAspectRatio) desiredAspectRatio = defaultAspectRatio;
|
|
|
|
var actualRatio = (width + 0.0)/height,
|
|
props = {
|
|
width: '100%',
|
|
height: '100%',
|
|
left: 0,
|
|
top: 0
|
|
};
|
|
|
|
if (actualRatio > desiredAspectRatio) {
|
|
// Width is largest so we blow up the height so we don't have letterboxing
|
|
var newHeight = (actualRatio / desiredAspectRatio) * 100;
|
|
|
|
props.height = newHeight + '%';
|
|
props.top = '-' + ((newHeight - 100) / 2) + '%';
|
|
} else if (actualRatio < desiredAspectRatio) {
|
|
// Height is largest, blow up the width
|
|
var newWidth = (desiredAspectRatio / actualRatio) * 100;
|
|
|
|
props.width = newWidth + '%';
|
|
props.left = '-' + ((newWidth - 100) / 2) + '%';
|
|
}
|
|
|
|
OT.$.css(element, props);
|
|
|
|
var video = element.querySelector('video');
|
|
if(video) {
|
|
if(rotated) {
|
|
var w = element.offsetWidth,
|
|
h = element.offsetHeight,
|
|
props = { width: h + 'px', height: w + 'px', marginTop: '', marginLeft: '' },
|
|
diff = w - h;
|
|
props.marginLeft = (diff / 2) + 'px';
|
|
props.marginTop = -(diff / 2) + 'px';
|
|
OT.$.css(video, props);
|
|
} else {
|
|
OT.$.css(video, { width: '', height: '', marginTop: '', marginLeft: ''});
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
var getOrCreateContainer = function getOrCreateContainer(elementOrDomId, insertMode) {
|
|
var container,
|
|
domId;
|
|
|
|
if (elementOrDomId && elementOrDomId.nodeName) {
|
|
// It looks like we were given a DOM element. Grab the id or generate
|
|
// one if it doesn't have one.
|
|
container = elementOrDomId;
|
|
if (!container.getAttribute('id') || container.getAttribute('id').length === 0) {
|
|
container.setAttribute('id', 'OT_' + OT.$.uuid());
|
|
}
|
|
|
|
domId = container.getAttribute('id');
|
|
}
|
|
else {
|
|
// We may have got an id, try and get it's DOM element.
|
|
container = OT.$(elementOrDomId);
|
|
domId = elementOrDomId || ('OT_' + OT.$.uuid());
|
|
}
|
|
|
|
if (!container) {
|
|
container = OT.$.createElement('div', {id: domId});
|
|
container.style.backgroundColor = "#000000";
|
|
document.body.appendChild(container);
|
|
}
|
|
else {
|
|
OT.$.emptyElement(container);
|
|
}
|
|
|
|
if(!(insertMode == null || insertMode == 'replace')) {
|
|
var placeholder = document.createElement('div');
|
|
placeholder.id = ('OT_' + OT.$.uuid());
|
|
if(insertMode == 'append') {
|
|
container.appendChild(placeholder);
|
|
container = placeholder;
|
|
} else if(insertMode == 'before') {
|
|
container.parentNode.insertBefore(placeholder, container);
|
|
container = placeholder;
|
|
} else if(insertMode == 'after') {
|
|
container.parentNode.insertBefore(placeholder, container.nextSibling);
|
|
container = placeholder;
|
|
}
|
|
}
|
|
|
|
return container;
|
|
};
|
|
|
|
// Creates the standard container that the Subscriber and Publisher use to hold
|
|
// their video element and other chrome.
|
|
OT.WidgetView = function(targetElement, properties) {
|
|
var container = getOrCreateContainer(targetElement, properties && properties.insertMode),
|
|
videoContainer = document.createElement('div'),
|
|
oldContainerStyles = {},
|
|
dimensionsObserver,
|
|
videoElement,
|
|
videoObserver,
|
|
posterContainer,
|
|
loadingContainer,
|
|
loading = true;
|
|
|
|
if (properties) {
|
|
width = properties.width;
|
|
height = properties.height;
|
|
|
|
if (width) {
|
|
if (typeof(width) == "number") {
|
|
width = width + "px";
|
|
}
|
|
}
|
|
|
|
if (height) {
|
|
if (typeof(height) == "number") {
|
|
height = height + "px";
|
|
}
|
|
}
|
|
|
|
container.style.width = width ? width : "264px";
|
|
container.style.height = height ? height : "198px";
|
|
container.style.overflow = "hidden";
|
|
|
|
if (properties.mirror === undefined || properties.mirror) OT.$.addClass(container, 'OT_mirrored');
|
|
}
|
|
|
|
if (properties.classNames) OT.$.addClass(container, properties.classNames);
|
|
OT.$.addClass(container, 'OT_loading');
|
|
|
|
|
|
OT.$.addClass(videoContainer, 'OT_video-container');
|
|
videoContainer.style.width = container.style.width;
|
|
videoContainer.style.height = container.style.height;
|
|
container.appendChild(videoContainer);
|
|
fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight);
|
|
|
|
loadingContainer = document.createElement("div");
|
|
OT.$.addClass(loadingContainer, "OT_video-loading");
|
|
videoContainer.appendChild(loadingContainer);
|
|
|
|
posterContainer = document.createElement("div");
|
|
OT.$.addClass(posterContainer, "OT_video-poster");
|
|
videoContainer.appendChild(posterContainer);
|
|
|
|
oldContainerStyles.width = container.offsetWidth;
|
|
oldContainerStyles.height = container.offsetHeight;
|
|
|
|
// Observe changes to the width and height and update the aspect ratio
|
|
dimensionsObserver = OT.$.observeStyleChanges(container, ['width', 'height'], function(changeSet) {
|
|
var width = changeSet.width ? changeSet.width[1] : container.offsetWidth,
|
|
height = changeSet.height ? changeSet.height[1] : container.offsetHeight;
|
|
|
|
fixAspectRatio(videoContainer, width, height, videoElement ? videoElement.aspectRatio : null);
|
|
});
|
|
|
|
|
|
// @todo observe if the video container or the video element get removed, if they do we should do some cleanup
|
|
videoObserver = OT.$.observeNodeOrChildNodeRemoval(container, function(removedNodes) {
|
|
if (!videoElement) return;
|
|
|
|
// This assumes a video element being removed is the main video element. This may
|
|
// not be the case.
|
|
var videoRemoved = removedNodes.some(function(node) { return node === videoContainer || node.nodeName === 'VIDEO'; });
|
|
|
|
if (videoRemoved) {
|
|
videoElement.destroy();
|
|
videoElement = null;
|
|
}
|
|
|
|
if (videoContainer) {
|
|
OT.$.removeElement(videoContainer);
|
|
videoContainer = null;
|
|
}
|
|
|
|
if (dimensionsObserver) {
|
|
dimensionsObserver.disconnect();
|
|
dimensionsObserver = null;
|
|
}
|
|
|
|
if (videoObserver) {
|
|
videoObserver.disconnect();
|
|
videoObserver = null;
|
|
}
|
|
});
|
|
|
|
this.destroy = function() {
|
|
if (dimensionsObserver) {
|
|
dimensionsObserver.disconnect();
|
|
dimensionsObserver = null;
|
|
}
|
|
|
|
if (videoObserver) {
|
|
videoObserver.disconnect();
|
|
videoObserver = null;
|
|
}
|
|
|
|
if (videoElement) {
|
|
videoElement.destroy();
|
|
videoElement = null;
|
|
}
|
|
|
|
if (container) {
|
|
OT.$.removeElement(container);
|
|
container = null;
|
|
}
|
|
};
|
|
|
|
Object.defineProperties(this, {
|
|
|
|
showPoster: {
|
|
get: function() {
|
|
return !OT.$.isDisplayNone(posterContainer);
|
|
},
|
|
set: function(shown) {
|
|
if(shown) {
|
|
OT.$.show(posterContainer);
|
|
}
|
|
else {
|
|
OT.$.hide(posterContainer);
|
|
}
|
|
}
|
|
},
|
|
|
|
poster: {
|
|
get: function() {
|
|
return OT.$.css(posterContainer, "backgroundImage");
|
|
},
|
|
set: function(src) {
|
|
OT.$.css(posterContainer, "backgroundImage", "url(" + src + ")");
|
|
}
|
|
},
|
|
|
|
loading: {
|
|
get: function() { return loading; },
|
|
set: function(l) {
|
|
loading = l;
|
|
|
|
if (loading) {
|
|
OT.$.addClass(container, 'OT_loading');
|
|
}
|
|
else {
|
|
OT.$.removeClass(container, 'OT_loading');
|
|
}
|
|
}
|
|
},
|
|
|
|
video: {
|
|
get: function() { return videoElement; },
|
|
set: function(video) {
|
|
// remove the old video element if it exists
|
|
// @todo this might not be safe, publishers/subscribers use this as well...
|
|
if (videoElement) videoElement.destroy();
|
|
|
|
video.appendTo(videoContainer);
|
|
videoElement = video;
|
|
|
|
videoElement.on({
|
|
orientationChanged: function(){
|
|
fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight, videoElement.aspectRatio, videoElement.isRotated);
|
|
}
|
|
});
|
|
|
|
fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight, videoElement ? videoElement.aspectRatio : null, videoElement ? videoElement.isRotated : null);
|
|
}
|
|
},
|
|
|
|
domElement: {
|
|
get: function() { return container; }
|
|
},
|
|
|
|
domId: {
|
|
get: function() { return container.getAttribute('id'); }
|
|
}
|
|
});
|
|
|
|
this.addError = function(errorMsg) {
|
|
container.innerHTML = "<p>" + errorMsg + "<p>";
|
|
OT.$.addClass(container, "OT_subscriber_error");
|
|
};
|
|
};
|
|
|
|
})(window);
|
|
// Web OT Helpers
|
|
(function(window) {
|
|
|
|
// Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth
|
|
var getUserMedia = (function() {
|
|
if (navigator.getUserMedia) {
|
|
return navigator.getUserMedia.bind(navigator);
|
|
} else if (navigator.mozGetUserMedia) {
|
|
return navigator.mozGetUserMedia.bind(navigator);
|
|
} else if (navigator.webkitGetUserMedia) {
|
|
return navigator.webkitGetUserMedia.bind(navigator);
|
|
}
|
|
})();
|
|
|
|
|
|
if (navigator.webkitGetUserMedia) {
|
|
// Stub for getVideoTracks for Chrome < 26
|
|
if (!webkitMediaStream.prototype.getVideoTracks) {
|
|
webkitMediaStream.prototype.getVideoTracks = function() {
|
|
return this.videoTracks;
|
|
};
|
|
}
|
|
|
|
// Stubs for getAudioTracks for Chrome < 26
|
|
if (!webkitMediaStream.prototype.getAudioTracks) {
|
|
webkitMediaStream.prototype.getAudioTracks = function() {
|
|
return this.audioTracks;
|
|
};
|
|
}
|
|
|
|
if (!webkitRTCPeerConnection.prototype.getLocalStreams) {
|
|
webkitRTCPeerConnection.prototype.getLocalStreams = function() {
|
|
return this.localStreams;
|
|
};
|
|
}
|
|
|
|
if (!webkitRTCPeerConnection.prototype.getRemoteStreams) {
|
|
webkitRTCPeerConnection.prototype.getRemoteStreams = function() {
|
|
return this.remoteStreams;
|
|
};
|
|
}
|
|
}
|
|
else if (navigator.mozGetUserMedia) {
|
|
// Firefox < 23 doesn't support get Video/Audio tracks, we'll just stub them out for now.
|
|
if (!MediaStream.prototype.getVideoTracks) {
|
|
MediaStream.prototype.getVideoTracks = function() {
|
|
return [];
|
|
};
|
|
}
|
|
|
|
if (!MediaStream.prototype.getAudioTracks) {
|
|
MediaStream.prototype.getAudioTracks = function() {
|
|
return [];
|
|
};
|
|
}
|
|
|
|
// This won't work as mozRTCPeerConnection is a weird internal Firefox
|
|
// object (a wrapped native object I think).
|
|
// if (!window.mozRTCPeerConnection.prototype.getLocalStreams) {
|
|
// window.mozRTCPeerConnection.prototype.getLocalStreams = function() {
|
|
// return this.localStreams;
|
|
// };
|
|
// }
|
|
|
|
// This won't work as mozRTCPeerConnection is a weird internal Firefox
|
|
// object (a wrapped native object I think).
|
|
// if (!window.mozRTCPeerConnection.prototype.getRemoteStreams) {
|
|
// window.mozRTCPeerConnection.prototype.getRemoteStreams = function() {
|
|
// return this.remoteStreams;
|
|
// };
|
|
// }
|
|
}
|
|
|
|
|
|
// Mozilla error strings and the equivalent W3C names. NOT_SUPPORTED_ERROR does not
|
|
// exist in the spec right now, so we'll include Mozilla's error description.
|
|
var mozToW3CErrors = {
|
|
PERMISSION_DENIED: 'PERMISSION_DENIED',
|
|
NOT_SUPPORTED_ERROR: "A constraint specified is not supported by the browser.",
|
|
MANDATORY_UNSATISFIED_ERROR: 'CONSTRAINT_NOT_SATISFIED'
|
|
};
|
|
|
|
// Chrome only seems to expose a single error with a code of 1 right now.
|
|
var chromeToW3CErrors = {
|
|
1: 'PERMISSION_DENIED'
|
|
};
|
|
|
|
|
|
var gumNamesToMessages = {
|
|
PERMISSION_DENIED: "User denied permission for scripts from this origin to access the media device.",
|
|
CONSTRAINT_NOT_SATISFIED: "One of the mandatory constraints could not be satisfied."
|
|
};
|
|
|
|
// Map vendor error strings to names and messages
|
|
var mapVendorErrorName = function mapVendorErrorName (vendorErrorName, vendorErrors) {
|
|
var errorName = vendorErrors[vendorErrorName],
|
|
errorMessage = gumNamesToMessages[errorName];
|
|
|
|
if (!errorMessage) {
|
|
// This doesn't map to a known error from the Media Capture spec, it's
|
|
// probably a custom vendor error message.
|
|
errorMessage = eventName;
|
|
errorName = vendorErrorName;
|
|
}
|
|
|
|
return {
|
|
name: errorName,
|
|
message: errorMessage
|
|
};
|
|
};
|
|
|
|
// Parse and normalise a getUserMedia error event from Chrome or Mozilla
|
|
//
|
|
// @ref http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError
|
|
//
|
|
var parseErrorEvent = function parseErrorObject (event) {
|
|
var error;
|
|
|
|
if (OT.$.isObject(event) && event.name) {
|
|
error = {
|
|
name: event.name,
|
|
message: event.message,
|
|
constraintName: event.constraintName
|
|
};
|
|
}
|
|
else if (OT.$.isObject(event)) {
|
|
error = mapVendorErrorName(event.code, chromeToW3CErrors);
|
|
|
|
// message and constraintName are probably missing if the
|
|
// property is also omitted, but just in case they aren't.
|
|
if (event.message) error.message = event.message;
|
|
if (event.constraintName) error.constraintName = event.constraintName;
|
|
}
|
|
else if (event && mozToW3CErrors.hasOwnProperty(event)) {
|
|
error = mapVendorErrorName(event, mozToW3CErrors);
|
|
}
|
|
else {
|
|
error = {
|
|
message: "Unknown Error while getting user media"
|
|
};
|
|
}
|
|
|
|
|
|
return error;
|
|
};
|
|
|
|
|
|
|
|
// Validates a Hash of getUserMedia constraints. Currently we only
|
|
// check to see if there is at least one non-false constraint.
|
|
var areInvalidConstraints = function(constraints) {
|
|
if (!constraints || !OT.$.isObject(constraints)) return true;
|
|
|
|
for (var key in constraints) {
|
|
if (constraints[key]) return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Returns true if the client supports Web RTC, false otherwise.
|
|
//
|
|
// Chrome Issues:
|
|
// * The explicit prototype.addStream check is because webkitRTCPeerConnection was
|
|
// partially implemented, but not functional, in Chrome 22.
|
|
//
|
|
// Firefox Issues:
|
|
// * No real support before Firefox 19
|
|
// * Firefox 19 has issues with generating Offers.
|
|
// * Firefox 20 doesn't interoperate with Chrome.
|
|
//
|
|
OT.$.supportsWebRTC = function() {
|
|
var _supportsWebRTC = false;
|
|
|
|
if (navigator.webkitGetUserMedia) {
|
|
_supportsWebRTC = typeof(webkitRTCPeerConnection) === 'function' && !!webkitRTCPeerConnection.prototype.addStream;
|
|
}
|
|
else if (navigator.mozGetUserMedia) {
|
|
var firefoxVersion = window.navigator.userAgent.toLowerCase().match(/Firefox\/([0-9\.]+)/i);
|
|
_supportsWebRTC = typeof(mozRTCPeerConnection) === 'function' && (firefoxVersion !== null && parseFloat(firefoxVersion[1], 10) > 20.0);
|
|
if (_supportsWebRTC) {
|
|
try {
|
|
new mozRTCPeerConnection();
|
|
_supportsWebRTC = true;
|
|
} catch (err) {
|
|
_supportsWebRTC = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
OT.$.supportsWebRTC = function() {
|
|
return _supportsWebRTC;
|
|
};
|
|
|
|
return _supportsWebRTC;
|
|
};
|
|
|
|
// Returns a String representing the supported WebRTC crypto scheme. The possible
|
|
// values are SDES_SRTP, DTLS_SRTP, and NONE;
|
|
//
|
|
// Broadly:
|
|
// * Firefox only supports DTLS
|
|
// * Older versions of Chrome (<= 24) only support SDES
|
|
// * Newer versions of Chrome (>= 25) support DTLS and SDES
|
|
//
|
|
OT.$.supportedCryptoScheme = function() {
|
|
if (!OT.$.supportsWebRTC()) return 'NONE';
|
|
|
|
var chromeVersion = window.navigator.userAgent.toLowerCase().match(/chrome\/([0-9\.]+)/i);
|
|
return chromeVersion && parseFloat(chromeVersion[1], 10) < 25 ? 'SDES_SRTP' : 'DTLS_SRTP';
|
|
};
|
|
|
|
// Returns true if the browser supports bundle
|
|
//
|
|
// Broadly:
|
|
// * Firefox doesn't support bundle
|
|
// * Chrome support bundle
|
|
//
|
|
OT.$.supportsBundle = function() {
|
|
return OT.$.supportsWebRTC() && OT.$.browser() === 'Chrome';
|
|
};
|
|
|
|
// Returns true if the browser supports rtcp mux
|
|
//
|
|
// Broadly:
|
|
// * Older versions of Firefox (<= 25) don't support rtcp mux
|
|
// * Older versions of Firefox (>= 26) support rtcp mux (not tested yet)
|
|
// * Chrome support bundle
|
|
//
|
|
OT.$.supportsRtcpMux = function() {
|
|
return OT.$.supportsWebRTC() && OT.$.browser() === 'Chrome';
|
|
};
|
|
|
|
// A wrapper for the builtin navigator.getUserMedia. In addition to the usual
|
|
// getUserMedia behaviour, this helper method also accepts a accessDialogOpened
|
|
// and accessDialogClosed callback.
|
|
//
|
|
// @memberof TB.$
|
|
// @private
|
|
//
|
|
// @param {Object} constraints
|
|
// A dictionary of constraints to pass to getUserMedia. See <a href='http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints'>MediaStreamConstraints</a> in the Media Capture and Streams spec for more info.
|
|
//
|
|
// @param {function} success
|
|
// Called when getUserMedia completes successfully. The callback will be passed a WebRTC Stream object.
|
|
//
|
|
// @param {function} failure
|
|
// Called when getUserMedia fails to access a user stream. It will be passed an object with a code property representing the error that occurred.
|
|
//
|
|
// @param {function} accessDialogOpened
|
|
// Called when the access allow/deny dialog is opened.
|
|
//
|
|
// @param {function} accessDialogClosed
|
|
// Called when the access allow/deny dialog is closed.
|
|
//
|
|
// @param {function} accessDenied
|
|
// Called when access is denied to the camera/mic. This will be either because
|
|
// the user has clicked deny or because a particular origin is permanently denied.
|
|
//
|
|
OT.$.getUserMedia = function(constraints, success, failure, accessDialogOpened, accessDialogClosed, accessDenied) {
|
|
// All constraints are false, we don't allow this. This may be valid later
|
|
// depending on how/if we integrate data channels.
|
|
if (areInvalidConstraints(constraints)) {
|
|
OT.error("Couldn't get UserMedia: All constraints were false");
|
|
// Using a ugly dummy-code for now.
|
|
failure.call(null, {
|
|
name: 'NO_VALID_CONSTRAINTS',
|
|
message: "Video and Audio was disabled, you need to enabled at least one"
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
var triggerOpenedTimer = null,
|
|
displayedPermissionDialog = false,
|
|
|
|
finaliseAccessDialog = function() {
|
|
if (triggerOpenedTimer) {
|
|
clearTimeout(triggerOpenedTimer);
|
|
}
|
|
|
|
if (displayedPermissionDialog && accessDialogClosed) accessDialogClosed();
|
|
},
|
|
|
|
triggerOpened = function() {
|
|
triggerOpenedTimer = null;
|
|
displayedPermissionDialog = true;
|
|
|
|
if (accessDialogOpened) accessDialogOpened();
|
|
},
|
|
|
|
onStream = function(stream) {
|
|
finaliseAccessDialog();
|
|
success.call(null, stream);
|
|
},
|
|
|
|
onError = function(event) {
|
|
finaliseAccessDialog();
|
|
var error = parseErrorEvent(event);
|
|
|
|
if (error.name === 'PERMISSION_DENIED' || error.name === 'PermissionDeniedError') {
|
|
accessDenied.call(null, error);
|
|
}
|
|
else {
|
|
failure.call(null, error);
|
|
}
|
|
};
|
|
|
|
try {
|
|
getUserMedia(constraints, onStream, onError);
|
|
} catch (e) {
|
|
OT.error("Couldn't get UserMedia: " + e.toString());
|
|
onError();
|
|
return;
|
|
}
|
|
|
|
// The "remember me" functionality of WebRTC only functions over HTTPS, if
|
|
// we aren't on HTTPS then we should definitely be displaying the access
|
|
// dialog.
|
|
//
|
|
// If we are on HTTPS, we'll wait 500ms to see if we get a stream
|
|
// immediately. If we do then the user had clicked "remember me". Otherwise
|
|
// we assume that the accessAllowed dialog is visible.
|
|
//
|
|
// @todo benchmark and see if 500ms is a reasonable number. It seems like
|
|
// we should know a lot quicker.
|
|
//
|
|
if (location.protocol.indexOf('https') === -1) {
|
|
// Execute after, this gives the client a chance to bind to the
|
|
// accessDialogOpened event.
|
|
triggerOpenedTimer = setTimeout(triggerOpened, 100);
|
|
}
|
|
else {
|
|
// wait a second and then trigger accessDialogOpened
|
|
triggerOpenedTimer = setTimeout(triggerOpened, 500);
|
|
}
|
|
};
|
|
|
|
OT.$.createPeerConnection = function (config, options) {
|
|
var NativeRTCPeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection);
|
|
return new NativeRTCPeerConnection(config, options);
|
|
};
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.VideoOrientation = {
|
|
ROTATED_NORMAL: "OTVideoOrientationRotatedNormal",
|
|
ROTATED_LEFT: "OTVideoOrientationRotatedLeft",
|
|
ROTATED_RIGHT: "OTVideoOrientationRotatedRight",
|
|
ROTATED_UPSIDE_DOWN: "OTVideoOrientationRotatedUpsideDown"
|
|
};
|
|
|
|
//
|
|
//
|
|
// var _videoElement = new OT.VideoElement({
|
|
// fallbackText: 'blah'
|
|
// });
|
|
//
|
|
// _videoElement.on({
|
|
// streamBound: function() {...},
|
|
// loadError: function() {...},
|
|
// error: function() {...}
|
|
// });
|
|
//
|
|
// _videoElement.bindToStream(webRtcStream); // => VideoElement
|
|
// _videoElement.appendTo(DOMElement) // => VideoElement
|
|
//
|
|
// _videoElement.stream // => Web RTC stream
|
|
// _videoElement.domElement // => DomNode
|
|
// _videoElement.parentElement // => DomNode
|
|
//
|
|
// _videoElement.imgData // => PNG Data string
|
|
//
|
|
// _videoElement.orientation = OT.VideoOrientation.ROTATED_LEFT;
|
|
//
|
|
// _videoElement.unbindStream();
|
|
// _videoElement.destroy() // => Completely cleans up and removes the video element
|
|
//
|
|
//
|
|
OT.VideoElement = function(options) {
|
|
var _stream,
|
|
_domElement,
|
|
_parentElement,
|
|
_streamBound = false,
|
|
_videoElementMovedWarning = false,
|
|
_options = OT.$.defaults(options || {}, {
|
|
fallbackText: 'Sorry, Web RTC is not available in your browser'
|
|
});
|
|
|
|
|
|
OT.$.eventing(this);
|
|
|
|
/// Private API
|
|
var _onVideoError = function(event) {
|
|
var reason = "There was an unexpected problem with the Video Stream: " + videoElementErrorCodeToStr(event.target.error.code);
|
|
this.trigger('error', null, reason, this);
|
|
}.bind(this),
|
|
|
|
_onStreamBound = function() {
|
|
_streamBound = true;
|
|
_domElement.addEventListener('error', _onVideoError, false);
|
|
this.trigger('streamBound', this);
|
|
}.bind(this),
|
|
|
|
_onStreamBoundError = function(reason) {
|
|
this.trigger('loadError', OT.ExceptionCodes.P2P_CONNECTION_FAILED, reason, this);
|
|
}.bind(this),
|
|
|
|
// The video element pauses itself when it's reparented, this is
|
|
// unfortunate. This function plays the video again and is triggered
|
|
// on the pause event.
|
|
_playVideoOnPause = function() {
|
|
if(!_videoElementMovedWarning) {
|
|
OT.warn("Video element paused, auto-resuming. If you intended to do this, use publishVideo(false) or subscribeToVideo(false) instead.");
|
|
_videoElementMovedWarning = true;
|
|
}
|
|
_domElement.play();
|
|
};
|
|
|
|
|
|
_domElement = createVideoElement(_options.fallbackText, _options.attributes);
|
|
|
|
_domElement.addEventListener('pause', _playVideoOnPause);
|
|
|
|
/// Public Properties
|
|
Object.defineProperties(this, {
|
|
stream: {get: function() {return _stream; }},
|
|
domElement: {get: function() {return _domElement; }},
|
|
parentElement: {get: function() {return _parentElement; }},
|
|
isBoundToStream: {get: function() { return _streamBound; }},
|
|
poster: {
|
|
get: function() { return _domElement.getAttribute('poster'); },
|
|
set: function(src) { _domElement.setAttribute('poster', src); }
|
|
}
|
|
});
|
|
|
|
|
|
/// Public methods
|
|
|
|
// Append the Video DOM element to a parent node
|
|
this.appendTo = function(parentDomElement) {
|
|
_parentElement = parentDomElement;
|
|
_parentElement.appendChild(_domElement);
|
|
|
|
return this;
|
|
};
|
|
|
|
// Bind a stream to the video element.
|
|
this.bindToStream = function(webRtcStream) {
|
|
_streamBound = false;
|
|
_stream = webRtcStream;
|
|
|
|
bindStreamToVideoElement(_domElement, _stream, _onStreamBound, _onStreamBoundError);
|
|
|
|
return this;
|
|
};
|
|
|
|
// Unbind the currently bound stream from the video element.
|
|
this.unbindStream = function() {
|
|
if (!_stream) return this;
|
|
|
|
if (_domElement) {
|
|
if (!navigator.mozGetUserMedia) {
|
|
// The browser would have released this on unload anyway, but
|
|
// we're being a good citizen.
|
|
window.URL.revokeObjectURL(_domElement.src);
|
|
}
|
|
else {
|
|
_domElement.mozSrcObject = null;
|
|
}
|
|
}
|
|
|
|
_stream = null;
|
|
|
|
return this;
|
|
};
|
|
|
|
this.setAudioVolume = function(value) {
|
|
if (_domElement) _domElement.volume = OT.$.roundFloat(value / 100, 2);
|
|
};
|
|
|
|
this.getAudioVolume = function() {
|
|
// Return the actual volume of the DOM element
|
|
if (_domElement) return parseInt(_domElement.volume * 100, 10);
|
|
return 50;
|
|
};
|
|
|
|
this.whenTimeIncrements = function(callback, context) {
|
|
if(_domElement) {
|
|
var lastTime, handler = function() {
|
|
if(!lastTime || lastTime >= _domElement.currentTime) {
|
|
lastTime = _domElement.currentTime;
|
|
} else {
|
|
_domElement.removeEventListener('timeupdate', handler, false);
|
|
callback.call(context, this);
|
|
}
|
|
}.bind(this);
|
|
_domElement.addEventListener('timeupdate', handler, false);
|
|
}
|
|
};
|
|
|
|
this.destroy = function() {
|
|
// unbind all events so they don't fire after the object is dead
|
|
this.off();
|
|
|
|
this.unbindStream();
|
|
|
|
if (_domElement) {
|
|
// Unbind this first, otherwise it will trigger when the
|
|
// video element is removed from the DOM.
|
|
_domElement.removeEventListener('pause', _playVideoOnPause);
|
|
|
|
OT.$.removeElement(_domElement);
|
|
_domElement = null;
|
|
}
|
|
|
|
_parentElement = null;
|
|
|
|
return undefined;
|
|
};
|
|
};
|
|
|
|
|
|
// Checking for window.defineProperty for IE compatibility, just so we don't throw exceptions when the script is included
|
|
if (OT.$.canDefineProperty) {
|
|
// Extracts a snapshot from a video element and returns it's as a PNG Data string.
|
|
Object.defineProperties(OT.VideoElement.prototype, {
|
|
imgData: {
|
|
get: function() {
|
|
var canvas = OT.$.createElement('canvas', {
|
|
width: this.domElement.videoWidth,
|
|
height: this.domElement.videoHeight,
|
|
style: {
|
|
display: 'none'
|
|
}
|
|
});
|
|
|
|
document.body.appendChild(canvas);
|
|
try {
|
|
canvas.getContext('2d').drawImage(this.domElement, 0, 0, canvas.width, canvas.height);
|
|
} catch(err) {
|
|
OT.warn("Cannot get image data yet");
|
|
return null;
|
|
}
|
|
var imgData = canvas.toDataURL('image/png');
|
|
|
|
OT.$.removeElement(canvas);
|
|
|
|
return imgData.replace("data:image/png;base64,", "").trim();
|
|
}
|
|
},
|
|
|
|
videoWidth: {
|
|
get: function() {
|
|
if(this._orientation && this._orientation.width) {
|
|
return this._orientation.width;
|
|
}
|
|
return this.domElement['video' + (this.isRotated ? 'Height' : 'Width')];
|
|
}
|
|
},
|
|
|
|
videoHeight: {
|
|
get: function() {
|
|
if(this._orientation && this._orientation.height) {
|
|
return this._orientation.height;
|
|
}
|
|
return this.domElement['video' + (this.isRotated ? 'Width' : 'Height')];
|
|
}
|
|
},
|
|
|
|
aspectRatio: {
|
|
get: function() {
|
|
return (this.videoWidth + 0.0) / this.videoHeight;
|
|
}
|
|
},
|
|
|
|
isRotated: {
|
|
get: function() {
|
|
return this._orientation && (this._orientation.videoOrientation == 'OTVideoOrientationRotatedLeft' || this._orientation.videoOrientation == 'OTVideoOrientationRotatedRight');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
var VideoOrientationTransforms = {
|
|
OTVideoOrientationRotatedNormal: "rotate(0deg)",
|
|
OTVideoOrientationRotatedLeft: "rotate(90deg)",
|
|
OTVideoOrientationRotatedRight: "rotate(-90deg)",
|
|
OTVideoOrientationRotatedUpsideDown: "rotate(180deg)"
|
|
};
|
|
|
|
// Checking for window.defineProperty for IE compatibility, just so we don't throw exceptions when the script is included
|
|
if (OT.$.canDefineProperty) {
|
|
Object.defineProperty(OT.VideoElement.prototype,'orientation', {
|
|
get: function() {
|
|
return this._orientation;
|
|
},
|
|
set: function(orientation) {
|
|
this._orientation = orientation;
|
|
|
|
var transform = VideoOrientationTransforms[orientation.videoOrientation] || VideoOrientationTransforms.ROTATED_NORMAL;
|
|
|
|
switch(OT.$.browser()) {
|
|
case 'Chrome':
|
|
case 'Safari':
|
|
this.domElement.style.webkitTransform = transform;
|
|
break;
|
|
|
|
case 'IE':
|
|
this.domElement.style.msTransform = transform;
|
|
break;
|
|
|
|
default:
|
|
// The standard version, just Firefox, Opera, and IE > 9
|
|
this.domElement.style.transform = transform;
|
|
}
|
|
|
|
this.trigger('orientationChanged');
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/// Private Helper functions
|
|
|
|
function createVideoElement(fallbackText, attributes) {
|
|
var videoElement = document.createElement('video');
|
|
videoElement.setAttribute('autoplay', '');
|
|
videoElement.innerHTML = fallbackText;
|
|
|
|
if (attributes) {
|
|
if (attributes.muted === true) {
|
|
delete attributes.muted;
|
|
videoElement.muted = 'true';
|
|
}
|
|
|
|
for (var key in attributes) {
|
|
videoElement.setAttribute(key, attributes[key]);
|
|
}
|
|
}
|
|
|
|
return videoElement;
|
|
}
|
|
|
|
|
|
// See http://www.w3.org/TR/2010/WD-html5-20101019/video.html#error-codes
|
|
var _videoErrorCodes = {};
|
|
// Checking for window.MediaError for IE compatibility, just so we don't throw exceptions when the script is included
|
|
if (window.MediaError) {
|
|
_videoErrorCodes[window.MediaError.MEDIA_ERR_ABORTED] = "The fetching process for the media resource was aborted by the user agent at the user's request.";
|
|
_videoErrorCodes[window.MediaError.MEDIA_ERR_NETWORK] = "A network error of some description caused the user agent to stop fetching the media resource, after the resource was established to be usable.";
|
|
_videoErrorCodes[window.MediaError.MEDIA_ERR_DECODE] = "An error of some description occurred while decoding the media resource, after the resource was established to be usable.";
|
|
_videoErrorCodes[window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED] = "The media resource indicated by the src attribute was not suitable. ";
|
|
}
|
|
|
|
function videoElementErrorCodeToStr(errorCode) {
|
|
return _videoErrorCodes[parseInt(errorCode, 10)] || "An unknown error occurred.";
|
|
}
|
|
|
|
|
|
function bindStreamToVideoElement(videoElement, webRTCStream, onStreamBound, onStreamBoundError) {
|
|
// Note: onloadedmetadata doesn't fire in Chrome for audio only crbug.com/110938
|
|
if (navigator.mozGetUserMedia || (webRTCStream.getVideoTracks().length > 0 && webRTCStream.getVideoTracks()[0].enabled)) {
|
|
|
|
var cleanup = function cleanup () {
|
|
clearTimeout(timeout);
|
|
videoElement.removeEventListener('loadedmetadata', onLoad, false);
|
|
videoElement.removeEventListener('error', onError, false);
|
|
},
|
|
|
|
onLoad = function onLoad (event) {
|
|
cleanup();
|
|
onStreamBound();
|
|
},
|
|
|
|
onError = function onError (event) {
|
|
cleanup();
|
|
onStreamBoundError("There was an unexpected problem with the Video Stream: " + videoElementErrorCodeToStr(event.target.error.code));
|
|
},
|
|
|
|
onStoppedLoading = function onStoppedLoading () {
|
|
// The stream ended before we fully bound it. Maybe the other end called
|
|
// stop on it or something else went wrong.
|
|
cleanup();
|
|
onStreamBoundError("Stream ended while trying to bind it to a video element.");
|
|
},
|
|
|
|
// Timeout if it takes too long
|
|
timeout = setTimeout(function() {
|
|
if (videoElement.currentTime === 0) {
|
|
onStreamBoundError("The video stream failed to connect. Please notify the site owner if this continues to happen.");
|
|
} else {
|
|
// This should never happen
|
|
OT.warn("Never got the loadedmetadata event but currentTime > 0");
|
|
onStreamBound();
|
|
}
|
|
}.bind(this), 30000);
|
|
|
|
|
|
videoElement.addEventListener('loadedmetadata', onLoad, false);
|
|
videoElement.addEventListener('error', onError, false);
|
|
webRTCStream.onended = onStoppedLoading;
|
|
} else {
|
|
onStreamBound();
|
|
}
|
|
|
|
// The official spec way is 'srcObject', we are slowly converging there.
|
|
if (videoElement.srcObject !== void 0) {
|
|
videoElement.srcObject = webRTCStream;
|
|
}
|
|
else if (videoElement.mozSrcObject !== void 0) {
|
|
videoElement.mozSrcObject = webRTCStream;
|
|
}
|
|
else {
|
|
videoElement.src = window.URL.createObjectURL(webRTCStream);
|
|
}
|
|
|
|
videoElement.play();
|
|
}
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// Singleton interval
|
|
var logQueue = [],
|
|
queueRunning = false;
|
|
|
|
|
|
OT.Analytics = function() {
|
|
|
|
var endPoint = OT.properties.loggingURL + "/logging/ClientEvent",
|
|
endPointQos = OT.properties.loggingURL + "/logging/ClientQos",
|
|
|
|
reportedErrors = {},
|
|
|
|
// Map of camel-cased keys to underscored
|
|
camelCasedKeys = {
|
|
payloadType: 'payload_type',
|
|
partnerId: 'partner_id',
|
|
streamId: 'stream_id',
|
|
sessionId: 'session_id',
|
|
connectionId: 'connection_id',
|
|
widgetType: 'widget_type',
|
|
widgetId: 'widget_id',
|
|
avgAudioBitrate: 'avg_audio_bitrate',
|
|
avgVideoBitrate: 'avg_video_bitrate',
|
|
localCandidateType: 'local_candidate_type',
|
|
remoteCandidateType: 'remote_candidate_type',
|
|
transportType: 'transport_type'
|
|
},
|
|
|
|
send = function(data, isQos, onSuccess, onError) {
|
|
OT.$.post(isQos ? endPointQos : endPoint, {
|
|
success: onSuccess,
|
|
error: onError,
|
|
data: data,
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded'
|
|
}
|
|
});
|
|
},
|
|
|
|
throttledPost = function() {
|
|
// Throttle logs so that they only happen 1 at a time
|
|
if (!queueRunning && logQueue.length > 0) {
|
|
queueRunning = true;
|
|
var curr = logQueue[0];
|
|
|
|
// Remove the current item and send the next log
|
|
var processNextItem = function() {
|
|
logQueue.shift();
|
|
queueRunning = false;
|
|
throttledPost();
|
|
};
|
|
|
|
if (curr) {
|
|
send(curr.data, curr.isQos, function() {
|
|
curr.onComplete();
|
|
setTimeout(processNextItem, 50);
|
|
}, function() {
|
|
OT.debug("Failed to send ClientEvent, moving on to the next item.");
|
|
|
|
// There was an error, move onto the next item
|
|
setTimeout(processNextItem, 50);
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
post = function(data, onComplete, isQos) {
|
|
logQueue.push({
|
|
data: data,
|
|
onComplete: onComplete,
|
|
isQos: isQos
|
|
});
|
|
|
|
throttledPost();
|
|
},
|
|
|
|
shouldThrottleError = function(code, type, partnerId) {
|
|
if (!partnerId) return false;
|
|
|
|
var errKey = [partnerId, type, code].join('_'),
|
|
//msgLimit = DynamicConfig.get('exceptionLogging', 'messageLimitPerPartner', partnerId);
|
|
msgLimit = 100;
|
|
if (msgLimit === null || msgLimit === undefined) return false;
|
|
|
|
return (reportedErrors[errKey] || 0) <= msgLimit;
|
|
};
|
|
|
|
// Log an error via ClientEvents.
|
|
//
|
|
// @param [String] code
|
|
// @param [String] type
|
|
// @param [String] message
|
|
// @param [Hash] details additional error details
|
|
//
|
|
// @param [Hash] options the options to log the client event with.
|
|
// @option options [String] action The name of the Event that we are logging. E.g. "TokShowLoaded". Required.
|
|
// @option options [String] variation Usually used for Split A/B testing, when you have multiple variations of the +_action+.
|
|
// @option options [String] payloadType A text description of the payload. Required.
|
|
// @option options [String] payload The payload. Required.
|
|
// @option options [String] sessionId The active OpenTok session, if there is one
|
|
// @option options [String] connectionId The active OpenTok connectionId, if there is one
|
|
// @option options [String] partnerId
|
|
// @option options [String] guid ...
|
|
// @option options [String] widgetId ...
|
|
// @option options [String] streamId ...
|
|
// @option options [String] section ...
|
|
// @option options [String] build ...
|
|
//
|
|
// Reports will be throttled to X reports (see exceptionLogging.messageLimitPerPartner
|
|
// from the dynamic config for X) of each error type for each partner. Reports can be
|
|
// disabled/enabled globally or on a per partner basis (per partner settings
|
|
// take precedence) using exceptionLogging.enabled.
|
|
//
|
|
this.logError = function(code, type, message, details, options) {
|
|
if (!options) options = {};
|
|
var partnerId = options.partnerId;
|
|
|
|
if (OT.Config.get('exceptionLogging', 'enabled', partnerId) !== true) {
|
|
return;
|
|
}
|
|
|
|
if (shouldThrottleError(code, type, partnerId)) {
|
|
//OT.log("ClientEvents.error has throttled an error of type " + type + "." + code + " for partner " + (partnerId || 'No Partner Id'));
|
|
return;
|
|
}
|
|
|
|
var errKey = [partnerId, type, code].join('_'),
|
|
|
|
payload = this.escapePayload(OT.$.extend(details || {}, {
|
|
message: payload,
|
|
userAgent: navigator.userAgent
|
|
}));
|
|
|
|
|
|
reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ?
|
|
reportedErrors[errKey] + 1 :
|
|
1;
|
|
|
|
return this.logEvent(OT.$.extend(options, {
|
|
action: type + '.' + code,
|
|
payloadType: payload[0],
|
|
payload: payload[1]
|
|
}));
|
|
};
|
|
|
|
// Log a client event to the analytics backend.
|
|
//
|
|
// @example Logs a client event called 'foo'
|
|
// OT.ClientEvents.log({
|
|
// action: 'foo',
|
|
// payload_type: "foo's payload",
|
|
// payload: 'bar',
|
|
// session_id: sessionId,
|
|
// connection_id: connectionId
|
|
// })
|
|
//
|
|
// @param [Hash] options the options to log the client event with.
|
|
// @option options [String] action The name of the Event that we are logging. E.g. "TokShowLoaded". Required.
|
|
// @option options [String] variation Usually used for Split A/B testing, when you have multiple variations of the +_action+.
|
|
// @option options [String] payloadType A text description of the payload. Required.
|
|
// @option options [String] payload The payload. Required.
|
|
// @option options [String] session_id The active OpenTok session, if there is one
|
|
// @option options [String] connection_id The active OpenTok connectionId, if there is one
|
|
// @option options [String] partner_id
|
|
// @option options [String] guid ...
|
|
// @option options [String] widget_id ...
|
|
// @option options [String] stream_id ...
|
|
// @option options [String] section ...
|
|
// @option options [String] build ...
|
|
//
|
|
this.logEvent = function(options) {
|
|
var partnerId = options.partnerId;
|
|
|
|
if (!options) options = {};
|
|
|
|
// Set a bunch of defaults
|
|
var data = OT.$.extend({
|
|
"variation" : "",
|
|
'guid' : this.getClientGuid(),
|
|
'widget_id' : "",
|
|
'session_id': '',
|
|
'connection_id': '',
|
|
'stream_id' : "",
|
|
'partner_id' : partnerId,
|
|
'source' : window.location.href,
|
|
'section' : "",
|
|
'build' : ""
|
|
}, options),
|
|
|
|
onComplete = function(){
|
|
// OT.log("logged: " + "{action: " + data["action"] + ", variation: " + data["variation"] + ", payload_type: " + data["payload_type"] + ", payload: " + data["payload"] + "}");
|
|
};
|
|
|
|
// We camel-case our names, but the ClientEvents backend wants them
|
|
// underscored...
|
|
for (var key in camelCasedKeys) {
|
|
if (camelCasedKeys.hasOwnProperty(key) && data[key]) {
|
|
data[camelCasedKeys[key]] = data[key];
|
|
delete data[key];
|
|
}
|
|
}
|
|
|
|
post(data, onComplete, false);
|
|
};
|
|
|
|
// Log a client QOS to the analytics backend.
|
|
//
|
|
this.logQOS = function(options) {
|
|
var partnerId = options.partnerId;
|
|
|
|
if (!options) options = {};
|
|
|
|
// Set a bunch of defaults
|
|
var data = OT.$.extend({
|
|
'guid' : this.getClientGuid(),
|
|
'widget_id' : "",
|
|
'session_id': '',
|
|
'connection_id': '',
|
|
'stream_id' : "",
|
|
'partner_id' : partnerId,
|
|
'source' : window.location.href,
|
|
'build' : "",
|
|
'duration' : 0 //in milliseconds
|
|
}, options),
|
|
|
|
onComplete = function(){
|
|
//OT.log("logged: " + "{action: " + data["action"] + ", variation: " + data["variation"] + ", payload_type: " + data["payload_type"] + ", payload: " + data["payload"] + "}");
|
|
};
|
|
|
|
// We camel-case our names, but the ClientEvents backend wants them
|
|
// underscored...
|
|
for (var key in camelCasedKeys) {
|
|
if (camelCasedKeys.hasOwnProperty(key) && data[key]) {
|
|
data[camelCasedKeys[key]] = data[key];
|
|
delete data[key];
|
|
}
|
|
}
|
|
|
|
post(data, onComplete, true);
|
|
};
|
|
|
|
// Converts +payload+ to two pipe seperated strings. Doesn't currently handle
|
|
// edgecases, e.g. escaping "\\|" will break stuff.
|
|
//
|
|
// *Note:* It strip any keys that have null values.
|
|
this.escapePayload = function(payload) {
|
|
var escapedPayload = [],
|
|
escapedPayloadDesc = [];
|
|
|
|
for (var key in payload) {
|
|
if (payload.hasOwnProperty(key) && payload[key] !== null && payload[key] !== undefined) {
|
|
escapedPayload.push( payload[key] ? payload[key].toString().replace('|', '\\|') : '' );
|
|
escapedPayloadDesc.push( key.toString().replace('|', '\\|') );
|
|
}
|
|
}
|
|
|
|
return [
|
|
escapedPayloadDesc.join('|'),
|
|
escapedPayload.join('|')
|
|
];
|
|
};
|
|
// Uses HTML5 local storage to save a client ID.
|
|
this.getClientGuid = function() {
|
|
var guid = OT.$.getCookie("opentok_client_id");
|
|
if (!guid) {
|
|
guid = OT.$.uuid();
|
|
OT.$.setCookie("opentok_client_id", guid);
|
|
}
|
|
return guid;
|
|
};
|
|
}
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// This is not obvious, so to prevent end-user frustration we'll let them know
|
|
// explicitly rather than failing with a bunch of permission errors. We don't
|
|
// handle this using an OT Exception as it's really only a development thing.
|
|
if (location.protocol === 'file:') {
|
|
alert("You cannot test a page using WebRTC through the file system due to browser permissions. You must run it over a web server.");
|
|
}
|
|
|
|
if (!window.OT) window.OT = {};
|
|
|
|
if (!window.URL && window.webkitURL) {
|
|
window.URL = window.webkitURL;
|
|
}
|
|
|
|
var _publisherCount = 0,
|
|
|
|
// Global parameters used by upgradeSystemRequirements
|
|
_intervalId,
|
|
_lastHash = document.location.hash,
|
|
|
|
// Cached DeviceManager
|
|
_deviceManager;
|
|
|
|
|
|
/**
|
|
* The first step in using the OpenTok API is to call the <code>TB.initSession()</code> method. Other methods of the TB object
|
|
* check for system requirements and set up error logging.
|
|
*
|
|
* @class TB
|
|
*/
|
|
|
|
/**
|
|
* <p class="mSummary">
|
|
* Initializes and returns the local session object for a specified session ID.
|
|
* </p>
|
|
* <p>
|
|
* You connect to an OpenTok session using the <code>connect()</code> method
|
|
* of the Session object returned by the <code>TB.initSession()</code> method.
|
|
* Note that calling <code>TB.initSession()</code> does not initiate communications
|
|
* with the cloud. It simply initializes the Session object that you can use to
|
|
* connect (and to perform other operations once connected).
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* For an example, see <a href="Session.html#connect">Session.connect()</a>.
|
|
* </p>
|
|
*
|
|
* @method TB.initSession
|
|
* @memberof TB
|
|
* @param {String} sessionId The session ID identifying the OpenTok session. For more information, see
|
|
* <a href="/opentok/tutorials/create-session/">Session creation</a>.
|
|
* @returns {Session} The session object through which all further interactions with the session will occur.
|
|
*/
|
|
OT.initSession = function(sessionId) {
|
|
var session = OT.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
session = new OT.Session(sessionId);
|
|
OT.sessions.add(session);
|
|
}
|
|
|
|
return session;
|
|
};
|
|
|
|
/**
|
|
* <p class="mSummary">
|
|
* Initializes and returns a Publisher object. You can then pass this Publisher
|
|
* object to <code>Session.publish()</code> to publish a stream to a session.
|
|
* </p>
|
|
* <p>
|
|
* <i>Note:</i> If you intend to reuse a Publisher object created using <code>TB.initPublisher()</code>
|
|
* to publish to different sessions sequentially, call either <code>Session.disconnect()</code> or
|
|
* <code>Session.unpublish()</code>. Do not call both. Then call the <code>preventDefault()</code> method
|
|
* of the <code>streamDestroyed</code> or <code>sessionDisconnected</code> event object to prevent the
|
|
* Publisher object from being removed from the page.
|
|
* </p>
|
|
* @param {String} apiKey The API key that TokBox provided you when you
|
|
* <a href="https://dashboard.tokbox.com/users/sign_in">signed up</a> for an OpenTok account.
|
|
* @param {String} targetElement The
|
|
* <code>id</code> attribute of the existing DOM element that the Publisher video replaces.
|
|
* If you do not specify a <code>targetElement</code>, the application
|
|
* appends a new DOM element to the HTML <code>body</code>.
|
|
*
|
|
* <p>
|
|
* The application throws an error if an element with an ID set to the <code>targetElement</code>
|
|
* value does not exist in the HTML DOM.
|
|
* </p>
|
|
*
|
|
* @param {Object} properties This is
|
|
* an <em>optional</em> object that contains the following properties (each of which are optional):
|
|
* </p>
|
|
* <ul>
|
|
* <li>
|
|
* <strong>height</strong> (Number) — The desired height, in pixels, of the
|
|
* displayed Publisher video stream (default: 198). <i>Note:</i> Use the
|
|
* <code>height</code> and <code>width</code> properties to set the dimensions
|
|
* of the publisher video; do not set the height and width of the DOM element
|
|
* (using CSS).
|
|
* </li>
|
|
* <li>
|
|
* <strong>mirror</strong> (Boolean) — Whether the publisher's video image
|
|
* is mirrored in the publisher's page<. The default value is <code>true</code>
|
|
* (the video image is mirrored). This property does not affect the display
|
|
* on other subscribers' web pages.
|
|
* </li>
|
|
* <li>
|
|
* <strong>name</strong> (String) — The name for this stream. The name appears at the bottom of
|
|
* Subscriber videos. The default value is "" (an empty string). Setting this to a string longer than
|
|
* 1000 characters results in an runtime exception.
|
|
* </li>
|
|
* <li>
|
|
* <strong>publishAudio</strong> (Boolean) — Whether to initially publish audio
|
|
* for the stream (default: <code>true</code>). This setting applies when you pass
|
|
* the Publisher object in a call to the <code>Session.publish()</code> method.
|
|
* </li>
|
|
* <li>
|
|
* <strong>publishVideo</strong> (Boolean) — Whether to initially publish video
|
|
* for the stream (default: <code>true</code>). This setting applies when you pass
|
|
* the Publisher object in a call to the <code>Session.publish()</code> method.
|
|
* </li>
|
|
* <li>
|
|
* <strong>style</strong> (Object) — An object containing properties that define the initial
|
|
* appearance of user interface controls of the Publisher. Currently, the <code>style</code> object
|
|
* includes one property: <code>nameDisplayMode</code>. Possible values for the <code>style.nameDisplayMode</code>
|
|
* property are: <code>"auto"</code> (the name is displayed when the stream is first displayed
|
|
* and when the user mouses over the display), <code>"off"</code> (the name is not displayed),
|
|
* and <code>"on"</code> (the name is displayed).</li>
|
|
* </li>
|
|
* <li>
|
|
* <strong>width</strong> (Number) — The desired width, in pixels, of the
|
|
* displayed Publisher video stream (default: 264). <i>Note:</i> Use the
|
|
* <code>height</code> and <code>width</code> properties to set the dimensions
|
|
* of the publisher video; do not set the height and width of the DOM element
|
|
* (using CSS).
|
|
* </li>
|
|
* </ul>
|
|
*
|
|
* @returns {Publisher} The Publisher object.
|
|
* @see Session#publish
|
|
* @method TB.initPublisher
|
|
* @memberof TB
|
|
*/
|
|
OT.initPublisher = function(apiKey, targetElement, properties, completionHandler) {
|
|
OT.debug("TB.initPublisher("+targetElement+")");
|
|
|
|
var publisher = new OT.Publisher();;
|
|
OT.publishers.add(publisher);
|
|
|
|
if (completionHandler && OT.$.isFunction(completionHandler)) {
|
|
var removeHandlersAndCallComplete = function removeHandlersAndCallComplete (error) {
|
|
publisher.off('initSuccess', removeHandlersAndCallComplete);
|
|
publisher.off('publishComplete', removeHandlersAndCallComplete);
|
|
|
|
completionHandler.apply(null, arguments);
|
|
};
|
|
|
|
publisher.once('initSuccess', removeHandlersAndCallComplete);
|
|
publisher.once('publishComplete', removeHandlersAndCallComplete);
|
|
}
|
|
|
|
publisher.publish(targetElement, properties);
|
|
|
|
return publisher;
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Checks if the system supports OpenTok for WebRTC.
|
|
* @return {Number} Whether the system supports OpenTok for WebRTC (1) or not (0).
|
|
* @see <a href="#upgradeSystemRequirements">TB.upgradeSystemRequirements()</a>
|
|
* @method TB.checkSystemRequirements
|
|
* @memberof TB
|
|
*/
|
|
OT.checkSystemRequirements = function() {
|
|
OT.debug("TB.checkSystemRequirements()");
|
|
|
|
var systemRequirementsMet = OT.$.supportsWebSockets() && OT.$.supportsWebRTC() ? this.HAS_REQUIREMENTS : this.NOT_HAS_REQUIREMENTS;
|
|
|
|
OT.checkSystemRequirements = function() {
|
|
OT.debug("TB.checkSystemRequirements()");
|
|
return systemRequirementsMet;
|
|
};
|
|
|
|
return systemRequirementsMet;
|
|
};
|
|
|
|
|
|
/**
|
|
* Displays information about system requirments for OpenTok for WebRTC. This information is displayed
|
|
* in an iframe element that fills the browser window.
|
|
* <p>
|
|
* <i>Note:</i> this information is displayed automatically when you call the <code>TB.initSession()</code>
|
|
* or the <code>TB.initPublisher()</code> method if the client does not support OpenTok for WebRTC.
|
|
* </p>
|
|
* @see <a href="#checkSystemRequirements">TB.checkSystemRequirements()</a>
|
|
* @method TB.upgradeSystemRequirements
|
|
* @memberof TB
|
|
*/
|
|
OT.upgradeSystemRequirements = function(){
|
|
// trigger after the TB environment has loaded
|
|
OT.onLoad( function() {
|
|
var id = '_upgradeFlash';
|
|
|
|
// Load the iframe over the whole page.
|
|
document.body.appendChild((function(){
|
|
var d = document.createElement('iframe');
|
|
d.id = id;
|
|
d.style.position = 'absolute';
|
|
d.style.position = 'fixed';
|
|
d.style.height = '100%';
|
|
d.style.width = '100%';
|
|
d.style.top = '0px';
|
|
d.style.left = '0px';
|
|
d.style.right = '0px';
|
|
d.style.bottom = '0px';
|
|
d.style.zIndex = 1000;
|
|
try {
|
|
d.style.backgroundColor = "rgba(0,0,0,0.2)";
|
|
} catch (err) {
|
|
// Old IE browsers don't support rgba and we still want to show the upgrade message
|
|
// but we just make the background of the iframe completely transparent.
|
|
d.style.backgroundColor = "transparent";
|
|
d.setAttribute("allowTransparency", "true");
|
|
}
|
|
d.setAttribute("frameBorder", "0");
|
|
d.frameBorder = "0";
|
|
d.scrolling = "no";
|
|
d.setAttribute("scrolling", "no");
|
|
d.src = OT.properties.assetURL + "/html/upgradeFlash.html#"+encodeURIComponent(document.location.href);
|
|
return d;
|
|
})());
|
|
|
|
// Now we need to listen to the event handler if the user closes this dialog.
|
|
// Since this is from an IFRAME within another domain we are going to listen to hash changes.
|
|
// The best cross browser solution is to poll for a change in the hashtag.
|
|
if (_intervalId) clearInterval(_intervalId);
|
|
_intervalId = setInterval(function(){
|
|
var hash = document.location.hash,
|
|
re = /^#?\d+&/;
|
|
if (hash !== _lastHash && re.test(hash)) {
|
|
_lastHash = hash;
|
|
if( hash.replace(re, '') === 'close_window'){
|
|
document.body.removeChild(document.getElementById(id));
|
|
document.location.hash = '';
|
|
}
|
|
}
|
|
}, 100);
|
|
});
|
|
};
|
|
|
|
|
|
OT.reportIssue = function(){
|
|
OT.warn("ToDo: haven't yet implemented TB.reportIssue");
|
|
};
|
|
|
|
OT.components = {};
|
|
OT.sessions = {};
|
|
|
|
// namespaces
|
|
OT.rtc = {};
|
|
|
|
// Define the APIKEY this is a global parameter which should not change
|
|
OT.APIKEY = (function(){
|
|
// Script embed
|
|
var script_src = (function(){
|
|
var s = document.getElementsByTagName('script');
|
|
s = s[s.length - 1];
|
|
s = s.getAttribute('src') || s.src;
|
|
return s;
|
|
})();
|
|
|
|
var m = script_src.match(/[\?\&]apikey=([^&]+)/i);
|
|
return m ? m[1] : '';
|
|
})();
|
|
|
|
OT.HAS_REQUIREMENTS = 1;
|
|
OT.NOT_HAS_REQUIREMENTS = 0;
|
|
|
|
/**
|
|
* Registers a method as an event listener for a specific event. Note that this is a static method of the TB class.
|
|
*
|
|
* <p>
|
|
* The TB object dispatches one type of event — an <code>exception</code> event. The following code adds an event
|
|
* listener for the <code>exception</code> event:
|
|
* </p>
|
|
*
|
|
* <pre>
|
|
* TB.addEventListener("exception", exceptionHandler);
|
|
*
|
|
* function exceptionHandler(event) {
|
|
* alert("exception event. \n code == " + event.code + "\n message == " + event.message);
|
|
* }
|
|
* </pre>
|
|
*
|
|
* <p>
|
|
* If a handler is not registered for an event, the event is ignored locally. If the event listener function does not exist,
|
|
* the event is ignored locally.
|
|
* </p>
|
|
* <p>
|
|
* Throws an exception if the <code>listener</code> name is invalid.
|
|
* </p>
|
|
*
|
|
* @param {String} type The string identifying the type of event.
|
|
*
|
|
* @param {Function} listener The function to be invoked when the TB object dispatches the event.
|
|
* @memberOf TB
|
|
* @method addEventListener
|
|
*/
|
|
|
|
/**
|
|
* Removes an event listener for a specific event. Note that this is a static method of the TB class.
|
|
*
|
|
* <p>
|
|
* Throws an exception if the <code>listener</code> name is invalid.
|
|
* </p>
|
|
*
|
|
* @param {String} type The string identifying the type of event.
|
|
*
|
|
* @param {Function} listener The event listener function to remove.
|
|
*
|
|
* @memberOf TB
|
|
* @method removeEventListener
|
|
*/
|
|
|
|
/**
|
|
* Dispatched by the TB class when the app encounters an exception.
|
|
* Note that you set up an event handler for the <code>exception</code> event by calling the
|
|
* <code>TB.addEventListener()</code> method, which is a <i>static</i> method.
|
|
*
|
|
* @name exception
|
|
* @event
|
|
* @borrows ExceptionEvent#message as this.message
|
|
* @memberof TB
|
|
* @see ExceptionEvent
|
|
*/
|
|
|
|
if (!window.OT) window.OT = OT;
|
|
if (!window.TB) window.TB = OT;
|
|
|
|
})(window);
|
|
(function(global) {
|
|
|
|
OT.Collection = function(idField) {
|
|
var _models = [],
|
|
_byId = {},
|
|
_idField = idField || 'id';
|
|
|
|
OT.$.eventing(this, true);
|
|
|
|
var onModelUpdate = function onModelUpdate (event) {
|
|
this.trigger('update', event);
|
|
this.trigger('update:'+event.target.id, event);
|
|
}.bind(this),
|
|
|
|
onModelIdUpdate = function onModelIdUpdate (oldId, newId) {
|
|
if (_byId[oldId] === void 0) return;
|
|
|
|
// Match their old index to their new id and remove any
|
|
// reference to the old one.
|
|
var index = _byId[oldId];
|
|
delete _byId[oldId];
|
|
_byId[newId] = index;
|
|
}.bind(this),
|
|
|
|
onModelDestroy = function onModelDestroyed (event) {
|
|
this.remove(event.target, event.reason);
|
|
}.bind(this);
|
|
|
|
|
|
this.reset = function() {
|
|
// Stop listening on the models, they are no longer our problem
|
|
_models.forEach(function(model) {
|
|
model.off('updated', onModelUpdate, this);
|
|
model.off('destroyed', onModelDestroy, this)
|
|
model.off('idUpdated', onModelIdUpdate, this);
|
|
}, this);
|
|
|
|
_models = [];
|
|
_byId = {};
|
|
};
|
|
|
|
this.destroy = function() {
|
|
_models.forEach(function(model) {
|
|
model.destroy(void 0, true);
|
|
});
|
|
|
|
this.reset();
|
|
this.off();
|
|
};
|
|
|
|
this.get = function(id) { return id && _byId[id] !== void 0 ? _models[_byId[id]] : void 0; };
|
|
this.has = function(id) { return id && _byId[id] !== void 0; };
|
|
|
|
this.toString = function() { return _models.toString(); };
|
|
|
|
// Return only models filtered by either a dict of properties
|
|
// or a filter function.
|
|
//
|
|
// @example Return all publishers with a streamId of 1
|
|
// OT.publishers.where({streamId: 1})
|
|
//
|
|
// @example The same thing but filtering using a filter function
|
|
// OT.publishers.where(function(publisher) {
|
|
// return publisher.stream.id === 4;
|
|
// });
|
|
//
|
|
// @example The same thing but filtering using a filter function
|
|
// executed with a specific this
|
|
// OT.publishers.where(function(publisher) {
|
|
// return publisher.stream.id === 4;
|
|
// }, self);
|
|
//
|
|
this.where = function(attrsOrFilterFn, context) {
|
|
if (OT.$.isFunction(attrsOrFilterFn)) return _models.filter(attrsOrFilterFn, context);
|
|
|
|
return _models.filter(function(model) {
|
|
for (var key in attrsOrFilterFn) {
|
|
if (model[key] !== attrsOrFilterFn[key]) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
};
|
|
|
|
// Similar to where in behaviour, except that it only returns
|
|
// the first match.
|
|
this.find = function(attrsOrFilterFn, context) {
|
|
var filterFn;
|
|
|
|
if (OT.$.isFunction(attrsOrFilterFn)) {
|
|
filterFn = attrsOrFilterFn;
|
|
}
|
|
else {
|
|
filterFn = function(model) {
|
|
for (var key in attrsOrFilterFn) {
|
|
if (model[key] !== attrsOrFilterFn[key]) return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
filterFn = filterFn.bind(context);
|
|
|
|
for (var i=0; i<_models.length; ++i) {
|
|
if (filterFn(_models[i]) === true) return _models[i];
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
this.add = function(model) {
|
|
var id = model[_idField];
|
|
|
|
if (this.has(id)) {
|
|
OT.warn("Model " + id + ' is already in the collection', _models);
|
|
return this;
|
|
}
|
|
|
|
_byId[id] = _models.push(model) - 1;
|
|
|
|
model.on('updated', onModelUpdate, this);
|
|
model.on('destroyed', onModelDestroy, this)
|
|
model.on('idUpdated', onModelIdUpdate, this);
|
|
|
|
this.trigger('add', model);
|
|
this.trigger('add:'+id, model);
|
|
|
|
return this;
|
|
};
|
|
|
|
this.remove = function(model, reason) {
|
|
var id = model[_idField];
|
|
|
|
_models.splice(_byId[id], 1);
|
|
|
|
// Shuffle everyone down one
|
|
for (var i=_byId[id]; i<_models.length; ++i) {
|
|
_byId[_models[i][_idField]] = i
|
|
}
|
|
|
|
delete _byId[id];
|
|
|
|
model.off('updated', onModelUpdate, this);
|
|
model.off('destroyed', onModelDestroy, this)
|
|
model.off('idUpdated', onModelIdUpdate, this);
|
|
|
|
this.trigger('remove', model, reason);
|
|
this.trigger('remove:'+id, model, reason);
|
|
|
|
return this;
|
|
};
|
|
|
|
OT.$.defineGetters(this, {
|
|
length: function() { return _models.length; }
|
|
});
|
|
};
|
|
|
|
}(this));
|
|
(function(window) {
|
|
|
|
/**
|
|
* The Event object defines the basic OpenTok event object that is passed to
|
|
* event listeners. Other OpenTok event classes implement the properties and methods of
|
|
* the Event object.</p>
|
|
*
|
|
* <p>For example, the Stream object dispatches a <code>streamPropertyChanged</code> event when
|
|
* the stream's properties are updated. You register an event listener using the <code>addEventListener()</code>
|
|
* method of the Stream object:</p>
|
|
*
|
|
* <pre>
|
|
* stream.addEventListener("streamPropertyChanged", streamPropertyChangedHandler);
|
|
*
|
|
* function streamPropertyChangedHandler(event) {
|
|
* alert("Properties changed for stream " + event.target.streamId);
|
|
* }</pre>
|
|
*
|
|
* @class Event
|
|
* @property {Boolean} cancelable Whether the event has a default behavior that is cancelable (<code>true</code>)
|
|
* or not (<code>false</code>). You can cancel the default behavior by calling the <code>preventDefault()</code> method
|
|
* of the Event object in the event listener function. (See <a href="#preventDefault">preventDefault()</a>.)
|
|
*
|
|
* @property {Object} target The object that dispatched the event.
|
|
*
|
|
* @property {String} type The type of event.
|
|
*/
|
|
OT.Event = OT.$.eventing.Event();
|
|
/**
|
|
* Prevents the default behavior associated with the event from taking place.
|
|
*
|
|
* <p>To see whether an event has a default behavior, check the <code>cancelable</code> property of the event object. </p>
|
|
*
|
|
* <p>Call the <code>preventDefault()</code> method in the event listener function for the event.</p>
|
|
*
|
|
* <p>The following events have default behaviors:</p>
|
|
*
|
|
* <ul>
|
|
*
|
|
* <li><code>sessionDisconnect</code> — See <a href="SessionDisconnectEvent.html#preventDefault">
|
|
* SessionDisconnectEvent.preventDefault()</a>.</li>
|
|
*
|
|
* <li><code>streamDestroyed</code> — See <a href="StreamEvent.html#preventDefault">
|
|
* StreamEvent.preventDefault()</a>.</li>
|
|
*
|
|
* </ul>
|
|
*
|
|
* @method #preventDefault
|
|
* @memberof Event
|
|
*/
|
|
/**
|
|
* Whether the default event behavior has been prevented via a call to <code>preventDefault()</code> (<code>true</code>)
|
|
* or not (<code>false</code>). See <a href="#preventDefault">preventDefault()</a>.
|
|
* @method #isDefaultPrevented
|
|
* @return {Boolean}
|
|
* @memberof Event
|
|
*/
|
|
|
|
// Event names lookup
|
|
OT.Event.names = {
|
|
// Activity Status for cams/mics
|
|
ACTIVE: "active",
|
|
INACTIVE: "inactive",
|
|
UNKNOWN: "unknown",
|
|
|
|
// Archive types
|
|
PER_SESSION: "perSession",
|
|
PER_STREAM: "perStream",
|
|
|
|
// TB Events
|
|
EXCEPTION: "exception",
|
|
ISSUE_REPORTED: "issueReported",
|
|
|
|
// Session Events
|
|
SESSION_CONNECTED: "sessionConnected",
|
|
SESSION_DISCONNECTED: "sessionDisconnected",
|
|
STREAM_CREATED: "streamCreated",
|
|
STREAM_DESTROYED: "streamDestroyed",
|
|
CONNECTION_CREATED: "connectionCreated",
|
|
CONNECTION_DESTROYED: "connectionDestroyed",
|
|
SIGNAL: "signal",
|
|
STREAM_PROPERTY_CHANGED: "streamPropertyChanged",
|
|
MICROPHONE_LEVEL_CHANGED: "microphoneLevelChanged",
|
|
|
|
|
|
// Publisher Events
|
|
RESIZE: "resize",
|
|
SETTINGS_BUTTON_CLICK: "settingsButtonClick",
|
|
DEVICE_INACTIVE: "deviceInactive",
|
|
INVALID_DEVICE_NAME: "invalidDeviceName",
|
|
ACCESS_ALLOWED: "accessAllowed",
|
|
ACCESS_DENIED: "accessDenied",
|
|
ACCESS_DIALOG_OPENED: 'accessDialogOpened',
|
|
ACCESS_DIALOG_CLOSED: 'accessDialogClosed',
|
|
ECHO_CANCELLATION_MODE_CHANGED: "echoCancellationModeChanged",
|
|
PUBLISHER_DESTROYED: 'destroyed',
|
|
|
|
// Subscriber Events
|
|
SUBSCRIBER_DESTROYED: 'destroyed',
|
|
|
|
// DeviceManager Events
|
|
DEVICES_DETECTED: "devicesDetected",
|
|
|
|
// DevicePanel Events
|
|
DEVICES_SELECTED: "devicesSelected",
|
|
CLOSE_BUTTON_CLICK: "closeButtonClick",
|
|
|
|
MICLEVEL : 'microphoneActivityLevel',
|
|
MICGAINCHANGED : 'microphoneGainChanged',
|
|
|
|
// Environment Loader
|
|
ENV_LOADED: 'envLoaded'
|
|
};
|
|
|
|
OT.ValueEvent = function (type,value){
|
|
OT.Event.call(this, type);
|
|
this.value = value;
|
|
|
|
};
|
|
|
|
OT.ExceptionCodes = {
|
|
JS_EXCEPTION: 2000,
|
|
AUTHENTICATION_ERROR: 1004,
|
|
INVALID_SESSION_ID: 1005,
|
|
CONNECT_FAILED: 1006,
|
|
CONNECT_REJECTED: 1007,
|
|
CONNECTION_TIMEOUT: 1008,
|
|
NOT_CONNECTED: 1010,
|
|
P2P_CONNECTION_FAILED: 1013,
|
|
API_RESPONSE_FAILURE: 1014,
|
|
UNABLE_TO_PUBLISH: 1500,
|
|
UNABLE_TO_SIGNAL: 1510,
|
|
UNABLE_TO_FORCE_DISCONNECT: 1520,
|
|
UNABLE_TO_FORCE_UNPUBLISH: 1530
|
|
};
|
|
|
|
/**
|
|
* The {@link TB} class dispatches <code>exception</code> events when the OpenTok API encounters
|
|
* an exception (error). The ExceptionEvent object defines the properties of the event
|
|
* object that is dispatched.
|
|
*
|
|
* <p>Note that you set up an event handler for the <code>exception</code> event by calling the
|
|
* <code>TB.addEventListener()</code> method, which is a <i>static</i> method.</p>
|
|
*
|
|
* @class ExceptionEvent
|
|
* @property {Number} code The error code. The following is a list of error codes:</p>
|
|
*
|
|
* <table class="docs_table">
|
|
* <tbody><tr>
|
|
* <td>
|
|
* <b>code</b>
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* <b>title</b>
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1000
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Failed To Load
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 1004
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Authentication error
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 1005
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Invalid Session ID
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1006
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Connect Failed
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1007
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Connect Rejected
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1008
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Connect Time-out
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1009
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Security Error
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1010
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Not Connected
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1011
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Invalid Parameter
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1013
|
|
* </td>
|
|
* <td>
|
|
* Connection Failed
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1014
|
|
* </td>
|
|
* <td>
|
|
* API Response Failure
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 1500
|
|
* </td>
|
|
* <td>
|
|
* Unable to Publish
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 1510
|
|
* </td>
|
|
* <td>
|
|
* Unable to Signal
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 1520
|
|
* </td>
|
|
* <td>
|
|
* Unable to Force Disconnect
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 1530
|
|
* </td>
|
|
* <td>
|
|
* Unable to Force Unpublish
|
|
* </td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>
|
|
* 1535
|
|
* </td>
|
|
* <td>
|
|
* Force Unpublish on Invalid Stream
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 2000
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Internal Error
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
* <tr>
|
|
* <td>
|
|
* 2010
|
|
*
|
|
* </td>
|
|
* <td>
|
|
* Report Issue Failure
|
|
* </td>
|
|
* </tr>
|
|
*
|
|
*
|
|
* </tbody></table>
|
|
*
|
|
* <p>Check the <code>message</code> property for more details about the error.</p>
|
|
*
|
|
* @property {String} message The error message.
|
|
* @property {String} title The error title.
|
|
* @augments Event
|
|
*/
|
|
OT.ExceptionEvent = function (type, message, title, code, component, target) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.message = message;
|
|
this.title = title;
|
|
this.code = code;
|
|
this.component = component;
|
|
this.target = target;
|
|
};
|
|
|
|
|
|
OT.IssueReportedEvent = function (type, issueId) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.issueId = issueId;
|
|
};
|
|
|
|
// Triggered when the JS dynamic config and the DOM have loaded.
|
|
OT.EnvLoadedEvent = function (type) {
|
|
OT.Event.call(this, type);
|
|
};
|
|
|
|
|
|
/**
|
|
* The Session object dispatches a ConnectionEvent object when a connection is created or destroyed.
|
|
*
|
|
* <h5><a href="example"></a>Example</h5>
|
|
*
|
|
* <p>The following code keeps a running total of the number of connections to a session
|
|
* by monitoring the <code>connections</code> property of the <code>sessionConnect</code>,
|
|
* <code>connectionCreated</code> and <code>connectionDestroyed</code> events:</p>
|
|
*
|
|
* <pre>var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var connectionCount = 0;
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectedHandler);
|
|
* session.addEventListener("connectionCreated", connectionCreatedHandler);
|
|
* session.addEventListener("connectionDestroyed", connectionDestroyedHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function sessionConnectedHandler(event) {
|
|
* connectionCount = event.connections.length;
|
|
* displayConnectionCount();
|
|
* }
|
|
*
|
|
* function connectionCreatedHandler(event) {
|
|
* connectionCount += event.connections.length;
|
|
* displayConnectionCount();
|
|
* }
|
|
*
|
|
* function connectionDestroyedHandler(event) {
|
|
* connectionCount -= event.connections.length;
|
|
* displayConnectionCount();
|
|
* }
|
|
*
|
|
* function displayConnectionCount() {
|
|
* document.getElementById("connectionCountField").value = connectionCount.toString();
|
|
* }</pre>
|
|
*
|
|
* <p>This example assumes that there is an input text field in the HTML DOM
|
|
* with the <code>id</code> set to <code>"connectionCountField"</code>:</p>
|
|
*
|
|
* <pre><input type="text" id="connectionCountField" value="0"></input></pre>
|
|
*
|
|
*
|
|
* @property {Array} connections An array of Connection objects for the connections that were created or deleted.
|
|
* You can compare the <code>connectionId</code> property to that of the <code>connection</code> property of the
|
|
* Session object to see if a connection refers to the local web page.
|
|
*
|
|
* @property {String} reason For a <code>connectionDestroyed</code> event,
|
|
* a description of why the connection ended. This property can have two values:
|
|
* </p>
|
|
* <ul>
|
|
* <li><code>"clientDisconnected"</code> — A client disconnected from the session by calling
|
|
* the <code>disconnect()</code> method of the Session object or by closing the browser.
|
|
* (See <a href="Session.html#disconnect">Session.disconnect()</a>.)</li>
|
|
*
|
|
* <li><code>"forceDisconnected"</code> — A moderator has disconnected the publisher from the session,
|
|
* by calling the <code>forceDisconnect()</code> method of the Session object.
|
|
* (See <a href="Session.html#forceDisconnect">Session.forceDisconnect()</a>.)</li>
|
|
*
|
|
* <li><code>"networkDisconnected"</code> — The network connection terminated abruptly (for example,
|
|
* the client lost their internet connection).</li>
|
|
* </ul>
|
|
*
|
|
* <p>Depending on the context, this description may allow the developer to refine
|
|
* the course of action they take in response to an event.</p>
|
|
*
|
|
* <p>For a <code>connectionCreated</code> event, this string is undefined.</p>
|
|
*
|
|
* @class ConnectionEvent
|
|
* @augments Event
|
|
*/
|
|
OT.ConnectionEvent = function (type, connections, reason) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.connections = connections;
|
|
this.reason = reason;
|
|
};
|
|
|
|
/**
|
|
* StreamEvent is an event that can have type "streamCreated" or "streamDestroyed". These events are dispatched when a client
|
|
* starts or stops publishing to a {@link Session}. This includes remote clients publishing on the session as well as the local
|
|
* client publishing to the session.
|
|
*
|
|
* <h4><a href="example_streamCreated"></a>Example — streamCreated event</h4>
|
|
* <p>The following code initializes a session and sets up an event listener for when
|
|
* a stream is created:</p>
|
|
*
|
|
* <pre>var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("streamCreated", streamCreatedHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function streamCreatedHandler(event) {
|
|
* for (var i = 0; i < event.streams.length; i++) {
|
|
* // Only display others' streams, not those that the client publishes.
|
|
* if (event.streams[i].connection.connectionId != event.target.connection.connectionId) {
|
|
* displayStream(stream);
|
|
* }
|
|
* }
|
|
* }
|
|
*
|
|
* function displayStream(stream) {
|
|
* var div = document.createElement('div');
|
|
* div.setAttribute('id', 'stream' + stream.streamId);
|
|
*
|
|
* var streamsContainer = document.getElementById('streamsContainer');
|
|
* streamsContainer.appendChild(div);
|
|
*
|
|
* subscriber = session.subscribe(stream, 'stream' + stream.streamId);
|
|
* }</pre>
|
|
*
|
|
* <p>For this example, in addition to the event handler for the <code>streamCreated</code>
|
|
* event, you would probably want to create an event handler for the <code>sessionConnected</code>
|
|
* event. This event handler can display the streams that are present when the session first
|
|
* connects. See {@link SessionConnectEvent}.</p>
|
|
*
|
|
* <h4><a href="example_streamDestroyed"></a>Example — streamDestroyed event</h4>
|
|
*
|
|
* <p>The following code initializes a session and sets up an event listener for when a
|
|
* stream ends:</p>
|
|
*
|
|
* <pre>var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("streamDestroyed", streamDestroyedHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function streamDestroyedHandler(event) {
|
|
* for (var i = 0; i < event.streams.length; i++) {
|
|
* var stream = event.streams[i];
|
|
* alert("Stream " + stream.name + " ended. " + event.reason);
|
|
* }
|
|
* }</pre>
|
|
*
|
|
* @class StreamEvent
|
|
* @property {Array} streams An array of Stream objects
|
|
* corresponding to the streams to which this event refers. This is usually an array containing only
|
|
* one Stream object, corresponding to the stream that was added (in the case of a <code>streamCreated</code>
|
|
* event) or deleted (in the case of a <code>streamDestroyed</code> event). However, the array may contain
|
|
* multiple Stream objects if multiple streams were added or deleted. A stream may be published by the local client
|
|
* or another client connected to the session.
|
|
*
|
|
* @property {String} reason For a <code>streamDestroyed</code> event,
|
|
* a description of why the session disconnected. This property can have one of the following values:
|
|
* </p>
|
|
* <ul>
|
|
* <li><code>"clientDisconnected"</code> — A client disconnected from the session by calling
|
|
* the <code>disconnect()</code> method of the Session object or by closing the browser.
|
|
* (See <a href="Session.html#disconnect">Session.disconnect()</a>.)</li>
|
|
*
|
|
* <li><code>"forceDisconnected"</code> — A moderator has disconnected the publisher of the
|
|
* stream from the session, by calling the <code>forceDisconnect()</code> method of the Session object.
|
|
* (See <a href="Session.html#forceDisconnect">Session.forceDisconnect()</a>.)</li>
|
|
*
|
|
* <li><code>"forceUnpublished"</code> — A moderator has forced the publisher of the stream to stop
|
|
* publishing the stream, by calling the <code>forceUnpublish()</code> method of the Session object.
|
|
* (See <a href="Session.html#forceUnpublish">Session.forceUnpublish()</a>.)</li>
|
|
*
|
|
* <li><code>"networkDisconnected"</code> — The network connection terminated abruptly (for example,
|
|
* the client lost their internet connection).</li>
|
|
*
|
|
* </ul>
|
|
*
|
|
* <p>Depending on the context, this description may allow the developer to refine
|
|
* the course of action they take in response to an event.</p>
|
|
*
|
|
* <p>For a <code>streamCreated</code> event, this string is undefined.</p>
|
|
*
|
|
*
|
|
* @property {Boolean} cancelable Whether the event has a default behavior that is cancelable (<code>true</code>) or not (<code>false</code>).
|
|
* You can cancel the default behavior by calling the <code>preventDefault()</code> method of
|
|
* the StreamEvent object in the event listener function. The <code>streamDestroyed</code>
|
|
* event is cancelable. (See <a href="#preventDefault">preventDefault()</a>.)
|
|
* @augments Event
|
|
*/
|
|
OT.StreamEvent = function (type, streams, reason, cancelable) {
|
|
OT.Event.call(this, type, cancelable);
|
|
|
|
this.streams = streams;
|
|
this.reason = reason;
|
|
};
|
|
|
|
/**
|
|
* Prevents the default behavior associated with the event from taking place.
|
|
*
|
|
* <p>For the <code>streamDestroyed</code> event, if the <code>reason</code> is set to <code>"forceDisconnected"</code>
|
|
* or <code>networkDisconnected</code>, the default behavior is that all Subscriber objects that are
|
|
* subscribed to the stream are unsubscribed (and removed from the HTML DOM). If you call the <code>preventDefault()</code>
|
|
* method in the event listener for the <code>streamDestroyed</code> event, the default behavior is prevented and
|
|
* you can, optionally, clean up Subscriber objects using your own code. See
|
|
* <a href="Session.html#getSubscribersForStream">Session.getSubscribersForStream()</a>.</p>
|
|
*
|
|
* <p>If the <code>reason</code> property is set to <code>"forceUnpublished"</code>, the default behavior
|
|
* is that the associated Subscriber or Publisher objects corresponding with the stream are unsubscribed
|
|
* or unpublished (and removed from the HTML DOM). If you call the <code>preventDefault()</code> method in the
|
|
* event listener for the <code>streamDestroyed</code> event, the default behavior is prevented and
|
|
* you can, optionally, clean up Subscriber or Publisher objects using your own code. See
|
|
* <a href="Session.html#getPublisherForStream">Session.getPublisherForStream()</a> and
|
|
* <a href="Session.html#getSubscribersForStream">Session.getSubscribersForStream()</a>.</p>
|
|
*
|
|
* <p>To see whether an event has a default behavior, check the <code>cancelable</code> property of the event object. </p>
|
|
*
|
|
* <p>Call the <code>preventDefault()</code> method in the event listener function for the event.</p>
|
|
*
|
|
* @method #preventDefault
|
|
* @memberof StreamEvent
|
|
*/
|
|
|
|
|
|
/**
|
|
* The Session object dispatches SessionConnectEvent object when a session has successfully connected in response to a call to
|
|
* the <code>connect()</code> method of the Session object.
|
|
*
|
|
* <p>
|
|
* For an example, see <a href="Session.html#connect">Session.connect()</a>.
|
|
* </p>
|
|
*
|
|
* @class SessionConnectEvent
|
|
* @property {Array} connections An array of Connection objects, representing connections to the session.
|
|
* (Note that each connection can publish multiple streams.)
|
|
* @property {Array} streams An array of Stream objects corresponding to the streams currently available in the session that has connected.
|
|
* @augments Event
|
|
*/
|
|
OT.SessionConnectEvent = function (type, connections, streams, archives) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.connections = connections;
|
|
this.streams = streams;
|
|
this.archives = archives;
|
|
this.groups = []; // Deprecated in OpenTok v0.91.48
|
|
};
|
|
|
|
/**
|
|
* The Session object dispatches SessionDisconnectEvent object when a session has disconnected. This event may be dispatched asynchronously in
|
|
* response to a successful call to the <code>disconnect()</code> method of the session object.
|
|
*
|
|
* <h4>
|
|
* <a href="example"></a>Example
|
|
* </h4>
|
|
* <p>
|
|
* The following code initializes a session and sets up an event listener for when a session is disconnected.
|
|
* </p>
|
|
* <pre>var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionDisconnected", sessionDisconnectedHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function sessionDisConnectedHandler(event) {
|
|
* alert("The session disconnected. " + event.reason);
|
|
* }
|
|
* </pre>
|
|
*
|
|
* @property {String} reason A description of why the session disconnected.
|
|
* This property can have two values:
|
|
* </p>
|
|
* <ul>
|
|
* <li><code>"clientDisconnected"</code> — A client disconnected from the session by calling
|
|
* the <code>disconnect()</code> method of the Session object or by closing the browser.
|
|
* ( See <a href="Session.html#disconnect">Session.disconnect()</a>.)</li>
|
|
* <li><code>"forceDisconnected"</code> — A moderator has disconnected you from the session
|
|
* by calling the <code>forceDisconnect()</code> method of the Session object. (See
|
|
* <a href="Session.html#forceDisconnect">Session.forceDisconnect()</a>.)</li>
|
|
* <li><code>"networkDisconnected"</code> — The network connection terminated abruptly (for example,
|
|
* the client lost their internet connection).</li>
|
|
* </ul>
|
|
*
|
|
* @class SessionDisconnectEvent
|
|
* @augments Event
|
|
*/
|
|
OT.SessionDisconnectEvent = function (type, reason, cancelable) {
|
|
OT.Event.call(this, type, cancelable);
|
|
|
|
this.reason = reason;
|
|
};
|
|
|
|
/**
|
|
* Prevents the default behavior associated with the event from taking place.
|
|
*
|
|
* <p>For the <code>sessionDisconnectEvent</code>, the default behavior is: all Subscriber objects are unsubscribed,
|
|
* and Publisher objects are destroyed. If you call the <code>preventDefault()</code> method in the
|
|
* event listener for the <code>sessionDisconnect</code> event, the default behavior is prevented (and
|
|
* you can, optionally, clean up Subscriber and Publisher objects using your own code).
|
|
*
|
|
* <p>To see whether an event has a default behavior, check the <code>cancelable</code> property of the event object. </p>
|
|
*
|
|
* <p>Call the <code>preventDefault()</code> method in the event listener function for the event.</p>
|
|
*
|
|
* @method #preventDefault
|
|
* @memberof SessionDisconnectEvent
|
|
*/
|
|
|
|
OT.VolumeEvent = function (type, streamId, volume) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.streamId = streamId;
|
|
this.volume = volume;
|
|
};
|
|
|
|
|
|
OT.DeviceEvent = function (type, camera, microphone) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.camera = camera;
|
|
this.microphone = microphone;
|
|
};
|
|
|
|
OT.DeviceStatusEvent = function (type, cameras, microphones, selectedCamera, selectedMicrophone) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.cameras = cameras;
|
|
this.microphones = microphones;
|
|
this.selectedCamera = selectedCamera;
|
|
this.selectedMicrophone = selectedMicrophone;
|
|
};
|
|
|
|
OT.ResizeEvent = function (type, widthFrom, widthTo, heightFrom, heightTo) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.widthFrom = widthFrom;
|
|
this.widthTo = widthTo;
|
|
this.heightFrom = heightFrom;
|
|
this.heightTo = heightTo;
|
|
};
|
|
|
|
/**
|
|
* The Session object dispatches a <code>streamPropertyChanged</code> event in the following circumstances:
|
|
*
|
|
* <ul>
|
|
*
|
|
* <li>When a publisher starts or stops publishing audio or video. This change causes the <code>hasAudio</code>
|
|
* or <code>hasVideo</code> property of the Stream object to change. This change results from a call to the
|
|
* <code>publishAudio()</code> or <code>publishVideo()</code> methods of the Publish object.</li>
|
|
*
|
|
* <li>When the <code>videoDimensions</code> property of a stream changes. For more information,
|
|
* see <a href="Stream.html#properties">Stream.videoDimensions</a>.</li>
|
|
*
|
|
* </ul>
|
|
*
|
|
* @class StreamPropertyChangedEvent
|
|
* @property {String} changedProperty The property of the stream that changed. This value is either <code>"hasAudio"</code>,
|
|
* <code>"hasVideo"</code>, or <code>"videoDimensions"</code>.
|
|
* @property {Stream} stream The Stream object for which a property has changed.
|
|
* @property {Object} newValue The new value of the property (after the change).
|
|
* @property {Object} oldValue The old value of the property (before the change).
|
|
*
|
|
* @see <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a></p>
|
|
* @see <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a></p>
|
|
* @see <a href="Stream.html#properties">Stream.videoDimensions</a></p>
|
|
* @augments Event
|
|
*/
|
|
OT.StreamPropertyChangedEvent = function (type, stream, changedProperty, oldValue, newValue) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.type = type;
|
|
this.stream = stream;
|
|
this.changedProperty = changedProperty;
|
|
this.oldValue = oldValue;
|
|
this.newValue = newValue;
|
|
};
|
|
|
|
OT.ArchiveEvent = function (type, archives) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.archives = archives;
|
|
};
|
|
|
|
OT.ArchiveStreamEvent = function (type, archive, streams) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.archive = archive;
|
|
this.streams = streams;
|
|
};
|
|
|
|
OT.StateChangedEvent = function (type, changedValues) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.changedValues = changedValues;
|
|
};
|
|
|
|
OT.ChangeFailedEvent = function (type, reasonCode, reason, failedValues) {
|
|
OT.Event.call(this, type);
|
|
|
|
this.reasonCode = reasonCode;
|
|
this.reason = reason;
|
|
this.failedValues = failedValues;
|
|
};
|
|
|
|
/**
|
|
* The Session object dispatches a signal event when the client receives a signal from the session.
|
|
*
|
|
* @class SignalEvent
|
|
* @property {String} type The type assigned to the signal (if there is one). Use the type to filter signals
|
|
* received (by adding an event handler for signal:type1 or signal:type2, etc.)
|
|
* @property {Object} data The data payload sent with the signal (if there is one).
|
|
* @property {Connection} from The Connection corresponding to the client that sent with the signal.
|
|
*
|
|
* @see <a href="Session.html#signal">Session.signal()</a></p>
|
|
* @see <a href="Session.html#events">Session events (signal and signal:type)</a></p>
|
|
* @augments Event
|
|
*/
|
|
OT.SignalEvent = function(type, data, from) {
|
|
OT.Event.call(this, type ? "signal:" + type : OT.Event.names.SIGNAL, false);
|
|
|
|
this.data = data;
|
|
this.from = from;
|
|
};
|
|
|
|
|
|
OT.StreamUpdatedEvent = function (stream, key, oldValue, newValue) {
|
|
OT.Event.call(this, 'updated');
|
|
|
|
this.target = stream;
|
|
this.changedProperty = key;
|
|
this.oldValue = oldValue;
|
|
this.newValue = newValue;
|
|
};
|
|
|
|
OT.DestroyedEvent = function(type, target, reason) {
|
|
OT.Event.call(this, type, false);
|
|
|
|
this.target = target;
|
|
this.reason = reason;
|
|
};
|
|
|
|
|
|
})(window);
|
|
// https://code.google.com/p/stringencoding/
|
|
// An implementation of http://encoding.spec.whatwg.org/#api
|
|
//
|
|
(function(global) {
|
|
'use strict';
|
|
|
|
if ( (global.TextEncoder !== void 0) && (global.TextDecoder !== void 0)) {
|
|
// defer to the native ones
|
|
// @todo is this a good idea?
|
|
return;
|
|
}
|
|
|
|
//
|
|
// Utilities
|
|
//
|
|
|
|
/**
|
|
* @param {number} a The number to test.
|
|
* @param {number} min The minimum value in the range, inclusive.
|
|
* @param {number} max The maximum value in the range, inclusive.
|
|
* @return {boolean} True if a >= min and a <= max.
|
|
*/
|
|
function inRange(a, min, max) {
|
|
return min <= a && a <= max;
|
|
}
|
|
|
|
/**
|
|
* @param {number} n The numerator.
|
|
* @param {number} d The denominator.
|
|
* @return {number} The result of the integer division of n by d.
|
|
*/
|
|
function div(n, d) {
|
|
return Math.floor(n / d);
|
|
}
|
|
|
|
|
|
//
|
|
// Implementation of Encoding specification
|
|
// http://dvcs.w3.org/hg/encoding/raw-file/tip/Overview.html
|
|
//
|
|
|
|
//
|
|
// 3. Terminology
|
|
//
|
|
|
|
//
|
|
// 4. Encodings
|
|
//
|
|
|
|
/** @const */ var EOF_byte = -1;
|
|
/** @const */ var EOF_code_point = -1;
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {Uint8Array} bytes Array of bytes that provide the stream.
|
|
*/
|
|
function ByteInputStream(bytes) {
|
|
/** @type {number} */
|
|
var pos = 0;
|
|
|
|
/** @return {number} Get the next byte from the stream. */
|
|
this.get = function() {
|
|
return (pos >= bytes.length) ? EOF_byte : Number(bytes[pos]);
|
|
};
|
|
|
|
/** @param {number} n Number (positive or negative) by which to
|
|
* offset the byte pointer. */
|
|
this.offset = function(n) {
|
|
pos += n;
|
|
if (pos < 0) {
|
|
throw new Error('Seeking past start of the buffer');
|
|
}
|
|
if (pos > bytes.length) {
|
|
throw new Error('Seeking past EOF');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @param {Array.<number>} test Array of bytes to compare against.
|
|
* @return {boolean} True if the start of the stream matches the test
|
|
* bytes.
|
|
*/
|
|
this.match = function(test) {
|
|
if (test.length > pos + bytes.length) {
|
|
return false;
|
|
}
|
|
var i;
|
|
for (i = 0; i < test.length; i += 1) {
|
|
if (Number(bytes[pos + i]) !== test[i]) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {Array.<number>} bytes The array to write bytes into.
|
|
*/
|
|
function ByteOutputStream(bytes) {
|
|
/** @type {number} */
|
|
var pos = 0;
|
|
|
|
/**
|
|
* @param {...number} var_args The byte or bytes to emit into the stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.emit = function(var_args) {
|
|
/** @type {number} */
|
|
var last = EOF_byte;
|
|
var i;
|
|
for (i = 0; i < arguments.length; ++i) {
|
|
last = Number(arguments[i]);
|
|
bytes[pos++] = last;
|
|
}
|
|
return last;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {string} string The source of code units for the stream.
|
|
*/
|
|
function CodePointInputStream(string) {
|
|
/**
|
|
* @param {string} string Input string of UTF-16 code units.
|
|
* @return {Array.<number>} Code points.
|
|
*/
|
|
function stringToCodePoints(string) {
|
|
/** @type {Array.<number>} */
|
|
var cps = [];
|
|
// Based on http://www.w3.org/TR/WebIDL/#idl-DOMString
|
|
var i = 0, n = string.length;
|
|
while (i < string.length) {
|
|
var c = string.charCodeAt(i);
|
|
if (!inRange(c, 0xD800, 0xDFFF)) {
|
|
cps.push(c);
|
|
} else if (inRange(c, 0xDC00, 0xDFFF)) {
|
|
cps.push(0xFFFD);
|
|
} else { // (inRange(cu, 0xD800, 0xDBFF))
|
|
if (i === n - 1) {
|
|
cps.push(0xFFFD);
|
|
} else {
|
|
var d = string.charCodeAt(i + 1);
|
|
if (inRange(d, 0xDC00, 0xDFFF)) {
|
|
var a = c & 0x3FF;
|
|
var b = d & 0x3FF;
|
|
i += 1;
|
|
cps.push(0x10000 + (a << 10) + b);
|
|
} else {
|
|
cps.push(0xFFFD);
|
|
}
|
|
}
|
|
}
|
|
i += 1;
|
|
}
|
|
return cps;
|
|
}
|
|
|
|
/** @type {number} */
|
|
var pos = 0;
|
|
/** @type {Array.<number>} */
|
|
var cps = stringToCodePoints(string);
|
|
|
|
/** @param {number} n The number of bytes (positive or negative)
|
|
* to advance the code point pointer by.*/
|
|
this.offset = function(n) {
|
|
pos += n;
|
|
if (pos < 0) {
|
|
throw new Error('Seeking past start of the buffer');
|
|
}
|
|
if (pos > cps.length) {
|
|
throw new Error('Seeking past EOF');
|
|
}
|
|
};
|
|
|
|
|
|
/** @return {number} Get the next code point from the stream. */
|
|
this.get = function() {
|
|
if (pos >= cps.length) {
|
|
return EOF_code_point;
|
|
}
|
|
return cps[pos];
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
*/
|
|
function CodePointOutputStream() {
|
|
/** @type {string} */
|
|
var string = '';
|
|
|
|
/** @return {string} The accumulated string. */
|
|
this.string = function() {
|
|
return string;
|
|
};
|
|
|
|
/** @param {number} c The code point to encode into the stream. */
|
|
this.emit = function(c) {
|
|
if (c <= 0xFFFF) {
|
|
string += String.fromCharCode(c);
|
|
} else {
|
|
c -= 0x10000;
|
|
string += String.fromCharCode(0xD800 + ((c >> 10) & 0x3ff));
|
|
string += String.fromCharCode(0xDC00 + (c & 0x3ff));
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {string} message Description of the error.
|
|
*/
|
|
function EncodingError(message) {
|
|
this.name = 'EncodingError';
|
|
this.message = message;
|
|
this.code = 0;
|
|
}
|
|
EncodingError.prototype = Error.prototype;
|
|
|
|
/**
|
|
* @param {boolean} fatal If true, decoding errors raise an exception.
|
|
* @param {number=} opt_code_point Override the standard fallback code point.
|
|
* @return {number} The code point to insert on a decoding error.
|
|
*/
|
|
function decoderError(fatal, opt_code_point) {
|
|
if (fatal) {
|
|
throw new EncodingError('Decoder error');
|
|
}
|
|
return opt_code_point || 0xFFFD;
|
|
}
|
|
|
|
/**
|
|
* @param {number} code_point The code point that could not be encoded.
|
|
*/
|
|
function encoderError(code_point) {
|
|
throw new EncodingError('The code point ' + code_point +
|
|
' could not be encoded.');
|
|
}
|
|
|
|
/**
|
|
* @param {string} label The encoding label.
|
|
* @return {?{name:string,labels:Array.<string>}}
|
|
*/
|
|
function getEncoding(label) {
|
|
label = String(label).trim().toLowerCase();
|
|
if (Object.prototype.hasOwnProperty.call(label_to_encoding, label)) {
|
|
return label_to_encoding[label];
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** @type {Array.<{encodings: Array.<{name:string,labels:Array.<string>}>,
|
|
* heading: string}>} */
|
|
var encodings = [
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'unicode-1-1-utf-8',
|
|
'utf-8',
|
|
'utf8'
|
|
],
|
|
'name': 'utf-8'
|
|
}
|
|
],
|
|
'heading': 'The Encoding'
|
|
},
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'cp864',
|
|
'ibm864'
|
|
],
|
|
'name': 'ibm864'
|
|
},
|
|
{
|
|
'labels': [
|
|
'cp866',
|
|
'ibm866'
|
|
],
|
|
'name': 'ibm866'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatin2',
|
|
'iso-8859-2',
|
|
'iso-ir-101',
|
|
'iso8859-2',
|
|
'iso_8859-2',
|
|
'l2',
|
|
'latin2'
|
|
],
|
|
'name': 'iso-8859-2'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatin3',
|
|
'iso-8859-3',
|
|
'iso_8859-3',
|
|
'iso-ir-109',
|
|
'l3',
|
|
'latin3'
|
|
],
|
|
'name': 'iso-8859-3'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatin4',
|
|
'iso-8859-4',
|
|
'iso_8859-4',
|
|
'iso-ir-110',
|
|
'l4',
|
|
'latin4'
|
|
],
|
|
'name': 'iso-8859-4'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatincyrillic',
|
|
'cyrillic',
|
|
'iso-8859-5',
|
|
'iso_8859-5',
|
|
'iso-ir-144'
|
|
],
|
|
'name': 'iso-8859-5'
|
|
},
|
|
{
|
|
'labels': [
|
|
'arabic',
|
|
'csisolatinarabic',
|
|
'ecma-114',
|
|
'iso-8859-6',
|
|
'iso_8859-6',
|
|
'iso-ir-127'
|
|
],
|
|
'name': 'iso-8859-6'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatingreek',
|
|
'ecma-118',
|
|
'elot_928',
|
|
'greek',
|
|
'greek8',
|
|
'iso-8859-7',
|
|
'iso_8859-7',
|
|
'iso-ir-126'
|
|
],
|
|
'name': 'iso-8859-7'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatinhebrew',
|
|
'hebrew',
|
|
'iso-8859-8',
|
|
'iso-8859-8-i',
|
|
'iso-ir-138',
|
|
'iso_8859-8',
|
|
'visual'
|
|
],
|
|
'name': 'iso-8859-8'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatin6',
|
|
'iso-8859-10',
|
|
'iso-ir-157',
|
|
'iso8859-10',
|
|
'l6',
|
|
'latin6'
|
|
],
|
|
'name': 'iso-8859-10'
|
|
},
|
|
{
|
|
'labels': [
|
|
'iso-8859-13'
|
|
],
|
|
'name': 'iso-8859-13'
|
|
},
|
|
{
|
|
'labels': [
|
|
'iso-8859-14',
|
|
'iso8859-14'
|
|
],
|
|
'name': 'iso-8859-14'
|
|
},
|
|
{
|
|
'labels': [
|
|
'iso-8859-15',
|
|
'iso_8859-15'
|
|
],
|
|
'name': 'iso-8859-15'
|
|
},
|
|
{
|
|
'labels': [
|
|
'iso-8859-16'
|
|
],
|
|
'name': 'iso-8859-16'
|
|
},
|
|
{
|
|
'labels': [
|
|
'koi8-r',
|
|
'koi8_r'
|
|
],
|
|
'name': 'koi8-r'
|
|
},
|
|
{
|
|
'labels': [
|
|
'koi8-u'
|
|
],
|
|
'name': 'koi8-u'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csmacintosh',
|
|
'mac',
|
|
'macintosh',
|
|
'x-mac-roman'
|
|
],
|
|
'name': 'macintosh'
|
|
},
|
|
{
|
|
'labels': [
|
|
'iso-8859-11',
|
|
'tis-620',
|
|
'windows-874'
|
|
],
|
|
'name': 'windows-874'
|
|
},
|
|
{
|
|
'labels': [
|
|
'windows-1250',
|
|
'x-cp1250'
|
|
],
|
|
'name': 'windows-1250'
|
|
},
|
|
{
|
|
'labels': [
|
|
'windows-1251',
|
|
'x-cp1251'
|
|
],
|
|
'name': 'windows-1251'
|
|
},
|
|
{
|
|
'labels': [
|
|
'ascii',
|
|
'ansi_x3.4-1968',
|
|
'csisolatin1',
|
|
'iso-8859-1',
|
|
'iso8859-1',
|
|
'iso_8859-1',
|
|
'l1',
|
|
'latin1',
|
|
'us-ascii',
|
|
'windows-1252'
|
|
],
|
|
'name': 'windows-1252'
|
|
},
|
|
{
|
|
'labels': [
|
|
'cp1253',
|
|
'windows-1253'
|
|
],
|
|
'name': 'windows-1253'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csisolatin5',
|
|
'iso-8859-9',
|
|
'iso-ir-148',
|
|
'l5',
|
|
'latin5',
|
|
'windows-1254'
|
|
],
|
|
'name': 'windows-1254'
|
|
},
|
|
{
|
|
'labels': [
|
|
'cp1255',
|
|
'windows-1255'
|
|
],
|
|
'name': 'windows-1255'
|
|
},
|
|
{
|
|
'labels': [
|
|
'cp1256',
|
|
'windows-1256'
|
|
],
|
|
'name': 'windows-1256'
|
|
},
|
|
{
|
|
'labels': [
|
|
'windows-1257'
|
|
],
|
|
'name': 'windows-1257'
|
|
},
|
|
{
|
|
'labels': [
|
|
'cp1258',
|
|
'windows-1258'
|
|
],
|
|
'name': 'windows-1258'
|
|
},
|
|
{
|
|
'labels': [
|
|
'x-mac-cyrillic',
|
|
'x-mac-ukrainian'
|
|
],
|
|
'name': 'x-mac-cyrillic'
|
|
}
|
|
],
|
|
'heading': 'Legacy single-byte encodings'
|
|
},
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'chinese',
|
|
'csgb2312',
|
|
'csiso58gb231280',
|
|
'gb2312',
|
|
'gbk',
|
|
'gb_2312',
|
|
'gb_2312-80',
|
|
'iso-ir-58',
|
|
'x-gbk'
|
|
],
|
|
'name': 'gbk'
|
|
},
|
|
{
|
|
'labels': [
|
|
'gb18030'
|
|
],
|
|
'name': 'gb18030'
|
|
},
|
|
{
|
|
'labels': [
|
|
'hz-gb-2312'
|
|
],
|
|
'name': 'hz-gb-2312'
|
|
}
|
|
],
|
|
'heading': 'Legacy multi-byte Chinese (simplified) encodings'
|
|
},
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'big5',
|
|
'big5-hkscs',
|
|
'cn-big5',
|
|
'csbig5',
|
|
'x-x-big5'
|
|
],
|
|
'name': 'big5'
|
|
}
|
|
],
|
|
'heading': 'Legacy multi-byte Chinese (traditional) encodings'
|
|
},
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'cseucpkdfmtjapanese',
|
|
'euc-jp',
|
|
'x-euc-jp'
|
|
],
|
|
'name': 'euc-jp'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csiso2022jp',
|
|
'iso-2022-jp'
|
|
],
|
|
'name': 'iso-2022-jp'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csshiftjis',
|
|
'ms_kanji',
|
|
'shift-jis',
|
|
'shift_jis',
|
|
'sjis',
|
|
'windows-31j',
|
|
'x-sjis'
|
|
],
|
|
'name': 'shift_jis'
|
|
}
|
|
],
|
|
'heading': 'Legacy multi-byte Japanese encodings'
|
|
},
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'cseuckr',
|
|
'csksc56011987',
|
|
'euc-kr',
|
|
'iso-ir-149',
|
|
'korean',
|
|
'ks_c_5601-1987',
|
|
'ks_c_5601-1989',
|
|
'ksc5601',
|
|
'ksc_5601',
|
|
'windows-949'
|
|
],
|
|
'name': 'euc-kr'
|
|
},
|
|
{
|
|
'labels': [
|
|
'csiso2022kr',
|
|
'iso-2022-kr'
|
|
],
|
|
'name': 'iso-2022-kr'
|
|
}
|
|
],
|
|
'heading': 'Legacy multi-byte Korean encodings'
|
|
},
|
|
{
|
|
'encodings': [
|
|
{
|
|
'labels': [
|
|
'utf-16',
|
|
'utf-16le'
|
|
],
|
|
'name': 'utf-16'
|
|
},
|
|
{
|
|
'labels': [
|
|
'utf-16be'
|
|
],
|
|
'name': 'utf-16be'
|
|
}
|
|
],
|
|
'heading': 'Legacy utf-16 encodings'
|
|
}
|
|
];
|
|
|
|
var name_to_encoding = {};
|
|
var label_to_encoding = {};
|
|
encodings.forEach(function(category) {
|
|
category.encodings.forEach(function(encoding) {
|
|
name_to_encoding[encoding.name] = encoding;
|
|
encoding.labels.forEach(function(label) {
|
|
label_to_encoding[label] = encoding;
|
|
});
|
|
});
|
|
});
|
|
|
|
//
|
|
// 5. Indexes
|
|
//
|
|
|
|
/**
|
|
* @param {number} pointer The |pointer| to search for.
|
|
* @param {Array.<?number>} index The |index| to search within.
|
|
* @return {?number} The code point corresponding to |pointer| in |index|,
|
|
* or null if |code point| is not in |index|.
|
|
*/
|
|
function indexCodePointFor(pointer, index) {
|
|
return (index || [])[pointer] || null;
|
|
}
|
|
|
|
/**
|
|
* @param {number} code_point The |code point| to search for.
|
|
* @param {Array.<?number>} index The |index| to search within.
|
|
* @return {?number} The first pointer corresponding to |code point| in
|
|
* |index|, or null if |code point| is not in |index|.
|
|
*/
|
|
function indexPointerFor(code_point, index) {
|
|
var pointer = index.indexOf(code_point);
|
|
return pointer === -1 ? null : pointer;
|
|
}
|
|
|
|
/** @type {Object.<string, (Array.<number>|Array.<Array.<number>>)>} */
|
|
var indexes = global['encoding-indexes'] || {};
|
|
|
|
/**
|
|
* @param {number} pointer The |pointer| to search for in the gb18030 index.
|
|
* @return {?number} The code point corresponding to |pointer| in |index|,
|
|
* or null if |code point| is not in the gb18030 index.
|
|
*/
|
|
function indexGB18030CodePointFor(pointer) {
|
|
if ((pointer > 39419 && pointer < 189000) || (pointer > 1237575)) {
|
|
return null;
|
|
}
|
|
var /** @type {number} */ offset = 0,
|
|
/** @type {number} */ code_point_offset = 0,
|
|
/** @type {Array.<Array.<number>>} */ index = indexes['gb18030'];
|
|
var i;
|
|
for (i = 0; i < index.length; ++i) {
|
|
var entry = index[i];
|
|
if (entry[0] <= pointer) {
|
|
offset = entry[0];
|
|
code_point_offset = entry[1];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return code_point_offset + pointer - offset;
|
|
}
|
|
|
|
/**
|
|
* @param {number} code_point The |code point| to locate in the gb18030 index.
|
|
* @return {number} The first pointer corresponding to |code point| in the
|
|
* gb18030 index.
|
|
*/
|
|
function indexGB18030PointerFor(code_point) {
|
|
var /** @type {number} */ offset = 0,
|
|
/** @type {number} */ pointer_offset = 0,
|
|
/** @type {Array.<Array.<number>>} */ index = indexes['gb18030'];
|
|
var i;
|
|
for (i = 0; i < index.length; ++i) {
|
|
var entry = index[i];
|
|
if (entry[1] <= code_point) {
|
|
offset = entry[1];
|
|
pointer_offset = entry[0];
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
return pointer_offset + code_point - offset;
|
|
}
|
|
|
|
//
|
|
// 7. The encoding
|
|
//
|
|
|
|
// 7.1 utf-8
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function UTF8Decoder(options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {number} */ utf8_code_point = 0,
|
|
/** @type {number} */ utf8_bytes_needed = 0,
|
|
/** @type {number} */ utf8_bytes_seen = 0,
|
|
/** @type {number} */ utf8_lower_boundary = 0;
|
|
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte) {
|
|
if (utf8_bytes_needed !== 0) {
|
|
return decoderError(fatal);
|
|
}
|
|
return EOF_code_point;
|
|
}
|
|
byte_pointer.offset(1);
|
|
|
|
if (utf8_bytes_needed === 0) {
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
if (inRange(bite, 0xC2, 0xDF)) {
|
|
utf8_bytes_needed = 1;
|
|
utf8_lower_boundary = 0x80;
|
|
utf8_code_point = bite - 0xC0;
|
|
} else if (inRange(bite, 0xE0, 0xEF)) {
|
|
utf8_bytes_needed = 2;
|
|
utf8_lower_boundary = 0x800;
|
|
utf8_code_point = bite - 0xE0;
|
|
} else if (inRange(bite, 0xF0, 0xF4)) {
|
|
utf8_bytes_needed = 3;
|
|
utf8_lower_boundary = 0x10000;
|
|
utf8_code_point = bite - 0xF0;
|
|
} else {
|
|
return decoderError(fatal);
|
|
}
|
|
utf8_code_point = utf8_code_point * Math.pow(64, utf8_bytes_needed);
|
|
return null;
|
|
}
|
|
if (!inRange(bite, 0x80, 0xBF)) {
|
|
utf8_code_point = 0;
|
|
utf8_bytes_needed = 0;
|
|
utf8_bytes_seen = 0;
|
|
utf8_lower_boundary = 0;
|
|
byte_pointer.offset(-1);
|
|
return decoderError(fatal);
|
|
}
|
|
utf8_bytes_seen += 1;
|
|
utf8_code_point = utf8_code_point + (bite - 0x80) *
|
|
Math.pow(64, utf8_bytes_needed - utf8_bytes_seen);
|
|
if (utf8_bytes_seen !== utf8_bytes_needed) {
|
|
return null;
|
|
}
|
|
var code_point = utf8_code_point;
|
|
var lower_boundary = utf8_lower_boundary;
|
|
utf8_code_point = 0;
|
|
utf8_bytes_needed = 0;
|
|
utf8_bytes_seen = 0;
|
|
utf8_lower_boundary = 0;
|
|
if (inRange(code_point, lower_boundary, 0x10FFFF) &&
|
|
!inRange(code_point, 0xD800, 0xDFFF)) {
|
|
return code_point;
|
|
}
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function UTF8Encoder(options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0xD800, 0xDFFF)) {
|
|
return encoderError(code_point);
|
|
}
|
|
if (inRange(code_point, 0x0000, 0x007f)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
var count, offset;
|
|
if (inRange(code_point, 0x0080, 0x07FF)) {
|
|
count = 1;
|
|
offset = 0xC0;
|
|
} else if (inRange(code_point, 0x0800, 0xFFFF)) {
|
|
count = 2;
|
|
offset = 0xE0;
|
|
} else if (inRange(code_point, 0x10000, 0x10FFFF)) {
|
|
count = 3;
|
|
offset = 0xF0;
|
|
}
|
|
var result = output_byte_stream.emit(
|
|
div(code_point, Math.pow(64, count)) + offset);
|
|
while (count > 0) {
|
|
var temp = div(code_point, Math.pow(64, count - 1));
|
|
result = output_byte_stream.emit(0x80 + (temp % 64));
|
|
count -= 1;
|
|
}
|
|
return result;
|
|
};
|
|
}
|
|
|
|
name_to_encoding['utf-8'].getEncoder = function(options) {
|
|
return new UTF8Encoder(options);
|
|
};
|
|
name_to_encoding['utf-8'].getDecoder = function(options) {
|
|
return new UTF8Decoder(options);
|
|
};
|
|
|
|
//
|
|
// 8. Legacy single-byte encodings
|
|
//
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {Array.<number>} index The encoding index.
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function SingleByteDecoder(index, options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte) {
|
|
return EOF_code_point;
|
|
}
|
|
byte_pointer.offset(1);
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
var code_point = index[bite - 0x80];
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {Array.<?number>} index The encoding index.
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function SingleByteEncoder(index, options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
var pointer = indexPointerFor(code_point, index);
|
|
if (pointer === null) {
|
|
encoderError(code_point);
|
|
}
|
|
return output_byte_stream.emit(pointer + 0x80);
|
|
};
|
|
}
|
|
|
|
(function() {
|
|
['ibm864', 'ibm866', 'iso-8859-2', 'iso-8859-3', 'iso-8859-4',
|
|
'iso-8859-5', 'iso-8859-6', 'iso-8859-7', 'iso-8859-8', 'iso-8859-10',
|
|
'iso-8859-13', 'iso-8859-14', 'iso-8859-15', 'iso-8859-16', 'koi8-r',
|
|
'koi8-u', 'macintosh', 'windows-874', 'windows-1250', 'windows-1251',
|
|
'windows-1252', 'windows-1253', 'windows-1254', 'windows-1255',
|
|
'windows-1256', 'windows-1257', 'windows-1258', 'x-mac-cyrillic'
|
|
].forEach(function(name) {
|
|
var encoding = name_to_encoding[name];
|
|
var index = indexes[name];
|
|
encoding.getDecoder = function(options) {
|
|
return new SingleByteDecoder(index, options);
|
|
};
|
|
encoding.getEncoder = function(options) {
|
|
return new SingleByteEncoder(index, options);
|
|
};
|
|
});
|
|
}());
|
|
|
|
//
|
|
// 9. Legacy multi-byte Chinese (simplified) encodings
|
|
//
|
|
|
|
// 9.1 gbk
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {boolean} gb18030 True if decoding gb18030, false otherwise.
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function GBKDecoder(gb18030, options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {number} */ gbk_first = 0x00,
|
|
/** @type {number} */ gbk_second = 0x00,
|
|
/** @type {number} */ gbk_third = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte && gbk_first === 0x00 &&
|
|
gbk_second === 0x00 && gbk_third === 0x00) {
|
|
return EOF_code_point;
|
|
}
|
|
if (bite === EOF_byte &&
|
|
(gbk_first !== 0x00 || gbk_second !== 0x00 || gbk_third !== 0x00)) {
|
|
gbk_first = 0x00;
|
|
gbk_second = 0x00;
|
|
gbk_third = 0x00;
|
|
decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
var code_point;
|
|
if (gbk_third !== 0x00) {
|
|
code_point = null;
|
|
if (inRange(bite, 0x30, 0x39)) {
|
|
code_point = indexGB18030CodePointFor(
|
|
(((gbk_first - 0x81) * 10 + (gbk_second - 0x30)) * 126 +
|
|
(gbk_third - 0x81)) * 10 + bite - 0x30);
|
|
}
|
|
gbk_first = 0x00;
|
|
gbk_second = 0x00;
|
|
gbk_third = 0x00;
|
|
if (code_point === null) {
|
|
byte_pointer.offset(-3);
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
if (gbk_second !== 0x00) {
|
|
if (inRange(bite, 0x81, 0xFE)) {
|
|
gbk_third = bite;
|
|
return null;
|
|
}
|
|
byte_pointer.offset(-2);
|
|
gbk_first = 0x00;
|
|
gbk_second = 0x00;
|
|
return decoderError(fatal);
|
|
}
|
|
if (gbk_first !== 0x00) {
|
|
if (inRange(bite, 0x30, 0x39) && gb18030) {
|
|
gbk_second = bite;
|
|
return null;
|
|
}
|
|
var lead = gbk_first;
|
|
var pointer = null;
|
|
gbk_first = 0x00;
|
|
var offset = bite < 0x7F ? 0x40 : 0x41;
|
|
if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFE)) {
|
|
pointer = (lead - 0x81) * 190 + (bite - offset);
|
|
}
|
|
code_point = pointer === null ? null :
|
|
indexCodePointFor(pointer, indexes['gbk']);
|
|
if (pointer === null) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
if (bite === 0x80) {
|
|
return 0x20AC;
|
|
}
|
|
if (inRange(bite, 0x81, 0xFE)) {
|
|
gbk_first = bite;
|
|
return null;
|
|
}
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {boolean} gb18030 True if decoding gb18030, false otherwise.
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function GBKEncoder(gb18030, options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['gbk']);
|
|
if (pointer !== null) {
|
|
var lead = div(pointer, 190) + 0x81;
|
|
var trail = pointer % 190;
|
|
var offset = trail < 0x3F ? 0x40 : 0x41;
|
|
return output_byte_stream.emit(lead, trail + offset);
|
|
}
|
|
if (pointer === null && !gb18030) {
|
|
return encoderError(code_point);
|
|
}
|
|
pointer = indexGB18030PointerFor(code_point);
|
|
var byte1 = div(div(div(pointer, 10), 126), 10);
|
|
pointer = pointer - byte1 * 10 * 126 * 10;
|
|
var byte2 = div(div(pointer, 10), 126);
|
|
pointer = pointer - byte2 * 10 * 126;
|
|
var byte3 = div(pointer, 10);
|
|
var byte4 = pointer - byte3 * 10;
|
|
return output_byte_stream.emit(byte1 + 0x81,
|
|
byte2 + 0x30,
|
|
byte3 + 0x81,
|
|
byte4 + 0x30);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['gbk'].getEncoder = function(options) {
|
|
return new GBKEncoder(false, options);
|
|
};
|
|
name_to_encoding['gbk'].getDecoder = function(options) {
|
|
return new GBKDecoder(false, options);
|
|
};
|
|
|
|
// 9.2 gb18030
|
|
name_to_encoding['gb18030'].getEncoder = function(options) {
|
|
return new GBKEncoder(true, options);
|
|
};
|
|
name_to_encoding['gb18030'].getDecoder = function(options) {
|
|
return new GBKDecoder(true, options);
|
|
};
|
|
|
|
// 9.3 hz-gb-2312
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function HZGB2312Decoder(options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {boolean} */ hzgb2312 = false,
|
|
/** @type {number} */ hzgb2312_lead = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte && hzgb2312_lead === 0x00) {
|
|
return EOF_code_point;
|
|
}
|
|
if (bite === EOF_byte && hzgb2312_lead !== 0x00) {
|
|
hzgb2312_lead = 0x00;
|
|
return decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
if (hzgb2312_lead === 0x7E) {
|
|
hzgb2312_lead = 0x00;
|
|
if (bite === 0x7B) {
|
|
hzgb2312 = true;
|
|
return null;
|
|
}
|
|
if (bite === 0x7D) {
|
|
hzgb2312 = false;
|
|
return null;
|
|
}
|
|
if (bite === 0x7E) {
|
|
return 0x007E;
|
|
}
|
|
if (bite === 0x0A) {
|
|
return null;
|
|
}
|
|
byte_pointer.offset(-1);
|
|
return decoderError(fatal);
|
|
}
|
|
if (hzgb2312_lead !== 0x00) {
|
|
var lead = hzgb2312_lead;
|
|
hzgb2312_lead = 0x00;
|
|
var code_point = null;
|
|
if (inRange(bite, 0x21, 0x7E)) {
|
|
code_point = indexCodePointFor((lead - 1) * 190 +
|
|
(bite + 0x3F), indexes['gbk']);
|
|
}
|
|
if (bite === 0x0A) {
|
|
hzgb2312 = false;
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
if (bite === 0x7E) {
|
|
hzgb2312_lead = 0x7E;
|
|
return null;
|
|
}
|
|
if (hzgb2312) {
|
|
if (inRange(bite, 0x20, 0x7F)) {
|
|
hzgb2312_lead = bite;
|
|
return null;
|
|
}
|
|
if (bite === 0x0A) {
|
|
hzgb2312 = false;
|
|
}
|
|
return decoderError(fatal);
|
|
}
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function HZGB2312Encoder(options) {
|
|
var fatal = options.fatal;
|
|
var hzgb2312 = false;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F) && hzgb2312) {
|
|
code_point_pointer.offset(-1);
|
|
hzgb2312 = false;
|
|
return output_byte_stream.emit(0x7E, 0x7D);
|
|
}
|
|
if (code_point === 0x007E) {
|
|
return output_byte_stream.emit(0x7E, 0x7E);
|
|
}
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
if (!hzgb2312) {
|
|
code_point_pointer.offset(-1);
|
|
hzgb2312 = true;
|
|
return output_byte_stream.emit(0x7E, 0x7B);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['gbk']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead = div(pointer, 190) + 1;
|
|
var trail = pointer % 190 - 0x3F;
|
|
if (!inRange(lead, 0x21, 0x7E) || !inRange(trail, 0x21, 0x7E)) {
|
|
return encoderError(code_point);
|
|
}
|
|
return output_byte_stream.emit(lead, trail);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['hz-gb-2312'].getEncoder = function(options) {
|
|
return new HZGB2312Encoder(options);
|
|
};
|
|
name_to_encoding['hz-gb-2312'].getDecoder = function(options) {
|
|
return new HZGB2312Decoder(options);
|
|
};
|
|
|
|
//
|
|
// 10. Legacy multi-byte Chinese (traditional) encodings
|
|
//
|
|
|
|
// 10.1 big5
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function Big5Decoder(options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {number} */ big5_lead = 0x00,
|
|
/** @type {?number} */ big5_pending = null;
|
|
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte steram to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
// NOTE: Hack to support emitting two code points
|
|
if (big5_pending !== null) {
|
|
var pending = big5_pending;
|
|
big5_pending = null;
|
|
return pending;
|
|
}
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte && big5_lead === 0x00) {
|
|
return EOF_code_point;
|
|
}
|
|
if (bite === EOF_byte && big5_lead !== 0x00) {
|
|
big5_lead = 0x00;
|
|
return decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
if (big5_lead !== 0x00) {
|
|
var lead = big5_lead;
|
|
var pointer = null;
|
|
big5_lead = 0x00;
|
|
var offset = bite < 0x7F ? 0x40 : 0x62;
|
|
if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0xA1, 0xFE)) {
|
|
pointer = (lead - 0x81) * 157 + (bite - offset);
|
|
}
|
|
if (pointer === 1133) {
|
|
big5_pending = 0x0304;
|
|
return 0x00CA;
|
|
}
|
|
if (pointer === 1135) {
|
|
big5_pending = 0x030C;
|
|
return 0x00CA;
|
|
}
|
|
if (pointer === 1164) {
|
|
big5_pending = 0x0304;
|
|
return 0x00EA;
|
|
}
|
|
if (pointer === 1166) {
|
|
big5_pending = 0x030C;
|
|
return 0x00EA;
|
|
}
|
|
var code_point = (pointer === null) ? null :
|
|
indexCodePointFor(pointer, indexes['big5']);
|
|
if (pointer === null) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
if (inRange(bite, 0x81, 0xFE)) {
|
|
big5_lead = bite;
|
|
return null;
|
|
}
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function Big5Encoder(options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['big5']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead = div(pointer, 157) + 0x81;
|
|
//if (lead < 0xA1) {
|
|
// return encoderError(code_point);
|
|
//}
|
|
var trail = pointer % 157;
|
|
var offset = trail < 0x3F ? 0x40 : 0x62;
|
|
return output_byte_stream.emit(lead, trail + offset);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['big5'].getEncoder = function(options) {
|
|
return new Big5Encoder(options);
|
|
};
|
|
name_to_encoding['big5'].getDecoder = function(options) {
|
|
return new Big5Decoder(options);
|
|
};
|
|
|
|
|
|
//
|
|
// 11. Legacy multi-byte Japanese encodings
|
|
//
|
|
|
|
// 11.1 euc.jp
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function EUCJPDecoder(options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {number} */ eucjp_first = 0x00,
|
|
/** @type {number} */ eucjp_second = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte) {
|
|
if (eucjp_first === 0x00 && eucjp_second === 0x00) {
|
|
return EOF_code_point;
|
|
}
|
|
eucjp_first = 0x00;
|
|
eucjp_second = 0x00;
|
|
return decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
|
|
var lead, code_point;
|
|
if (eucjp_second !== 0x00) {
|
|
lead = eucjp_second;
|
|
eucjp_second = 0x00;
|
|
code_point = null;
|
|
if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) {
|
|
code_point = indexCodePointFor((lead - 0xA1) * 94 + bite - 0xA1,
|
|
indexes['jis0212']);
|
|
}
|
|
if (!inRange(bite, 0xA1, 0xFE)) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
if (eucjp_first === 0x8E && inRange(bite, 0xA1, 0xDF)) {
|
|
eucjp_first = 0x00;
|
|
return 0xFF61 + bite - 0xA1;
|
|
}
|
|
if (eucjp_first === 0x8F && inRange(bite, 0xA1, 0xFE)) {
|
|
eucjp_first = 0x00;
|
|
eucjp_second = bite;
|
|
return null;
|
|
}
|
|
if (eucjp_first !== 0x00) {
|
|
lead = eucjp_first;
|
|
eucjp_first = 0x00;
|
|
code_point = null;
|
|
if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) {
|
|
code_point = indexCodePointFor((lead - 0xA1) * 94 + bite - 0xA1,
|
|
indexes['jis0208']);
|
|
}
|
|
if (!inRange(bite, 0xA1, 0xFE)) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
if (bite === 0x8E || bite === 0x8F || (inRange(bite, 0xA1, 0xFE))) {
|
|
eucjp_first = bite;
|
|
return null;
|
|
}
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function EUCJPEncoder(options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
if (code_point === 0x00A5) {
|
|
return output_byte_stream.emit(0x5C);
|
|
}
|
|
if (code_point === 0x203E) {
|
|
return output_byte_stream.emit(0x7E);
|
|
}
|
|
if (inRange(code_point, 0xFF61, 0xFF9F)) {
|
|
return output_byte_stream.emit(0x8E, code_point - 0xFF61 + 0xA1);
|
|
}
|
|
|
|
var pointer = indexPointerFor(code_point, indexes['jis0208']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead = div(pointer, 94) + 0xA1;
|
|
var trail = pointer % 94 + 0xA1;
|
|
return output_byte_stream.emit(lead, trail);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['euc-jp'].getEncoder = function(options) {
|
|
return new EUCJPEncoder(options);
|
|
};
|
|
name_to_encoding['euc-jp'].getDecoder = function(options) {
|
|
return new EUCJPDecoder(options);
|
|
};
|
|
|
|
// 11.2 iso-2022-jp
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function ISO2022JPDecoder(options) {
|
|
var fatal = options.fatal;
|
|
/** @enum */
|
|
var state = {
|
|
ASCII: 0,
|
|
escape_start: 1,
|
|
escape_middle: 2,
|
|
escape_final: 3,
|
|
lead: 4,
|
|
trail: 5,
|
|
Katakana: 6
|
|
};
|
|
var /** @type {number} */ iso2022jp_state = state.ASCII,
|
|
/** @type {boolean} */ iso2022jp_jis0212 = false,
|
|
/** @type {number} */ iso2022jp_lead = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite !== EOF_byte) {
|
|
byte_pointer.offset(1);
|
|
}
|
|
switch (iso2022jp_state) {
|
|
default:
|
|
case state.ASCII:
|
|
if (bite === 0x1B) {
|
|
iso2022jp_state = state.escape_start;
|
|
return null;
|
|
}
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
return EOF_code_point;
|
|
}
|
|
return decoderError(fatal);
|
|
|
|
case state.escape_start:
|
|
if (bite === 0x24 || bite === 0x28) {
|
|
iso2022jp_lead = bite;
|
|
iso2022jp_state = state.escape_middle;
|
|
return null;
|
|
}
|
|
if (bite !== EOF_byte) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
iso2022jp_state = state.ASCII;
|
|
return decoderError(fatal);
|
|
|
|
case state.escape_middle:
|
|
var lead = iso2022jp_lead;
|
|
iso2022jp_lead = 0x00;
|
|
if (lead === 0x24 && (bite === 0x40 || bite === 0x42)) {
|
|
iso2022jp_jis0212 = false;
|
|
iso2022jp_state = state.lead;
|
|
return null;
|
|
}
|
|
if (lead === 0x24 && bite === 0x28) {
|
|
iso2022jp_state = state.escape_final;
|
|
return null;
|
|
}
|
|
if (lead === 0x28 && (bite === 0x42 || bite === 0x4A)) {
|
|
iso2022jp_state = state.ASCII;
|
|
return null;
|
|
}
|
|
if (lead === 0x28 && bite === 0x49) {
|
|
iso2022jp_state = state.Katakana;
|
|
return null;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
byte_pointer.offset(-1);
|
|
} else {
|
|
byte_pointer.offset(-2);
|
|
}
|
|
iso2022jp_state = state.ASCII;
|
|
return decoderError(fatal);
|
|
|
|
case state.escape_final:
|
|
if (bite === 0x44) {
|
|
iso2022jp_jis0212 = true;
|
|
iso2022jp_state = state.lead;
|
|
return null;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
byte_pointer.offset(-2);
|
|
} else {
|
|
byte_pointer.offset(-3);
|
|
}
|
|
iso2022jp_state = state.ASCII;
|
|
return decoderError(fatal);
|
|
|
|
case state.lead:
|
|
if (bite === 0x0A) {
|
|
iso2022jp_state = state.ASCII;
|
|
return decoderError(fatal, 0x000A);
|
|
}
|
|
if (bite === 0x1B) {
|
|
iso2022jp_state = state.escape_start;
|
|
return null;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
return EOF_code_point;
|
|
}
|
|
iso2022jp_lead = bite;
|
|
iso2022jp_state = state.trail;
|
|
return null;
|
|
|
|
case state.trail:
|
|
iso2022jp_state = state.lead;
|
|
if (bite === EOF_byte) {
|
|
return decoderError(fatal);
|
|
}
|
|
var code_point = null;
|
|
var pointer = (iso2022jp_lead - 0x21) * 94 + bite - 0x21;
|
|
if (inRange(iso2022jp_lead, 0x21, 0x7E) &&
|
|
inRange(bite, 0x21, 0x7E)) {
|
|
code_point = (iso2022jp_jis0212 === false) ?
|
|
indexCodePointFor(pointer, indexes['jis0208']) :
|
|
indexCodePointFor(pointer, indexes['jis0212']);
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
|
|
case state.Katakana:
|
|
if (bite === 0x1B) {
|
|
iso2022jp_state = state.escape_start;
|
|
return null;
|
|
}
|
|
if (inRange(bite, 0x21, 0x5F)) {
|
|
return 0xFF61 + bite - 0x21;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
return EOF_code_point;
|
|
}
|
|
return decoderError(fatal);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function ISO2022JPEncoder(options) {
|
|
var fatal = options.fatal;
|
|
/** @enum */
|
|
var state = {
|
|
ASCII: 0,
|
|
lead: 1,
|
|
Katakana: 2
|
|
};
|
|
var /** @type {number} */ iso2022jp_state = state.ASCII;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if ((inRange(code_point, 0x0000, 0x007F) ||
|
|
code_point === 0x00A5 || code_point === 0x203E) &&
|
|
iso2022jp_state !== state.ASCII) {
|
|
code_point_pointer.offset(-1);
|
|
iso2022jp_state = state.ASCII;
|
|
return output_byte_stream.emit(0x1B, 0x28, 0x42);
|
|
}
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
if (code_point === 0x00A5) {
|
|
return output_byte_stream.emit(0x5C);
|
|
}
|
|
if (code_point === 0x203E) {
|
|
return output_byte_stream.emit(0x7E);
|
|
}
|
|
if (inRange(code_point, 0xFF61, 0xFF9F) &&
|
|
iso2022jp_state !== state.Katakana) {
|
|
code_point_pointer.offset(-1);
|
|
iso2022jp_state = state.Katakana;
|
|
return output_byte_stream.emit(0x1B, 0x28, 0x49);
|
|
}
|
|
if (inRange(code_point, 0xFF61, 0xFF9F)) {
|
|
return output_byte_stream.emit(code_point - 0xFF61 - 0x21);
|
|
}
|
|
if (iso2022jp_state !== state.lead) {
|
|
code_point_pointer.offset(-1);
|
|
iso2022jp_state = state.lead;
|
|
return output_byte_stream.emit(0x1B, 0x24, 0x42);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['jis0208']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead = div(pointer, 94) + 0x21;
|
|
var trail = pointer % 94 + 0x21;
|
|
return output_byte_stream.emit(lead, trail);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['iso-2022-jp'].getEncoder = function(options) {
|
|
return new ISO2022JPEncoder(options);
|
|
};
|
|
name_to_encoding['iso-2022-jp'].getDecoder = function(options) {
|
|
return new ISO2022JPDecoder(options);
|
|
};
|
|
|
|
// 11.3 shift_jis
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function ShiftJISDecoder(options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {number} */ shiftjis_lead = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte && shiftjis_lead === 0x00) {
|
|
return EOF_code_point;
|
|
}
|
|
if (bite === EOF_byte && shiftjis_lead !== 0x00) {
|
|
shiftjis_lead = 0x00;
|
|
return decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
if (shiftjis_lead !== 0x00) {
|
|
var lead = shiftjis_lead;
|
|
shiftjis_lead = 0x00;
|
|
if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFC)) {
|
|
var offset = (bite < 0x7F) ? 0x40 : 0x41;
|
|
var lead_offset = (lead < 0xA0) ? 0x81 : 0xC1;
|
|
var code_point = indexCodePointFor((lead - lead_offset) * 188 +
|
|
bite - offset, indexes['jis0208']);
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
byte_pointer.offset(-1);
|
|
return decoderError(fatal);
|
|
}
|
|
if (inRange(bite, 0x00, 0x80)) {
|
|
return bite;
|
|
}
|
|
if (inRange(bite, 0xA1, 0xDF)) {
|
|
return 0xFF61 + bite - 0xA1;
|
|
}
|
|
if (inRange(bite, 0x81, 0x9F) || inRange(bite, 0xE0, 0xFC)) {
|
|
shiftjis_lead = bite;
|
|
return null;
|
|
}
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function ShiftJISEncoder(options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x0080)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
if (code_point === 0x00A5) {
|
|
return output_byte_stream.emit(0x5C);
|
|
}
|
|
if (code_point === 0x203E) {
|
|
return output_byte_stream.emit(0x7E);
|
|
}
|
|
if (inRange(code_point, 0xFF61, 0xFF9F)) {
|
|
return output_byte_stream.emit(code_point - 0xFF61 + 0xA1);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['jis0208']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead = div(pointer, 188);
|
|
var lead_offset = lead < 0x1F ? 0x81 : 0xC1;
|
|
var trail = pointer % 188;
|
|
var offset = trail < 0x3F ? 0x40 : 0x41;
|
|
return output_byte_stream.emit(lead + lead_offset, trail + offset);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['shift_jis'].getEncoder = function(options) {
|
|
return new ShiftJISEncoder(options);
|
|
};
|
|
name_to_encoding['shift_jis'].getDecoder = function(options) {
|
|
return new ShiftJISDecoder(options);
|
|
};
|
|
|
|
//
|
|
// 12. Legacy multi-byte Korean encodings
|
|
//
|
|
|
|
// 12.1 euc-kr
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function EUCKRDecoder(options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {number} */ euckr_lead = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte && euckr_lead === 0) {
|
|
return EOF_code_point;
|
|
}
|
|
if (bite === EOF_byte && euckr_lead !== 0) {
|
|
euckr_lead = 0x00;
|
|
return decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
if (euckr_lead !== 0x00) {
|
|
var lead = euckr_lead;
|
|
var pointer = null;
|
|
euckr_lead = 0x00;
|
|
|
|
if (inRange(lead, 0x81, 0xC6)) {
|
|
var temp = (26 + 26 + 126) * (lead - 0x81);
|
|
if (inRange(bite, 0x41, 0x5A)) {
|
|
pointer = temp + bite - 0x41;
|
|
} else if (inRange(bite, 0x61, 0x7A)) {
|
|
pointer = temp + 26 + bite - 0x61;
|
|
} else if (inRange(bite, 0x81, 0xFE)) {
|
|
pointer = temp + 26 + 26 + bite - 0x81;
|
|
}
|
|
}
|
|
|
|
if (inRange(lead, 0xC7, 0xFD) && inRange(bite, 0xA1, 0xFE)) {
|
|
pointer = (26 + 26 + 126) * (0xC7 - 0x81) + (lead - 0xC7) * 94 +
|
|
(bite - 0xA1);
|
|
}
|
|
|
|
var code_point = (pointer === null) ? null :
|
|
indexCodePointFor(pointer, indexes['euc-kr']);
|
|
if (pointer === null) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
if (code_point === null) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
}
|
|
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
|
|
if (inRange(bite, 0x81, 0xFD)) {
|
|
euckr_lead = bite;
|
|
return null;
|
|
}
|
|
|
|
return decoderError(fatal);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function EUCKREncoder(options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['euc-kr']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead, trail;
|
|
if (pointer < ((26 + 26 + 126) * (0xC7 - 0x81))) {
|
|
lead = div(pointer, (26 + 26 + 126)) + 0x81;
|
|
trail = pointer % (26 + 26 + 126);
|
|
var offset = trail < 26 ? 0x41 : trail < 26 + 26 ? 0x47 : 0x4D;
|
|
return output_byte_stream.emit(lead, trail + offset);
|
|
}
|
|
pointer = pointer - (26 + 26 + 126) * (0xC7 - 0x81);
|
|
lead = div(pointer, 94) + 0xC7;
|
|
trail = pointer % 94 + 0xA1;
|
|
return output_byte_stream.emit(lead, trail);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['euc-kr'].getEncoder = function(options) {
|
|
return new EUCKREncoder(options);
|
|
};
|
|
name_to_encoding['euc-kr'].getDecoder = function(options) {
|
|
return new EUCKRDecoder(options);
|
|
};
|
|
|
|
// 12.2 iso-2022-kr
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function ISO2022KRDecoder(options) {
|
|
var fatal = options.fatal;
|
|
/** @enum */
|
|
var state = {
|
|
ASCII: 0,
|
|
escape_start: 1,
|
|
escape_middle: 2,
|
|
escape_end: 3,
|
|
lead: 4,
|
|
trail: 5
|
|
};
|
|
var /** @type {number} */ iso2022kr_state = state.ASCII,
|
|
/** @type {number} */ iso2022kr_lead = 0x00;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite !== EOF_byte) {
|
|
byte_pointer.offset(1);
|
|
}
|
|
switch (iso2022kr_state) {
|
|
default:
|
|
case state.ASCII:
|
|
if (bite === 0x0E) {
|
|
iso2022kr_state = state.lead;
|
|
return null;
|
|
}
|
|
if (bite === 0x0F) {
|
|
return null;
|
|
}
|
|
if (bite === 0x1B) {
|
|
iso2022kr_state = state.escape_start;
|
|
return null;
|
|
}
|
|
if (inRange(bite, 0x00, 0x7F)) {
|
|
return bite;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
return EOF_code_point;
|
|
}
|
|
return decoderError(fatal);
|
|
case state.escape_start:
|
|
if (bite === 0x24) {
|
|
iso2022kr_state = state.escape_middle;
|
|
return null;
|
|
}
|
|
if (bite !== EOF_byte) {
|
|
byte_pointer.offset(-1);
|
|
}
|
|
iso2022kr_state = state.ASCII;
|
|
return decoderError(fatal);
|
|
case state.escape_middle:
|
|
if (bite === 0x29) {
|
|
iso2022kr_state = state.escape_end;
|
|
return null;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
byte_pointer.offset(-1);
|
|
} else {
|
|
byte_pointer.offset(-2);
|
|
}
|
|
iso2022kr_state = state.ASCII;
|
|
return decoderError(fatal);
|
|
case state.escape_end:
|
|
if (bite === 0x43) {
|
|
iso2022kr_state = state.ASCII;
|
|
return null;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
byte_pointer.offset(-2);
|
|
} else {
|
|
byte_pointer.offset(-3);
|
|
}
|
|
iso2022kr_state = state.ASCII;
|
|
return decoderError(fatal);
|
|
case state.lead:
|
|
if (bite === 0x0A) {
|
|
iso2022kr_state = state.ASCII;
|
|
return decoderError(fatal, 0x000A);
|
|
}
|
|
if (bite === 0x0E) {
|
|
return null;
|
|
}
|
|
if (bite === 0x0F) {
|
|
iso2022kr_state = state.ASCII;
|
|
return null;
|
|
}
|
|
if (bite === EOF_byte) {
|
|
return EOF_code_point;
|
|
}
|
|
iso2022kr_lead = bite;
|
|
iso2022kr_state = state.trail;
|
|
return null;
|
|
case state.trail:
|
|
iso2022kr_state = state.lead;
|
|
if (bite === EOF_byte) {
|
|
return decoderError(fatal);
|
|
}
|
|
var code_point = null;
|
|
if (inRange(iso2022kr_lead, 0x21, 0x46) &&
|
|
inRange(bite, 0x21, 0x7E)) {
|
|
code_point = indexCodePointFor((26 + 26 + 126) *
|
|
(iso2022kr_lead - 1) +
|
|
26 + 26 + bite - 1,
|
|
indexes['euc-kr']);
|
|
} else if (inRange(iso2022kr_lead, 0x47, 0x7E) &&
|
|
inRange(bite, 0x21, 0x7E)) {
|
|
code_point = indexCodePointFor((26 + 26 + 126) * (0xC7 - 0x81) +
|
|
(iso2022kr_lead - 0x47) * 94 +
|
|
(bite - 0x21),
|
|
indexes['euc-kr']);
|
|
}
|
|
if (code_point !== null) {
|
|
return code_point;
|
|
}
|
|
return decoderError(fatal);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function ISO2022KREncoder(options) {
|
|
var fatal = options.fatal;
|
|
/** @enum */
|
|
var state = {
|
|
ASCII: 0,
|
|
lead: 1
|
|
};
|
|
var /** @type {boolean} */ iso2022kr_initialization = false,
|
|
/** @type {number} */ iso2022kr_state = state.ASCII;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
if (!iso2022kr_initialization) {
|
|
iso2022kr_initialization = true;
|
|
output_byte_stream.emit(0x1B, 0x24, 0x29, 0x43);
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0x0000, 0x007F) &&
|
|
iso2022kr_state !== state.ASCII) {
|
|
code_point_pointer.offset(-1);
|
|
iso2022kr_state = state.ASCII;
|
|
return output_byte_stream.emit(0x0F);
|
|
}
|
|
if (inRange(code_point, 0x0000, 0x007F)) {
|
|
return output_byte_stream.emit(code_point);
|
|
}
|
|
if (iso2022kr_state !== state.lead) {
|
|
code_point_pointer.offset(-1);
|
|
iso2022kr_state = state.lead;
|
|
return output_byte_stream.emit(0x0E);
|
|
}
|
|
var pointer = indexPointerFor(code_point, indexes['euc-kr']);
|
|
if (pointer === null) {
|
|
return encoderError(code_point);
|
|
}
|
|
var lead, trail;
|
|
if (pointer < (26 + 26 + 126) * (0xC7 - 0x81)) {
|
|
lead = div(pointer, (26 + 26 + 126)) + 1;
|
|
trail = pointer % (26 + 26 + 126) - 26 - 26 + 1;
|
|
if (!inRange(lead, 0x21, 0x46) || !inRange(trail, 0x21, 0x7E)) {
|
|
return encoderError(code_point);
|
|
}
|
|
return output_byte_stream.emit(lead, trail);
|
|
}
|
|
pointer = pointer - (26 + 26 + 126) * (0xC7 - 0x81);
|
|
lead = div(pointer, 94) + 0x47;
|
|
trail = pointer % 94 + 0x21;
|
|
if (!inRange(lead, 0x47, 0x7E) || !inRange(trail, 0x21, 0x7E)) {
|
|
return encoderError(code_point);
|
|
}
|
|
return output_byte_stream.emit(lead, trail);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['iso-2022-kr'].getEncoder = function(options) {
|
|
return new ISO2022KREncoder(options);
|
|
};
|
|
name_to_encoding['iso-2022-kr'].getDecoder = function(options) {
|
|
return new ISO2022KRDecoder(options);
|
|
};
|
|
|
|
|
|
//
|
|
// 13. Legacy utf-16 encodings
|
|
//
|
|
|
|
// 13.1 utf-16
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {boolean} utf16_be True if big-endian, false if little-endian.
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function UTF16Decoder(utf16_be, options) {
|
|
var fatal = options.fatal;
|
|
var /** @type {?number} */ utf16_lead_byte = null,
|
|
/** @type {?number} */ utf16_lead_surrogate = null;
|
|
/**
|
|
* @param {ByteInputStream} byte_pointer The byte stream to decode.
|
|
* @return {?number} The next code point decoded, or null if not enough
|
|
* data exists in the input stream to decode a complete code point.
|
|
*/
|
|
this.decode = function(byte_pointer) {
|
|
var bite = byte_pointer.get();
|
|
if (bite === EOF_byte && utf16_lead_byte === null &&
|
|
utf16_lead_surrogate === null) {
|
|
return EOF_code_point;
|
|
}
|
|
if (bite === EOF_byte && (utf16_lead_byte !== null ||
|
|
utf16_lead_surrogate !== null)) {
|
|
return decoderError(fatal);
|
|
}
|
|
byte_pointer.offset(1);
|
|
if (utf16_lead_byte === null) {
|
|
utf16_lead_byte = bite;
|
|
return null;
|
|
}
|
|
var code_point;
|
|
if (utf16_be) {
|
|
code_point = (utf16_lead_byte << 8) + bite;
|
|
} else {
|
|
code_point = (bite << 8) + utf16_lead_byte;
|
|
}
|
|
utf16_lead_byte = null;
|
|
if (utf16_lead_surrogate !== null) {
|
|
var lead_surrogate = utf16_lead_surrogate;
|
|
utf16_lead_surrogate = null;
|
|
if (inRange(code_point, 0xDC00, 0xDFFF)) {
|
|
return 0x10000 + (lead_surrogate - 0xD800) * 0x400 +
|
|
(code_point - 0xDC00);
|
|
}
|
|
byte_pointer.offset(-2);
|
|
return decoderError(fatal);
|
|
}
|
|
if (inRange(code_point, 0xD800, 0xDBFF)) {
|
|
utf16_lead_surrogate = code_point;
|
|
return null;
|
|
}
|
|
if (inRange(code_point, 0xDC00, 0xDFFF)) {
|
|
return decoderError(fatal);
|
|
}
|
|
return code_point;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {boolean} utf16_be True if big-endian, false if little-endian.
|
|
* @param {{fatal: boolean}} options
|
|
*/
|
|
function UTF16Encoder(utf16_be, options) {
|
|
var fatal = options.fatal;
|
|
/**
|
|
* @param {ByteOutputStream} output_byte_stream Output byte stream.
|
|
* @param {CodePointInputStream} code_point_pointer Input stream.
|
|
* @return {number} The last byte emitted.
|
|
*/
|
|
this.encode = function(output_byte_stream, code_point_pointer) {
|
|
function convert_to_bytes(code_unit) {
|
|
var byte1 = code_unit >> 8;
|
|
var byte2 = code_unit & 0x00FF;
|
|
if (utf16_be) {
|
|
return output_byte_stream.emit(byte1, byte2);
|
|
}
|
|
return output_byte_stream.emit(byte2, byte1);
|
|
}
|
|
var code_point = code_point_pointer.get();
|
|
if (code_point === EOF_code_point) {
|
|
return EOF_byte;
|
|
}
|
|
code_point_pointer.offset(1);
|
|
if (inRange(code_point, 0xD800, 0xDFFF)) {
|
|
encoderError(code_point);
|
|
}
|
|
if (code_point <= 0xFFFF) {
|
|
return convert_to_bytes(code_point);
|
|
}
|
|
var lead = div((code_point - 0x10000), 0x400) + 0xD800;
|
|
var trail = ((code_point - 0x10000) % 0x400) + 0xDC00;
|
|
convert_to_bytes(lead);
|
|
return convert_to_bytes(trail);
|
|
};
|
|
}
|
|
|
|
name_to_encoding['utf-16'].getEncoder = function(options) {
|
|
return new UTF16Encoder(false, options);
|
|
};
|
|
name_to_encoding['utf-16'].getDecoder = function(options) {
|
|
return new UTF16Decoder(false, options);
|
|
};
|
|
|
|
// 13.2 utf-16be
|
|
name_to_encoding['utf-16be'].getEncoder = function(options) {
|
|
return new UTF16Encoder(true, options);
|
|
};
|
|
name_to_encoding['utf-16be'].getDecoder = function(options) {
|
|
return new UTF16Decoder(true, options);
|
|
};
|
|
|
|
|
|
// NOTE: currently unused
|
|
/**
|
|
* @param {string} label The encoding label.
|
|
* @param {ByteInputStream} input_stream The byte stream to test.
|
|
*/
|
|
function detectEncoding(label, input_stream) {
|
|
if (input_stream.match([0xFF, 0xFE])) {
|
|
input_stream.offset(2);
|
|
return 'utf-16';
|
|
}
|
|
if (input_stream.match([0xFE, 0xFF])) {
|
|
input_stream.offset(2);
|
|
return 'utf-16be';
|
|
}
|
|
if (input_stream.match([0xEF, 0xBB, 0xBF])) {
|
|
input_stream.offset(3);
|
|
return 'utf-8';
|
|
}
|
|
return label;
|
|
}
|
|
|
|
/**
|
|
* @param {string} label The encoding label.
|
|
* @param {ByteInputStream} input_stream The byte stream to test.
|
|
*/
|
|
function consumeBOM(label, input_stream) {
|
|
if (input_stream.match([0xFF, 0xFE]) && label === 'utf-16') {
|
|
input_stream.offset(2);
|
|
return;
|
|
}
|
|
if (input_stream.match([0xFE, 0xFF]) && label == 'utf-16be') {
|
|
input_stream.offset(2);
|
|
return;
|
|
}
|
|
if (input_stream.match([0xEF, 0xBB, 0xBF]) && label == 'utf-8') {
|
|
input_stream.offset(3);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Implementation of Text Encoding Web API
|
|
//
|
|
|
|
/** @const */ var DEFAULT_ENCODING = 'utf-8';
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {string=} opt_encoding The label of the encoding;
|
|
* defaults to 'utf-8'.
|
|
* @param {{fatal: boolean}=} options
|
|
*/
|
|
function TextEncoder(opt_encoding, options) {
|
|
if (!this || this === global) {
|
|
return new TextEncoder(opt_encoding, options);
|
|
}
|
|
opt_encoding = opt_encoding ? String(opt_encoding) : DEFAULT_ENCODING;
|
|
options = Object(options);
|
|
/** @private */
|
|
this._encoding = getEncoding(opt_encoding);
|
|
if (this._encoding === null || (this._encoding.name !== 'utf-8' &&
|
|
this._encoding.name !== 'utf-16' &&
|
|
this._encoding.name !== 'utf-16be'))
|
|
throw new TypeError('Unknown encoding: ' + opt_encoding);
|
|
/* @private @type {boolean} */
|
|
this._streaming = false;
|
|
/** @private */
|
|
this._encoder = null;
|
|
/* @private @type {{fatal: boolean}=} */
|
|
this._options = { fatal: Boolean(options.fatal) };
|
|
|
|
if (Object.defineProperty) {
|
|
Object.defineProperty(
|
|
this, 'encoding',
|
|
{ get: function() { return this._encoding.name; } });
|
|
} else {
|
|
this.encoding = this._encoding.name;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
TextEncoder.prototype = {
|
|
/**
|
|
* @param {string=} opt_string The string to encode.
|
|
* @param {{stream: boolean}=} options
|
|
*/
|
|
encode: function encode(opt_string, options) {
|
|
opt_string = opt_string ? String(opt_string) : '';
|
|
options = Object(options);
|
|
// TODO: any options?
|
|
if (!this._streaming) {
|
|
this._encoder = this._encoding.getEncoder(this._options);
|
|
}
|
|
this._streaming = Boolean(options.stream);
|
|
|
|
var bytes = [];
|
|
var output_stream = new ByteOutputStream(bytes);
|
|
var input_stream = new CodePointInputStream(opt_string);
|
|
while (input_stream.get() !== EOF_code_point) {
|
|
this._encoder.encode(output_stream, input_stream);
|
|
}
|
|
if (!this._streaming) {
|
|
var last_byte;
|
|
do {
|
|
last_byte = this._encoder.encode(output_stream, input_stream);
|
|
} while (last_byte !== EOF_byte);
|
|
this._encoder = null;
|
|
}
|
|
return new Uint8Array(bytes);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* @constructor
|
|
* @param {string=} opt_encoding The label of the encoding;
|
|
* defaults to 'utf-8'.
|
|
* @param {{fatal: boolean}=} options
|
|
*/
|
|
function TextDecoder(opt_encoding, options) {
|
|
if (!this || this === global) {
|
|
return new TextDecoder(opt_encoding, options);
|
|
}
|
|
opt_encoding = opt_encoding ? String(opt_encoding) : DEFAULT_ENCODING;
|
|
options = Object(options);
|
|
/** @private */
|
|
this._encoding = getEncoding(opt_encoding);
|
|
if (this._encoding === null)
|
|
throw new TypeError('Unknown encoding: ' + opt_encoding);
|
|
|
|
/* @private @type {boolean} */
|
|
this._streaming = false;
|
|
/** @private */
|
|
this._decoder = null;
|
|
/* @private @type {{fatal: boolean}=} */
|
|
this._options = { fatal: Boolean(options.fatal) };
|
|
|
|
if (Object.defineProperty) {
|
|
Object.defineProperty(
|
|
this, 'encoding',
|
|
{ get: function() { return this._encoding.name; } });
|
|
} else {
|
|
this.encoding = this._encoding.name;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
// TODO: Issue if input byte stream is offset by decoder
|
|
// TODO: BOM detection will not work if stream header spans multiple calls
|
|
// (last N bytes of previous stream may need to be retained?)
|
|
TextDecoder.prototype = {
|
|
/**
|
|
* @param {ArrayBufferView=} opt_view The buffer of bytes to decode.
|
|
* @param {{stream: boolean}=} options
|
|
*/
|
|
decode: function decode(opt_view, options) {
|
|
if (opt_view && !('buffer' in opt_view && 'byteOffset' in opt_view &&
|
|
'byteLength' in opt_view)) {
|
|
throw new TypeError('Expected ArrayBufferView');
|
|
} else if (!opt_view) {
|
|
opt_view = new Uint8Array(0);
|
|
}
|
|
options = Object(options);
|
|
|
|
if (!this._streaming) {
|
|
this._decoder = this._encoding.getDecoder(this._options);
|
|
}
|
|
this._streaming = Boolean(options.stream);
|
|
|
|
var bytes = new Uint8Array(opt_view.buffer,
|
|
opt_view.byteOffset,
|
|
opt_view.byteLength);
|
|
var input_stream = new ByteInputStream(bytes);
|
|
|
|
if (!this._BOMseen) {
|
|
// TODO: Don't do this until sufficient bytes are present
|
|
this._BOMseen = true;
|
|
consumeBOM(this._encoding.name, input_stream);
|
|
}
|
|
|
|
var output_stream = new CodePointOutputStream(), code_point;
|
|
while (input_stream.get() !== EOF_byte) {
|
|
code_point = this._decoder.decode(input_stream);
|
|
if (code_point !== null && code_point !== EOF_code_point) {
|
|
output_stream.emit(code_point);
|
|
}
|
|
}
|
|
if (!this._streaming) {
|
|
do {
|
|
code_point = this._decoder.decode(input_stream);
|
|
if (code_point !== null && code_point !== EOF_code_point) {
|
|
output_stream.emit(code_point);
|
|
}
|
|
} while (code_point !== EOF_code_point &&
|
|
input_stream.get() != EOF_byte);
|
|
this._decoder = null;
|
|
}
|
|
return output_stream.string();
|
|
}
|
|
};
|
|
|
|
global['TextEncoder'] = global['TextEncoder'] || TextEncoder;
|
|
global['TextDecoder'] = global['TextDecoder'] || TextDecoder;
|
|
}(this));
|
|
(function(global) {
|
|
|
|
// Rumor Messaging for JS
|
|
//
|
|
// https://tbwiki.tokbox.com/index.php/Rumor_:_Messaging_FrameWork
|
|
//
|
|
// @todo Rumor {
|
|
// Add error codes for all the error cases
|
|
// Add Dependability commands
|
|
// }
|
|
|
|
OT.Rumor = {
|
|
MessageType: {
|
|
// This is used to subscribe to address/addresses. The address/addresses the
|
|
// client specifies here is registered on the server. Once any message is sent to
|
|
// that address/addresses, the client receives that message.
|
|
SUBSCRIBE: 0,
|
|
|
|
// This is used to unsubscribe to address / addresses. Once the client unsubscribe
|
|
// to an address, it will stop getting messages sent to that address.
|
|
UNSUBSCRIBE: 1,
|
|
|
|
// This is used to send messages to arbitrary address/ addresses. Messages can be
|
|
// anything and Rumor will not care about what is included.
|
|
MESSAGE: 2,
|
|
|
|
// This will be the first message that the client sends to the server. It includes
|
|
// the uniqueId for that client connection and a disconnect_notify address that will
|
|
// be notified once the client disconnects.
|
|
CONNECT: 3,
|
|
|
|
// This will be the message used by the server to notify an address that a client disconnected.
|
|
DISCONNECT: 4,
|
|
|
|
//Enhancements to support Keepalives
|
|
PING: 7,
|
|
PONG: 8,
|
|
STATUS: 9
|
|
}
|
|
};
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
var BUFFER_DRAIN_INTERVAL = 100, // The interval between polling the websocket's send buffer
|
|
BUFFER_DRAIN_MAX_RETRIES = 10, // The total number of times to retest the websocket's send buffer
|
|
WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000,
|
|
|
|
// Magic Connectivity Timeout Constant: We wait 3*the keep alive interval,
|
|
// on the third keep alive we trigger the timeout if we haven't received the
|
|
// server pong.
|
|
WEB_SOCKET_CONNECTIVITY_TIMEOUT = 5*WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100
|
|
|
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Close_codes
|
|
// http://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.CloseCodes.html
|
|
var wsCloseErrorCodes = {
|
|
1002: "The endpoint is terminating the connection due to a protocol error. (CLOSE_PROTOCOL_ERROR)",
|
|
1003: "The connection is being terminated because the endpoint received data of a type it cannot accept (for example, a text-only endpoint received binary data). (CLOSE_UNSUPPORTED)",
|
|
1004: "The endpoint is terminating the connection because a data frame was received that is too large. (CLOSE_TOO_LARGE)",
|
|
1005: "Indicates that no status code was provided even though one was expected. (CLOSE_NO_STATUS)",
|
|
1006: "Used to indicate that a connection was closed abnormally (that is, with no close frame being sent) when a status code is expected. (CLOSE_ABNORMAL)",
|
|
1007: "Indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 [RFC3629] data within a text message)",
|
|
1008: "Indicates that an endpoint is terminating the connection because it has received a message that violates its policy. This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy",
|
|
1009: "Indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process",
|
|
1011: "Indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request",
|
|
|
|
// .... codes in the 4000-4999 range are available for use by applications.
|
|
4001: "Connectivity loss was detected as it was too long since the socket received the last PONG message"
|
|
};
|
|
|
|
OT.Rumor.SocketError = function(code, message) {
|
|
this.code = code;
|
|
this.message = message;
|
|
};
|
|
|
|
// The NativeSocket bit is purely to make testing simpler, it defaults to WebSocket
|
|
// so in normal operation you would omit it.
|
|
OT.Rumor.Socket = function(messagingServer, notifyDisconnectAddress, NativeSocket) {
|
|
var server = messagingServer,
|
|
webSocket,
|
|
id,
|
|
onOpen,
|
|
onError,
|
|
onClose,
|
|
onMessage,
|
|
connectCallback,
|
|
bufferDrainTimeout, // Timer to poll whether th send buffer has been drained
|
|
connectTimeout,
|
|
lastMessageTimestamp, // The timestamp of the last message received
|
|
keepAliveTimer; // Timer for the connectivity checks
|
|
|
|
|
|
//// Private API
|
|
var stateChanged = function(newState) {
|
|
switch (newState) {
|
|
case 'disconnected':
|
|
case 'error':
|
|
webSocket = null;
|
|
if (onClose) {
|
|
var error;
|
|
if(hasLostConnectivity()) {
|
|
error = new Error(wsCloseErrorCodes[4001]);
|
|
error.code = 4001;
|
|
}
|
|
onClose(error);
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
setState = OT.$.statable(this, ['disconnected', 'error', 'connected', 'connecting', 'disconnecting'], 'disconnected', stateChanged),
|
|
|
|
validateCallback = function validateCallback (name, callback) {
|
|
if (callback === null || !OT.$.isFunction(callback) ) {
|
|
throw new Error("The Rumor.Socket " + name + " callback must be a valid function or null");
|
|
}
|
|
},
|
|
|
|
error = function error (errorMessage) {
|
|
OT.error("Rumor.Socket: " + errorMessage);
|
|
|
|
var error = new OT.Rumor.SocketError(null, errorMessage || "Unknown Socket Error");
|
|
|
|
if (connectTimeout) clearTimeout(connectTimeout);
|
|
|
|
setState('error');
|
|
|
|
if (this.previousState === 'connecting' && connectCallback) {
|
|
connectCallback(error, null);
|
|
connectCallback = null;
|
|
}
|
|
|
|
if (onError) onError(error);
|
|
}.bind(this),
|
|
|
|
// Immediately close the socket, only used by disconnectWhenSendBufferIsDrained
|
|
close = function close() {
|
|
setState('disconnecting');
|
|
|
|
if (bufferDrainTimeout) {
|
|
clearTimeout(bufferDrainTimeout);
|
|
bufferDrainTimeout = null;
|
|
}
|
|
console.info("CALLED CLOSE ON WEBSOCKET");
|
|
webSocket.close();
|
|
},
|
|
|
|
// Ensure that the WebSocket send buffer is fully drained before disconnecting
|
|
// the socket. If the buffer doesn't drain after a certain length of time
|
|
// we give up and close it anyway.
|
|
disconnectWhenSendBufferIsDrained = function disconnectWhenSendBufferIsDrained (bufferDrainRetries) {
|
|
if (!webSocket) return;
|
|
|
|
if (bufferDrainRetries === void 0) bufferDrainRetries = 0;
|
|
if (bufferDrainTimeout) clearTimeout(bufferDrainTimeout);
|
|
|
|
if (webSocket.bufferedAmount > 0 && (bufferDrainRetries + 1) <= BUFFER_DRAIN_MAX_RETRIES) {
|
|
bufferDrainTimeout = setTimeout(disconnectWhenSendBufferIsDrained, BUFFER_DRAIN_INTERVAL, bufferDrainRetries+1);
|
|
}
|
|
else {
|
|
close();
|
|
}
|
|
},
|
|
|
|
hasLostConnectivity = function hasLostConnectivity () {
|
|
if (!lastMessageTimestamp) return false;
|
|
|
|
return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT;
|
|
},
|
|
|
|
sendKeepAlive = function sendKeepAlive () {
|
|
if (!this.is('connected')) return;
|
|
|
|
if ( hasLostConnectivity() ) {
|
|
webSocketDisconnected({code: 4001});
|
|
}
|
|
else {
|
|
webSocket.send(OT.Rumor.Message.Ping().serialize());
|
|
keepAliveTimer = setTimeout(sendKeepAlive.bind(this), WEB_SOCKET_KEEP_ALIVE_INTERVAL);
|
|
}
|
|
}.bind(this);
|
|
|
|
|
|
//// Private Event Handlers
|
|
var webSocketConnected = function webSocketConnected () {
|
|
if (connectTimeout) clearTimeout(connectTimeout);
|
|
|
|
// Connect to Rumor by registering our connection id and the
|
|
// app server address to notify if we disconnect.
|
|
//
|
|
// We don't need to wait for a reply to this message.
|
|
webSocket.send(OT.Rumor.Message.Connect(id, notifyDisconnectAddress).serialize());
|
|
|
|
setState('connected');
|
|
if (connectCallback) {
|
|
connectCallback(null, id);
|
|
connectCallback = null;
|
|
}
|
|
|
|
if (onOpen) onOpen(id);
|
|
|
|
setTimeout(function() {
|
|
lastMessageTimestamp = OT.$.now();
|
|
sendKeepAlive();
|
|
}, WEB_SOCKET_KEEP_ALIVE_INTERVAL);
|
|
},
|
|
|
|
webSocketConnectTimedOut = function webSocketConnectTimedOut () {
|
|
error("Timed out while waiting for the Rumor socket to connect.");
|
|
},
|
|
|
|
webSocketError = function webSocketError (errorEvent) {
|
|
var errorMessage = "Unknown Socket Error"; // @fixme We MUST be able to do better than this!
|
|
|
|
// All errors seem to result in disconnecting the socket, the close event
|
|
// has a close reason and code which gives some error context. This,
|
|
// combined with the fact that the errorEvent argument contains no
|
|
// error info at all, means we'll delay triggering the error handlers
|
|
// until the socket is closed.
|
|
// error(errorMessage);
|
|
},
|
|
|
|
webSocketDisconnected = function webSocketDisconnected (closeEvent) {
|
|
if (connectTimeout) clearTimeout(connectTimeout);
|
|
if (keepAliveTimer) clearTimeout(keepAliveTimer);
|
|
|
|
if (closeEvent.code !== 1000 && closeEvent.code !== 1001) {
|
|
var reason = closeEvent.reason || closeEvent.message;
|
|
if (!reason && wsCloseErrorCodes.hasOwnProperty(closeEvent.code)) reason = wsCloseErrorCodes[closeEvent.code];
|
|
|
|
error("Rumor Socket Disconnected: " + reason);
|
|
}
|
|
|
|
if (this.isNot('error')) setState('disconnected');
|
|
}.bind(this),
|
|
|
|
webSocketReceivedMessage = function webSocketReceivedMessage (message) {
|
|
lastMessageTimestamp = OT.$.now();
|
|
|
|
if (onMessage) {
|
|
var msg = OT.Rumor.Message.deserialize(message.data);
|
|
|
|
if (msg.type !== OT.Rumor.MessageType.PONG) {
|
|
onMessage(msg.toAddress, msg.data);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
//// Public API
|
|
|
|
this.publish = function (topics, message) {
|
|
webSocket.send(OT.Rumor.Message.Publish(topics, message).serialize());
|
|
};
|
|
|
|
this.subscribe = function(topics) {
|
|
webSocket.send(OT.Rumor.Message.Subscribe(topics).serialize());
|
|
};
|
|
|
|
this.unsubscribe = function(topics) {
|
|
webSocket.send(OT.Rumor.Message.Unsubscribe(topics).serialize());
|
|
};
|
|
|
|
this.connect = function (connectionId, complete) {
|
|
if (this.is('connecting', 'connected')) {
|
|
complete(new OT.Rumor.SocketError(null, "Rumor.Socket cannot connect when it is already connecting or connected."));
|
|
return;
|
|
}
|
|
|
|
id = connectionId;
|
|
connectCallback = complete;
|
|
|
|
try {
|
|
setState('connecting');
|
|
|
|
webSocket = new (NativeSocket || WebSocket)(server);
|
|
webSocket.binaryType = 'arraybuffer';
|
|
|
|
webSocket.onopen = webSocketConnected;
|
|
webSocket.onclose = webSocketDisconnected;
|
|
webSocket.onerror = webSocketError;
|
|
webSocket.onmessage = webSocketReceivedMessage;
|
|
|
|
connectTimeout = setTimeout(webSocketConnectTimedOut, OT.Rumor.Socket.CONNECT_TIMEOUT);
|
|
}
|
|
catch(e) {
|
|
OT.error(e);
|
|
|
|
// @todo add an actual error message
|
|
error("Could not connect to the Rumor socket, possibly because of a blocked port.")
|
|
}
|
|
};
|
|
|
|
this.disconnect = function() {
|
|
if (connectTimeout) clearTimeout(connectTimeout);
|
|
if (keepAliveTimer) clearTimeout(keepAliveTimer);
|
|
|
|
if (!webSocket) {
|
|
if (this.isNot('error')) setState('disconnected');
|
|
return;
|
|
}
|
|
|
|
if (webSocket.readyState === 3/* CLOSED */) {
|
|
if (this.isNot('error')) setState('disconnected');
|
|
}
|
|
else {
|
|
if (this.is('connected')) {
|
|
// Look! We are nice to the rumor server ;-)
|
|
webSocket.send(OT.Rumor.Message.Disconnect().serialize());
|
|
}
|
|
|
|
// Wait until the socket is ready to close
|
|
disconnectWhenSendBufferIsDrained();
|
|
}
|
|
};
|
|
|
|
|
|
|
|
Object.defineProperties(this, {
|
|
id: {
|
|
get: function() { return id; }
|
|
},
|
|
|
|
onOpen: {
|
|
set: function(callback) {
|
|
validateCallback('onOpen', callback);
|
|
onOpen = callback;
|
|
},
|
|
|
|
get: function() { return onOpen; }
|
|
},
|
|
|
|
onError: {
|
|
set: function(callback) {
|
|
validateCallback('onError', callback);
|
|
onError = callback;
|
|
},
|
|
|
|
get: function() { return onError; }
|
|
},
|
|
|
|
onClose: {
|
|
set: function(callback) {
|
|
validateCallback('onClose', callback);
|
|
onClose = callback;
|
|
},
|
|
|
|
get: function() { return onClose; }
|
|
},
|
|
|
|
onMessage: {
|
|
set: function(callback) {
|
|
validateCallback('onMessage', callback);
|
|
onMessage = callback;
|
|
},
|
|
|
|
get: function() { return onMessage; }
|
|
}
|
|
});
|
|
};
|
|
|
|
// The number of ms to wait for the websocket to connect
|
|
OT.Rumor.Socket.CONNECT_TIMEOUT = 15000;
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
//
|
|
//
|
|
// @references
|
|
// * https://tbwiki.tokbox.com/index.php/Rumor_Message_Packet
|
|
// * https://tbwiki.tokbox.com/index.php/Rumor_Protocol
|
|
//
|
|
OT.Rumor.Message = function (type, toAddress, headers, data) {
|
|
this.type = type;
|
|
this.toAddress = toAddress;
|
|
this.headers = headers;
|
|
this.data = data;
|
|
};
|
|
|
|
|
|
OT.Rumor.Message.prototype.serialize = function () {
|
|
var bitStream = '',
|
|
str = "",
|
|
offset = 8,
|
|
cBuf = 7,
|
|
address = new Array(this.toAddress.length),
|
|
headerKey = new Array(this.headers.length),
|
|
headerVal = new Array(this.headers.length),
|
|
dataView;
|
|
|
|
// The number of addresses
|
|
cBuf++;
|
|
|
|
// Write out the address.
|
|
for (var i = 0; i < this.toAddress.length; i++) {
|
|
address[i] = new TextEncoder('utf-8').encode(this.toAddress[i]);
|
|
cBuf += 2;
|
|
cBuf += address[i].length;
|
|
}
|
|
|
|
// The number of parameters
|
|
cBuf++;
|
|
|
|
// Write out the params
|
|
for (var i = 0; i < this.headers.length; i++) {
|
|
headerKey[i] = new TextEncoder('utf-8').encode(this.headers[i].key);
|
|
headerVal[i] = new TextEncoder('utf-8').encode(this.headers[i].val);
|
|
cBuf += 4;
|
|
cBuf += headerKey[i].length;
|
|
cBuf += headerVal[i].length;
|
|
}
|
|
|
|
dataView = new TextEncoder('utf-8').encode(this.data);
|
|
cBuf += dataView.length;
|
|
|
|
// Let's allocate a binary blob of this size
|
|
var buffer = new ArrayBuffer(cBuf);
|
|
var uint8View = new Uint8Array(buffer, 0, cBuf);
|
|
|
|
// We don't include the header in the lenght.
|
|
cBuf -= 4;
|
|
|
|
// Write out size (in network order)
|
|
uint8View[0] = (cBuf & 0xFF000000) >>> 24;
|
|
uint8View[1] = (cBuf & 0x00FF0000) >>> 16;
|
|
uint8View[2] = (cBuf & 0x0000FF00) >>> 8;
|
|
uint8View[3] = (cBuf & 0x000000FF) >>> 0;
|
|
|
|
// Write out reserved bytes
|
|
uint8View[4] = 0;
|
|
uint8View[5] = 0;
|
|
|
|
// Write out message type
|
|
uint8View[6] = this.type;
|
|
uint8View[7] = this.toAddress.length;
|
|
|
|
// Now just copy over the encoded values..
|
|
for (var i = 0; i < address.length; i++) {
|
|
strArray = address[i];
|
|
uint8View[offset++] = strArray.length >> 8 & 0xFF;
|
|
uint8View[offset++] = strArray.length >> 0 & 0xFF;
|
|
for (var j = 0; j < strArray.length; j++) {
|
|
uint8View[offset++] = strArray[j];
|
|
}
|
|
}
|
|
|
|
uint8View[offset++] = headerKey.length;
|
|
|
|
// Write out the params
|
|
for (var i = 0; i < headerKey.length; i++) {
|
|
strArray = headerKey[i];
|
|
uint8View[offset++] = strArray.length >> 8 & 0xFF;
|
|
uint8View[offset++] = strArray.length >> 0 & 0xFF;
|
|
for (var j = 0; j < strArray.length; j++) {
|
|
uint8View[offset++] = strArray[j];
|
|
}
|
|
|
|
strArray = headerVal[i];
|
|
uint8View[offset++] = strArray.length >> 8 & 0xFF;
|
|
uint8View[offset++] = strArray.length >> 0 & 0xFF;
|
|
for (var j = 0; j < strArray.length; j++) {
|
|
uint8View[offset++] = strArray[j];
|
|
}
|
|
}
|
|
|
|
// And finally the data
|
|
for (var i = 0; i < dataView.length; i++) {
|
|
uint8View[offset++] = dataView[i];
|
|
}
|
|
|
|
return buffer;
|
|
};
|
|
|
|
OT.Rumor.Message.deserialize = function (buffer) {
|
|
var cBuf = 0;
|
|
var type;
|
|
var offset = 8;
|
|
var uint8View = new Uint8Array(buffer);
|
|
|
|
// Write out size (in network order)
|
|
cBuf += uint8View[0] << 24;
|
|
cBuf += uint8View[1] << 16;
|
|
cBuf += uint8View[2] << 8;
|
|
cBuf += uint8View[3] << 0;
|
|
|
|
type = uint8View[6];
|
|
var address = [];
|
|
|
|
for (var i = 0; i < uint8View[7]; i++) {
|
|
length = uint8View[offset++] << 8;
|
|
length += uint8View[offset++];
|
|
var strView = new Uint8Array(buffer, offset, length);
|
|
address[i] = new TextDecoder('utf-8').decode(strView);
|
|
offset += length;
|
|
}
|
|
|
|
var headerlen = uint8View[offset++];
|
|
var headers = [];
|
|
|
|
for (var i = 0; i < headerlen; i++) {
|
|
length = uint8View[offset++] << 8;
|
|
length += uint8View[offset++];
|
|
var strView = new Uint8Array(buffer, offset, length);
|
|
var keyStr = new TextDecoder('utf-8').decode(strView);
|
|
offset += length;
|
|
|
|
length = uint8View[offset++] << 8;
|
|
length += uint8View[offset++];
|
|
strView = new Uint8Array(buffer, offset, length);
|
|
var valStr = new TextDecoder('utf-8').decode(strView);
|
|
headers[i] = { key : keyStr, val : valStr };
|
|
offset += length;
|
|
}
|
|
|
|
var dataView = new Uint8Array(buffer, offset);
|
|
var data = new TextDecoder('utf-8').decode(dataView);
|
|
|
|
return new OT.Rumor.Message(type, address, headers, data);
|
|
};
|
|
|
|
|
|
OT.Rumor.Message.Connect = function (uniqueId, notifyDisconnectAddress) {
|
|
var headers = [
|
|
{key: 'uniqueId', val: uniqueId},
|
|
{key: 'notifyDisconnectAddress', val: notifyDisconnectAddress}
|
|
];
|
|
|
|
return new OT.Rumor.Message(OT.Rumor.MessageType.CONNECT, [], headers, "");
|
|
};
|
|
|
|
OT.Rumor.Message.Disconnect = function () {
|
|
return new OT.Rumor.Message(OT.Rumor.MessageType.DISCONNECT, [], [], "");
|
|
};
|
|
|
|
OT.Rumor.Message.Subscribe = function(topics) {
|
|
return new OT.Rumor.Message(OT.Rumor.MessageType.SUBSCRIBE, topics, [], "");
|
|
};
|
|
|
|
OT.Rumor.Message.Unsubscribe = function(topics) {
|
|
return new OT.Rumor.Message(OT.Rumor.MessageType.UNSUBSCRIBE, topics, [], "");
|
|
};
|
|
|
|
OT.Rumor.Message.Publish = function(topics, message, headers) {
|
|
return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, [], message);
|
|
};
|
|
|
|
// This message is used to implement keepalives on the persistent
|
|
// socket connection between the client and server. Every time the
|
|
// client sends a PING to the server, the server will respond with
|
|
// a PONG.
|
|
OT.Rumor.Message.Ping = function() {
|
|
return new OT.Rumor.Message(OT.Rumor.MessageType.PING, [], [], "");
|
|
};
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
// Rumor Messaging for JS
|
|
//
|
|
// https://tbwiki.tokbox.com/index.php/Raptor_Messages_(Sent_as_a_RumorMessage_payload_in_JSON)
|
|
//
|
|
// @todo Raptor {
|
|
// Look at disconnection cleanup: i.e. subscriber + publisher cleanup
|
|
// Add error codes for all the error cases
|
|
// Write unit tests for SessionInfo
|
|
// Write unit tests for Session
|
|
// Make use of the new DestroyedEvent
|
|
// Move Analytics out of Raptor into Session
|
|
// Remove dependency on OT.properties
|
|
// OT.Capabilities must be part of the Raptor namespace
|
|
// Add Dependability commands
|
|
// Think about noConflict, or whether we should just use the OT namespace
|
|
// Think about how to expose OT.publishers, OT.subscribers, and OT.sessions if messaging was
|
|
// being included as a component
|
|
// Another solution to the problem of having publishers/subscribers/etc would be to make
|
|
// Raptor Socket a separate component from Dispatch (dispatch being more business logic)
|
|
// Look at the coupling of OT.sessions to OT.Raptor.Socket
|
|
// }
|
|
//
|
|
// @todo Raptor Docs {
|
|
// Document payload formats for incoming messages (what are the payloads for STREAM CREATED/MODIFIED for example)
|
|
// Document how keepalives work
|
|
// Document all the Raptor actions and types
|
|
// Document the session connect flow (including error cases)
|
|
// }
|
|
|
|
|
|
OT.Raptor = {
|
|
Actions: {
|
|
//General
|
|
CONNECT: 100,
|
|
CREATE: 101,
|
|
UPDATE: 102,
|
|
DELETE: 103,
|
|
STATE: 104,
|
|
|
|
//Moderation
|
|
FORCE_DISCONNECT: 105,
|
|
FORCE_UNPUBLISH: 106,
|
|
SIGNAL: 107,
|
|
|
|
//Archives
|
|
CREATE_ARCHIVE: 108,
|
|
CLOSE_ARCHIVE: 109,
|
|
START_RECORDING_SESSION: 110,
|
|
STOP_RECORDING_SESSION: 111,
|
|
START_RECORDING_STREAM: 112,
|
|
STOP_RECORDING_STREAM: 113,
|
|
LOAD_ARCHIVE: 114,
|
|
START_PLAYBACK: 115,
|
|
STOP_PLAYBACK: 116,
|
|
|
|
//AppState
|
|
APPSTATE_PUT: 117,
|
|
APPSTATE_DELETE: 118,
|
|
|
|
// JSEP
|
|
OFFER: 119,
|
|
ANSWER: 120,
|
|
PRANSWER: 121,
|
|
CANDIDATE: 122,
|
|
SUBSCRIBE: 123,
|
|
UNSUBSCRIBE: 124,
|
|
QUERY: 125,
|
|
SDP_ANSWER: 126,
|
|
|
|
//KeepAlive
|
|
PONG: 127,
|
|
REGISTER: 128, //Used for registering streams.
|
|
|
|
QUALITY_CHANGED: 129
|
|
},
|
|
|
|
Types: {
|
|
//RPC
|
|
RPC_REQUEST: 100,
|
|
RPC_RESPONSE: 101,
|
|
|
|
//EVENT
|
|
STREAM: 102,
|
|
ARCHIVE: 103,
|
|
CONNECTION: 104,
|
|
APPSTATE: 105,
|
|
CONNECTIONCOUNT: 106,
|
|
MODERATION: 107,
|
|
SIGNAL: 108,
|
|
SUBSCRIBER: 110,
|
|
|
|
//JSEP Protocol
|
|
JSEP: 109
|
|
}
|
|
};
|
|
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
//// Some helper functions that are used for debugging and logging
|
|
|
|
var typeToName = {},
|
|
actionToName = {};
|
|
|
|
// Create a lookup table of Raptor message types (integer) to a human readable strings
|
|
var messageTypes = OT.Raptor.Types;
|
|
for (var name in messageTypes) {
|
|
typeToName[messageTypes[name]] = name;
|
|
}
|
|
|
|
// Create a lookup table of Raptor message actions (integer) to a human readable strings
|
|
var messageActions = OT.Raptor.Actions;
|
|
for (var name in messageActions) {
|
|
actionToName[messageActions[name]] = name;
|
|
}
|
|
|
|
|
|
OT.Raptor.serializeMessage = function (message) {
|
|
return JSON.stringify(message);
|
|
};
|
|
|
|
|
|
// Deserialising a Raptor message mainly means doing a JSON.parse on it.
|
|
// We do decorate the final message with a few extra helper properies though.
|
|
//
|
|
// These include:
|
|
// * typeName: A human readable version of the Raptor type. E.g. STREAM instead of 102
|
|
// * actionName: A human readable version of the Raptor action. E.g. CREATE instead of 101
|
|
// * signature: typeName and actionName combined. This is mainly for debugging. E.g. A type
|
|
// of 102 and an action of 101 would result in a signature of "STREAM:CREATE"
|
|
//
|
|
OT.Raptor.deserializeMessage = function (msg) {
|
|
var message = JSON.parse(msg);
|
|
|
|
if (message.type) {
|
|
message.type = parseInt(message.type, 10);
|
|
message.typeName = typeToName[message.type] || null;
|
|
}
|
|
|
|
if (message.action) {
|
|
message.action = parseInt(message.action, 10);
|
|
message.actionName = actionToName[message.action] || null;
|
|
}
|
|
|
|
message.signature = message.typeName + ':' + message.actionName;
|
|
|
|
return message;
|
|
};
|
|
|
|
|
|
OT.Raptor.Message = {};
|
|
OT.Raptor.Message.connect = function (widgetId, connectionId, sessionId, apiKey, token, p2pEnabled) {
|
|
var payload = {
|
|
credentials: {
|
|
connectionId: connectionId,
|
|
soAccessState: 2,
|
|
supportsWebRTC: true,
|
|
p2pEnabled: p2pEnabled,
|
|
GUID: widgetId,
|
|
widgetId: widgetId,
|
|
partnerId: apiKey
|
|
},
|
|
sessionId: sessionId,
|
|
params: {
|
|
tokenPermissions: {
|
|
apiKey: apiKey
|
|
},
|
|
token: token
|
|
},
|
|
uniqueId: connectionId
|
|
};
|
|
|
|
return OT.Raptor.serializeMessage({
|
|
id: OT.$.uuid(),
|
|
type: OT.Raptor.Types.RPC_REQUEST,
|
|
action: OT.Raptor.Actions.CONNECT,
|
|
payload: payload,
|
|
replyTo: connectionId
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.getSessionState = function (connectionId, sessionId, connectionsRequired) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: OT.$.uuid(),
|
|
type: OT.Raptor.Types.RPC_REQUEST,
|
|
action: OT.Raptor.Actions.STATE,
|
|
payload: {
|
|
sessionId: sessionId,
|
|
connectionsRequired: connectionsRequired || true
|
|
},
|
|
replyTo: connectionId
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.forceDisconnect = function (fromConnectionId, connectionIdToDisconnect, sessionId) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.RPC_REQUEST,
|
|
action: OT.Raptor.Actions.FORCE_DISCONNECT,
|
|
payload: {
|
|
connectionId: connectionIdToDisconnect,
|
|
sessionId: sessionId
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
|
|
|
|
OT.Raptor.Message.streamCreate = function (connectionId, sessionId, publisherId, name, videoOrientation, videoWidth, videoHeight, hasAudio, hasVideo, p2pEnabled) {
|
|
var payload = {
|
|
key: sessionId,
|
|
value: {
|
|
p2pEnabled: p2pEnabled || false,
|
|
publisherId: publisherId,
|
|
connection: {
|
|
connectionId: connectionId
|
|
},
|
|
type: "WebRTC",
|
|
name: name || '',
|
|
creationTime: Date.now(), // Unix timestamp, time in milliseconds since 1970 UTC
|
|
orientation: {
|
|
width: videoWidth,
|
|
height: videoHeight,
|
|
videoOrientation: videoOrientation || "OTVideoOrientationRotatedNormal"
|
|
},
|
|
hasAudio: hasAudio !== void 0 ? hasAudio : true,
|
|
hasVideo: hasVideo !== void 0 ? hasVideo : true
|
|
}
|
|
};
|
|
|
|
return OT.Raptor.serializeMessage({
|
|
id: connectionId, // HACK until buf fixed in symphony
|
|
type: OT.Raptor.Types.STREAM,
|
|
action: OT.Raptor.Actions.CREATE,
|
|
payload: payload,
|
|
replyTo: ''
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.streamDestroy = function (connectionId, sessionId, streamId) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: connectionId,
|
|
type: OT.Raptor.Types.STREAM,
|
|
action: OT.Raptor.Actions.DELETE,
|
|
payload: {
|
|
key: sessionId + "/STREAMS/" + streamId
|
|
},
|
|
replyTo: connectionId
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.streamModify = function (connectionId, sessionId, streamId, key, value) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: connectionId,
|
|
type: OT.Raptor.Types.STREAM,
|
|
action: OT.Raptor.Actions.UPDATE,
|
|
payload: {
|
|
key: [sessionId, 'STREAMS', streamId, key].join('/'),
|
|
value: value
|
|
},
|
|
replyTo: connectionId
|
|
});
|
|
};
|
|
|
|
OT.Raptor.Message.forceUnpublish = function (fromConnectionId, sessionId, streamIdToUnpublish) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.RPC_REQUEST,
|
|
action: OT.Raptor.Actions.FORCE_UNPUBLISH,
|
|
payload: {
|
|
sessionId: sessionId,
|
|
connectionId: fromConnectionId,
|
|
streamId: streamIdToUnpublish,
|
|
webRTCStream: true
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.jsepOffer = function (fromConnectionId, toConnectionId, streamId, sdp) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.JSEP,
|
|
action: OT.Raptor.Actions.OFFER,
|
|
payload: {
|
|
fromAddress: fromConnectionId,
|
|
toAddresses: toConnectionId,
|
|
sdp: sdp,
|
|
streamId: streamId
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.jsepAnswer = function (fromConnectionId, toConnectionId, streamId, sdp) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.JSEP,
|
|
action: OT.Raptor.Actions.ANSWER,
|
|
payload: {
|
|
fromAddress: fromConnectionId,
|
|
toAddresses: toConnectionId,
|
|
sdp: sdp,
|
|
streamId: streamId
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
OT.Raptor.Message.jsepSubscribe = function (fromConnectionId, toConnectionId, streamId, subscribeToVideo, subscribeToAudio) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.JSEP,
|
|
action: OT.Raptor.Actions.SUBSCRIBE,
|
|
payload: {
|
|
keyManagemenMethod: OT.$.supportedCryptoScheme(),
|
|
bundleSupport: OT.$.supportsBundle(),
|
|
rtcpMuxSupport: OT.$.supportsRtcpMux(),
|
|
fromAddress: fromConnectionId,
|
|
toAddresses: toConnectionId,
|
|
streamId: streamId,
|
|
hasVideo: subscribeToVideo,
|
|
hasAudio: subscribeToAudio
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
|
|
|
|
OT.Raptor.Message.jsepUnsubscribe = function (fromConnectionId, toConnectionId, streamId) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.JSEP,
|
|
action: OT.Raptor.Actions.UNSUBSCRIBE,
|
|
payload: {
|
|
fromAddress: fromConnectionId,
|
|
toAddresses: toConnectionId,
|
|
streamId: streamId
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
OT.Raptor.Message.jsepCandidate = function (fromConnectionId, toConnectionId, streamId, candidate) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: fromConnectionId,
|
|
type: OT.Raptor.Types.JSEP,
|
|
action: OT.Raptor.Actions.CANDIDATE,
|
|
payload: {
|
|
fromAddress: fromConnectionId,
|
|
toAddresses: toConnectionId,
|
|
candidate: candidate,
|
|
streamId: streamId
|
|
},
|
|
replyTo: fromConnectionId
|
|
});
|
|
};
|
|
|
|
OT.Raptor.Message.subscriberModify = function (connectionId, sessionId, streamId, subscriberId, key, value) {
|
|
return OT.Raptor.serializeMessage({
|
|
id: OT.$.uuid(),
|
|
type: OT.Raptor.Types.SUBSCRIBER,
|
|
action: OT.Raptor.Actions.UPDATE,
|
|
payload: {
|
|
key: [sessionId, 'SUBSCRIBER', streamId, connectionId, key].join('/'),
|
|
value: value
|
|
},
|
|
replyTo: connectionId
|
|
});
|
|
};
|
|
|
|
|
|
OT.Raptor.Message.signal = function(connectionId, sessionId, type, to, data) {
|
|
var payload = {
|
|
id: OT.$.uuid(),
|
|
fromAddress: connectionId
|
|
};
|
|
|
|
// Massage the to field into an array and default it to the session id
|
|
// if we're sending to no one
|
|
if (to && !Array.isArray(to)) {
|
|
payload.toAddresses = [to];
|
|
}
|
|
else if (!to || to.length === 0) {
|
|
// if it was omitted then we'll send the signal to the entire session.
|
|
payload.toAddresses = [sessionId];
|
|
}
|
|
else {
|
|
payload.toAddresses = to;
|
|
}
|
|
|
|
if (type !== void 0) payload.type = type;
|
|
if (data !== void 0) payload.data = data;
|
|
|
|
return OT.Raptor.serializeMessage({
|
|
id: connectionId,
|
|
type: OT.Raptor.Types.SIGNAL,
|
|
action: OT.Raptor.Actions.SIGNAL,
|
|
payload: payload,
|
|
replyTo: connectionId
|
|
});
|
|
};
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
var MAX_SIGNAL_DATA_LENGTH = 8192;
|
|
var MAX_SIGNAL_TYPE_LENGTH = 128;
|
|
|
|
//
|
|
// Error Codes:
|
|
// 413 - Type too long
|
|
// 400 - Type is invalid
|
|
// 413 - Data too long
|
|
// 400 - Data is invalid (can't be parsed as JSON)
|
|
// 429 - Rate limit exceeded
|
|
// 500 - Websocket connection is down
|
|
// 404 - To connection does not exist
|
|
// 400 - To is invalid
|
|
//
|
|
OT.Signal = function(sessionId, fromConnectionId, options) {
|
|
var isInvalidType = function(type) {
|
|
// Our format matches the unreserved characters from the URI RFC: http://www.ietf.org/rfc/rfc3986
|
|
return !/^[a-zA-Z0-9\-\._~]+$/.exec(type);
|
|
},
|
|
|
|
validateTo = function(toAddress) {
|
|
if (!toAddress) {
|
|
return {code: 400, reason: "The signal type was null or an empty String. Either set it to a non-empty String value or omit it"};
|
|
}
|
|
else if ( !Array.isArray(toAddress) ) {
|
|
return {code: 400, reason: "The To field was invalid"};
|
|
}
|
|
|
|
for (var i=0; i<toAddress.length; i++) {
|
|
if ( !(toAddress[i] instanceof OT.Connection || toAddress[i] instanceof OT.Session) ) {
|
|
return {code: 400, reason: "The To field was invalid"};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
},
|
|
|
|
validateType = function(type) {
|
|
var error = null;
|
|
|
|
if (type === null || type === void 0) {
|
|
error = {code: 400, reason: "The signal type was null or undefined. Either set it to a String value or omit it"};
|
|
}
|
|
else if (type.length > MAX_SIGNAL_TYPE_LENGTH) {
|
|
error = {code: 413, reason: "The signal type was too long, the maximum length of it is " + MAX_SIGNAL_TYPE_LENGTH + " characters"};
|
|
}
|
|
else if ( isInvalidType(type) ) {
|
|
error = {code: 400, reason: "The signal type was invalid, it can only contain letters, numbers, '-', '_', and '~'."};
|
|
}
|
|
|
|
return error;
|
|
},
|
|
|
|
validateData = function(data) {
|
|
var error = null;
|
|
if (data === null || data === void 0) {
|
|
error = {code: 400, reason: "The signal data was null or undefined. Either set it to a String value or omit it"};
|
|
}
|
|
else {
|
|
try {
|
|
if (JSON.stringify(data).length > MAX_SIGNAL_DATA_LENGTH) {
|
|
error = {code: 413, reason: "The data field was too long, the maximum size of it is " + MAX_SIGNAL_DATA_LENGTH + " characters"};
|
|
}
|
|
}
|
|
catch(e) {
|
|
error = {code: 400, reason: "The data field was not valid JSON"};
|
|
}
|
|
}
|
|
|
|
return error;
|
|
};
|
|
|
|
|
|
this.toRaptorMessage = function() {
|
|
var to;
|
|
|
|
if (this.to) {
|
|
to = this.to.map(function(thing) {
|
|
return typeof(thing) === 'string' ? thing : thing.id;
|
|
});
|
|
}
|
|
|
|
return OT.Raptor.Message.signal(fromConnectionId, sessionId, this.type, to, this.data);
|
|
};
|
|
|
|
this.toHash = function() {
|
|
var h = OT.$.clone(options);
|
|
if (h.to === void 0) h.to = null;
|
|
if (h.data === void 0) h.data = null;
|
|
|
|
return h;
|
|
};
|
|
|
|
|
|
this.error = null;
|
|
|
|
if (options) {
|
|
if (options.hasOwnProperty('data')) {
|
|
this.data = OT.$.clone(options.data);
|
|
this.error = validateData(this.data);
|
|
}
|
|
|
|
if (options.hasOwnProperty('to')) {
|
|
if (!Array.isArray(options.to)) {
|
|
this.to = [options.to];
|
|
}
|
|
else {
|
|
this.to = OT.$.clone(options.to);
|
|
}
|
|
|
|
if (!this.error) this.error = validateTo(this.to)
|
|
}
|
|
|
|
if (options.hasOwnProperty('type')) {
|
|
if (!this.error) this.error = validateType(options.type)
|
|
this.type = options.type;
|
|
}
|
|
}
|
|
|
|
this.valid = this.error === null;
|
|
};
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
|
|
function SignalError(code, reason, signal) {
|
|
this.code = code;
|
|
this.reason = reason;
|
|
this.signal = signal;
|
|
}
|
|
|
|
// The Dispatcher bit is purely to make testing simpler, it defaults to OT.Raptor.Dispatcher
|
|
// so in normal operation you would omit it. Note: it takes a constructor, not an instance.
|
|
OT.Raptor.Socket = function(widgetId, messagingServer, Dispatcher) {
|
|
var _socketUrl = OT.properties.messagingProtocol + "://" + messagingServer + ":" + OT.properties.messagingPort + "/rumorwebsocketsv2",
|
|
_symphony = "symphony." + messagingServer,
|
|
_sessionId,
|
|
_rumor,
|
|
_dispatcher,
|
|
_completion,
|
|
_capabilities = new OT.Capabilities([]),
|
|
_analytics = new OT.Analytics();
|
|
|
|
|
|
//// Private API
|
|
var setState = OT.$.statable(this, ['disconnected', 'connecting', 'connected', 'error', 'disconnecting'], 'disconnected'),
|
|
|
|
logAnalyticsEvent = function logAnalyticsEvent (variation, payloadType, payload, options) {
|
|
var event = {
|
|
action: 'Connect',
|
|
variation: variation,
|
|
payload_type: payloadType,
|
|
payload: payload,
|
|
session_id: _sessionId,
|
|
partner_id: OT.APIKEY,
|
|
widget_id: widgetId,
|
|
widget_type: 'Controller'
|
|
};
|
|
|
|
if (options) event = OT.$.extend(options, event);
|
|
_analytics.logEvent(event);
|
|
},
|
|
|
|
onConnectComplete = function onConnectComplete (error, sessionState, prefix) {
|
|
if (error) {
|
|
logAnalyticsEvent('Failure', "reason|webSocketServerUrl", prefix + error.code + ':' + error.message + '|' + _socketUrl);
|
|
|
|
setState('error');
|
|
}
|
|
else {
|
|
logAnalyticsEvent('Success', "webSocketServerUrl", _socketUrl, {connectionId: _rumor.id});
|
|
|
|
setState('connected');
|
|
_capabilities = new OT.Capabilities(sessionState.permissions);
|
|
}
|
|
|
|
_completion.apply(null, arguments);
|
|
},
|
|
|
|
onClose = function onClose (err) {
|
|
var session = OT.sessions.get(_sessionId),
|
|
connection = session.connection,
|
|
reason = this.is('disconnecting') ? "clientDisconnected" : "networkDisconnected";
|
|
|
|
if(err && err.code == 4001) {
|
|
reason = "networkTimedout";
|
|
}
|
|
|
|
setState('disconnected');
|
|
|
|
if (!connection) return;
|
|
|
|
if (connection.destroyedReason) {
|
|
console.debug("OT.Raptor.Socket: Socket was closed but the connection had already been destroyed. Reason: " + connection.destroyedReason);
|
|
return;
|
|
}
|
|
|
|
connection.destroy( reason );
|
|
}.bind(this),
|
|
|
|
onError = function onError () {
|
|
// @todo what does having an error mean? Are they always fatal? Are we disconnected now?
|
|
};
|
|
|
|
|
|
//// Public API
|
|
|
|
|
|
// Check whether this socket connection has permissions to perform a particular
|
|
// action.
|
|
this.permittedTo = function (action) {
|
|
return _capabilities[action] === 1;
|
|
};
|
|
|
|
this.connect = function (token, sessionInfo, completion) {
|
|
if (!this.is('disconnected', 'error')) {
|
|
OT.warn("Cannot connect the Raptor Socket as it is currently connected. You should disconnect first.");
|
|
return;
|
|
}
|
|
|
|
setState('connecting');
|
|
_sessionId = sessionInfo.sessionId;
|
|
_completion = completion;
|
|
|
|
var connectionId = OT.$.uuid(),
|
|
session = OT.sessions.get(_sessionId);
|
|
|
|
|
|
var analyticsPayload = [_socketUrl, navigator.userAgent, OT.properties.version, window.externalHost ? "yes" : "no"];
|
|
logAnalyticsEvent(
|
|
'Attempt',
|
|
"webSocketServerUrl|userAgent|sdkVersion|chromeFrame",
|
|
analyticsPayload.map(function(e) { return e.replace('|', '\\|'); }).join('|')
|
|
);
|
|
|
|
_rumor = new OT.Rumor.Socket(_socketUrl, _symphony);
|
|
_rumor.onClose = onClose;
|
|
_rumor.onMessage = _dispatcher.dispatch.bind(_dispatcher);
|
|
|
|
_rumor.connect(connectionId, function(error) {
|
|
if (error) {
|
|
onConnectComplete(error, null, "RumorConnection:");
|
|
return;
|
|
}
|
|
|
|
// we do this here to avoid getting connect errors twice
|
|
_rumor.onError = onError;
|
|
|
|
OT.debug("Raptor Socket connected to " + _sessionId + " on " + messagingServer);
|
|
|
|
_rumor.subscribe([_sessionId]);
|
|
|
|
//connect to session
|
|
var connectMessage = OT.Raptor.Message.connect(widgetId, _rumor.id, _sessionId, OT.APIKEY, token, sessionInfo.p2pEnabled);
|
|
this.publish(connectMessage);
|
|
}.bind(this));
|
|
};
|
|
|
|
|
|
this.disconnect = function () {
|
|
if (this.is('disconnected')) return;
|
|
|
|
setState('disconnecting');
|
|
_rumor.disconnect();
|
|
};
|
|
|
|
// Publishs +message+ to the Symphony app server.
|
|
this.publish = function (message) {
|
|
if (_rumor.isNot('connected')) {
|
|
OT.error("OT.Raptor.Socket: cannot publish until the socket is connected." + message);
|
|
return;
|
|
}
|
|
|
|
OT.debug("OT.Raptor.Socket Publish: " + message);
|
|
_rumor.publish([_symphony], message);
|
|
};
|
|
|
|
// Register a new stream against _sessionId
|
|
this.createStream = function(publisherId, name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo) {
|
|
var session = OT.sessions.get(_sessionId),
|
|
message = OT.Raptor.Message.streamCreate( _rumor.id,
|
|
_sessionId,
|
|
publisherId,
|
|
name,
|
|
orientation,
|
|
encodedWidth,
|
|
encodedHeight,
|
|
hasAudio,
|
|
hasVideo,
|
|
session.sessionInfo.p2pEnabled );
|
|
|
|
this.publish(message);
|
|
};
|
|
|
|
this.updateStream = function(streamId, key, value) {
|
|
this.publish( OT.Raptor.Message.streamModify(_rumor.id, _sessionId, streamId, key, value) );
|
|
};
|
|
|
|
this.destroyStream = function(streamId) {
|
|
this.publish( OT.Raptor.Message.streamDestroy(_rumor.id, _sessionId, streamId) );
|
|
};
|
|
|
|
this.modifySubscriber = function(subscriber, key, value) {
|
|
this.publish( OT.Raptor.Message.subscriberModify(_rumor.id, _sessionId, subscriber.streamId, subscriber.widgetId, key, value) );
|
|
};
|
|
|
|
this.forceDisconnect = function(connectionIdToDisconnect) {
|
|
this.publish( OT.Raptor.Message.forceDisconnect(_rumor.id, connectionIdToDisconnect, _sessionId) );
|
|
};
|
|
|
|
this.forceUnpublish = function(streamId) {
|
|
this.publish( OT.Raptor.Message.forceUnpublish(_rumor.id, _sessionId, streamId) );
|
|
};
|
|
|
|
this.jsepSubscribe = function(toConnectionId, streamId, subscribeToVideo, subscribeToAudio) {
|
|
this.publish( OT.Raptor.Message.jsepSubscribe(_rumor.id, toConnectionId, streamId, subscribeToVideo, subscribeToAudio) );
|
|
};
|
|
|
|
this.jsepUnsubscribe = function(toConnectionId, streamId) {
|
|
this.publish( OT.Raptor.Message.jsepUnsubscribe(_rumor.id, toConnectionId, streamId) );
|
|
};
|
|
|
|
this.jsepCandidate = function(toConnectionId, streamId, candidate) {
|
|
this.publish( OT.Raptor.Message.jsepCandidate(_rumor.id, toConnectionId, streamId, candidate) );
|
|
};
|
|
|
|
this.jsepOffer = function(toConnectionId, streamId, offerSDP) {
|
|
this.publish( OT.Raptor.Message.jsepOffer(_rumor.id, toConnectionId, streamId, offerSDP) );
|
|
};
|
|
|
|
this.jsepAnswer = function(toConnectionId, streamId, answerSDP) {
|
|
this.publish( OT.Raptor.Message.jsepAnswer(_rumor.id, toConnectionId, streamId, answerSDP) );
|
|
};
|
|
|
|
|
|
this.signal = function(options, completion) {
|
|
var signal = new OT.Signal(_sessionId, _rumor.id, options || {});
|
|
|
|
if (!signal.valid) {
|
|
if (completion && OT.$.isFunction(completion)) {
|
|
completion( new SignalError(signal.error.code, signal.error.reason, signal.toHash()) );
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
this.publish( signal.toRaptorMessage() );
|
|
if (completion && OT.$.isFunction(completion)) completion(null, signal.toHash());
|
|
};
|
|
|
|
OT.$.defineGetters(this, {
|
|
id: function() { return _rumor.id; },
|
|
capabilities: function() { return _capabilities; },
|
|
sessionId: function() { return _sessionId; }
|
|
});
|
|
|
|
_dispatcher = new (Dispatcher || OT.Raptor.Dispatcher)(this, function (error, sessionState) {
|
|
onConnectComplete.call(this, error, sessionState, "ConnectToSession:");
|
|
});
|
|
};
|
|
|
|
|
|
}(this));
|
|
(function(global) {
|
|
|
|
// Connect error codes and reasons that Raptor can return.
|
|
var connectErrorReasons = {
|
|
409: "This P2P session already has 2 participants.",
|
|
410: "The session already has four participants.",
|
|
1004: "The token passed is invalid."
|
|
};
|
|
|
|
|
|
// @todo hide these
|
|
OT.publishers = new OT.Collection('guid'); // Publishers are id'd by their guid
|
|
OT.subscribers = new OT.Collection('widgetId'); // Subscribers are id'd by their widgetId
|
|
OT.sessions = new OT.Collection();
|
|
|
|
function parseStream(dict, session) {
|
|
var stream = new OT.Stream( dict.streamId,
|
|
session.connections.get(dict.connection.connectionId),
|
|
dict.name,
|
|
dict.streamData,
|
|
dict.type,
|
|
dict.creationTime,
|
|
dict.hasAudio,
|
|
dict.hasVideo,
|
|
dict.orientation ? dict.orientation.videoOrientation : null,
|
|
dict.peerId,
|
|
dict.quality,
|
|
dict.orientation ? dict.orientation.width : null,
|
|
dict.orientation ? dict.orientation.height : null );
|
|
|
|
// This is a code smell, but it's necessary for now as the part of the code
|
|
// that re-parents streams to publishers is in a different object that cannot
|
|
// access the raw stream packet
|
|
if (dict.publisherId) {
|
|
stream.publisherId = dict.publisherId;
|
|
}
|
|
|
|
return stream;
|
|
}
|
|
|
|
function parseAndAddStreamToSession(dict, session) {
|
|
if (session.streams.has(dict.streamId)) return;
|
|
|
|
var stream = parseStream(dict, session);
|
|
session.streams.add( stream );
|
|
|
|
return stream;
|
|
}
|
|
|
|
|
|
OT.Raptor.Dispatcher = function (socket, connectionCompletion) {
|
|
this.socket = socket;
|
|
this.connectionCompletion = connectionCompletion;
|
|
};
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatch = function(addresses, encodedMessage) {
|
|
var message = OT.Raptor.deserializeMessage(encodedMessage);
|
|
|
|
if (!message.typeName) {
|
|
OT.error("OT.Raptor.dispatch: Invalid message type (" + message.type + ")");
|
|
return;
|
|
}
|
|
|
|
if (!message.actionName) {
|
|
OT.error("OT.Raptor.dispatch: Invalid action (" + message.action + ") for " + message.typeName);
|
|
OT.error(message);
|
|
return;
|
|
}
|
|
|
|
OT.debug("OT.Raptor.dispatch " + message.signature + ": " + encodedMessage);
|
|
|
|
switch(message.type) {
|
|
case OT.Raptor.Types.RPC_RESPONSE:
|
|
this.dispatchRPCResponse(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.CONNECTION:
|
|
this.dispatchConnection(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.CONNECTIONCOUNT:
|
|
this.dispatchConnectionCount(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.STREAM:
|
|
this.dispatchStream(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.SUBSCRIBER:
|
|
this.dispatchSubscriber(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.MODERATION:
|
|
this.dispatchModeration(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.JSEP:
|
|
this.dispatchJsep(message);
|
|
break;
|
|
|
|
case OT.Raptor.Types.SIGNAL:
|
|
this.dispatchSignal(message);
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
OT.warn("OT.Raptor.dispatch: Type " + message.typeName + " is not currently implemented");
|
|
}
|
|
};
|
|
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchRPCResponse = function (message) {
|
|
switch (message.action) {
|
|
case OT.Raptor.Actions.CONNECT:
|
|
if (message.payload.connectSuccess == false) {
|
|
var error = new OT.Error(OT.ExceptionCodes.CONNECT_REJECTED, connectErrorReasons[message.payload.reason] || "Failed to connect");
|
|
this.connectionCompletion.call(null, error);
|
|
}
|
|
else {
|
|
this.socket.publish(OT.Raptor.Message.getSessionState(this.socket.id, message.payload.sessionId, true));
|
|
}
|
|
break;
|
|
|
|
|
|
case OT.Raptor.Actions.STATE:
|
|
var state = message.payload.value,
|
|
session = OT.sessions.get(message.payload.key),
|
|
connection;
|
|
|
|
state.streams = [];
|
|
state.connections = [];
|
|
state.archives = [];
|
|
|
|
if (state.hasOwnProperty("CONNECTIONS")) {
|
|
for (var id in state.CONNECTIONS) {
|
|
var connection = OT.Connection.fromHash(state.CONNECTIONS[id]);
|
|
state.connections.push(connection);
|
|
session.connections.add(connection)
|
|
}
|
|
|
|
delete state.CONNECTIONS;
|
|
}
|
|
|
|
if (state.hasOwnProperty("STREAMS")) {
|
|
for (var id in state.STREAMS) {
|
|
state.streams.push( parseAndAddStreamToSession(state.STREAMS[id], session) );
|
|
}
|
|
|
|
delete state.STREAMS;
|
|
}
|
|
|
|
|
|
this.connectionCompletion.call(null, null, state);
|
|
break;
|
|
|
|
|
|
default:
|
|
OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
}
|
|
};
|
|
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchConnection = function (message) {
|
|
var session = OT.sessions.get(message.payload.value.sessionId),
|
|
connection;
|
|
|
|
if (!session) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine session for " + message.payload.value.sessionId + ' on ' + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
switch (message.action) {
|
|
case OT.Raptor.Actions.CREATE:
|
|
connection = OT.Connection.fromHash(message.payload.value);
|
|
if (session.connection && connection.id !== session.connection.id) session.connections.add( connection );
|
|
break;
|
|
|
|
|
|
case OT.Raptor.Actions.DELETE:
|
|
connection = session.connections.get(message.payload.value.connectionId);
|
|
connection.destroy(message.payload.reason);
|
|
break;
|
|
|
|
|
|
default:
|
|
OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
}
|
|
};
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchConnectionCount = function (message) {
|
|
// Note: we don't use any of this in the client right now.
|
|
|
|
// switch (message.action) {
|
|
// case OT.Raptor.Actions.UPDATE:
|
|
// connectionCount = message.payload.value;
|
|
// OT.debug("OT.Raptor.dispatch: Connection count: " + connectionCount);
|
|
// break;
|
|
|
|
|
|
// default:
|
|
// OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
// }
|
|
};
|
|
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchStream = function (message) {
|
|
var key = message.payload.key.split('/'),
|
|
sessionId = key[0],
|
|
session,
|
|
stream;
|
|
|
|
|
|
if (sessionId) session = OT.sessions.get(sessionId);
|
|
|
|
if (!session) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine sessionId, or the session does not exist, for " + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
switch (message.action) {
|
|
case OT.Raptor.Actions.REGISTER:
|
|
stream = parseStream(message.payload.value, session);
|
|
|
|
// @todo refactor this later
|
|
if (stream.publisherId) {
|
|
var publisher = OT.publishers.get(stream.publisherId);
|
|
|
|
if (publisher) {
|
|
publisher._.streamRegisteredHandler(stream);
|
|
}
|
|
else {
|
|
OT.warn("OT.Raptor.dispatch: Could find a publisher " + stream.publisherId + " for " + message.signature);
|
|
}
|
|
}
|
|
break;
|
|
|
|
|
|
case OT.Raptor.Actions.CREATE:
|
|
parseAndAddStreamToSession(message.payload.value, session)
|
|
break;
|
|
|
|
|
|
case OT.Raptor.Actions.UPDATE:
|
|
if (key[1]) stream = session.streams.get(key[1]);
|
|
|
|
if (!stream) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine streamId, or the stream does not exist, for " + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
stream.update(key[2], message.payload.value);
|
|
break;
|
|
|
|
|
|
case OT.Raptor.Actions.DELETE:
|
|
if (key[2]) stream = session.streams.get(key[2]);
|
|
|
|
if (!stream) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine streamId, or the stream does not exist, for " + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
stream.destroy(message.payload.reason);
|
|
break;
|
|
|
|
|
|
default:
|
|
OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
}
|
|
};
|
|
|
|
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchModeration = function (message) {
|
|
// Force disconnect and unpublish are handled in CONNECTION:DELETE and STREAM:DELETE respectively.
|
|
return;
|
|
|
|
// var session = OT.sessions.get(this.socket.sessionId);
|
|
|
|
// if (!session) {
|
|
// OT.error("OT.Raptor.dispatch: " + message.signature + " ERROR - sessionId must be provided and be valid");
|
|
// return;
|
|
// }
|
|
|
|
// switch (message.action) {
|
|
// case OT.Raptor.Actions.FORCE_DISCONNECT:
|
|
// if (!message.payload.connectionId) {
|
|
// OT.error("OT.Raptor.dispatch: " + message.signature + " ERROR - connectionId must be provided");
|
|
// return;
|
|
// }
|
|
|
|
// var connection = session.connections.get(message.payload.connectionId);
|
|
// connection.destroy("forceDisconnected");
|
|
// break;
|
|
|
|
|
|
// case OT.Raptor.Actions.FORCE_UNPUBLISH:
|
|
// if (!message.payload.streamId) {
|
|
// OT.error("OT.Raptor.dispatch " + message.signature + " ERROR - streamId must be provided");
|
|
// return;
|
|
// }
|
|
|
|
// var stream = session.streams.get(message.payload.streamId);
|
|
// stream.destroy("forceUnpublished");
|
|
// break;
|
|
|
|
|
|
// default:
|
|
// OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
// }
|
|
};
|
|
|
|
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchJsep = function (message) {
|
|
var fromConnection,
|
|
streamId = message.payload.streamId,
|
|
actors;
|
|
|
|
switch (message.action) {
|
|
// Messages for Subscribers
|
|
case OT.Raptor.Actions.OFFER:
|
|
actors = [];
|
|
var subscriber = OT.subscribers.find({streamId: streamId});
|
|
if (subscriber) actors.push(subscriber);
|
|
break;
|
|
|
|
|
|
// Messages for Publishers
|
|
case OT.Raptor.Actions.ANSWER:
|
|
case OT.Raptor.Actions.PRANSWER:
|
|
case OT.Raptor.Actions.SUBSCRIBE:
|
|
case OT.Raptor.Actions.UNSUBSCRIBE:
|
|
actors = OT.publishers.where({streamId: streamId});
|
|
break;
|
|
|
|
|
|
// Messages for Publishers and Subscribers
|
|
case OT.Raptor.Actions.CANDIDATE:
|
|
// send to whichever of your publisher or subscribers are subscribing/publishing that stream
|
|
actors = OT.publishers.where({streamId: streamId}).concat(OT.subscribers.where({streamId: streamId}));
|
|
break;
|
|
|
|
|
|
default:
|
|
OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
return;
|
|
}
|
|
|
|
if (actors.length) {
|
|
// This is a bit hacky. We don't have the session in the message so we iterate
|
|
// until we find the actor that the message relates to this stream, and then
|
|
// we grab the session from it.
|
|
fromConnection = actors[0].session.connections.get(message.payload.fromAddress);
|
|
if(!fromConnection && message.payload.fromAddress.match(/^symphony\./)) {
|
|
fromConnection = new OT.Connection(message.payload.fromAddress,
|
|
Date.now(), null, { supportsWebRTC: true });
|
|
actors[0].session.connections.add(fromConnection);
|
|
} else if(!fromConnection) {
|
|
OT.warn("Messsage comes from a connection (" + message.payload.fromAddress + ") that we do not know about.");
|
|
}
|
|
}
|
|
|
|
actors.forEach(function(actor) {
|
|
actor.processMessage(message.action, fromConnection, message.payload);
|
|
});
|
|
};
|
|
|
|
|
|
// The payload is:
|
|
// key: "{session_id}}/SUBSCRIBER/{stream_id}/{subscriber_id}"
|
|
// value: Integer { Value between 0 and some non-zero max ( Max not decided yet ) }*
|
|
//
|
|
// * The complete range need not be used but this provision is just so that future facing we might need to have more granularity.
|
|
//
|
|
OT.Raptor.Dispatcher.prototype.dispatchSubscriber = function (message) {
|
|
var key = message.payload.key.split('/'),
|
|
session = OT.sessions.get(key[0]),
|
|
subscriber,
|
|
stream;
|
|
|
|
if (!session) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine sessionId, or the session does not exist, for " + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
stream = key[2] ? session.streams.get(key[2]) : null;
|
|
|
|
if (!stream) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine streamId, or the stream does not exist, for " + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
// Find the subscriber that matches this stream, connection, and session
|
|
subscriber = OT.subscribers.find(function(subscriber) {
|
|
return subscriber.streamId === stream.id
|
|
&& subscriber.session.id === session.id
|
|
});
|
|
|
|
if (!subscriber) {
|
|
OT.error("OT.Raptor.dispatch: Unable to determine subscriberId, or the subscriber does not exist, for " + message.signature + " message!");
|
|
// @todo error
|
|
return;
|
|
}
|
|
|
|
|
|
switch (message.action) {
|
|
case OT.Raptor.Actions.QUALITY_CHANGED:
|
|
subscriber.updateQuality(parseInt(message.payload.value, 10));
|
|
break;
|
|
|
|
|
|
default:
|
|
OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
}
|
|
};
|
|
|
|
|
|
OT.Raptor.Dispatcher.prototype.dispatchSignal = function (message) {
|
|
if (message.action !== OT.Raptor.Actions.SIGNAL) {
|
|
OT.warn("OT.Raptor.dispatch: " + message.signature + " is not currently implemented");
|
|
return;
|
|
}
|
|
|
|
var session = OT.sessions.get(this.socket.sessionId);
|
|
|
|
if (!session) {
|
|
OT.error("OT.Raptor.dispatch: " + message.signature + " ERROR - sessionId must be provided and be valid");
|
|
return;
|
|
}
|
|
|
|
session._.dispatchSignal( session.connections.get(message.payload.fromAddress),
|
|
message.payload.type,
|
|
message.payload.data);
|
|
};
|
|
|
|
}(this));
|
|
(function(window) {
|
|
|
|
// Helper to synchronise several startup tasks and then dispatch a unified
|
|
// 'envLoaded' event.
|
|
//
|
|
// This depends on:
|
|
// * OT
|
|
// * OT.Config
|
|
//
|
|
function EnvironmentLoader() {
|
|
var _configReady = false,
|
|
_domReady = false,
|
|
|
|
isReady = function() {
|
|
return _domReady && _configReady;
|
|
},
|
|
|
|
onLoaded = function() {
|
|
if (isReady()) {
|
|
OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_LOADED));
|
|
}
|
|
},
|
|
|
|
onDomReady = function() {
|
|
_domReady = true;
|
|
|
|
// This is making an assumption about there being only one "window"
|
|
// that we care about.
|
|
OT.$.on(window, "unload", function() {
|
|
OT.publishers.destroy();
|
|
OT.subscribers.destroy();
|
|
OT.sessions.destroy();
|
|
});
|
|
|
|
// The Dynamic Config won't load until the DOM is ready
|
|
OT.Config.load(OT.properties.configURL);
|
|
|
|
onLoaded();
|
|
},
|
|
|
|
configLoaded = function() {
|
|
_configReady = true;
|
|
OT.Config.off('dynamicConfigChanged', configLoaded);
|
|
OT.Config.off('dynamicConfigLoadFailed', configLoadFailed);
|
|
|
|
onLoaded();
|
|
},
|
|
|
|
configLoadFailed = function() {
|
|
configLoaded();
|
|
};
|
|
|
|
OT.Config.on('dynamicConfigChanged', configLoaded);
|
|
OT.Config.on('dynamicConfigLoadFailed', configLoadFailed);
|
|
if (document.readyState == "complete" || (document.readyState == "interactive" && document.body)) {
|
|
onDomReady();
|
|
} else {
|
|
if (document.addEventListener) {
|
|
document.addEventListener("DOMContentLoaded", onDomReady, false);
|
|
} else if (document.attachEvent) {
|
|
// This is so onLoad works in IE, primarily so we can show the upgrade to Chrome popup
|
|
document.attachEvent("onreadystatechange", function() {
|
|
if (document.readyState == "complete") onDomReady();
|
|
});
|
|
}
|
|
}
|
|
|
|
this.onLoad = function(cb) {
|
|
if (isReady()) {
|
|
cb();
|
|
return;
|
|
}
|
|
|
|
OT.on(OT.Event.names.ENV_LOADED, cb);
|
|
};
|
|
}
|
|
|
|
var EnvLoader = new EnvironmentLoader();
|
|
|
|
OT.onLoad = function(cb, context) {
|
|
if (!context) {
|
|
EnvLoader.onLoad(cb);
|
|
}
|
|
else {
|
|
EnvLoader.onLoad(
|
|
cb.bind(context)
|
|
);
|
|
}
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.Error = function(code, message) {
|
|
this.code = code;
|
|
this.message = message;
|
|
};
|
|
|
|
var errorsCodesToTitle = {
|
|
1000: "Failed To Load",
|
|
1004: "Authentication error",
|
|
1005: "Invalid Session ID",
|
|
1006: "Connect Failed",
|
|
1007: "Connect Rejected",
|
|
1008: "Connect Time-out",
|
|
1009: "Security Error",
|
|
1010: "Not Connected",
|
|
1011: "Invalid Parameter",
|
|
1012: "Peer-to-peer Stream Play Failed",
|
|
1013: "Connection Failed",
|
|
1014: "API Response Failure",
|
|
1500: "Unable to Publish",
|
|
1510: "Unable to Signal",
|
|
1520: "Unable to Force Disconnect",
|
|
1530: "Unable to Force Unpublish",
|
|
1540: "Unable to record archive",
|
|
1550: "Unable to play back archive",
|
|
1560: "Unable to create archive",
|
|
1570: "Unable to load archive",
|
|
2000: "Internal Error",
|
|
2001: "Embed Failed",
|
|
3000: "Archive load exception",
|
|
3001: "Archive create exception",
|
|
3002: "Playback stop exception",
|
|
3003: "Playback start exception",
|
|
3004: "Record start exception",
|
|
3005: "Record stop exception",
|
|
3006: "Archive load exception",
|
|
3007: "Session recording in progress",
|
|
3008: "Archive recording internal failure",
|
|
4000: "WebSocket Connection Failed",
|
|
4001: "WebSocket Network Disconnected"
|
|
};
|
|
|
|
var analytics;
|
|
|
|
function _exceptionHandler(component, msg, errorCode, context) {
|
|
var title = errorsCodesToTitle[errorCode],
|
|
contextCopy = context ? OT.$.clone(context) : {};
|
|
|
|
OT.error("TB.exception :: title: " + title + " (" + errorCode + ") msg: " + msg);
|
|
|
|
if (!contextCopy.partnerId) contextCopy.partnerId = OT.APIKEY;
|
|
|
|
try {
|
|
if (!analytics) analytics = new OT.Analytics();
|
|
analytics.logError(errorCode, 'tb.exception', title, {details:msg}, contextCopy);
|
|
|
|
OT.dispatchEvent(
|
|
new OT.ExceptionEvent(OT.Event.names.EXCEPTION, msg, title, errorCode, component, component)
|
|
);
|
|
} catch(err) {
|
|
OT.error("TB.exception :: Failed to dispatch exception - " + err.toString());
|
|
// Don't throw an error because this is asynchronous
|
|
// don't do an exceptionHandler because that would be recursive
|
|
}
|
|
}
|
|
|
|
// @todo redo this when we have time to tidy up
|
|
//
|
|
// @example
|
|
//
|
|
// TB.handleJsException("Descriptive error message", 2000, {
|
|
// session: session,
|
|
// target: stream|publisher|subscriber|session|etc
|
|
// });
|
|
//
|
|
OT.handleJsException = function(errorMsg, code, options) {
|
|
options = options || {};
|
|
|
|
var context,
|
|
session = options.session;
|
|
|
|
if (session) {
|
|
context = {
|
|
sessionId: session.sessionId
|
|
};
|
|
|
|
if (session.connected) context.connectionId = session.connection.connectionId;
|
|
if (!options.target) options.target = session;
|
|
}
|
|
else if (options.sessionId) {
|
|
context = {
|
|
sessionId: options.sessionId
|
|
};
|
|
|
|
if (!options.target) options.target = null;
|
|
}
|
|
|
|
_exceptionHandler(options.target, errorMsg, code, context);
|
|
};
|
|
|
|
|
|
// @todo redo this when we have time to tidy up
|
|
//
|
|
// Public callback for exceptions from Flash.
|
|
//
|
|
// Called from Flash like:
|
|
// TB.exceptionHandler('publisher_1234,1234', "Descriptive Error Message", "Error Title", 2000, contextObj)
|
|
//
|
|
OT.exceptionHandler = function(componentId, msg, errorTitle, errorCode, context) {
|
|
var target;
|
|
|
|
if (componentId) {
|
|
target = OT.components[componentId];
|
|
|
|
if (!target) {
|
|
OT.warn("Could not find the component with component ID " + componentId);
|
|
}
|
|
}
|
|
|
|
_exceptionHandler(target, msg, errorCode, context);
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.ConnectionCapabilities = function(capabilitiesHash) {
|
|
// Private helper methods
|
|
var castCapabilities = function(capabilitiesHash) {
|
|
capabilitiesHash.supportsWebRTC = OT.$.castToBoolean(capabilitiesHash.supportsWebRTC);
|
|
|
|
return capabilitiesHash;
|
|
};
|
|
|
|
|
|
// Private data
|
|
var _caps = castCapabilities(capabilitiesHash);
|
|
|
|
|
|
this.supportsWebRTC = _caps.supportsWebRTC;
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
/**
|
|
* The Connection object represents a connection to an OpenTok session.
|
|
* The Session object has a <code>connection</code> property that is a Connection object.
|
|
* It represents the local client's connection. (A client only has a connection once the
|
|
* client has successfully called the <code>connect()</code> method of the {@link Session} object.)
|
|
* The Stream object has a <code>connection</code> property that is a Connection object.
|
|
* It represents the stream publisher's connection.
|
|
*
|
|
* @class Connection
|
|
* @property {String} id The ID of this connection.
|
|
* @property {Number} creationTime The timestamp for the creation of the connection. This value is calculated in milliseconds.
|
|
You can convert this value to a Date object by calling <code>new Date(creationTime)</code>, where <code>creationTime</code>
|
|
is the <code>creationTime</code> property of the Connection object.
|
|
* @property {String} data A string containing metadata describing the
|
|
connection. When you generate a user token string pass the connection data string to the
|
|
<code>generate_token()</code> method of our
|
|
<a href="/opentok/libraries/server/">server-side libraries</a>. You can also generate a token and define connection data on the
|
|
<a href="https://dashboard.tokbox.com/projects">Dashboard</a> page.
|
|
*/
|
|
OT.Connection = function(id, creationTime, data, capabilitiesHash) {
|
|
var destroyedReason;
|
|
|
|
this.id = this.connectionId = id;
|
|
this.creationTime = creationTime ? Number(creationTime) : null;
|
|
this.data = data;
|
|
this.capabilities = new OT.ConnectionCapabilities(capabilitiesHash);
|
|
this.quality = null;
|
|
|
|
OT.$.eventing(this);
|
|
|
|
this.destroy = function(reason, quiet) {
|
|
destroyedReason = reason || 'clientDisconnected';
|
|
|
|
if (quiet !== true) {
|
|
this.dispatchEvent(
|
|
new OT.DestroyedEvent(
|
|
'destroyed', // This should be OT.Event.names.CONNECTION_DESTROYED, but
|
|
// the value of that is currently shared with Session
|
|
this,
|
|
destroyedReason
|
|
)
|
|
);
|
|
}
|
|
}.bind(this);
|
|
|
|
Object.defineProperties(this, {
|
|
destroyed: {
|
|
get: function() { return destroyedReason !== void 0; },
|
|
enumerable: true
|
|
},
|
|
|
|
destroyedReason: {
|
|
get: function() { return destroyedReason; },
|
|
enumerable: true
|
|
}
|
|
});
|
|
};
|
|
|
|
OT.Connection.fromHash = function(hash) {
|
|
return new OT.Connection(hash.connectionId,
|
|
hash.creationTime,
|
|
hash.data,
|
|
{ supportsWebRTC: hash.supportsWebRTC } );
|
|
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
var validPropertyNames = ['hasAudio', 'hasVideo', 'quality', 'name', 'videoDimensions', 'orientation'];
|
|
|
|
|
|
/**
|
|
* Specifies a stream. A stream is a representation of a published stream in a session. When a client calls the
|
|
* <a href="Session.html#publish">Session.publish() method</a>, a new stream is created. Properties of the Stream
|
|
* object provide information about the stream.
|
|
*
|
|
* <p>When a stream is added to a session, the Session object dispatches a <code>streamCreatedEvent</code>.
|
|
* When a stream is destroyed, the Session object dispatches a <code>streamDestroyed</code> event. The
|
|
* StreamEvent object, which defines these event objects, has a <code>stream</code> property, which is an
|
|
* array of Stream object. For details and a code example, see {@link StreamEvent}.</p>
|
|
*
|
|
* <p>When a connection to a session is made, the Session object dispatches a <code>sessionConnected</code>
|
|
* event, defined by the SessionConnectEvent object. The SessionConnectEvent object has a <code>streams</code>
|
|
* property, which is an array of Stream objects pertaining to the streams in the session at that time.
|
|
* For details and a code example, see {@link SessionConnectEvent}.</p>
|
|
*
|
|
* @class Stream
|
|
* @property {Connection} connection The Connection object corresponding
|
|
* to the connection that is publishing the stream. You can compare this to to the <code>connection</code>
|
|
* property of the Session object to see if the stream is being published by the local web page.
|
|
*
|
|
* @property {Number} creationTime The timestamp for the creation
|
|
* of the stream. This value is calculated in milliseconds. You can convert this value to a
|
|
* Date object by calling <code>new Date(creationTime)</code>, where <code>creationTime</code> is the
|
|
* <code>creationTime</code> property of the Stream object.
|
|
*
|
|
* @property {Boolean} hasAudio Whether the stream has audio. This property can change if the publisher
|
|
* turns on or off audio (by calling <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a>).
|
|
* When this occurs, the {@link Session} object dispatches a <code>streamPropertyChanged</code> event
|
|
* (see {@link StreamPropertyChangedEvent}.)
|
|
*
|
|
* @property {Boolean} hasVideo Whether the stream has video. This property can change if the publisher
|
|
* turns on or off video (by calling <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a>).
|
|
* When this occurs, the {@link Session} object dispatches a <code>streamPropertyChanged</code> event
|
|
* (see {@link StreamPropertyChangedEvent}.)
|
|
*
|
|
* @property {Object} videoDimensions This object has two properties: <code>width</code> and <code>height</code>. Both are
|
|
* numbers. The <code>width</code> property is the width of the encoded stream; the <code>height</code> property is the
|
|
* height of the encoded stream. (These are independent of the actual width of Publisher and Subscriber objects corresponding
|
|
* to the stream.) This property can change if a stream published from an iOS device resizes, based on a change in the device
|
|
* orientation. When this occurs, the {@link Session} object dispatches a <code>streamPropertyChanged</code> event (see
|
|
* {@link StreamPropertyChangedEvent}.)
|
|
*
|
|
* @property {String} name The name of the stream. Publishers can specify a name when publishing a stream
|
|
* (using the <code>publish()</code> method of the publisher's Session object).
|
|
*/
|
|
OT.Stream = function(id, connection, name, data, type, creationTime, hasAudio, hasVideo, orientation, peerId, quality, width, height) {
|
|
var destroyedReason;
|
|
|
|
this.id = this.streamId = id;
|
|
this.connection = connection;
|
|
this.name = name;
|
|
this.data = data;
|
|
this.type = type || 'basic';
|
|
this.creationTime = creationTime ? Number(creationTime) : null;
|
|
this.hasAudio = OT.$.castToBoolean(hasAudio, true);
|
|
this.hasVideo = OT.$.castToBoolean(hasVideo, true);
|
|
this.peerId = peerId;
|
|
this.quality = quality;
|
|
this.videoDimensions = { width: (width || 640), height: (height || 480), orientation: (orientation || OT.VideoOrientation.ROTATED_NORMAL) };
|
|
|
|
OT.$.eventing(this);
|
|
|
|
// Confusingly, this should not be called when you want to change
|
|
// the stream properties. This is used by Raptor dispatch to notify
|
|
// the stream that it's properies have been successfully updated
|
|
//
|
|
// @todo make this sane. Perhaps use setters for the properties that can
|
|
// send the appropriate Raptor message. This would require that Streams
|
|
// have access to their session.
|
|
//
|
|
this.update = function(key, value) {
|
|
if (validPropertyNames.indexOf(key) === -1) {
|
|
OT.warn('Unknown stream property "' + key + '" was modified to "' + value + '".');
|
|
return;
|
|
}
|
|
|
|
var oldValue = this[key],
|
|
newValue = value;
|
|
|
|
switch(key) {
|
|
case 'hasAudio':
|
|
case 'hasVideo':
|
|
newValue = OT.$.castToBoolean(newValue, true);
|
|
this[key] = newValue;
|
|
break;
|
|
|
|
case 'quality':
|
|
case 'name':
|
|
this[key] = newValue;
|
|
break;
|
|
|
|
case 'orientation':
|
|
this.videoDimensions = { width: newValue.width, height: newValue.height, orientation: newValue.videoOrientation };
|
|
}
|
|
|
|
// We map the 'orientation' key (that comes from Raptor) to 'videoDimensions' when
|
|
// dispatching the updated event, as this is actual property that is modified on the stream
|
|
var event = new OT.StreamUpdatedEvent(this, key, oldValue, newValue);
|
|
this.dispatchEvent(event);
|
|
};
|
|
|
|
this.destroy = function(reason, quiet) {
|
|
destroyedReason = reason || 'clientDisconnected';
|
|
|
|
if (quiet !== true) {
|
|
this.dispatchEvent(
|
|
new OT.DestroyedEvent(
|
|
'destroyed', // This should be OT.Event.names.STREAM_DESTROYED, but
|
|
// the value of that is currently shared with Session
|
|
this,
|
|
destroyedReason
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
Object.defineProperties(this, {
|
|
destroyed: {
|
|
get: function() { return destroyedReason !== void 0; },
|
|
enumerable: true
|
|
},
|
|
|
|
destroyedReason: {
|
|
get: function() { return destroyedReason; },
|
|
enumerable: true
|
|
}
|
|
});
|
|
};
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// Normalise these
|
|
var NativeRTCSessionDescription = (window.mozRTCSessionDescription || window.RTCSessionDescription); // order is very important: "RTCSessionDescription" defined in Firefox Nighly but useless
|
|
var NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate);
|
|
|
|
// Helper function to forward Ice Candidates via +messageDelegate+
|
|
var iceCandidateForwarder = function(messageDelegate) {
|
|
return function(event) {
|
|
OT.debug("IceCandidateForwarder: Ice Candidate");
|
|
|
|
if (event.candidate) {
|
|
messageDelegate(OT.Raptor.Actions.CANDIDATE, event.candidate);
|
|
}
|
|
else {
|
|
OT.debug("IceCandidateForwarder: No more ICE candidates.");
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
// Process incoming Ice Candidates from a remote connection (which have been
|
|
// forwarded via iceCandidateForwarder). The Ice Candidates cannot be processed
|
|
// until a PeerConnection is available. Once a PeerConnection becomes available
|
|
// the pending PeerConnections can be processed by calling processPending.
|
|
//
|
|
// @example
|
|
//
|
|
// var iceProcessor = new IceCandidateProcessor();
|
|
// iceProcessor.process(iceMessage1);
|
|
// iceProcessor.process(iceMessage2);
|
|
// iceProcessor.process(iceMessage3);
|
|
//
|
|
// iceProcessor.peerConnection = peerConnection;
|
|
// iceProcessor.processPending();
|
|
//
|
|
var IceCandidateProcessor = function() {
|
|
var _pendingIceCandidates = [],
|
|
_peerConnection = null;
|
|
|
|
|
|
Object.defineProperty(this, 'peerConnection', {
|
|
set: function(peerConnection) {
|
|
_peerConnection = peerConnection;
|
|
}
|
|
});
|
|
|
|
this.process = function(message) {
|
|
var iceCandidate = new NativeRTCIceCandidate(message.candidate);
|
|
|
|
if (_peerConnection) {
|
|
_peerConnection.addIceCandidate(iceCandidate);
|
|
}
|
|
else {
|
|
_pendingIceCandidates.push(iceCandidate);
|
|
}
|
|
};
|
|
|
|
this.processPending = function() {
|
|
while(_pendingIceCandidates.length) {
|
|
_peerConnection.addIceCandidate(_pendingIceCandidates.shift());
|
|
}
|
|
};
|
|
};
|
|
|
|
// Removes all Confort Noise from +sdp+.
|
|
//
|
|
// See https://jira.tokbox.com/browse/OPENTOK-7176
|
|
//
|
|
var removeComfortNoise = function removeComfortNoise (sdp) {
|
|
// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
|
|
var matcher = /a=rtpmap:(\d+) CN\/\d+/i,
|
|
payloadTypes = [],
|
|
audioMediaLineIndex,
|
|
sdpLines,
|
|
match;
|
|
|
|
// Icky code. This filter operation has two side effects in addition
|
|
// to doing the actual filtering:
|
|
// 1. extract all the payload types from the rtpmap CN lines
|
|
// 2. find the index of the audio media line
|
|
//
|
|
sdpLines = sdp.split("\r\n").filter(function(line, index) {
|
|
if (line.indexOf('m=audio') !== -1) audioMediaLineIndex = index;
|
|
|
|
match = line.match(matcher);
|
|
if (match !== null) {
|
|
payloadTypes.push(match[1]);
|
|
|
|
// remove this line as it contains CN
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (payloadTypes.length && audioMediaLineIndex) {
|
|
// Remove all CN payload types from the audio media line.
|
|
sdpLines[audioMediaLineIndex] = sdpLines[audioMediaLineIndex].replace( new RegExp(payloadTypes.join('|'), 'ig') , '').replace(/\s+/g, ' ');
|
|
}
|
|
|
|
return sdpLines.join("\r\n");
|
|
};
|
|
|
|
// Attempt to completely process +offer+. This will:
|
|
// * set the offer as the remote description
|
|
// * create an answer and
|
|
// * set the new answer as the location description
|
|
//
|
|
// If there are no issues, the +success+ callback will be executed on completion.
|
|
// Errors during any step will result in the +failure+ callback being executed.
|
|
//
|
|
var offerProcessor = function(peerConnection, offer, success, failure) {
|
|
var generateErrorCallback = function(message) {
|
|
return function(errorReason) {
|
|
if (failure) failure(message, errorReason);
|
|
};
|
|
},
|
|
|
|
setLocalDescription = function(answer) {
|
|
answer.sdp = removeComfortNoise(answer.sdp);
|
|
|
|
peerConnection.setLocalDescription(
|
|
answer,
|
|
|
|
// Success
|
|
function() {
|
|
success(answer);
|
|
},
|
|
|
|
// Failure
|
|
generateErrorCallback('SetLocalDescription:Error while setting LocalDescription')
|
|
);
|
|
},
|
|
|
|
createAnswer = function(onSuccess) {
|
|
peerConnection.createAnswer(
|
|
// Success
|
|
setLocalDescription,
|
|
|
|
// Failure
|
|
generateErrorCallback('CreateAnswer:Error while setting createAnswer'),
|
|
|
|
null, // MediaConstraints
|
|
false // createProvisionalAnswer
|
|
);
|
|
};
|
|
|
|
// Workaround for a Chrome issue. Add in the SDES crypto line into offers
|
|
// from Firefox
|
|
if (offer.sdp.indexOf('a=crypto') === -1) {
|
|
var crypto_line = "a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:FakeFakeFakeFakeFakeFakeFakeFakeFakeFake\\r\\n";
|
|
|
|
// insert the fake crypto line for every M line
|
|
offer.sdp = offer.sdp.replace(/^c=IN(.*)$/gmi, "c=IN$1\r\n"+crypto_line);
|
|
}
|
|
|
|
if (offer.sdp.indexOf('a=rtcp-fb') === -1) {
|
|
var rtcp_fb_line = "a=rtcp-fb:* ccm fir\r\na=rtcp-fb:* nack ";
|
|
|
|
// insert the fake crypto line for every M line
|
|
offer.sdp = offer.sdp.replace(/^m=video(.*)$/gmi, "m=video$1\r\n"+rtcp_fb_line);
|
|
}
|
|
|
|
peerConnection.setRemoteDescription(
|
|
offer,
|
|
|
|
// Success
|
|
createAnswer,
|
|
|
|
// Failure
|
|
generateErrorCallback('SetRemoteDescription:Error while setting RemoteDescription')
|
|
);
|
|
|
|
};
|
|
|
|
// Attempt to completely process a subscribe message. This will:
|
|
// * create an Offer
|
|
// * set the new offer as the location description
|
|
//
|
|
// If there are no issues, the +success+ callback will be executed on completion.
|
|
// Errors during any step will result in the +failure+ callback being executed.
|
|
//
|
|
var suscribeProcessor = function(peerConnection, success, failure) {
|
|
var constraints = {
|
|
mandatory: {},
|
|
optional: []
|
|
},
|
|
|
|
generateErrorCallback = function(message) {
|
|
return function(errorReason) {
|
|
if (failure) failure(message, errorReason);
|
|
};
|
|
},
|
|
|
|
setLocalDescription = function(offer) {
|
|
offer.sdp = removeComfortNoise(offer.sdp);
|
|
|
|
peerConnection.setLocalDescription(
|
|
offer,
|
|
|
|
// Success
|
|
function() {
|
|
success(offer);
|
|
},
|
|
|
|
// Failure
|
|
generateErrorCallback('SetLocalDescription:Error while setting LocalDescription')
|
|
);
|
|
};
|
|
|
|
|
|
// For interop with FireFox. Disable Data Channel in createOffer.
|
|
if (navigator.mozGetUserMedia) {
|
|
constraints.mandatory.MozDontOfferDataChannel = true;
|
|
}
|
|
|
|
peerConnection.createOffer(
|
|
// Success
|
|
setLocalDescription,
|
|
|
|
// Failure
|
|
generateErrorCallback('CreateOffer:Error while creating Offer'),
|
|
|
|
constraints
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Negotiates a WebRTC PeerConnection.
|
|
*
|
|
* Responsible for:
|
|
* * offer-answer exchange
|
|
* * iceCandidates
|
|
* * notification of remote streams being added/removed
|
|
*
|
|
*/
|
|
OT.PeerConnection = function(config) {
|
|
var _peerConnection,
|
|
_iceProcessor = new IceCandidateProcessor(),
|
|
_offer,
|
|
_answer,
|
|
_state = 'new',
|
|
_iceCandidatesGathered = false,
|
|
_messageDelegates = [],
|
|
_gettingStats,
|
|
_createTime = OT.$.now();
|
|
|
|
OT.$.eventing(this);
|
|
|
|
// if ice servers doesn't exist Firefox will throw an exception. Chrome
|
|
// interprets this as "Use my default STUN servers" whereas FF reads it
|
|
// as "Don't use STUN at all". *Grumble*
|
|
if (!config.iceServers) config.iceServers = [];
|
|
|
|
// Private methods
|
|
var delegateMessage = function(type, messagePayload) {
|
|
if (_messageDelegates.length) {
|
|
// We actually only ever send to the first delegate. This is because
|
|
// each delegate actually represents a Publisher/Subscriber that
|
|
// shares a single PeerConnection. If we sent to all delegates it
|
|
// would result in each message being processed multiple times by
|
|
// each PeerConnection.
|
|
_messageDelegates[0](type, messagePayload);
|
|
}
|
|
}.bind(this),
|
|
|
|
|
|
setupPeerConnection = function() {
|
|
if (!_peerConnection) {
|
|
try {
|
|
OT.debug("Creating peer connection config \"" + JSON.stringify(config) + "\".");
|
|
|
|
|
|
_peerConnection = OT.$.createPeerConnection(config, {
|
|
optional: [
|
|
{DtlsSrtpKeyAgreement: true}
|
|
]
|
|
});
|
|
} catch(e) {
|
|
triggerError("NewPeerConnection: " + e.message);
|
|
return null;
|
|
}
|
|
|
|
_iceProcessor.peerConnection = _peerConnection;
|
|
|
|
_peerConnection.onicecandidate = iceCandidateForwarder(delegateMessage);
|
|
_peerConnection.onaddstream = onRemoteStreamAdded.bind(this);
|
|
_peerConnection.onremovestream = onRemoteStreamRemoved.bind(this);
|
|
|
|
if (_peerConnection.onsignalingstatechange !== undefined) {
|
|
_peerConnection.onsignalingstatechange = routeStateChanged.bind(this);
|
|
} else if (_peerConnection.onstatechange !== undefined) {
|
|
_peerConnection.onstatechange = routeStateChanged.bind(this);
|
|
}
|
|
}
|
|
|
|
return _peerConnection;
|
|
}.bind(this),
|
|
|
|
// Clean up the Peer Connection and trigger the close event.
|
|
// This function can be called safely multiple times, it will
|
|
// only trigger the close event once (per PeerConnection object)
|
|
tearDownPeerConnection = function() {
|
|
// Our connection is dead, stop processing ICE candidates
|
|
if (_iceProcessor) _iceProcessor.peerConnection = null;
|
|
|
|
if (_peerConnection !== null) {
|
|
_peerConnection = null;
|
|
this.trigger('close');
|
|
}
|
|
},
|
|
|
|
routeStateChanged = function(event) {
|
|
var newState;
|
|
|
|
if (typeof(event) === 'string') {
|
|
// The newest version of the API
|
|
newState = event;
|
|
}
|
|
else if (event.target && event.target.signalingState) {
|
|
// The slightly older version
|
|
newState = event.target.signalingState;
|
|
}
|
|
else {
|
|
// At least six months old version. Positively ancient, yeah?
|
|
newState = event.target.readyState;
|
|
}
|
|
|
|
|
|
OT.debug('PeerConnection.stateChange: ' + newState);
|
|
if (newState && newState.toLowerCase() !== _state) {
|
|
_state = newState.toLowerCase();
|
|
OT.debug('PeerConnection.stateChange: ' + _state);
|
|
|
|
switch(_state) {
|
|
case 'closed':
|
|
tearDownPeerConnection.call(this);
|
|
|
|
break;
|
|
case 'failed':
|
|
triggerError('ICEWorkflow: Ice state failed');
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
|
|
getLocalStreams = function() {
|
|
var streams;
|
|
|
|
if (_peerConnection.getLocalStreams) {
|
|
streams = _peerConnection.getLocalStreams();
|
|
}
|
|
else if (_peerConnection.localStreams) {
|
|
streams = _peerConnection.localStreams;
|
|
}
|
|
else {
|
|
throw new Error("Invalid Peer Connection object implements no method for retrieving local streams");
|
|
}
|
|
|
|
// Force streams to be an Array, rather than a "Sequence" object,
|
|
// which is browser dependent and does not behaviour like an Array
|
|
// in every case.
|
|
return Array.prototype.slice.call(streams);
|
|
},
|
|
|
|
getRemoteStreams = function() {
|
|
var streams;
|
|
|
|
if (_peerConnection.getRemoteStreams) {
|
|
streams = _peerConnection.getRemoteStreams();
|
|
}
|
|
else if (_peerConnection.remoteStreams) {
|
|
streams = _peerConnection.remoteStreams;
|
|
}
|
|
else {
|
|
throw new Error("Invalid Peer Connection object implements no method for retrieving remote streams");
|
|
}
|
|
|
|
// Force streams to be an Array, rather than a "Sequence" object,
|
|
// which is browser dependent and does not behaviour like an Array
|
|
// in every case.
|
|
return Array.prototype.slice.call(streams);
|
|
},
|
|
|
|
generateErrorCallback = function(forMethod, message) {
|
|
return function(errorReason) {
|
|
triggerError.call(this, "PeerConnection." + forMethod + ": " + message + ": " + errorReason);
|
|
}.bind(this);
|
|
},
|
|
|
|
/// PeerConnection signaling
|
|
onRemoteStreamAdded = function(event) {
|
|
this.trigger('streamAdded', event.stream);
|
|
},
|
|
|
|
onRemoteStreamRemoved = function(event) {
|
|
this.trigger('streamRemoved', event.stream);
|
|
},
|
|
|
|
// ICE Negotiation messages
|
|
|
|
|
|
// Relays a SDP payload (+sdp+), that is part of a message of type +messageType+
|
|
// via the registered message delegators
|
|
relaySDP = function(messageType, sdp) {
|
|
delegateMessage(messageType, sdp);
|
|
},
|
|
|
|
// Process an offer that
|
|
processOffer = function(message) {
|
|
var offer = new NativeRTCSessionDescription(message.sdp),
|
|
|
|
// Relays +answer+ Answer
|
|
relayAnswer = function(answer) {
|
|
relaySDP(OT.Raptor.Actions.ANSWER, answer);
|
|
},
|
|
|
|
reportError = function(message, errorReason) {
|
|
triggerError(message + ":" + errorReason + ":PeerConnection.offerProcessor");
|
|
};
|
|
|
|
setupPeerConnection();
|
|
|
|
_remoteDescriptionType = offer.type;
|
|
_remoteDescription = offer;
|
|
|
|
offerProcessor(
|
|
_peerConnection,
|
|
offer,
|
|
relayAnswer,
|
|
reportError
|
|
);
|
|
},
|
|
|
|
processAnswer = function(message) {
|
|
if (!message.sdp) {
|
|
OT.error("PeerConnection.processMessage: Weird message, no SDP.");
|
|
return;
|
|
}
|
|
|
|
_answer = new NativeRTCSessionDescription(message.sdp);
|
|
|
|
_remoteDescriptionType = _answer.type;
|
|
_remoteDescription = _answer;
|
|
|
|
_peerConnection.setRemoteDescription(_answer, function () {
|
|
OT.debug("setRemoteDescription succeeded");
|
|
}, function (errReason) {
|
|
triggerError("SetRemoteDescription:Error while setting RemoteDescription: " + errReason);
|
|
});
|
|
_iceProcessor.processPending();
|
|
},
|
|
|
|
processSubscribe = function(message) {
|
|
OT.debug("PeerConnection.processSubscribe: Sending offer to subscriber.");
|
|
|
|
setupPeerConnection();
|
|
|
|
suscribeProcessor(
|
|
_peerConnection,
|
|
|
|
// Success: Relay Offer
|
|
function(offer) {
|
|
_offer = offer;
|
|
relaySDP(OT.Raptor.Actions.OFFER, _offer);
|
|
},
|
|
|
|
// Failure
|
|
function(message, errorReason) {
|
|
triggerError(message + ":" + errorReason + ": PeerConnection.suscribeProcessor");
|
|
}
|
|
);
|
|
},
|
|
|
|
triggerError = function(errorReason) {
|
|
OT.error(errorReason);
|
|
this.trigger('error', errorReason);
|
|
}.bind(this);
|
|
|
|
this.addLocalStream = function(webRTCStream) {
|
|
setupPeerConnection();
|
|
_peerConnection.addStream(webRTCStream);
|
|
};
|
|
|
|
this.disconnect = function() {
|
|
_iceProcessor = null;
|
|
|
|
if (_peerConnection) {
|
|
var currentState = (_peerConnection.signalingState || _peerConnection.readyState);
|
|
if (currentState && currentState.toLowerCase() !== 'closed') _peerConnection.close();
|
|
|
|
// In theory, calling close on the PeerConnection should trigger a statechange
|
|
// event with 'close'. For some reason I'm not seeing this in FF, hence we're
|
|
// calling it manually below
|
|
tearDownPeerConnection.call(this);
|
|
}
|
|
|
|
this.off();
|
|
};
|
|
|
|
this.processMessage = function(type, message) {
|
|
OT.debug("PeerConnection.processMessage: Received " + type + " from " + message.fromAddress);
|
|
OT.debug(message);
|
|
|
|
switch(type) {
|
|
case OT.Raptor.Actions.SUBSCRIBE:
|
|
processSubscribe.call(this, message);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.OFFER:
|
|
processOffer.call(this, message);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.ANSWER:
|
|
// case OT.Raptor.Actions.PRANSWER:
|
|
processAnswer.call(this, message);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.CANDIDATE:
|
|
_iceProcessor.process(message);
|
|
break;
|
|
|
|
default:
|
|
OT.debug("PeerConnection.processMessage: Received an unexpected message of type " + type + " from " + message.fromAddress + ": " + JSON.stringify(message));
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
this.registerMessageDelegate = function(delegateFn) {
|
|
return _messageDelegates.push(delegateFn);
|
|
};
|
|
|
|
this.unregisterMessageDelegate = function(delegateFn) {
|
|
var index = _messageDelegates.indexOf(delegateFn);
|
|
|
|
if ( index !== -1 ) {
|
|
_messageDelegates.splice(index, 1);
|
|
}
|
|
return _messageDelegates.length;
|
|
};
|
|
|
|
/**
|
|
* Retrieves the PeerConnection stats.
|
|
*
|
|
* TODO document what the format of the final reports that +callback+ gets is
|
|
*
|
|
* @ignore
|
|
* @private
|
|
* @memberof PeerConnection
|
|
* @param callback {Function} this will be triggered once the stats a are ready.
|
|
* It takes a single argument which is the stats report, which may be undefined
|
|
* if there is presently no stats.
|
|
*/
|
|
this.getStats = function(prevStats, callback) {
|
|
// need to make sure that this isn't called again when in the middle of processing
|
|
if (_gettingStats == true) {
|
|
OT.warn("PeerConnection.getStats: Already getting the stats!");
|
|
return;
|
|
}
|
|
|
|
// locking this function
|
|
_gettingStats = true;
|
|
|
|
// get the previous timestamp (seconds)
|
|
var now = OT.$.now();
|
|
var time_difference = (now - prevStats["timeStamp"]) / 1000; // how many seconds has passed
|
|
|
|
// now update the date
|
|
prevStats["timeStamp"] = now;
|
|
|
|
/* this parses a result if there it contains the video bitrate */
|
|
var parseAvgVideoBitrate = function(result) {
|
|
var last_bytesSent = prevStats["videoBytesTransferred"] || 0;
|
|
|
|
if (result.stat("googFrameHeightSent")) {
|
|
prevStats["videoBytesTransferred"] = result.stat("bytesSent");
|
|
return Math.round((prevStats["videoBytesTransferred"] - last_bytesSent) * 8 / time_difference);
|
|
} else if (result.stat("googFrameHeightReceived")) {
|
|
prevStats["videoBytesTransferred"] = result.stat("bytesReceived");
|
|
return Math.round((prevStats["videoBytesTransferred"] - last_bytesSent) * 8 / time_difference);
|
|
} else {
|
|
return NaN;
|
|
}
|
|
};
|
|
|
|
/* this parses a result if there it contains the audio bitrate */
|
|
var parseAvgAudioBitrate = function(result) {
|
|
var last_bytesSent = prevStats["audioBytesTransferred"] || 0;
|
|
|
|
if (result.stat("audioInputLevel")) {
|
|
prevStats["audioBytesTransferred"] = result.stat("bytesSent");
|
|
return Math.round((prevStats["audioBytesTransferred"] - last_bytesSent) * 8 / time_difference);
|
|
} else if (result.stat("audioOutputLevel")) {
|
|
prevStats["audioBytesTransferred"] = result.stat("bytesReceived");
|
|
return Math.round((prevStats["audioBytesTransferred"] - last_bytesSent) * 8 / time_difference);
|
|
} else {
|
|
return NaN;
|
|
}
|
|
};
|
|
|
|
var parsed_stats = {};
|
|
var parseStatsReports = function(stats) {
|
|
if (stats.result) {
|
|
var result_list = stats.result();
|
|
for (var result_index = 0; result_index < result_list.length; result_index++) {
|
|
var report = {};
|
|
var result = result_list[result_index];
|
|
if (result.stat) {
|
|
|
|
if(result.stat("googActiveConnection") === 'true') {
|
|
parsed_stats.localCandidateType = result.stat('googLocalCandidateType');
|
|
parsed_stats.remoteCandidateType = result.stat('googRemoteCandidateType');
|
|
parsed_stats.transportType = result.stat('googTransportType');
|
|
}
|
|
|
|
var avgVideoBitrate = parseAvgVideoBitrate(result);
|
|
if (!isNaN(avgVideoBitrate)) {
|
|
parsed_stats["avgVideoBitrate"] = avgVideoBitrate;
|
|
}
|
|
|
|
var avgAudioBitrate = parseAvgAudioBitrate(result);
|
|
if (!isNaN(avgAudioBitrate)) {
|
|
parsed_stats["avgAudioBitrate"] = avgAudioBitrate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_gettingStats = false;
|
|
callback(parsed_stats);
|
|
}
|
|
parsed_stats["duration"] = Math.round(now - _createTime);
|
|
|
|
var parseStats = function(stats) {
|
|
for (var key in stats) {
|
|
if (stats.hasOwnProperty(key) &&
|
|
(stats[key].type === 'outboundrtp' || stats[key].type === 'inboundrtp')) {
|
|
var res = stats[key];
|
|
// Find the bandwidth info for video
|
|
if (res.id.indexOf('video') !== -1) {
|
|
var avgVideoBitrate = parseAvgVideoBitrate(res);
|
|
if(!isNaN(avgVideoBitrate)) {
|
|
parsed_stats.avgVideoBitrate = avgVideoBitrate;
|
|
}
|
|
|
|
} else if (res.id.indexOf('audio') !== -1) {
|
|
var avgAudioBitrate = parseAvgAudioBitrate(res);
|
|
if(!isNaN(avgAudioBitrate)) {
|
|
parsed_stats.avgAudioBitrate = avgAudioBitrate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_gettingStats = false;
|
|
callback(parsed_stats);
|
|
}
|
|
|
|
|
|
var needsNewGetStats = function() {
|
|
var firefoxVersion = window.navigator.userAgent.toLowerCase()
|
|
.match(/Firefox\/([0-9\.]+)/i);
|
|
var needs = (firefoxVersion !== null && parseFloat(firefoxVersion[1], 10) >= 27.0);
|
|
needsNewGetStats = function() { return needs; };
|
|
return needs;
|
|
};
|
|
|
|
if (_peerConnection && _peerConnection.getStats) {
|
|
if(needsNewGetStats()) {
|
|
_peerConnection.getStats(null, parseStats, function(err) {
|
|
OT.warn('Error collecting stats', err);
|
|
_gettingStats = false;
|
|
});
|
|
} else {
|
|
_peerConnection.getStats(parseStatsReports);
|
|
}
|
|
} else {
|
|
// there was no peer connection yet or getStats isn't implemented in this enviroment
|
|
_gettingStats = false;
|
|
callback(parsed_stats);
|
|
}
|
|
};
|
|
|
|
Object.defineProperty(this, 'remoteStreams', {
|
|
get: function() {
|
|
return _peerConnection ? getRemoteStreams() : [];
|
|
}
|
|
});
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
var _peerConnections = {};
|
|
|
|
OT.PeerConnections = {
|
|
add: function(remoteConnection, stream, config) {
|
|
var key = remoteConnection.id + "_" + stream.id,
|
|
ref = _peerConnections[key];
|
|
|
|
if (!ref) {
|
|
ref = _peerConnections[key] = {
|
|
count: 0,
|
|
pc: new OT.PeerConnection(config)
|
|
};
|
|
}
|
|
|
|
// increase the PCs ref count by 1
|
|
ref.count += 1;
|
|
|
|
return ref.pc;
|
|
},
|
|
|
|
remove: function(remoteConnection, stream) {
|
|
var key = remoteConnection.id + "_" + stream.id,
|
|
ref = _peerConnections[key];
|
|
|
|
if (ref) {
|
|
ref.count -= 1;
|
|
|
|
if (ref.count === 0) {
|
|
ref.pc.disconnect();
|
|
delete _peerConnections[key];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
/**
|
|
* Abstracts PeerConnection related stuff away from OT.Publisher.
|
|
*
|
|
* Responsible for:
|
|
* * setting up the underlying PeerConnection (delegates to OT.PeerConnections)
|
|
* * triggering a connected event when the Peer connection is opened
|
|
* * triggering a disconnected event when the Peer connection is closed
|
|
* * providing a destroy method
|
|
* * providing a processMessage method
|
|
*
|
|
* Once the PeerConnection is connected and the video element playing it triggers the connected event
|
|
*
|
|
* Triggers the following events
|
|
* * connected
|
|
* * disconnected
|
|
*/
|
|
OT.PublisherPeerConnection = function(remoteConnection, session, stream, webRTCStream) {
|
|
var _peerConnection,
|
|
_hasRelayCandidates = false;
|
|
|
|
// Private
|
|
var _onPeerClosed = function() {
|
|
this.destroy();
|
|
this.trigger('disconnected', this);
|
|
},
|
|
|
|
// Note: All Peer errors are fatal right now.
|
|
_onPeerError = function(errorReason) {
|
|
this.trigger('error', null, errorReason, this);
|
|
this.destroy();
|
|
},
|
|
|
|
_relayMessageToPeer = function(type, payload) {
|
|
if (!_hasRelayCandidates){
|
|
var extractCandidates = type === OT.Raptor.Actions.CANDIDATE ||
|
|
type === OT.Raptor.Actions.OFFER ||
|
|
type === OT.Raptor.Actions.ANSWER ||
|
|
type === OT.Raptor.Actions.PRANSWER ;
|
|
|
|
if (extractCandidates) {
|
|
var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp;
|
|
_hasRelayCandidates = message.indexOf('typ relay') !== -1;
|
|
}
|
|
}
|
|
|
|
switch(type) {
|
|
case OT.Raptor.Actions.ANSWER:
|
|
case OT.Raptor.Actions.PRANSWER:
|
|
session._.jsepAnswer(remoteConnection.id, stream, payload);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.OFFER:
|
|
this.trigger('connected');
|
|
session._.jsepOffer(remoteConnection.id, stream, payload);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.CANDIDATE:
|
|
session._.jsepCandidate(remoteConnection.id, stream, payload);
|
|
}
|
|
}.bind(this);
|
|
|
|
|
|
OT.$.eventing(this);
|
|
|
|
// Public
|
|
this.destroy = function() {
|
|
// Clean up our PeerConnection
|
|
if (_peerConnection) {
|
|
OT.PeerConnections.remove(remoteConnection, stream);
|
|
}
|
|
|
|
_peerConnection.off();
|
|
_peerConnection = null;
|
|
};
|
|
|
|
this.processMessage = function(type, message) {
|
|
_peerConnection.processMessage(type, message);
|
|
};
|
|
|
|
this.getStats = function(prevStats, callback) {
|
|
_peerConnection.getStats(prevStats, callback);
|
|
}
|
|
|
|
// Init
|
|
this.init = function() {
|
|
var iceServers = session.sessionInfo.iceServers.map(function(is) {
|
|
var iceServer = OT.$.clone(is);
|
|
|
|
if (iceServer.url.trim().substr(0, 5) === 'turn:') {
|
|
// Make the username sessionId:connectionId:streamId for tracking purposes.
|
|
iceServer.username = session.id + '.' + session.connection.id + '.' + stream.id;
|
|
}
|
|
|
|
return iceServer;
|
|
});
|
|
|
|
_peerConnection = OT.PeerConnections.add(remoteConnection, stream, {
|
|
iceServers: iceServers
|
|
});
|
|
|
|
_peerConnection.on({
|
|
close: _onPeerClosed,
|
|
error: _onPeerError
|
|
}, this);
|
|
|
|
_peerConnection.registerMessageDelegate(_relayMessageToPeer);
|
|
_peerConnection.addLocalStream(webRTCStream);
|
|
|
|
Object.defineProperty(this, 'remoteConnection', {value: remoteConnection});
|
|
|
|
Object.defineProperty(this, 'hasRelayCandidates', {
|
|
get: function() { return _hasRelayCandidates; }
|
|
});
|
|
}
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
/**
|
|
* Abstracts PeerConnection related stuff away from OT.Subscriber.
|
|
*
|
|
* Responsible for:
|
|
* * setting up the underlying PeerConnection (delegates to OT.PeerConnections)
|
|
* * triggering a connected event when the Peer connection is opened
|
|
* * triggering a disconnected event when the Peer connection is closed
|
|
* * creating a video element when a stream is added
|
|
* * responding to stream removed intelligently
|
|
* * providing a destroy method
|
|
* * providing a processMessage method
|
|
*
|
|
* Once the PeerConnection is connected and the video element playing it triggers the connected event
|
|
*
|
|
* Triggers the following events
|
|
* * connected
|
|
* * disconnected
|
|
* * remoteStreamAdded
|
|
* * remoteStreamRemoved
|
|
* * error
|
|
*
|
|
*/
|
|
|
|
OT.SubscriberPeerConnection = function(remoteConnection, session, stream, properties) {
|
|
var _peerConnection,
|
|
_hasRelayCandidates = false;
|
|
|
|
// Private
|
|
var _onPeerClosed = function() {
|
|
this.destroy();
|
|
this.trigger('disconnected', this);
|
|
},
|
|
|
|
_onRemoteStreamAdded = function(remoteRTCStream) {
|
|
this.trigger('remoteStreamAdded', remoteRTCStream, this);
|
|
},
|
|
|
|
_onRemoteStreamRemoved = function(remoteRTCStream) {
|
|
this.trigger('remoteStreamRemoved', remoteRTCStream, this);
|
|
},
|
|
|
|
// Note: All Peer errors are fatal right now.
|
|
_onPeerError = function(errorReason) {
|
|
this.trigger('error', null, errorReason, this);
|
|
},
|
|
|
|
_relayMessageToPeer = function(type, payload) {
|
|
if (!_hasRelayCandidates){
|
|
var extractCandidates = type === OT.Raptor.Actions.CANDIDATE ||
|
|
type === OT.Raptor.Actions.OFFER ||
|
|
type === OT.Raptor.Actions.ANSWER ||
|
|
type === OT.Raptor.Actions.PRANSWER ;
|
|
|
|
if (extractCandidates) {
|
|
var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp;
|
|
_hasRelayCandidates = message.indexOf('typ relay') !== -1;
|
|
}
|
|
}
|
|
|
|
switch(type) {
|
|
case OT.Raptor.Actions.ANSWER:
|
|
case OT.Raptor.Actions.PRANSWER:
|
|
this.trigger('connected');
|
|
session._.jsepAnswer(remoteConnection.id, stream, payload);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.OFFER:
|
|
session._.jsepOffer(remoteConnection.id, stream, payload);
|
|
break;
|
|
|
|
case OT.Raptor.Actions.CANDIDATE:
|
|
session._.jsepCandidate(remoteConnection.id, stream, payload);
|
|
}
|
|
}.bind(this),
|
|
|
|
// Helper method used by subscribeToAudio/subscribeToVideo
|
|
_setEnabledOnStreamTracksCurry = function(isVideo) {
|
|
var method = 'get' + (isVideo ? 'Video' : 'Audio') + 'Tracks';
|
|
|
|
return function(enabled) {
|
|
var remoteStreams = _peerConnection.remoteStreams,
|
|
tracks,
|
|
stream;
|
|
|
|
if (remoteStreams.length === 0 || !remoteStreams[0][method]) {
|
|
// either there is no remote stream or we are in a browser that doesn't
|
|
// expose the media tracks (Firefox)
|
|
return;
|
|
}
|
|
|
|
for (var i=0, num=remoteStreams.length; i<num; ++i) {
|
|
stream = remoteStreams[i];
|
|
tracks = stream[method]();
|
|
|
|
for (var k=0, numTracks=tracks.length; k < numTracks; ++k){
|
|
tracks[k].enabled=enabled;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
|
|
OT.$.eventing(this);
|
|
|
|
// Public
|
|
this.destroy = function() {
|
|
if (_peerConnection) {
|
|
var numDelegates = _peerConnection.unregisterMessageDelegate(_relayMessageToPeer);
|
|
|
|
// Only clean up the PeerConnection if there isn't another Subscriber using it
|
|
if (numDelegates === 0) {
|
|
// Unsubscribe us from the stream, if it hasn't already been destroyed
|
|
if (session && session.connected && stream && !stream.destroyed) {
|
|
// Notify the server components
|
|
session._.jsepUnsubscribe(stream);
|
|
}
|
|
|
|
// Ref: OPENTOK-2458 disable all audio tracks before removing it.
|
|
this.subscribeToAudio(false);
|
|
}
|
|
OT.PeerConnections.remove(remoteConnection, stream.streamId);
|
|
}
|
|
_peerConnection = null;
|
|
this.off();
|
|
};
|
|
|
|
this.processMessage = function(type, message) {
|
|
_peerConnection.processMessage(type, message);
|
|
};
|
|
|
|
this.getStats = function(prevStats, callback) {
|
|
_peerConnection.getStats(prevStats, callback);
|
|
};
|
|
|
|
this.subscribeToAudio = _setEnabledOnStreamTracksCurry(false);
|
|
this.subscribeToVideo = _setEnabledOnStreamTracksCurry(true);
|
|
|
|
Object.defineProperty(this, 'hasRelayCandidates', {
|
|
get: function() { return _hasRelayCandidates; }
|
|
});
|
|
|
|
|
|
// Init
|
|
this.init = function() {
|
|
var iceServers = session.sessionInfo.iceServers.map(function(is) {
|
|
var iceServer = OT.$.clone(is);
|
|
|
|
if (iceServer.url.trim().substr(0, 5) === 'turn:') {
|
|
// Make the username sessionId:connectionId:streamId for tracking purposes.
|
|
iceServer.username = session.id + '.' + session.connection.id + '.' + stream.id;
|
|
}
|
|
|
|
return iceServer;
|
|
});
|
|
|
|
_peerConnection = OT.PeerConnections.add(remoteConnection, stream.streamId, {
|
|
iceServers: iceServers
|
|
});
|
|
|
|
_peerConnection.on({
|
|
close: _onPeerClosed,
|
|
streamAdded: _onRemoteStreamAdded,
|
|
streamRemoved: _onRemoteStreamRemoved,
|
|
error: _onPeerError
|
|
}, this);
|
|
|
|
var numDelegates = _peerConnection.registerMessageDelegate(_relayMessageToPeer);
|
|
|
|
// If there are already remoteStreams, add them immediately
|
|
if (_peerConnection.remoteStreams.length > 0) {
|
|
_peerConnection.remoteStreams.forEach(_onRemoteStreamAdded, this);
|
|
}
|
|
else if (numDelegates === 1) {
|
|
// We only bother with the PeerConnection negotiation if we are the only delegate.
|
|
session._.jsepSubscribe(stream, properties.subscribeToVideo, properties.subscribeToAudio);
|
|
}
|
|
};
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// Manages N Chrome elements
|
|
OT.Chrome = function(properties) {
|
|
var _visible = false,
|
|
_widgets = {},
|
|
|
|
// Private helper function
|
|
_set = function(name, widget) {
|
|
widget.parent = this;
|
|
widget.appendTo(properties.parent);
|
|
|
|
_widgets[name] = widget;
|
|
|
|
Object.defineProperty(this, name, {
|
|
get: function() { return _widgets[name]; }
|
|
});
|
|
};
|
|
|
|
if (!properties.parent) {
|
|
// @todo raise an exception
|
|
return;
|
|
}
|
|
|
|
OT.$.eventing(this);
|
|
|
|
this.destroy = function() {
|
|
this.off();
|
|
this.hide();
|
|
|
|
for (var name in _widgets) {
|
|
_widgets[name].destroy();
|
|
}
|
|
};
|
|
|
|
this.show = function() {
|
|
_visible = true;
|
|
|
|
for (var name in _widgets) {
|
|
_widgets[name].show();
|
|
}
|
|
};
|
|
|
|
this.hide = function() {
|
|
_visible = false;
|
|
|
|
for (var name in _widgets) {
|
|
_widgets[name].hide();
|
|
}
|
|
};
|
|
|
|
|
|
// Adds the widget to the chrome and to the DOM. Also creates a accessor
|
|
// property for it on the chrome.
|
|
//
|
|
// @example
|
|
// chrome.set('foo', new FooWidget());
|
|
// chrome.foo.setDisplayMode('on');
|
|
//
|
|
// @example
|
|
// chrome.set({
|
|
// foo: new FooWidget(),
|
|
// bar: new BarWidget()
|
|
// });
|
|
// chrome.foo.setDisplayMode('on');
|
|
//
|
|
this.set = function(widgetName, widget) {
|
|
if (typeof(widgetName) === "string" && widget) {
|
|
_set.call(this, widgetName, widget);
|
|
}
|
|
else {
|
|
for (var name in widgetName) {
|
|
if (widgetName.hasOwnProperty(name)) {
|
|
_set.call(this, name, widgetName[name]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
if (!OT.Chrome.Behaviour) OT.Chrome.Behaviour = {};
|
|
|
|
// A mixin to encapsulate the basic widget behaviour. This needs a better name,
|
|
// it's not actually a widget. It's actually "Behaviour that can be applied to
|
|
// an object to make it support the basic Chrome widget workflow"...but that would
|
|
// probably been too long a name.
|
|
OT.Chrome.Behaviour.Widget = function(widget, options) {
|
|
var _options = options || {},
|
|
_mode,
|
|
_previousMode;
|
|
|
|
//
|
|
// @param [String] mode
|
|
// 'on', 'off', or 'auto'
|
|
//
|
|
widget.setDisplayMode = function(mode) {
|
|
var newMode = mode || 'auto';
|
|
if (_mode === newMode) return;
|
|
|
|
OT.$.removeClass(this.domElement, 'OT_mode-' + _mode);
|
|
OT.$.addClass(this.domElement, 'OT_mode-' + newMode);
|
|
|
|
_previousMode = _mode;
|
|
_mode = newMode;
|
|
};
|
|
|
|
widget.show = function() {
|
|
this.setDisplayMode(_previousMode);
|
|
if (_options.onShow) _options.onShow();
|
|
|
|
return this;
|
|
};
|
|
|
|
widget.hide = function() {
|
|
this.setDisplayMode('off');
|
|
if (_options.onHide) _options.onHide();
|
|
|
|
return this;
|
|
};
|
|
|
|
widget.destroy = function() {
|
|
if (_options.onDestroy) _options.onDestroy(this.domElement);
|
|
if (this.domElement) OT.$.removeElement(this.domElement);
|
|
|
|
return widget;
|
|
};
|
|
|
|
widget.appendTo = function(parent) {
|
|
// create the element under parent
|
|
this.domElement = OT.$.createElement(_options.nodeName || 'div',
|
|
_options.htmlAttributes,
|
|
_options.htmlContent);
|
|
|
|
if (_options.onCreate) _options.onCreate(this.domElement);
|
|
|
|
// if the mode isn't auto, then we can directly set it
|
|
if (_options.mode != "auto") {
|
|
widget.setDisplayMode(_options.mode);
|
|
} else {
|
|
// we set it to on at first, and then apply the desired mode
|
|
// this will let the proper widgets nicely fade away
|
|
widget.setDisplayMode('on');
|
|
setTimeout(function() {
|
|
widget.setDisplayMode(_options.mode);
|
|
}, 2000);
|
|
}
|
|
|
|
// add the widget to the parent
|
|
parent.appendChild(this.domElement);
|
|
|
|
return widget;
|
|
};
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// NamePanel Chrome Widget
|
|
//
|
|
// mode (String)
|
|
// Whether to display the name. Possible values are: "auto" (the name is displayed
|
|
// when the stream is first displayed and when the user mouses over the display),
|
|
// "off" (the name is not displayed), and "on" (the name is displayed).
|
|
//
|
|
// displays a name
|
|
// can be shown/hidden
|
|
// can be destroyed
|
|
OT.Chrome.NamePanel = function(options) {
|
|
var _name = options.name;
|
|
|
|
if (!_name || _name.trim().length === '') {
|
|
_name = null;
|
|
|
|
// THere's no name, just flip the mode off
|
|
options.mode = 'off';
|
|
}
|
|
|
|
// This behaviour must be implemented to make the widget behaviour work.
|
|
// @fixme This is a nasty code smell
|
|
var _domElement;
|
|
Object.defineProperty(this, 'domElement', {
|
|
get: function() { return _domElement; },
|
|
set: function(domElement) { _domElement = domElement; }
|
|
})
|
|
|
|
// Mixin common widget behaviour
|
|
OT.Chrome.Behaviour.Widget(this, {
|
|
mode: options.mode,
|
|
nodeName: 'h1',
|
|
htmlContent: _name,
|
|
htmlAttributes: {className: 'OT_name'}
|
|
});
|
|
|
|
Object.defineProperty(this, 'name', {
|
|
set: function(name) {
|
|
if (!_name) this.setDisplayMode('auto');
|
|
|
|
_name = name;
|
|
_domElement.innerHTML = _name;
|
|
}.bind(this)
|
|
});
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.Chrome.MuteButton = function(options) {
|
|
var _onClickCb,
|
|
_muted = options.muted || false;
|
|
|
|
// This behaviour must be implemented to make the widget behaviour work.
|
|
// @fixme This is a nasty code smell
|
|
var _domElement;
|
|
Object.defineProperty(this, 'domElement', {
|
|
get: function() { return _domElement; },
|
|
set: function(domElement) { _domElement = domElement; }
|
|
})
|
|
|
|
// Private Event Callbacks
|
|
var attachEvents = function(elem) {
|
|
_onClickCb = onClick.bind(this);
|
|
elem.addEventListener('click', _onClickCb, false);
|
|
},
|
|
|
|
detachEvents = function(elem) {
|
|
_onClickCb = null;
|
|
elem.removeEventListener('click', _onClickCb, false);
|
|
},
|
|
|
|
onClick = function(event) {
|
|
_muted = !_muted;
|
|
|
|
if (_muted) {
|
|
OT.$.addClass(_domElement, 'OT_active');
|
|
this.parent.trigger('muted', this);
|
|
}
|
|
else {
|
|
OT.$.removeClass(_domElement, 'OT_active');
|
|
this.parent.trigger('unmuted', this);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Mixin common widget behaviour
|
|
var classNames = _muted ? 'OT_mute OT_active' : 'OT_mute';
|
|
OT.Chrome.Behaviour.Widget(this, {
|
|
mode: options.mode,
|
|
nodeName: 'button',
|
|
htmlContent: 'Mute',
|
|
htmlAttributes: {className: classNames},
|
|
onCreate: attachEvents.bind(this),
|
|
onDestroy: detachEvents.bind(this)
|
|
});
|
|
};
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.Chrome.MicVolume = function(options) {
|
|
var _onClickCb,
|
|
_muted = options.muted || false;
|
|
|
|
// This behaviour must be implemented to make the widget behaviour work.
|
|
// @fixme This is a nasty code smell
|
|
var _domElement;
|
|
Object.defineProperty(this, 'domElement', {
|
|
get: function() { return _domElement; },
|
|
set: function(domElement) { _domElement = domElement; }
|
|
})
|
|
|
|
// Private Event Callbacks
|
|
var attachEvents = function(elem) {
|
|
_onClickCb = onClick.bind(this);
|
|
elem.addEventListener('click', _onClickCb, false);
|
|
},
|
|
|
|
detachEvents = function(elem) {
|
|
_onClickCb = null;
|
|
elem.removeEventListener('click', _onClickCb, false);
|
|
},
|
|
|
|
onClick = function(event) {
|
|
_muted = !_muted;
|
|
|
|
if (_muted) {
|
|
OT.$.addClass(_domElement, 'active');
|
|
this.parent.trigger('muted', this);
|
|
}
|
|
else {
|
|
OT.$.removeClass(_domElement, 'active');
|
|
this.parent.trigger('unmuted', this);
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Mixin common widget behaviour
|
|
OT.Chrome.Behaviour.Widget(this, {
|
|
mode: options.mode,
|
|
nodeName: 'button',
|
|
htmlContent: 'Mute',
|
|
htmlAttributes: {className: 'OT_mic-volume'},
|
|
onCreate: attachEvents.bind(this),
|
|
onDestroy: detachEvents.bind(this)
|
|
});
|
|
};
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.Chrome.SettingsPanelButton = function(options) {
|
|
var _onClickCb;
|
|
|
|
// Private Event Callbacks
|
|
var attachEvents = function(elem) {
|
|
_onClickCb = onClick.bind(this);
|
|
elem.addEventListener('click', _onClickCb, false);
|
|
},
|
|
|
|
detachEvents = function(elem) {
|
|
_onClickCb = null;
|
|
elem.removeEventListener('click', _onClickCb, false);
|
|
},
|
|
|
|
onClick = function(event) {
|
|
this.parent.trigger('SettingsPanel:open', this);
|
|
return false;
|
|
};
|
|
|
|
|
|
// This behaviour must be implemented to make the widget behaviour work.
|
|
// @fixme This is a nasty code smell
|
|
var _domElement;
|
|
Object.defineProperty(this, 'domElement', {
|
|
get: function() { return _domElement; },
|
|
set: function(domElement) { _domElement = domElement; }
|
|
})
|
|
|
|
// Mixin common widget behaviour
|
|
OT.Chrome.Behaviour.Widget(this, {
|
|
mode: options.mode,
|
|
nodeName: 'button',
|
|
htmlContent: 'Settings',
|
|
htmlAttributes: {className: 'OT_settings-panel'},
|
|
onCreate: attachEvents.bind(this),
|
|
onDestroy: detachEvents.bind(this)
|
|
});
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.Chrome.SettingsPanel = function(options) {
|
|
if (!options.stream) {
|
|
// @todo raise error
|
|
return;
|
|
}
|
|
|
|
var webRTCStream = options.stream;
|
|
|
|
// This behaviour must be implemented to make the widget behaviour work.
|
|
// @fixme This is a nasty code smell
|
|
var _domElement;
|
|
Object.defineProperty(this, 'domElement', {
|
|
get: function() { return _domElement; },
|
|
set: function(domElement) { _domElement = domElement; }
|
|
})
|
|
|
|
var renderDialog = function() {
|
|
var camLabel = webRTCStream.getVideoTracks().length ? webRTCStream.getVideoTracks()[0].label : "None",
|
|
micLabel = webRTCStream.getAudioTracks().length ? webRTCStream.getAudioTracks()[0].label : "None";
|
|
|
|
_domElement.innerHTML = "<dl>\
|
|
<dt>Cam</dt>\
|
|
<dd>" + camLabel + "</dd>\
|
|
<dt>Mic</dt>\
|
|
<dd>" + micLabel + "</dd>\
|
|
</dl>";
|
|
|
|
|
|
var closeButton = OT.$.createButton('Close', {
|
|
className: 'OT_close'
|
|
}, {
|
|
click: onClose.bind(this)
|
|
});
|
|
|
|
_domElement.appendChild(closeButton);
|
|
},
|
|
|
|
onShow = function() {
|
|
renderDialog.call(this);
|
|
},
|
|
|
|
onClose = function() {
|
|
this.parent.trigger('SettingsPanel:close', this);
|
|
return false;
|
|
};
|
|
|
|
|
|
// Mixin common widget behaviour
|
|
OT.Chrome.Behaviour.Widget(this, {
|
|
mode: options.mode,
|
|
nodeName: 'section',
|
|
htmlContent: 'Settings',
|
|
htmlAttributes: {className: 'OT_settings-panel'},
|
|
onCreate: renderDialog.bind(this),
|
|
onShow: onShow.bind(this)
|
|
});
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
OT.Chrome.OpenTokButton = function(options) {
|
|
// This behaviour must be implemented to make the widget behaviour work.
|
|
// @fixme This is a nasty code smell
|
|
var _domElement;
|
|
this.__defineGetter__("domElement", function() { return _domElement; });
|
|
this.__defineSetter__("domElement", function(domElement) { _domElement = domElement; });
|
|
|
|
// Mixin common widget behaviour
|
|
OT.Chrome.Behaviour.Widget(this, {
|
|
mode: options ? options.mode : null,
|
|
nodeName: 'span',
|
|
htmlContent: 'OpenTok',
|
|
htmlAttributes: {
|
|
className: 'OT_opentok'
|
|
}
|
|
});
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
/* Stylable Notes
|
|
|
|
* RTC doesn't need to wait until anything is loaded (flash needs to wait until the Flash component loads)
|
|
* Some bits are controlled by multiple flags, i.e. buttonDisplayMode and nameDisplayMode.
|
|
* When there are multiple flags how is the final setting chosen?
|
|
* When some style bits are set updates will need to be pushed through to the Chrome
|
|
*/
|
|
|
|
// Mixes the StylableComponent behaviour into the +self+ object. It will
|
|
// also set the default styles to +initialStyles+.
|
|
//
|
|
// @note This Mixin is dependent on OT.Eventing.
|
|
//
|
|
//
|
|
// @example
|
|
//
|
|
// function SomeObject {
|
|
// OT.StylableComponent(this, {
|
|
// name: 'SomeObject',
|
|
// foo: 'bar'
|
|
// });
|
|
// }
|
|
//
|
|
// var obj = new SomeObject();
|
|
// obj.getStyle('foo'); // => 'bar'
|
|
// obj.setStyle('foo', 'baz')
|
|
// obj.getStyle('foo'); // => 'baz'
|
|
// obj.getStyle(); // => {name: 'SomeObject', foo: 'baz'}
|
|
//
|
|
OT.StylableComponent = function(self, initalStyles) {
|
|
if (!self.trigger) {
|
|
throw new Error("OT.StylableComponent is dependent on the mixin OT.$.eventing. Ensure that this is included in the object before StylableComponent.");
|
|
}
|
|
|
|
// Broadcast style changes as the styleValueChanged event
|
|
var onStyleChange = function(key, value, oldValue) {
|
|
if (oldValue) {
|
|
self.trigger('styleValueChanged', key, value, oldValue);
|
|
}
|
|
else {
|
|
self.trigger('styleValueChanged', key, value);
|
|
}
|
|
};
|
|
|
|
var _style = new Style(initalStyles, onStyleChange);
|
|
|
|
/**
|
|
* Returns an object that has the properties that define the current user interface controls of the Publisher.
|
|
* You can modify the properties of this object and pass the object to the <code>setStyle()</code> method of the
|
|
* Publisher object. (See the documentation for <a href="#setStyle">setStyle()</a> to see the styles that define
|
|
* this object.)
|
|
* @return {Object} The object that defines the styles of the Publisher.
|
|
* @see <a href="#setStyle">setStyle()</a>
|
|
* @method #getStyle
|
|
* @memberOf Publisher
|
|
*/
|
|
|
|
/**
|
|
* Returns an object that has the properties that define the current user interface controls of the Subscriber.
|
|
* You can modify the properties of this object and pass the object to the <code>setStyle()</code> method of the
|
|
* Subscriber object. (See the documentation for <a href="#setStyle">setStyle()</a> to see the styles that define
|
|
* this object.)
|
|
* @return {Object} The object that defines the styles of the Subscriber.
|
|
* @see <a href="#setStyle">setStyle()</a>
|
|
* @method #getStyle
|
|
* @memberOf Subscriber
|
|
*/
|
|
// If +key+ is falsly then all styles will be returned.
|
|
self.getStyle = function(key) {
|
|
return _style.get(key);
|
|
};
|
|
|
|
/**
|
|
* Sets properties that define the appearance of some user interface controls of the Publisher.
|
|
*
|
|
* <p>You can either pass one parameter or two parameters to this method.</p>
|
|
*
|
|
* <p>If you pass one parameter, <code>style</code>, it is an object that has one property: <code>nameDisplayMode</code>.
|
|
* Possible values for the <code>style.nameDisplayMode</code> property are: "auto" (the name is displayed when the stream
|
|
* is first displayed and when the user mouses over the display), "off" (the name is not displayed), and "on"
|
|
* (the name is displayed).
|
|
*
|
|
* <p>For example, the following code passes one parameter to the method:</p>
|
|
*
|
|
* <pre>myPublisher.setStyle({nameDisplayMode: "off"});</pre>
|
|
*
|
|
* <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are key-value pair that
|
|
* define one property of the display style. For example, the following code passes two parameter values
|
|
* to the method:</p>
|
|
*
|
|
* <pre>myPublisher.setStyle("nameDisplayMode", "off");</pre>
|
|
*
|
|
* <p>You can set the initial settings when you call the <code>Session.publish()</code>
|
|
* or <code>TB.initPublisher()</code> method. Pass a <code>style</code> property as part of the
|
|
* <code>properties</code> parameter of the method.</p>
|
|
*
|
|
* <p>The TB object dispatches an <code>exception</code> event if you pass in an invalid style to the method.
|
|
* The <code>code</code> property of the ExceptionEvent object is set to 1011.</p>
|
|
*
|
|
* @param {Object} style Either an object containing properties that define the style, or a String defining this
|
|
* single style property to set.
|
|
* @param {String} value The value to set for the <code>style</code> passed in. Pass a value for this parameter
|
|
* only if the value of the <code>style</code> parameter is a String.</p>
|
|
*
|
|
* @see <a href="#getStyle">getStyle()</a>
|
|
* @return {Publisher} The Publisher object
|
|
* @see <a href="#setStyle">setStyle()</a>
|
|
*
|
|
* @see <a href="Session.html#subscribe">Session.publish()</a>
|
|
* @see <a href="TB.html#initPublisher">TB.initPublisher()</a>
|
|
* @method #setStyle
|
|
* @memberOf Publisher
|
|
*/
|
|
|
|
/**
|
|
* Sets properties that define the appearance of some user interface controls of the Subscriber.
|
|
*
|
|
* <p>You can either pass one parameter or two parameters to this method.</p>
|
|
*
|
|
* <p>If you pass one parameter, <code>style</code>, it is an object that has one property: <code>nameDisplayMode</code>.
|
|
* Possible values for the <code>style.nameDisplayMode</code> property are: "auto" (the name is displayed when the stream
|
|
* is first displayed and when the user mouses over the display), "off" (the name is not displayed), and "on"
|
|
* (the name is displayed).
|
|
*
|
|
* <p>For example, the following code passes one parameter to the method:</p>
|
|
*
|
|
* <pre>mySubscriber.setStyle({nameDisplayMode: "off"});</pre>
|
|
*
|
|
* <p>If you pass two parameters, <code>style</code> and <code>value</code>, they are key-value pair that
|
|
* define one property of the display style. For example, the following code passes two parameter values
|
|
* to the method:</p>
|
|
*
|
|
* <pre>mySubscriber.setStyle("nameDisplayMode", "off");</pre>
|
|
*
|
|
* <p>You can set the initial settings when you call the <code>Session.subscribe()</code> method.
|
|
* Pass a <code>style</code> property as part of the <code>properties</code> parameter of the method.</p>
|
|
*
|
|
* <p>The TB object dispatches an <code>exception</code> event if you pass in an invalid style to the method.
|
|
* The <code>code</code> property of the ExceptionEvent object is set to 1011.</p>
|
|
*
|
|
* @param {Object} style Either an object containing properties that define the style, or a String defining this
|
|
* single style property to set.
|
|
* @param {String} value The value to set for the <code>style</code> passed in. Pass a value for this parameter
|
|
* only if the value of the <code>style</code> parameter is a String.</p>
|
|
*
|
|
* @returns {Subscriber} The Subscriber object.
|
|
*
|
|
* @see <a href="#getStyle">getStyle()</a>
|
|
* @see <a href="#setStyle">setStyle()</a>
|
|
*
|
|
* @see <a href="Session.html#subscribe">Session.subscribe()</a>
|
|
* @method #setStyle
|
|
* @memberOf Subscriber
|
|
*/
|
|
self.setStyle = function(keyOrStyleHash, value, silent) {
|
|
if (typeof(keyOrStyleHash) !== 'string') {
|
|
_style.setAll(keyOrStyleHash, silent);
|
|
}
|
|
else {
|
|
_style.set(keyOrStyleHash, value);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
};
|
|
|
|
var Style = function(initalStyles, onStyleChange) {
|
|
var _COMPONENT_STYLES = [
|
|
"showMicButton",
|
|
"showSpeakerButton",
|
|
"showSettingsButton",
|
|
"showCameraToggleButton",
|
|
"nameDisplayMode",
|
|
"buttonDisplayMode",
|
|
"showSaveButton",
|
|
"showRecordButton",
|
|
"showRecordStopButton",
|
|
"showReRecordButton",
|
|
"showPauseButton",
|
|
"showPlayButton",
|
|
"showPlayStopButton",
|
|
"showStopButton",
|
|
"backgroundImageURI",
|
|
"showControlPanel",
|
|
"showRecordCounter",
|
|
"showPlayCounter",
|
|
"showControlBar",
|
|
"showPreviewTime"
|
|
],
|
|
|
|
_validStyleValues = {
|
|
buttonDisplayMode: ["auto", "off", "on"],
|
|
nameDisplayMode: ["auto", "off", "on"],
|
|
showSettingsButton: [true, false],
|
|
showMicButton: [true, false],
|
|
showCameraToggleButton: [true, false],
|
|
showSaveButton: [true, false],
|
|
backgroundImageURI: null,
|
|
showControlBar: [true, false],
|
|
showPlayCounter: [true, false],
|
|
showRecordCounter: [true, false],
|
|
showPreviewTime: [true, false]
|
|
},
|
|
|
|
_style = {},
|
|
|
|
|
|
// Validates the style +key+ and also whether +value+ is valid for +key+
|
|
isValidStyle = function(key, value) {
|
|
return key === 'backgroundImageURI' ||
|
|
( _validStyleValues.hasOwnProperty(key) &&
|
|
_validStyleValues[key].indexOf(value) !== -1 );
|
|
},
|
|
|
|
castValue = function(value) {
|
|
switch(value) {
|
|
case 'true':
|
|
return true;
|
|
case 'false':
|
|
return false;
|
|
default:
|
|
return value;
|
|
}
|
|
};
|
|
|
|
|
|
// Returns a shallow copy of the styles.
|
|
this.getAll = function() {
|
|
var style = OT.$.clone(_style);
|
|
|
|
for (var i in style) {
|
|
if (_COMPONENT_STYLES.indexOf(i) < 0) {
|
|
// Strip unnecessary properties out, should this happen on Set?
|
|
delete style[i];
|
|
}
|
|
}
|
|
|
|
return style;
|
|
};
|
|
|
|
this.get = function(key) {
|
|
if (key) {
|
|
return _style[key];
|
|
}
|
|
|
|
// We haven't been asked for any specific key, just return the lot
|
|
return this.getAll();
|
|
};
|
|
|
|
// *note:* this will not trigger onStyleChange if +silent+ is truthy
|
|
this.setAll = function(newStyles, silent) {
|
|
var oldValue, newValue;
|
|
|
|
for (var key in newStyles) {
|
|
newValue = castValue(newStyles[key]);
|
|
|
|
if (isValidStyle(key, newValue)) {
|
|
oldValue = _style[key];
|
|
|
|
if (newValue !== oldValue) {
|
|
_style[key] = newValue;
|
|
if (!silent) onStyleChange(key, newValue, oldValue);
|
|
}
|
|
}
|
|
else {
|
|
OT.warn("Style.setAll::Invalid style property passed " + key + " : " + newValue);
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
this.set = function(key, value) {
|
|
OT.debug("Publisher.setStyle: " + key.toString());
|
|
|
|
var newValue = castValue(value),
|
|
oldValue;
|
|
|
|
if (!isValidStyle(key, newValue)) {
|
|
OT.warn("Style.set::Invalid style property passed " + key + " : " + newValue);
|
|
return this;
|
|
}
|
|
|
|
oldValue = _style[key];
|
|
if (newValue !== oldValue) {
|
|
_style[key] = newValue;
|
|
|
|
onStyleChange(key, value, oldValue);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
if (initalStyles) this.setAll(initalStyles, true);
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
/**
|
|
* A Publishers Microphone.
|
|
*
|
|
* TODO
|
|
* * bind to changes in mute/unmute/volume/etc and respond to them
|
|
*/
|
|
OT.Microphone = function(webRTCStream, muted) {
|
|
var _muted,
|
|
_gain = 50;
|
|
|
|
|
|
Object.defineProperty(this, 'muted', {
|
|
get: function() { return _muted; },
|
|
set: function(muted) {
|
|
if (_muted === muted) return;
|
|
|
|
_muted = muted;
|
|
|
|
var audioTracks = webRTCStream.getAudioTracks();
|
|
|
|
for (var i=0, num=audioTracks.length; i<num; ++i) {
|
|
audioTracks[i].enabled = !_muted;
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this, 'gain', {
|
|
get: function() { return _gain; },
|
|
|
|
set: function(gain) {
|
|
OT.warn("OT.Microphone.gain IS NOT YET IMPLEMENTED");
|
|
|
|
_gain = gain;
|
|
}
|
|
});
|
|
|
|
// Set the initial value
|
|
if (muted !== undefined) {
|
|
this.muted = muted === true;
|
|
}
|
|
else if (webRTCStream.getAudioTracks().length) {
|
|
this.muted = !webRTCStream.getAudioTracks()[0].enabled;
|
|
}
|
|
else {
|
|
this.muted = false;
|
|
}
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// A Factory method for generating simple state machine classes.
|
|
//
|
|
// @usage
|
|
// var StateMachine = OT.generateSimpleStateMachine('start', ['start', 'middle', 'end', {
|
|
// start: ['middle'],
|
|
// middle: ['end'],
|
|
// end: ['start']
|
|
// }]);
|
|
//
|
|
// var states = new StateMachine();
|
|
// state.current; // <-- start
|
|
// state.set('middle');
|
|
//
|
|
OT.generateSimpleStateMachine = function(initialState, states, transitions) {
|
|
var validStates = states.slice(),
|
|
validTransitions = OT.$.clone(transitions);
|
|
|
|
var isValidState = function (state) {
|
|
return validStates.indexOf(state) !== -1;
|
|
}
|
|
|
|
var isValidTransition = function(fromState, toState) {
|
|
return validTransitions[fromState] && validTransitions[fromState].indexOf(toState) !== -1;
|
|
};
|
|
|
|
return function(stateChangeFailed) {
|
|
var currentState = initialState,
|
|
previousState = null;
|
|
|
|
function signalChangeFailed(message, newState) {
|
|
stateChangeFailed({
|
|
message: message,
|
|
newState: newState,
|
|
currentState: currentState,
|
|
previousState: previousState
|
|
});
|
|
}
|
|
|
|
// Validates +newState+. If it's invalid it triggers stateChangeFailed and returns false.
|
|
function handleInvalidStateChanges(newState) {
|
|
if (!isValidState(newState)) {
|
|
signalChangeFailed("'" + newState + "' is not a valid state", newState)
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!isValidTransition(currentState, newState)) {
|
|
signalChangeFailed("'" + currentState + "' cannot transition to '" + newState + "'", newState)
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
this.set = function(newState) {
|
|
if (!handleInvalidStateChanges(newState)) return;
|
|
|
|
previousState = currentState;
|
|
currentState = newState;
|
|
};
|
|
|
|
Object.defineProperties(this, {
|
|
current: {
|
|
get: function() { return currentState; }
|
|
},
|
|
|
|
subscribing: {
|
|
get: function() { return currentState === 'Subscribing'; }
|
|
}
|
|
});
|
|
};
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// Models a Subscriber's subscribing State
|
|
//
|
|
// Valid States:
|
|
// NotSubscribing (the initial state
|
|
// Init (basic setup of DOM
|
|
// ConnectingToPeer (Failure Cases -> No Route, Bad Offer, Bad Answer
|
|
// BindingRemoteStream (Failure Cases -> Anything to do with the media being invalid, the media never plays
|
|
// Subscribing (this is 'onLoad'
|
|
// Failed (terminal state, with a reason that maps to one of the failure cases above
|
|
//
|
|
//
|
|
// Valid Transitions:
|
|
// NotSubscribing ->
|
|
// Init
|
|
//
|
|
// Init ->
|
|
// ConnectingToPeer
|
|
// | BindingRemoteStream (if we are subscribing to ourselves and we alreay have a stream
|
|
// | NotSubscribing (destroy()
|
|
//
|
|
// ConnectingToPeer ->
|
|
// BindingRemoteStream
|
|
// | NotSubscribing
|
|
// | Failed
|
|
// | NotSubscribing (destroy()
|
|
//
|
|
// BindingRemoteStream ->
|
|
// Subscribing
|
|
// | Failed
|
|
// | NotSubscribing (destroy()
|
|
//
|
|
// Subscribing ->
|
|
// NotSubscribing (unsubscribe
|
|
// | Failed (probably a peer connection failure after we began subscribing
|
|
//
|
|
// Failed -> (terminal error state)
|
|
//
|
|
//
|
|
// @example
|
|
// var state = new SubscribingState(function(change) {
|
|
// console.log(change.message);
|
|
// });
|
|
//
|
|
// state.set('Init');
|
|
// state.current; -> 'Init'
|
|
//
|
|
// state.set('Subscribing'); -> triggers stateChangeFailed and logs out the error message
|
|
//
|
|
//
|
|
|
|
var validStates = [ 'NotSubscribing', 'Init', 'ConnectingToPeer', 'BindingRemoteStream', 'Subscribing', 'Failed' ],
|
|
|
|
validTransitions = {
|
|
NotSubscribing: ['NotSubscribing', 'Init'],
|
|
Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream'],
|
|
ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed'],
|
|
BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed'],
|
|
Subscribing: ['NotSubscribing', 'Failed'],
|
|
Failed: []
|
|
},
|
|
|
|
initialState = 'NotSubscribing';
|
|
|
|
OT.SubscribingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
|
|
|
|
Object.defineProperty(OT.SubscribingState.prototype, 'attemptingToSubscribe', {
|
|
get: function() { return [ 'Init', 'ConnectingToPeer', 'BindingRemoteStream' ].indexOf(this.current) !== -1; }
|
|
});
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// Models a Publisher's publishing State
|
|
//
|
|
// Valid States:
|
|
// NotPublishing
|
|
// GetUserMedia
|
|
// BindingMedia
|
|
// MediaBound
|
|
// PublishingToSession
|
|
// Publishing
|
|
// Failed
|
|
//
|
|
//
|
|
// Valid Transitions:
|
|
// NotPublishing ->
|
|
// GetUserMedia
|
|
//
|
|
// GetUserMedia ->
|
|
// BindingMedia
|
|
// | Failed (Failure Reasons -> stream error, constraints, permission denied
|
|
// | NotPublishing (destroy()
|
|
//
|
|
//
|
|
// BindingMedia ->
|
|
// MediaBound
|
|
// | Failed (Failure Reasons -> Anything to do with the media being invalid, the media never plays
|
|
// | NotPublishing (destroy()
|
|
//
|
|
// MediaBound ->
|
|
// PublishingToSession (MediaBound could transition to PublishingToSession if a stand-alone publish is bound to a session
|
|
// | Failed (Failure Reasons -> media issues with a stand-alone publisher
|
|
// | NotPublishing (destroy()
|
|
//
|
|
// PublishingToSession
|
|
// Publishing
|
|
// | Failed (Failure Reasons -> timeout while waiting for ack of stream registered. We do not do this right now
|
|
// | NotPublishing (destroy()
|
|
//
|
|
//
|
|
// Publishing ->
|
|
// NotPublishing (Unpublish
|
|
// | Failed (Failure Reasons -> loss of network, media error, anything that causes *all* Peer Connections to fail (less than all failing is just an error, all is failure)
|
|
// | NotPublishing (destroy()
|
|
//
|
|
// Failed -> (Terminal state
|
|
//
|
|
//
|
|
|
|
|
|
var validStates = [ 'NotPublishing', 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession', 'Publishing', 'Failed' ],
|
|
|
|
validTransitions = {
|
|
NotPublishing: ['NotPublishing', 'GetUserMedia'],
|
|
GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing'],
|
|
BindingMedia: ['MediaBound', 'Failed', 'NotPublishing'],
|
|
MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed'],
|
|
PublishingToSession: ['NotPublishing', 'Publishing', 'Failed'],
|
|
Publishing: ['NotPublishing', 'MediaBound', 'Failed'],
|
|
Failed: []
|
|
},
|
|
|
|
initialState = 'NotPublishing';
|
|
|
|
OT.PublishingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
|
|
|
|
Object.defineProperties(OT.PublishingState.prototype, {
|
|
attemptingToPublish: {
|
|
get: function() { return [ 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession' ].indexOf(this.current) !== -1; }
|
|
},
|
|
|
|
publishing: {
|
|
get: function() { return this.current === 'Publishing'; }
|
|
}
|
|
});
|
|
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// The default constraints
|
|
var defaultConstraints = {
|
|
audio: true,
|
|
video: true
|
|
};
|
|
|
|
/**
|
|
* The Publisher object provides the mechanism through which control of the
|
|
* published stream is accomplished. Calling the <code>TB.initPublisher</code> method of a
|
|
* Session object creates a Publisher object. </p>
|
|
*
|
|
* <p>The following code instantiates a session, and publishes an audio-video stream
|
|
* upon connection to the session: </p>
|
|
*
|
|
* <pre>
|
|
* var API_KEY = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectHandler);
|
|
* session.connect(API_KEY, token);
|
|
*
|
|
* function sessionConnectHandler(event) {
|
|
* var div = document.createElement('div');
|
|
* div.setAttribute('id', 'publisher');
|
|
*
|
|
* var publisherContainer = document.getElementById('publisherContainer');
|
|
* // This example assumes that a publisherContainer div exists
|
|
* publisherContainer.appendChild(div);
|
|
*
|
|
* var publisherProperties = {width: 400, height:300, name:"Bob's stream"};
|
|
* publisher = TB.initPublisher(API_KEY, 'publisher', publisherProperties);
|
|
* session.publish(publisher);
|
|
* }
|
|
* </pre>
|
|
*
|
|
* <p>This example creates a Publisher object and adds its video to a DOM element named <code>publisher</code>
|
|
* by calling the <code>TB.initPublisher()</code> method. It then publishes a stream to the session by calling
|
|
* the <code>publish()</code> method of the Session object.</p>
|
|
*
|
|
* @property id The DOM ID of the Publisher.
|
|
* @property stream The {@link Stream} object corresponding the the stream of the Publihser.
|
|
* @property session The {@link Session} to which the Publisher belongs.
|
|
*
|
|
* @see <a href="TB.html#initPublisher">TB.initPublisher</a>
|
|
* @see <a href="Session.html#publish">Session.publish()</a>
|
|
*
|
|
* @class Publisher
|
|
* @augments EventDispatcher
|
|
*/
|
|
OT.Publisher = function() {
|
|
// Check that the client meets the minimum requirements, if they don't the upgrade
|
|
// flow will be triggered.
|
|
if (!OT.checkSystemRequirements()) {
|
|
OT.upgradeSystemRequirements();
|
|
return;
|
|
}
|
|
|
|
var _guid = OT.Publisher.nextId(),
|
|
_domId,
|
|
_container,
|
|
_targetElement,
|
|
_stream,
|
|
_webRTCStream,
|
|
_session,
|
|
_peerConnections = {},
|
|
_loaded = false,
|
|
_publishProperties,
|
|
_publishStartTime,
|
|
_streamCreatedTimeout,
|
|
_microphone,
|
|
_chrome,
|
|
_analytics = new OT.Analytics(),
|
|
_validResolutions = [
|
|
{width: 320, height: 240},
|
|
{width: 640, height: 480},
|
|
{width: 1280, height: 720}
|
|
],
|
|
_qosIntervals = {},
|
|
_gettingStats = 0,
|
|
_prevStats = {
|
|
"timeStamp" : OT.$.now()
|
|
},
|
|
_state;
|
|
|
|
OT.$.eventing(this);
|
|
|
|
OT.StylableComponent(this, {
|
|
showMicButton: true,
|
|
showSettingsButton: true,
|
|
showCameraToggleButton: true,
|
|
nameDisplayMode: "auto",
|
|
buttonDisplayMode: "auto",
|
|
backgroundImageURI: null
|
|
});
|
|
|
|
/// Private Methods
|
|
var logAnalyticsEvent = function(action, variation, payloadType, payload) {
|
|
_analytics.logEvent({
|
|
action: action,
|
|
variation: variation,
|
|
payload_type: payloadType,
|
|
payload: payload,
|
|
session_id: _session ? _session.sessionId : null,
|
|
connection_id: _session && _session.connected ? _session.connection.connectionId : null,
|
|
partner_id: _session ? _session.apiKey : OT.APIKEY,
|
|
streamId: _stream ? _stream.id : null,
|
|
widget_id: _guid,
|
|
widget_type: 'Publisher'
|
|
});
|
|
},
|
|
|
|
isValidResolution = function(width, height) {
|
|
for (var i=0; i<_validResolutions.length; ++i) {
|
|
if (_validResolutions[i].width == width && _validResolutions[i].height == height) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
recordQOS = function(connection_id) {
|
|
var QoS_blob = {
|
|
widget_type: 'Publisher',
|
|
stream_type : 'WebRTC',
|
|
sessionId: _session ? _session.sessionId : null,
|
|
connectionId: _session && _session.connected ? _session.connection.connectionId : null,
|
|
partnerId: _session ? _session.apiKey : OT.APIKEY,
|
|
streamId: _stream ? _stream.id : null,
|
|
widgetId: _guid,
|
|
version: OT.properties.version,
|
|
media_server_name: _session ? _session.sessionInfo.messagingServer : null,
|
|
p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
|
|
duration: new Date().getTime() -_publishStartTime.getTime(),
|
|
remote_connection_id: connection_id
|
|
};
|
|
|
|
// get stats for each connection id
|
|
_peerConnections[connection_id].getStats(_prevStats, function(stats) {
|
|
if (stats) {
|
|
for (var stat_index in stats) {
|
|
QoS_blob[stat_index] = stats[stat_index];
|
|
}
|
|
}
|
|
_analytics.logQOS(QoS_blob);
|
|
});
|
|
},
|
|
|
|
/// Private Events
|
|
|
|
stateChangeFailed = function(changeFailed) {
|
|
OT.error("Publisher State Change Failed: ", changeFailed.message);
|
|
OT.debug(changeFailed);
|
|
},
|
|
|
|
onLoaded = function() {
|
|
OT.debug("OT.Publisher.onLoaded");
|
|
|
|
_state.set('MediaBound');
|
|
_container.loading = false;
|
|
_loaded = true;
|
|
|
|
_createChrome.call(this);
|
|
|
|
this.trigger('initSuccess', this);
|
|
this.trigger('loaded', this);
|
|
},
|
|
|
|
onLoadFailure = function(reason) {
|
|
logAnalyticsEvent('publish', 'Failure', 'reason', "Publisher PeerConnection Error: " + reason);
|
|
|
|
_state.set('Failed');
|
|
this.trigger('publishError', "Publisher PeerConnection Error: " + reason);
|
|
|
|
OT.handleJsException("Publisher PeerConnection Error: " + reason, OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
|
|
session: _session,
|
|
target: this
|
|
});
|
|
},
|
|
|
|
onStreamAvailable = function(webOTStream) {
|
|
OT.debug("OT.Publisher.onStreamAvailable");
|
|
|
|
_state.set('BindingMedia');
|
|
|
|
cleanupLocalStream();
|
|
_webRTCStream = webOTStream;
|
|
|
|
_microphone = new OT.Microphone(_webRTCStream, !_publishProperties.publishAudio);
|
|
this.publishVideo(_publishProperties.publishVideo);
|
|
|
|
this.dispatchEvent(
|
|
new OT.Event(OT.Event.names.ACCESS_ALLOWED, false)
|
|
);
|
|
|
|
_targetElement = new OT.VideoElement({
|
|
attributes: {muted:true}
|
|
});
|
|
|
|
_targetElement.on({
|
|
streamBound: onLoaded,
|
|
loadError: onLoadFailure,
|
|
error: onVideoError
|
|
}, this)
|
|
.bindToStream(_webRTCStream);
|
|
|
|
_container.video = _targetElement;
|
|
},
|
|
|
|
onStreamAvailableError = function(error) {
|
|
OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
|
|
|
|
_state.set('Failed');
|
|
this.trigger('publishError', error.message);
|
|
|
|
if (_container) _container.destroy();
|
|
|
|
logAnalyticsEvent('publish', 'Failure', 'reason', "Publisher failed to access camera/mic: " + error.message);
|
|
|
|
OT.handleJsException("Publisher failed to access camera/mic: " + error.message, 2000, {
|
|
session: _session,
|
|
target: this
|
|
});
|
|
},
|
|
|
|
// The user has clicked the 'deny' button the the allow access dialog (or it's set to always deny)
|
|
onAccessDenied = function(error) {
|
|
OT.error('OT.Publisher.onStreamAvailableError Permission Denied');
|
|
|
|
_state.set('Failed');
|
|
this.trigger('publishError', error.message);
|
|
|
|
logAnalyticsEvent('publish', 'Failure', 'reason', 'Publisher Access Denied: Permission Denied');
|
|
|
|
var event = new OT.Event(OT.Event.names.ACCESS_DENIED),
|
|
defaultAction = function() {
|
|
if (!event.isDefaultPrevented() && _container) _container.destroy();
|
|
};
|
|
|
|
this.dispatchEvent(event, defaultAction);
|
|
},
|
|
|
|
onAccessDialogOpened = function() {
|
|
logAnalyticsEvent('accessDialog', 'Opened', '', '');
|
|
|
|
this.dispatchEvent(
|
|
new OT.Event(OT.Event.names.ACCESS_DIALOG_OPENED, false)
|
|
);
|
|
},
|
|
|
|
onAccessDialogClosed = function() {
|
|
logAnalyticsEvent('accessDialog', 'Closed', '', '');
|
|
|
|
this.dispatchEvent(
|
|
new OT.Event(OT.Event.names.ACCESS_DIALOG_CLOSED, false)
|
|
);
|
|
},
|
|
|
|
onVideoError = function(errorCode, errorReason) {
|
|
OT.error('OT.Publisher.onVideoError');
|
|
|
|
var message = errorReason + (errorCode ? ' (' + errorCode + ')' : '');
|
|
logAnalyticsEvent('stream', null, 'reason', "Publisher while playing stream: " + message);
|
|
|
|
_state.set('Failed');
|
|
|
|
if (_state.attemptingToPublish) {
|
|
this.trigger('publishError', message);
|
|
}
|
|
else {
|
|
this.trigger('error', message);
|
|
}
|
|
|
|
OT.handleJsException("Publisher error playing stream: " + message, 2000, {
|
|
session: _session,
|
|
target: this
|
|
});
|
|
},
|
|
|
|
onPeerDisconnected = function(peerConnection) {
|
|
OT.debug("OT.Subscriber has been disconnected from the Publisher's PeerConnection");
|
|
|
|
this.cleanupSubscriber(peerConnection.remoteConnection.id);
|
|
},
|
|
|
|
onPeerConnectionFailure = function(code, reason, peerConnection) {
|
|
logAnalyticsEvent('publish', 'Failure', 'reason|hasRelayCandidates', [
|
|
reason + ": Publisher PeerConnection with connection " + (peerConnection && peerConnection.remoteConnection && peerConnection.remoteConnection.id) + " failed",
|
|
peerConnection.hasRelayCandidates
|
|
].join('|'));
|
|
|
|
OT.handleJsException("Publisher PeerConnection Error: " + reason, 2000, {
|
|
session: _session,
|
|
target: this
|
|
});
|
|
|
|
// We don't call cleanupSubscriber as it also logs a
|
|
// disconnected analytics event, which we don't want in this
|
|
// instance. The duplication is crufty though and should
|
|
// be tidied up.
|
|
if (peerConnection.remoteConnection) {
|
|
clearInterval(_qosIntervals[peerConnection.remoteConnection.id]);
|
|
delete _qosIntervals[peerConnection.remoteConnection.id]
|
|
|
|
delete _peerConnections[peerConnection.remoteConnection.id];
|
|
}
|
|
peerConnection.off();
|
|
},
|
|
|
|
getStats = function(callback) {
|
|
if (_gettingStats > 0) {
|
|
OT.debug("Still getting stats");
|
|
return;
|
|
}
|
|
|
|
var getStatsBlob = {};
|
|
|
|
// loops through all the peer connections get get the stats for them
|
|
for (var conn_id in _peerConnections) {
|
|
|
|
// this locks the getStats function so it isn't accidentally called
|
|
// again while already in the middle of getting stats
|
|
_gettingStats++;
|
|
|
|
getStatsBlob[conn_id] = null;
|
|
|
|
// just so we don't lose the connection id
|
|
(function(connection_id) {
|
|
_peerConnections[connection_id].getStats(function(parsed_stats) {
|
|
|
|
// one down
|
|
_gettingStats--;
|
|
|
|
if (parsed_stats) {
|
|
getStatsBlob[connection_id] = parsed_stats;
|
|
}
|
|
|
|
// if this reaches zero, that means all of the peer connections
|
|
// have returned their stats reports
|
|
if (_gettingStats == 0) {
|
|
callback(getStatsBlob);
|
|
}
|
|
});
|
|
})(conn_id);
|
|
}
|
|
},
|
|
|
|
/// Private Helpers
|
|
|
|
// Clean up our LocalMediaStream
|
|
cleanupLocalStream = function() {
|
|
if (_webRTCStream) {
|
|
// Stop revokes our access cam and mic access for this instance
|
|
// of localMediaStream.
|
|
_webRTCStream.stop();
|
|
_webRTCStream = null;
|
|
}
|
|
},
|
|
|
|
createPeerConnectionForRemote = function(remoteConnection) {
|
|
var peerConnection = _peerConnections[remoteConnection.id];
|
|
|
|
if (!peerConnection) {
|
|
var startConnectingTime = OT.$.now();
|
|
|
|
logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
|
|
|
|
// Cleanup our subscriber when they disconnect
|
|
remoteConnection.on('destroyed', this.cleanupSubscriber.bind(this, remoteConnection.id));
|
|
|
|
peerConnection = _peerConnections[remoteConnection.id] = new OT.PublisherPeerConnection(
|
|
remoteConnection,
|
|
_session,
|
|
_stream,
|
|
_webRTCStream
|
|
);
|
|
|
|
peerConnection.on({
|
|
connected: function() {
|
|
logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
|
|
parseInt(OT.$.now() - startConnectingTime, 10),
|
|
peerConnection.hasRelayCandidates
|
|
].join('|'));
|
|
|
|
// start recording the QoS for this peer connection
|
|
_qosIntervals[remoteConnection.id] = setInterval(function() {
|
|
recordQOS(remoteConnection.id)
|
|
}, 30000);
|
|
},
|
|
disconnected: onPeerDisconnected,
|
|
error: onPeerConnectionFailure
|
|
}, this);
|
|
|
|
peerConnection.init();
|
|
}
|
|
|
|
return peerConnection;
|
|
},
|
|
|
|
/// Chrome
|
|
|
|
// If mode is false, then that is the mode. If mode is true then we'll
|
|
// definitely display the button, but we'll defer the model to the
|
|
// Publishers buttonDisplayMode style property.
|
|
chromeButtonMode = function(mode) {
|
|
if (mode === false) return 'off';
|
|
|
|
var defaultMode = this.getStyle('buttonDisplayMode');
|
|
|
|
// The default model is false, but it's overridden by +mode+ being true
|
|
if (defaultMode === false) return 'on';
|
|
|
|
// defaultMode is either true or auto.
|
|
return defaultMode;
|
|
},
|
|
|
|
updateChromeForStyleChange = function(key, value, oldValue) {
|
|
if (!_chrome) return;
|
|
|
|
switch(key) {
|
|
case 'nameDisplayMode':
|
|
_chrome.name.setDisplayMode(value);
|
|
break;
|
|
|
|
case 'buttonDisplayMode':
|
|
case 'showMicButton':
|
|
case 'showSettingsButton':
|
|
// _chrome.settingsPanelButton.setDisplayMode(
|
|
// chromeButtonMode.call(this, this.getStyle('showSettingsButton'))
|
|
// );
|
|
|
|
// _chrome.muteButton.setDisplayMode(
|
|
// chromeButtonMode.call(this, this.getStyle('showMicButton'))
|
|
// );
|
|
}
|
|
},
|
|
|
|
_createChrome = function() {
|
|
_chrome = new OT.Chrome({
|
|
parent: _container.domElement
|
|
}).set({
|
|
name: new OT.Chrome.NamePanel({
|
|
name: _publishProperties.name,
|
|
mode: this.getStyle('nameDisplayMode')
|
|
}),
|
|
|
|
// settingsPanelButton: new OT.Chrome.SettingsPanelButton({
|
|
// mode: chromeButtonMode.call(this, this.getStyle('showSettingsButton'))
|
|
// }),
|
|
|
|
// Disabled until we can change the mic Volume through WebRTC
|
|
muteButton: new OT.Chrome.MuteButton({
|
|
muted: _publishProperties.publishAudio === false,
|
|
mode: chromeButtonMode.call(this, this.getStyle('showMicButton'))
|
|
}),
|
|
|
|
opentokButton: new OT.Chrome.OpenTokButton()
|
|
}).on({
|
|
// 'SettingsPanel:open': function() {
|
|
// if (!_chrome.settingsPanel) {
|
|
// // Add the settings panel, hide it initially
|
|
// _chrome.set(
|
|
// 'settingsPanel',
|
|
// new OT.Chrome.SettingsPanel({
|
|
// stream: _webRTCStream,
|
|
// mode: 'on'
|
|
// })
|
|
// );
|
|
// }
|
|
// else {
|
|
// _chrome.settingsPanel.setDisplayMode('on');
|
|
// }
|
|
|
|
// OT.$.addClass(_container, 'OT_reversed');
|
|
// },
|
|
|
|
// 'SettingsPanel:close': function() {
|
|
// OT.$.removeClass(_container, 'OT_reversed');
|
|
|
|
// // Hide the settings panel after our animation is complete
|
|
// setTimeout(function() {
|
|
// _chrome.settingsPanel.setDisplayMode('off');
|
|
// }, 3000);
|
|
// },
|
|
|
|
|
|
muted: this.publishAudio.bind(this, false),
|
|
unmuted: this.publishAudio.bind(this, true)
|
|
});
|
|
},
|
|
|
|
reset = function() {
|
|
if (_chrome) {
|
|
_chrome.destroy();
|
|
_chrome = null;
|
|
}
|
|
|
|
this.disconnect();
|
|
|
|
_microphone = null;
|
|
|
|
if (_targetElement) {
|
|
_targetElement.destroy();
|
|
_targetElement = null;
|
|
}
|
|
|
|
cleanupLocalStream();
|
|
|
|
if (_container) {
|
|
_container.destroy();
|
|
_container = null;
|
|
}
|
|
|
|
if (this.session) this._.unpublishFromSession(this.session);
|
|
|
|
// clear all the intervals
|
|
for (var conn_id in _qosIntervals) {
|
|
clearInterval(_qosIntervals[conn_id]);
|
|
delete _qosIntervals[conn_id];
|
|
}
|
|
|
|
_domId = null;
|
|
_stream = null;
|
|
_loaded = false;
|
|
|
|
_session = null;
|
|
_properties = null;
|
|
|
|
_state.set('NotPublishing');
|
|
}.bind(this);
|
|
|
|
|
|
this.publish = function(targetElement, properties) {
|
|
OT.debug("OT.Publisher: publish");
|
|
|
|
if ( _state.attemptingToPublish || _state.publishing ) reset();
|
|
_state.set('GetUserMedia');
|
|
|
|
_publishProperties = OT.$.defaults(properties || {}, {
|
|
publishAudio : true,
|
|
publishVideo : true,
|
|
mirror: true
|
|
});
|
|
|
|
_publishProperties.constraints = OT.$.defaults(_publishProperties.constraints || {}, defaultConstraints);
|
|
|
|
if (_publishProperties.style) {
|
|
this.setStyle(_publishProperties.style, null, true);
|
|
}
|
|
|
|
if (_publishProperties.name) {
|
|
_publishProperties.name = _publishProperties.name.toString();
|
|
}
|
|
|
|
_publishProperties.classNames = 'OT_root OT_publisher';
|
|
|
|
// Defer actually creating the publisher DOM nodes until we know
|
|
// the DOM is actually loaded.
|
|
OT.onLoad(function() {
|
|
_container = new OT.WidgetView(targetElement, _publishProperties);
|
|
_domId = _container.domId;
|
|
|
|
OT.$.getUserMedia(
|
|
_publishProperties.constraints,
|
|
onStreamAvailable.bind(this),
|
|
onStreamAvailableError.bind(this),
|
|
onAccessDialogOpened.bind(this),
|
|
onAccessDialogClosed.bind(this),
|
|
onAccessDenied.bind(this)
|
|
);
|
|
}, this);
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Starts publishing audio (if it is currently not being published)
|
|
* when the <code>value</code> is <code>true</code>; stops publishing audio
|
|
* (if it is currently being published) when the <code>value</code> is <code>false</code>.
|
|
*
|
|
* @param {Boolean} value Whether to start publishing audio (<code>true</code>)
|
|
* or not (<code>false</code>).
|
|
*
|
|
* @see <a href="TB.html#initPublisher">TB.initPublisher()</a>
|
|
* @see <a href="Stream.html#hasAudio">Stream.hasAudio</a>
|
|
* @see StreamPropertyChangedEvent
|
|
* @method #publishAudio
|
|
* @memberOf Publisher
|
|
*/
|
|
this.publishAudio = function(value) {
|
|
_publishProperties.publishAudio = value;
|
|
|
|
if (_microphone) {
|
|
_microphone.muted = !value;
|
|
}
|
|
|
|
if (_session && _stream) {
|
|
_session._.modifyStream(_stream.streamId, "hasAudio", value);
|
|
}
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Starts publishing video (if it is currently not being published)
|
|
* when the <code>value</code> is <code>true</code>; stops publishing video
|
|
* (if it is currently being published) when the <code>value</code> is <code>false</code>.
|
|
*
|
|
* @param {Boolean} value Whether to start publishing video (<code>true</code>)
|
|
* or not (<code>false</code>).
|
|
*
|
|
* @see <a href="TB.html#initPublisher">TB.initPublisher()</a>
|
|
* @see <a href="Stream.html#hasVideo">Stream.hasVideo</a>
|
|
* @see StreamPropertyChangedEvent
|
|
* @method #publishVideo
|
|
* @memberOf Publisher
|
|
*/
|
|
this.publishVideo = function(value) {
|
|
var oldValue = _publishProperties.publishVideo;
|
|
_publishProperties.publishVideo = value;
|
|
|
|
if (_session && _stream && _publishProperties.publishVideo !== oldValue) {
|
|
_session._.modifyStream(_stream.streamId, "hasVideo", value);
|
|
}
|
|
|
|
// We currently do this event if the value of publishVideo has not changed
|
|
// This is because the state of the video tracks enabled flag may not match
|
|
// the value of publishVideo at this point. This will be tidied up shortly.
|
|
if (_webRTCStream) {
|
|
var videoTracks = _webRTCStream.getVideoTracks();
|
|
for (var i=0, num=videoTracks.length; i<num; ++i) {
|
|
videoTracks[i].enabled = value;
|
|
}
|
|
}
|
|
|
|
if(_container) {
|
|
_container.showPoster = !value;
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
this.recordQOS = function() {
|
|
|
|
// have to record QoS for every peer connection
|
|
for (var conn_id in _peerConnections) {
|
|
recordQOS(conn_id);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deletes the Publisher object and removes it from the HTML DOM.
|
|
* @method #destroy
|
|
* @memberOf Publisher
|
|
* @return {Publisher} The Publisher.
|
|
*/
|
|
this.destroy = function(/* unused */ reason, quiet) {
|
|
reset();
|
|
|
|
if (quiet !== true) {
|
|
this.dispatchEvent(
|
|
new OT.DestroyedEvent(
|
|
OT.Event.names.PUBLISHER_DESTROYED,
|
|
this,
|
|
reason
|
|
),
|
|
this.off.bind(this)
|
|
);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* @methodOf Publisher
|
|
* @private
|
|
*/
|
|
this.disconnect = function() {
|
|
// Close the connection to each of our subscribers
|
|
for (var fromConnectionId in _peerConnections) {
|
|
this.cleanupSubscriber(fromConnectionId);
|
|
}
|
|
};
|
|
|
|
this.cleanupSubscriber = function(fromConnectionId) {
|
|
var pc = _peerConnections[fromConnectionId];
|
|
|
|
clearInterval(_qosIntervals[fromConnectionId]);
|
|
delete _qosIntervals[fromConnectionId];
|
|
|
|
if (pc) {
|
|
pc.destroy();
|
|
delete _peerConnections[fromConnectionId];
|
|
|
|
logAnalyticsEvent('disconnect', 'PeerConnection', 'subscriberConnection', fromConnectionId);
|
|
}
|
|
};
|
|
|
|
|
|
this.processMessage = function(type, fromConnection, message) {
|
|
OT.debug("OT.Publisher.processMessage: Received " + type + " from " + fromConnection.id);
|
|
OT.debug(message);
|
|
|
|
switch (type) {
|
|
case OT.Raptor.Actions.UNSUBSCRIBE:
|
|
this.cleanupSubscriber(fromConnection.id);
|
|
break;
|
|
|
|
default:
|
|
var peerConnection = createPeerConnectionForRemote.call(this, fromConnection);
|
|
peerConnection.processMessage(type, message);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the base-64-encoded string of PNG data representing the Publisher video.
|
|
*
|
|
* <p>You can use the string as the value for a data URL scheme passed to the src parameter of
|
|
* an image file, as in the following:</p>
|
|
*
|
|
* <pre>
|
|
* var imgData = publisher.getImgData();
|
|
*
|
|
* var img = document.createElement("img");
|
|
* img.setAttribute("src", "data:image/png;base64," + imgData);
|
|
* var imgWin = window.open("about:blank", "Screenshot");
|
|
* imgWin.document.write("<body></body>");
|
|
* imgWin.document.body.appendChild(img);
|
|
* </pre>
|
|
*
|
|
* @method #getImgData
|
|
* @memberOf Publisher
|
|
* @return {String} The base-64 encoded string. Returns an empty string if there is no video.
|
|
*/
|
|
|
|
this.getImgData = function() {
|
|
if (!_loaded) {
|
|
OT.error("OT.Publisher.getImgData: Cannot getImgData before the Publisher is publishing.");
|
|
|
|
return null;
|
|
}
|
|
|
|
return _targetElement.imgData;
|
|
};
|
|
|
|
|
|
// API Compatibility layer for Flash Publisher, this could do with some tidyup.
|
|
this._ = {
|
|
publishToSession: function(session) {
|
|
// Add session property to Publisher
|
|
this.session = session;
|
|
|
|
var createStream = function() {
|
|
// Bail if this.session is gone, it means we were unpublished
|
|
// before createStream could finish.
|
|
if (!this.session) return;
|
|
|
|
_state.set('PublishingToSession');
|
|
|
|
if (_streamCreatedTimeout) {
|
|
clearTimeout(_streamCreatedTimeout);
|
|
}
|
|
_streamCreatedTimeout = setTimeout(function () {
|
|
logAnalyticsEvent('publish', 'Failure', 'reason', 'StreamCreated: Timed out waiting for streamRegistered');
|
|
this.trigger('publishError', "StreamCreated: Timed out waiting for streamRegistered");
|
|
}.bind(this), 30000);
|
|
|
|
session._.createStream( this.guid,
|
|
_publishProperties && _publishProperties.name ? _publishProperties.name : "",
|
|
OT.VideoOrientation.ROTATED_NORMAL,
|
|
_targetElement.videoWidth, // actual width and height
|
|
_targetElement.videoHeight, // of the video stream.
|
|
_publishProperties.publishAudio,
|
|
_publishProperties.publishVideo );
|
|
};
|
|
|
|
if (_loaded) createStream.call(this);
|
|
else this.on("initSuccess", createStream, this);
|
|
|
|
logAnalyticsEvent('publish', 'Attempt', 'streamType', 'WebRTC');
|
|
|
|
return this;
|
|
}.bind(this),
|
|
|
|
unpublishFromSession: function(session) {
|
|
if (!this.session || session.id !== this.session.id) {
|
|
OT.warn("The publisher " + this.guid + " is trying to unpublish from a session " + session.id + " it is not attached to");
|
|
return this;
|
|
}
|
|
|
|
if (session.connected && this.stream) {
|
|
session._.destroyStream(this.stream.id);
|
|
}
|
|
|
|
// Disconnect immediately, rather than wait for the WebSocket to
|
|
// reply to our destroyStream message.
|
|
this.disconnect();
|
|
this.session = null;
|
|
|
|
// We're back to being a stand-alone publisher again.
|
|
_state.set('MediaBound');
|
|
|
|
logAnalyticsEvent('unpublish', 'Success', 'sessionId', session.id);
|
|
|
|
return this;
|
|
}.bind(this),
|
|
|
|
// Called once our stream has been ack'd as created by the session
|
|
streamRegisteredHandler: function(stream) {
|
|
clearTimeout(_streamCreatedTimeout);
|
|
_streamCreatedTimeout = null;
|
|
|
|
logAnalyticsEvent('publish', 'Success', 'streamType', 'WebRTC');
|
|
|
|
this.stream = stream;
|
|
this.stream.on('destroyed', this.disconnect, this);
|
|
|
|
var oldGuid = _guid;
|
|
_guid = OT.Publisher.nextId();
|
|
|
|
// Our publisher has changed guids, let people that rely
|
|
// on the GUID be non-volatile know.
|
|
if (oldGuid) {
|
|
this.trigger('idUpdated', oldGuid, _guid);
|
|
}
|
|
|
|
_state.set('Publishing');
|
|
_publishStartTime = new Date();
|
|
|
|
this.trigger('publishSuccess');
|
|
}.bind(this)
|
|
};
|
|
|
|
this.detectDevices = function() {
|
|
OT.warn("Fixme: Haven't implemented detectDevices");
|
|
};
|
|
|
|
this.detectMicActivity = function() {
|
|
OT.warn("Fixme: Haven't implemented detectMicActivity");
|
|
};
|
|
|
|
this.getEchoCancellationMode = function() {
|
|
OT.warn("Fixme: Haven't implemented getEchoCancellationMode");
|
|
return "fullDuplex";
|
|
};
|
|
|
|
this.setMicrophoneGain = function(value) {
|
|
OT.warn("Fixme: Haven't implemented setMicrophoneGain");
|
|
};
|
|
|
|
this.getMicrophoneGain = function() {
|
|
OT.warn("Fixme: Haven't implemented getMicrophoneGain");
|
|
return 0.5;
|
|
};
|
|
|
|
this.setCamera = function(value) {
|
|
OT.warn("Fixme: Haven't implemented setCamera");
|
|
};
|
|
|
|
this.setMicrophone = function(value) {
|
|
OT.warn("Fixme: Haven't implemented setMicrophone");
|
|
};
|
|
|
|
|
|
Object.defineProperties(this, {
|
|
id: {
|
|
get: function() { return _domId; },
|
|
enumerable: true
|
|
},
|
|
|
|
guid: {
|
|
get: function() { return _guid; },
|
|
enumerable: true
|
|
},
|
|
|
|
stream: {
|
|
get: function() { return _stream; },
|
|
set: function(stream) { _stream = stream; },
|
|
enumerable: true
|
|
},
|
|
|
|
streamId: {
|
|
get: function() {
|
|
if (!_stream) return null;
|
|
|
|
return _stream.id;
|
|
},
|
|
enumerable: true
|
|
},
|
|
|
|
targetElement: {
|
|
get: function() { return _targetElement.domElement; }
|
|
},
|
|
|
|
domId: {
|
|
get: function() { return _domId; }
|
|
},
|
|
|
|
session: {
|
|
get: function() { return _session; },
|
|
set: function(session) { _session = session; },
|
|
enumerable: true
|
|
},
|
|
|
|
isWebRTC: {
|
|
get: function() { return true; }
|
|
},
|
|
|
|
loading: {
|
|
get: function(){ return _container && _container.loading }
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(this._, 'webRtcStream', {
|
|
get: function() { return _webRTCStream; }
|
|
});
|
|
|
|
this.on('styleValueChanged', updateChromeForStyleChange, this);
|
|
_state = new OT.PublishingState(stateChangeFailed);
|
|
|
|
|
|
/**
|
|
* Dispatched when the user has clicked the Allow button, granting the
|
|
* app access to the camera and microphone.
|
|
* @name accessAllowed
|
|
* @event
|
|
* @memberof Publisher
|
|
*/
|
|
|
|
/**
|
|
* Dispatched when the user has clicked the Deny button, preventing the
|
|
* app from having access to the camera and microphone.
|
|
* @name accessDenied
|
|
* @event
|
|
* @memberof Publisher
|
|
*/
|
|
|
|
/**
|
|
* Dispatched when the Allow/Deny dialog box is opened. (This is the dialog box in which the user can grant
|
|
* the app access to the camera and microphone.)
|
|
* @name accessDialogOpened
|
|
* @event
|
|
* @memberof Publisher
|
|
*/
|
|
|
|
/**
|
|
* Dispatched when the Allow/Deny box is closed. (This is the dialog box in which the user can grant
|
|
* the app access to the camera and microphone.)
|
|
* @name accessDialogClosed
|
|
* @event
|
|
* @memberof Publisher
|
|
*/
|
|
|
|
};
|
|
|
|
// Helper function to generate unique publisher ids
|
|
OT.Publisher.nextId = OT.$.uuid;
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
|
|
/**
|
|
* The Subscriber object is a representation of the local video element that is playing back a remote stream.
|
|
* The Subscriber object includes methods that let you disable and enable local audio playback for the subscribed stream.
|
|
* The <code>subscribe()</code> method of the {@link Session} object returns a Subscriber object.
|
|
*
|
|
* @property {String} id The DOM ID of the Subscriber.
|
|
* @property {Stream} stream The stream to which you are subscribing.
|
|
* @class Subscriber
|
|
* @augments EventDispatcher
|
|
*/
|
|
OT.Subscriber = function(targetElement, options) {
|
|
var _widgetId = OT.$.uuid(),
|
|
_domId = targetElement || _widgetId,
|
|
_container,
|
|
_streamContainer,
|
|
_chrome,
|
|
_stream,
|
|
_fromConnectionId,
|
|
_peerConnection,
|
|
_session = options.session,
|
|
_subscribeStartTime,
|
|
_startConnectingTime,
|
|
_qosInterval,
|
|
_properties = OT.$.clone(options),
|
|
_analytics = new OT.Analytics(),
|
|
_audioVolume = 50,
|
|
_gettingStats = 0,
|
|
_state,
|
|
_subscribeAudioFalseWorkaround, // OPENTOK-6844
|
|
_prevStats = {
|
|
"timeStamp" : OT.$.now()
|
|
};
|
|
|
|
|
|
if (!_session) {
|
|
OT.handleJsException("Subscriber must be passed a session option", 2000, {
|
|
session: _session,
|
|
target: this
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
OT.$.eventing(this);
|
|
|
|
OT.StylableComponent(this, {
|
|
nameDisplayMode: "auto",
|
|
buttonDisplayMode: "auto",
|
|
backgroundImageURI: null
|
|
});
|
|
|
|
var logAnalyticsEvent = function(action, variation, payloadType, payload) {
|
|
_analytics.logEvent({
|
|
action: action,
|
|
variation: variation,
|
|
payload_type: payloadType,
|
|
payload: payload,
|
|
stream_id: _stream ? _stream.id : null,
|
|
session_id: _session ? _session.sessionId : null,
|
|
connection_id: _session && _session.connected ? _session.connection.connectionId : null,
|
|
partner_id: _session && _session.connected ? _session.sessionInfo.partnerId : null,
|
|
widget_id: _widgetId,
|
|
widget_type: 'Subscriber'
|
|
});
|
|
},
|
|
|
|
recordQOS = function() {
|
|
if(_state.subscribing && _session && _session.connected) {
|
|
var QoS_blob = {
|
|
widget_type: 'Subscriber',
|
|
stream_type : 'WebRTC',
|
|
session_id: _session ? _session.sessionId : null,
|
|
connectionId: _session ? _session.connection.connectionId : null,
|
|
media_server_name: _session ? _session.sessionInfo.messagingServer : null,
|
|
p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
|
|
partner_id: _session ? _session.apiKey : null,
|
|
stream_id: _stream.id,
|
|
widget_id: _widgetId,
|
|
version: OT.properties.version,
|
|
duration: parseInt(OT.$.now() - _subscribeStartTime, 10),
|
|
remote_connection_id: _stream.connection.connectionId
|
|
};
|
|
|
|
|
|
// get stats for each connection id
|
|
_peerConnection.getStats(_prevStats, function(stats) {
|
|
if (stats) {
|
|
for (stat_index in stats) {
|
|
QoS_blob[stat_index] = stats[stat_index];
|
|
}
|
|
}
|
|
_analytics.logQOS(QoS_blob);
|
|
});
|
|
}
|
|
},
|
|
|
|
stateChangeFailed = function(changeFailed) {
|
|
OT.error("Subscriber State Change Failed: ", changeFailed.message);
|
|
OT.debug(changeFailed);
|
|
},
|
|
|
|
onLoaded = function() {
|
|
if (_state.subscribing || !_streamContainer) return;
|
|
|
|
OT.debug("OT.Subscriber.onLoaded");
|
|
|
|
_state.set('Subscribing');
|
|
_subscribeStartTime = OT.$.now();
|
|
|
|
logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
|
|
parseInt(_subscribeStartTime - _startConnectingTime, 10),
|
|
_peerConnection && _peerConnection.hasRelayCandidates
|
|
].join('|'));
|
|
|
|
_qosInterval = setInterval(recordQOS, 30000);
|
|
|
|
if(_subscribeAudioFalseWorkaround) {
|
|
_subscribeAudioFalseWorkaround = null;
|
|
this.subscribeToVideo(false);
|
|
}
|
|
|
|
_container.loading = false;
|
|
|
|
_createChrome.call(this);
|
|
|
|
this.trigger('subscribeSuccess', this);
|
|
this.trigger('loaded', this);
|
|
|
|
|
|
logAnalyticsEvent('subscribe', 'Success', 'streamId', _stream.id);
|
|
},
|
|
|
|
onDisconnected = function() {
|
|
OT.debug("OT.Subscriber has been disconnected from the Publisher's PeerConnection");
|
|
|
|
if (_state.attemptingToSubscribe) {
|
|
// subscribing error
|
|
_state.set('Failed');
|
|
this.trigger('subscribeError', "ClientDisconnected");
|
|
}
|
|
else if (_state.subscribing) {
|
|
_state.set('Failed');
|
|
|
|
// we were disconnected after we were already subscribing
|
|
// probably do nothing?
|
|
}
|
|
|
|
this.disconnect();
|
|
},
|
|
|
|
|
|
onPeerConnectionFailure = function(code, reason) {
|
|
if (_state.attemptingToSubscribe) {
|
|
// We weren't subscribing yet so this was a failure in setting
|
|
// up the PeerConnection or receiving the initial stream.
|
|
logAnalyticsEvent('createPeerConnection', 'Failure', 'reason|hasRelayCandidates', [
|
|
"Subscriber PeerConnection Error: " + reason,
|
|
_peerConnection && _peerConnection.hasRelayCandidates
|
|
].join('|'));
|
|
|
|
_state.set('Failed');
|
|
this.trigger('subscribeError', reason);
|
|
}
|
|
else if (_state.subscribing) {
|
|
// we were disconnected after we were already subscribing
|
|
_state.set('Failed');
|
|
this.trigger('error', reason);
|
|
}
|
|
|
|
this.disconnect();
|
|
|
|
logAnalyticsEvent('subscribe', 'Failure', 'reason', reason + ":Subscriber PeerConnection Error");
|
|
|
|
OT.handleJsException("Subscriber PeerConnection Error: " + reason, OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
|
|
session: _session,
|
|
target: this
|
|
});
|
|
_showError.call(this, reason);
|
|
},
|
|
|
|
onRemoteStreamAdded = function(webOTStream) {
|
|
OT.debug("OT.Subscriber.onRemoteStreamAdded");
|
|
|
|
_state.set('BindingRemoteStream');
|
|
|
|
// Disable the audio/video, if needed
|
|
this.subscribeToAudio(_properties.subscribeToAudio);
|
|
|
|
var preserver = _subscribeAudioFalseWorkaround;
|
|
this.subscribeToVideo(_properties.subscribeToVideo);
|
|
_subscribeAudioFalseWorkaround = preserver;
|
|
|
|
var streamElement = new OT.VideoElement();
|
|
|
|
// Initialize the audio volume
|
|
streamElement.setAudioVolume(_audioVolume);
|
|
streamElement.on({
|
|
streamBound: onLoaded,
|
|
loadError: onPeerConnectionFailure,
|
|
error: onPeerConnectionFailure
|
|
}, this);
|
|
|
|
streamElement.bindToStream(webOTStream);
|
|
_container.video = streamElement;
|
|
|
|
_streamContainer = streamElement;
|
|
|
|
_streamContainer.orientation = {
|
|
width: _stream.videoDimensions.width,
|
|
height: _stream.videoDimensions.height,
|
|
videoOrientation: _stream.videoDimensions.orientation
|
|
};
|
|
|
|
logAnalyticsEvent('createPeerConnection', 'StreamAdded', '', '');
|
|
this.trigger('streamAdded', this);
|
|
},
|
|
|
|
onRemoteStreamRemoved = function(webOTStream) {
|
|
OT.debug("OT.Subscriber.onStreamRemoved");
|
|
|
|
if (_streamContainer.stream == webOTStream) {
|
|
_streamContainer.destroy();
|
|
_streamContainer = null;
|
|
}
|
|
|
|
|
|
this.trigger('streamRemoved', this);
|
|
},
|
|
|
|
streamUpdated = function(event) {
|
|
switch(event.changedProperty) {
|
|
case 'orientation':
|
|
_streamContainer.orientation = {
|
|
width: _stream.videoDimensions.width,
|
|
height: _stream.videoDimensions.height,
|
|
videoOrientation: _stream.videoDimensions.orientation
|
|
};
|
|
break;
|
|
|
|
case 'hasVideo':
|
|
if(_container) {
|
|
_container.showPoster = !(_stream.hasVideo && _properties.subscribeToVideo);
|
|
}
|
|
|
|
break;
|
|
|
|
case 'hasAudio':
|
|
// noop
|
|
}
|
|
},
|
|
|
|
/// Chrome
|
|
|
|
updateChromeForStyleChange = function(key, value, oldValue) {
|
|
if (!_chrome) return;
|
|
|
|
switch(key) {
|
|
case 'nameDisplayMode':
|
|
_chrome.name.setDisplayMode(value);
|
|
break;
|
|
|
|
case 'buttonDisplayMode':
|
|
// _chrome.muteButton.setDisplayMode(value);
|
|
}
|
|
},
|
|
|
|
_createChrome = function() {
|
|
_chrome = new OT.Chrome({
|
|
parent: _container.domElement
|
|
}).set({
|
|
name: new OT.Chrome.NamePanel({
|
|
name: _properties.name,
|
|
mode: this.getStyle('nameDisplayMode')
|
|
}),
|
|
|
|
// // Disabled until we can change the mic Volume through WebRTC
|
|
// muteButton: new OT.Chrome.MicVolume({
|
|
// muted: false,
|
|
// mode: this.getStyle('buttonDisplayMode')
|
|
// }),
|
|
|
|
opentokButton: new OT.Chrome.OpenTokButton()
|
|
}).on({
|
|
muted: function() {
|
|
// @todo turn off audio on the web rtc stream
|
|
},
|
|
|
|
unmuted: function() {
|
|
// @todo turn on audio on the web rtc stream
|
|
}
|
|
});
|
|
},
|
|
|
|
_showError = function(errorMsg) {
|
|
// Display the error message inside the container, assuming it's
|
|
// been created by now.
|
|
if (_container) _container.addError(errorMsg);
|
|
};
|
|
|
|
|
|
this.recordQOS = function() {
|
|
recordQOS();
|
|
};
|
|
|
|
this.subscribe = function(stream) {
|
|
OT.debug("OT.Subscriber: subscribe to " + stream.id);
|
|
|
|
if (_state.subscribing) {
|
|
// @todo error
|
|
OT.error("OT.Subscriber.Subscribe: Cannot subscribe, already subscribing.");
|
|
return false;
|
|
}
|
|
|
|
_state.set('Init');
|
|
|
|
if (!stream) {
|
|
// @todo error
|
|
OT.error("OT.Subscriber: No stream parameter.");
|
|
return false;
|
|
}
|
|
|
|
if (_stream) {
|
|
// @todo error
|
|
OT.error("OT.Subscriber: Already subscribed");
|
|
return false;
|
|
}
|
|
|
|
_stream = stream;
|
|
_stream.on({
|
|
updated: streamUpdated,
|
|
destroyed: this.disconnect
|
|
}, this);
|
|
|
|
_fromConnectionId = stream.connection.connectionId;
|
|
_properties.name = _stream.name;
|
|
_properties.classNames = 'OT_root OT_subscriber';
|
|
|
|
if (_properties.style) {
|
|
this.setStyle(_properties.style, null, true);
|
|
}
|
|
if (_properties.audioVolume) {
|
|
this.setAudioVolume(_properties.audioVolume);
|
|
}
|
|
|
|
_properties.subscribeToAudio = OT.$.castToBoolean(_properties.subscribeToAudio, true);
|
|
_properties.subscribeToVideo = OT.$.castToBoolean(_properties.subscribeToVideo, true);
|
|
|
|
_container = new OT.WidgetView(targetElement, _properties);
|
|
_domId = _container.domId;
|
|
|
|
if(!_properties.subscribeToVideo && OT.$.browser() == 'Chrome') {
|
|
_subscribeAudioFalseWorkaround = true;
|
|
_properties.subscribeToVideo = true;
|
|
}
|
|
|
|
_startConnectingTime = OT.$.now();
|
|
|
|
if (_stream.connection.id !== _session.connection.id) {
|
|
logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
|
|
|
|
_state.set('ConnectingToPeer');
|
|
|
|
_peerConnection = new OT.SubscriberPeerConnection(_stream.connection, _session, _stream, _properties);
|
|
|
|
_peerConnection.on({
|
|
disconnected: onDisconnected,
|
|
error: onPeerConnectionFailure,
|
|
remoteStreamAdded: onRemoteStreamAdded,
|
|
remoteStreamRemoved: onRemoteStreamRemoved
|
|
}, this);
|
|
|
|
// initialize the peer connection AFTER we've added the event listeners
|
|
_peerConnection.init();
|
|
}
|
|
else {
|
|
logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
|
|
|
|
// Subscribe to yourself edge-case
|
|
onRemoteStreamAdded.call(this, _session.getPublisherForStream(_stream)._.webRtcStream);
|
|
}
|
|
|
|
logAnalyticsEvent('subscribe', 'Attempt', 'streamId', _stream.id);
|
|
|
|
return this;
|
|
};
|
|
|
|
this.destroy = function(/* unused */ reason, quiet) {
|
|
clearInterval(_qosInterval);
|
|
_qosInterval = null;
|
|
|
|
this.disconnect();
|
|
|
|
if (_chrome) {
|
|
_chrome.destroy();
|
|
_chrome = null;
|
|
}
|
|
|
|
if (_container) {
|
|
_container.destroy();
|
|
_container = null;
|
|
}
|
|
|
|
if (_stream && !_stream.destroyed) logAnalyticsEvent('unsubscribe', null, 'streamId', _stream.id);
|
|
|
|
_domId = null;
|
|
_stream = null;
|
|
|
|
_session = null;
|
|
_properties = null;
|
|
|
|
if (quiet !== true) {
|
|
this.dispatchEvent(
|
|
new OT.DestroyedEvent(
|
|
OT.Event.names.SUBSCRIBER_DESTROYED,
|
|
this,
|
|
reason
|
|
),
|
|
this.off.bind(this)
|
|
);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
this.disconnect = function() {
|
|
_state.set('NotSubscribing');
|
|
|
|
if (_streamContainer) {
|
|
_streamContainer.destroy();
|
|
_streamContainer = null;
|
|
}
|
|
|
|
if (_peerConnection) {
|
|
_peerConnection.destroy();
|
|
_peerConnection = null;
|
|
|
|
logAnalyticsEvent('disconnect', 'PeerConnection', 'streamId', _stream.id);
|
|
}
|
|
};
|
|
|
|
this.processMessage = function(type, fromConnection, message) {
|
|
OT.debug("OT.Subscriber.processMessage: Received " + type + " message from " + fromConnection.id);
|
|
OT.debug(message);
|
|
|
|
if (_fromConnectionId != fromConnection.id) {
|
|
_fromConnectionId = fromConnection.id;
|
|
}
|
|
|
|
if (_peerConnection) {
|
|
_peerConnection.processMessage(type, message);
|
|
}
|
|
};
|
|
|
|
this.updateQuality = function(quality) {
|
|
// Currently, this only disables the video and disregards
|
|
// the quality.
|
|
OT.warn("Due to high packet loss and low bandwidth, video has been disabled");
|
|
this.subscribeToVideo(false);
|
|
this.dispatchEvent(new OT.Event("videoDisabled"));
|
|
};
|
|
|
|
/**
|
|
* Return the base-64-encoded string of PNG data representing the Subscriber video.
|
|
*
|
|
* <p>You can use the string as the value for a data URL scheme passed to the src parameter of
|
|
* an image file, as in the following:</p>
|
|
*
|
|
* <pre>
|
|
* var imgData = subscriber.getImgData();
|
|
*
|
|
* var img = document.createElement("img");
|
|
* img.setAttribute("src", "data:image/png;base64," + imgData);
|
|
* var imgWin = window.open("about:blank", "Screenshot");
|
|
* imgWin.document.write("<body></body>");
|
|
* imgWin.document.body.appendChild(img);
|
|
* </pre>
|
|
* @method #getImgData
|
|
* @memberOf Subscriber
|
|
* @return {String} The base-64 encoded string. Returns an empty string if there is no video.
|
|
*/
|
|
this.getImgData = function() {
|
|
if (!this.subscribing) {
|
|
OT.error("OT.Subscriber.getImgData: Cannot getImgData before the Subscriber is subscribing.");
|
|
return null;
|
|
}
|
|
|
|
return _streamContainer.imgData;
|
|
};
|
|
|
|
/**
|
|
* Sets the audio volume, between 0 and 100, of the Subscriber.
|
|
*
|
|
* <p>You can set the initial volume when you call the <code>Session.subscribe()</code>
|
|
* method. Pass a <code>audioVolume</code> property of the <code>properties</code> parameter
|
|
* of the method.</p>
|
|
*
|
|
* @param {Number} value The audio volume, between 0 and 100.
|
|
*
|
|
* @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the following:
|
|
*
|
|
* <pre>mySubscriber.setAudioVolume(50).setStyle(newStyle);</pre>
|
|
*
|
|
* @see <a href="#getAudioVolume">getAudioVolume()</a>
|
|
* @see <a href="Session.html#subscribe">Session.subscribe()</a>
|
|
* @method #setAudioVolume
|
|
* @memberOf Subscriber
|
|
*/
|
|
this.setAudioVolume = function(value) {
|
|
value = parseInt(value, 10);
|
|
if (isNaN(value)) {
|
|
OT.error("OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100");
|
|
return this;
|
|
}
|
|
_audioVolume = Math.max(0, Math.min(100, value));
|
|
if (_audioVolume != value) {
|
|
OT.warn("OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100");
|
|
}
|
|
if (_streamContainer) {
|
|
_streamContainer.setAudioVolume(_audioVolume);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Returns the audio volume, between 0 and 100, of the Subscriber.
|
|
*
|
|
* <p>Generally you use this method in conjunction with the <code>setAudioVolume()</code> method.</p>
|
|
*
|
|
* @return {Number} The audio volume, between 0 and 100, of the Subscriber.
|
|
* @see <a href="#setAudioVolume">setAudioVolume()</a>
|
|
* @method #getAudioVolume
|
|
* @memberOf Subscriber
|
|
*/
|
|
this.getAudioVolume = function() {
|
|
if (_streamContainer) return _streamContainer.getAudioVolume();
|
|
else return _audioVolume;
|
|
};
|
|
|
|
/**
|
|
* Toggles audio on and off. Starts subscribing to audio (if it is available and currently not being
|
|
* subscribed to) when the <code>value</code> is <code>true</code>; stops subscribing to audio
|
|
* (if it is currently being subscribed to) when the <code>value</code> is <code>false</code>.
|
|
* <p>
|
|
* <i>Note:</i> This method only affects the local playback of audio. It has no impact on the audio
|
|
* for other connections subscribing to the same stream. If the Publsher is not publishing audio,
|
|
* enabling the Subscriber audio will have no practical effect.
|
|
* </p>
|
|
*
|
|
* @param {Boolean} value Whether to start subscribing to audio (<code>true</code>) or not (<code>false</code>).
|
|
*
|
|
* @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the following:
|
|
*
|
|
* <pre>mySubscriber.subscribeToAudio(true).subscribeToVideo(false);</pre>
|
|
*
|
|
* @see <a href="#subscribeToVideo">subscribeToVideo()</a>
|
|
* @see <a href="Session.html#subscribe">Session.subscribe()</a>
|
|
* @see <a href="StreamPropertyChangedEvent.html">StreamPropertyChangedEvent</a>
|
|
*
|
|
* @method #subscribeToAudio
|
|
* @memberOf Subscriber
|
|
*/
|
|
this.subscribeToAudio = function(p_value) {
|
|
var value = OT.$.castToBoolean(p_value, true);
|
|
|
|
if (_peerConnection) {
|
|
_peerConnection.subscribeToAudio(value);
|
|
|
|
if (_session && _stream && value !== _properties.subscribeToAudio) {
|
|
_session._.modifySubscriber(this, "hasAudio", value);
|
|
}
|
|
}
|
|
|
|
_properties.subscribeToAudio = value;
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* Toggles video on and off. Starts subscribing to video (if it is available and currently not being
|
|
* subscribed to) when the <code>value</code> is <code>true</code>; stops subscribing to video
|
|
* (if it is currently being subscribed to) when the <code>value</code> is <code>false</code>.
|
|
* <p>
|
|
* <i>Note:</i> This method only affects the local playback of video. It has no impact on the video
|
|
* for other connections subscribing to the same stream. If the Publsher is not publishing video,
|
|
* enabling the Subscriber video will have no practical video.
|
|
* </p>
|
|
*
|
|
* @param {Boolean} value Whether to start subscribing to video (<code>true</code>) or not (<code>false</code>).
|
|
*
|
|
* @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the following:
|
|
*
|
|
* <pre>mySubscriber.subscribeToVideo(true).subscribeToAudio(false);</pre>
|
|
*
|
|
* @see <a href="#subscribeToAudio">subscribeToAudio()</a>
|
|
* @see <a href="Session.html#subscribe">Session.subscribe()</a>
|
|
* @see <a href="StreamPropertyChangedEvent.html">StreamPropertyChangedEvent</a>
|
|
*
|
|
* @method #subscribeToVideo
|
|
* @memberOf Subscriber
|
|
*/
|
|
this.subscribeToVideo = function(p_value) {
|
|
if(_subscribeAudioFalseWorkaround && p_value == true) {
|
|
// Turn off the workaround if they enable the video
|
|
_subscribeAudioFalseWorkaround = false;
|
|
return;
|
|
}
|
|
|
|
var value = OT.$.castToBoolean(p_value, true);
|
|
|
|
if(_container) {
|
|
_container.showPoster = !(value && _stream.hasVideo);
|
|
if(value && _container.video) {
|
|
_container.loading = value;
|
|
_container.video.whenTimeIncrements(function(){
|
|
_container.loading = false;
|
|
}, this);
|
|
}
|
|
}
|
|
|
|
if (_peerConnection) {
|
|
_peerConnection.subscribeToVideo(value);
|
|
|
|
if (_session && _stream && value !== _properties.subscribeToVideo) {
|
|
_session._.modifySubscriber(this, "hasVideo", value);
|
|
}
|
|
}
|
|
|
|
_properties.subscribeToVideo = value;
|
|
|
|
return this;
|
|
};
|
|
|
|
Object.defineProperties(this, {
|
|
id: {
|
|
get: function() { return _domId; },
|
|
enumerable: true
|
|
},
|
|
|
|
widgetId: {
|
|
get: function() { return _widgetId; }
|
|
},
|
|
|
|
stream: {
|
|
get: function() { return _stream; },
|
|
enumerable: true
|
|
},
|
|
|
|
streamId: {
|
|
get: function() {
|
|
if (!_stream) return null;
|
|
|
|
return _stream.id;
|
|
},
|
|
enumerable: true
|
|
},
|
|
|
|
targetElement: {
|
|
get: function() { return _streamContainer ? _streamContainer.domElement : null; }
|
|
},
|
|
|
|
subscribing: {
|
|
get: function() { return _state.subscribing; },
|
|
enumerable: true
|
|
},
|
|
|
|
isWebRTC: {
|
|
get: function() { return true; }
|
|
},
|
|
|
|
loading: {
|
|
get: function(){ return _container && _container.loading }
|
|
},
|
|
|
|
session: {
|
|
get: function() { return _session; }
|
|
}
|
|
});
|
|
|
|
this.on('styleValueChanged', updateChromeForStyleChange, this);
|
|
|
|
_state = new OT.SubscribingState(stateChangeFailed);
|
|
|
|
/**
|
|
* Dispatched when the OpenTok media server stops sending video to the subscriber.
|
|
* This feature of the OpenTok media server has a subscriber drop the video stream
|
|
* when connectivity degrades. The subscriber continues to receive the audio stream,
|
|
* if there is one. This feature is only available in sessions that use the OpenTok
|
|
* media server, not in peer-to-peer sessions.
|
|
* @name videoDisabled
|
|
* @event
|
|
* @memberof Subscriber
|
|
*/
|
|
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
OT.SessionInfo = function(xmlDocument) {
|
|
var sessionXML = null;
|
|
|
|
this.sessionId = null;
|
|
this.partnerId = null;
|
|
this.sessionStatus = null;
|
|
this.p2pEnabled = false;
|
|
|
|
this.messagingServer = null;
|
|
this.iceServers = null;
|
|
|
|
OT.log("SessionInfo Response:")
|
|
OT.log(xmlDocument);
|
|
|
|
if (xmlDocument && xmlDocument.documentElement && xmlDocument.documentElement.firstElementChild !== null) {
|
|
sessionXML = xmlDocument.documentElement.firstElementChild;
|
|
}
|
|
|
|
var element = sessionXML.firstElementChild;
|
|
do {
|
|
switch (element.localName) {
|
|
case "session_id":
|
|
this.sessionId = element.textContent;
|
|
break;
|
|
|
|
case "partner_id":
|
|
this.partnerId = element.textContent;
|
|
break;
|
|
|
|
case "session_status":
|
|
this.sessionStatus = element.textContent;
|
|
break;
|
|
|
|
case "messaging_server_url":
|
|
this.messagingServer = element.textContent;
|
|
break;
|
|
|
|
case "ice_servers":
|
|
// <ice_servers>
|
|
// <ice_server url="foo.com" />
|
|
// <ice_server url="bar.com" credential="xxx" />
|
|
// </ice_servers>
|
|
|
|
this.iceServers = normaliseIceServers( parseIceServersXml(element.childNodes) );
|
|
break;
|
|
|
|
case "properties":
|
|
var property = element.firstElementChild;
|
|
if (property) {
|
|
do {
|
|
if (property.localName === "p2p" && property.firstElementChild !== null) {
|
|
this.p2pEnabled = (property.firstElementChild.textContent === "enabled");
|
|
break;
|
|
}
|
|
} while (property = property.nextElementSibling);
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
// OT.debug("OT.SessionInfo element was not handled (" + element.localName + ")");
|
|
break;
|
|
}
|
|
|
|
} while (element = element.nextElementSibling);
|
|
|
|
if (!this.iceServers || this.iceServers.length === 0) {
|
|
// We haven't got any ICE Servers from Anvil, default to something
|
|
OT.warn("SessionInfo contained not ICE Servers, using the default");
|
|
this.iceServers = [ {"url": "stun:stun.l.google.com:19302"} ];
|
|
}
|
|
|
|
//we've parsed the XML into the object
|
|
|
|
sessionXML = null;
|
|
};
|
|
|
|
|
|
// Retrieves Session Info for +session+. The SessionInfo object will be passed
|
|
// to the +onSuccess+ callback. The +onFailure+ callback will be passed an error
|
|
// object and the DOMEvent that relates to the error.
|
|
OT.SessionInfo.get = function(session, onSuccess, onFailure) {
|
|
var sessionInfoURL = OT.properties.apiURL + '/session/' + session.id + "?extended=true",
|
|
|
|
startTime = OT.$.now(),
|
|
|
|
validateRawSessionInfo = function(sessionInfo) {
|
|
session.logEvent('Instrumentation', null, 'gsi', OT.$.now() - startTime);
|
|
|
|
var error = parseErrorFromXMLDocument(sessionInfo);
|
|
|
|
if (error === false) {
|
|
onGetResponseCallback(session, onSuccess, sessionInfo);
|
|
}
|
|
else {
|
|
onGetErrorCallback(session, onFailure, error);
|
|
}
|
|
};
|
|
|
|
session.logEvent('getSessionInfo', 'Attempt', 'api_url', OT.properties.apiURL);
|
|
|
|
OT.$.getXML(sessionInfoURL, {
|
|
headers: {"X-TB-TOKEN-AUTH": session.token, "X-TB-VERSION": 1},
|
|
|
|
success: validateRawSessionInfo,
|
|
|
|
error: function(event) {
|
|
onGetErrorCallback(session, onFailure, parseErrorFromXMLDocument(event.target.responseXML));
|
|
}
|
|
});
|
|
};
|
|
|
|
var messageServerToClientErrorCodes = {};
|
|
messageServerToClientErrorCodes['404'] = OT.ExceptionCodes.INVALID_SESSION_ID;
|
|
messageServerToClientErrorCodes['403'] = OT.ExceptionCodes.AUTHENTICATION_ERROR;
|
|
|
|
// Return the error in +xmlDocument+, if there is one. Otherwise it will return
|
|
// false.
|
|
parseErrorFromXMLDocument = function(xmlDocument) {
|
|
if (xmlDocument && xmlDocument.documentElement && xmlDocument.documentElement.firstElementChild !== null) {
|
|
var errorNodes = xmlDocument.evaluate('//error', xmlDocument.documentElement, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null ),
|
|
numErrorNodes = errorNodes.snapshotLength;
|
|
|
|
if (numErrorNodes === 0) return false;
|
|
|
|
for (var i=0; i<numErrorNodes; ++i) {
|
|
var errorNode = errorNodes.snapshotItem(i);
|
|
|
|
return {
|
|
code: errorNode.getAttribute('code'),
|
|
message: errorNode.firstElementChild.getAttribute('message')
|
|
};
|
|
}
|
|
}
|
|
|
|
// There was an error, but we couldn't find the error info.
|
|
return {
|
|
code: null,
|
|
message: "Unknown error: getSessionInfo XML response was badly formed " +
|
|
(xmlDocument && xmlDocument.documentElement && xmlDocument.documentElement.innerHTML)
|
|
};
|
|
};
|
|
|
|
onGetResponseCallback = function(session, onSuccess, rawSessionInfo) {
|
|
session.logEvent('getSessionInfo', 'Success', 'api_url', OT.properties.apiURL);
|
|
|
|
onSuccess( new OT.SessionInfo(rawSessionInfo) );
|
|
};
|
|
|
|
onGetErrorCallback = function(session, onFailure, error) {
|
|
TB.handleJsException("TB.SessionInfoError :: Unable to get session info " + error.message, messageServerToClientErrorCodes[error.code], {
|
|
session: session
|
|
});
|
|
|
|
session.logEvent('Connect', 'Failure', 'errorMessage', "GetSessionInfo:" + error.code + ": Unable to get session info " + error.message);
|
|
onFailure(error, session);
|
|
};
|
|
|
|
parseIceServersXml = function (nodes) {
|
|
var iceServers = [],
|
|
iceServer,
|
|
attributes;
|
|
|
|
for (var i=0, numNodes=nodes.length; i<numNodes; ++i) {
|
|
if (nodes[i].localName === 'ice_server') {
|
|
// @todo parse either a URL node, or a URL and credentials
|
|
attributes = nodes[i].attributes;
|
|
iceServer = {
|
|
url: attributes.getNamedItem('url').nodeValue
|
|
};
|
|
|
|
if (attributes.getNamedItem('credential') && attributes.getNamedItem('credential').nodeValue.length) {
|
|
iceServer.credential = attributes.getNamedItem('credential').nodeValue;
|
|
}
|
|
|
|
iceServers.push(iceServer);
|
|
}
|
|
}
|
|
|
|
return iceServers;
|
|
};
|
|
|
|
// This is ugly. Here we are handling the following:
|
|
// 1. Firefox doesn't support TURN until version 25. We fallback to STUN
|
|
// in previous versions.
|
|
//
|
|
// 2. In FF >= 23 the new syntax for declaring Ice Servers with credentials
|
|
// is used. i.e. url, credential, and username as opposed to just url and credential.
|
|
//
|
|
// 3. FF >= 27 supports the TURN transport parameter, in older versions we need
|
|
// to strip it out.
|
|
//
|
|
normaliseIceServers = function (iceServers) {
|
|
var userAgent = navigator.userAgent.match(/(Firefox)\/([0-9]+\.[0-9]+)/),
|
|
firefoxVersion = userAgent ? parseFloat(userAgent[2], 10) : void 0,
|
|
bits;
|
|
|
|
return iceServers.map(function(iceServer) {
|
|
// We don't need to normalise STUN, just pass it on through
|
|
if (iceServer.url.trim().substr(0, 5) !== 'turn:') {
|
|
return iceServer;
|
|
}
|
|
|
|
if (userAgent !== null) {
|
|
// TURN is broken in these versions of FF: Fallback to STUN.
|
|
if (firefoxVersion < 25) {
|
|
return {url: iceServer.url.replace("turn:", "stun:")};
|
|
}
|
|
|
|
// FF < 27 doesn't support the TURN transport parameter, so we strip it out
|
|
if (firefoxVersion < 27 && iceServer.url.indexOf('?') !== -1) {
|
|
iceServer.url = iceServer.url.trim().split('?')[0];
|
|
}
|
|
}
|
|
|
|
bits = iceServer.url.trim().split(/[:@]/);
|
|
|
|
return {
|
|
username: bits[1],
|
|
credential: iceServer.credential,
|
|
url: bits[0] + ':' + bits[2] + (bits.length === 4 ? ':' + bits[3] : '')
|
|
};
|
|
});
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
/**
|
|
* A class defining properties of the <code>capabilities</code> property of a
|
|
* Session object. See <a href="Session.html#capabilites">Session.capabilities</a>.
|
|
* <p>
|
|
* All Capabilities properties are undefined until you have connected to a session
|
|
* and the Session object has dispatched the <code>sessionConnected</code> event.
|
|
* <p>
|
|
* For more information on token roles, see the <a href="server_side_libraries.html#generate_token">generate_token()</a>
|
|
* method of the OpenTok server-side libraries.
|
|
*
|
|
* @class Capabilities
|
|
*
|
|
* @property {Number} forceDisconnect Specifies whether you can call
|
|
the <code>Session.forceDisconnect()</code> method (1) or not (0). To call the <code>Session.forceDisconnect()</code> method,
|
|
the user must have a token that is assigned the role of moderator.
|
|
* @property {Number} forceUnpublish Specifies whether you can call
|
|
the <code>Session.forceUnpublish()</code> method (1) or not (0). To call the <code>Session.forceUnpublish()</code> method,
|
|
the user must have a token that is assigned the role of moderator.
|
|
* @property {Number} publish Specifies whether you can publish to the session (1) or not (0).
|
|
The ability to publish is based on a few factors. To publish, the user must have a token that
|
|
is assigned a role that supports publishing. There must be a connected camera and microphone.
|
|
* @property {Number} subscribe Specifies whether you can subscribe to streams
|
|
in the session (1) or not (0). Currently, this capability is available for all users on all platforms.
|
|
* @property {Number} supportsWebRTC Whether the client supports WebRTC (1) or not (0).
|
|
*/
|
|
OT.Capabilities = function(permissions) {
|
|
this.publish = permissions.indexOf('publish') !== -1 ? 1 : 0;
|
|
this.subscribe = permissions.indexOf('subscribe') !== -1 ? 1 : 0;
|
|
this.forceUnpublish = permissions.indexOf('forceunpublish') !== -1 ? 1 : 0;
|
|
this.forceDisconnect = permissions.indexOf('forcedisconnect') !== -1 ? 1 : 0;
|
|
this.supportsWebRTC = OT.$.supportsWebRTC() ? 1 : 0;
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
|
|
// Wraps up some common error handling for implementing callbacks
|
|
// for work requests made to the OT backend via the Messenger.
|
|
var RemoteWork = function RemoteWork (parent, success, error, options) {
|
|
var REQUEST_TIMEOUT = 30000,
|
|
timeoutInterval,
|
|
exceptionCodesIndicatingFailure = {};
|
|
|
|
var destroy = function() {
|
|
clearTimeout(timeoutInterval);
|
|
parent.off('exception', onException);
|
|
},
|
|
|
|
onException = function(event) {
|
|
if (!exceptionCodesIndicatingFailure.hasOwnProperty(event.code)) return;
|
|
|
|
// We don't even know if this relates to this particular forceDisconnct...
|
|
this.failed(exceptionCodesIndicatingFailure[event.code]);
|
|
},
|
|
|
|
onTimeout = function() {
|
|
var reason = options && options.timeoutMessage ? options.timeoutMessage : "Timed out while waiting for the server to respond.";
|
|
this.failed(reason);
|
|
};
|
|
|
|
|
|
this.failsOnExceptionCodes = function(codes) {
|
|
exceptionCodesIndicatingFailure = codes;
|
|
};
|
|
|
|
this.succeeded = function() {
|
|
destroy();
|
|
if (completionHandler) OT.$.callAsync(completionHandler, null);
|
|
};
|
|
|
|
this.failed = function(reason) {
|
|
destroy();
|
|
if (completionHandler) OT.$.callAsync(completionHandler, new OT.Error(null, reason));
|
|
};
|
|
|
|
parent.on('exception', onException, this);
|
|
timeoutInterval = setTimeout(onTimeout.bind(this), REQUEST_TIMEOUT);
|
|
};
|
|
|
|
|
|
/**
|
|
* The Session object returned by the <code>TB.initSession()</code> method provides access to much of the OpenTok functionality.
|
|
*
|
|
* @class Session
|
|
* @augments EventDispatcher
|
|
*
|
|
* @property {Capabilities} capabilities A {@link Capabilities} object that includes information about the capabilities of the client.
|
|
* All properties of the <code>capabilities</code> object are undefined until you have connected to a session
|
|
* and the Session object has dispatched the <code>sessionConnected</code> event.
|
|
* @property {Connection} connection The {@link Connection} object for this session. The connection property is only available once the
|
|
* Session object dispatches the sessionConnected event. The Session object asynchronously dispatches a sessionConnected event in response
|
|
* to a successful call to the connect() method. See: <a href="Session#connect">connect</a> and {@link Connection}.
|
|
* @property {String} sessionId The session ID for this session. You pass this value into the <code>TB.initSession()</code> method when you create
|
|
* the Session object. (Note: a Session object is not connected to the OpenTok server until you
|
|
* call the connect() method of the object and the object dispatches a connected event. See {@link TB.initSession} and {@link connect}).
|
|
* For more information on sessions and session IDs, see
|
|
* <a href="/opentok/tutorials/create-session/">Session creation</a>.
|
|
*/
|
|
OT.Session = function(sessionId) {
|
|
// Check that the client meets the minimum requirements, if they don't the upgrade
|
|
// flow will be triggered.
|
|
if (!OT.checkSystemRequirements()) {
|
|
OT.upgradeSystemRequirements();
|
|
return;
|
|
}
|
|
|
|
var _initialConnection = true,
|
|
_apiKey,
|
|
_token,
|
|
_sessionId = sessionId,
|
|
_socket,
|
|
_widgetId = OT.$.uuid(),
|
|
_analytics = new OT.Analytics(),
|
|
_connectionId,
|
|
_callbacks = {
|
|
forceDisconnect: {},
|
|
forceUnpublish: {}
|
|
};
|
|
|
|
|
|
OT.$.eventing(this);
|
|
var setState = OT.$.statable(this, ['disconnected', 'connecting', 'connected', 'disconnecting'], 'disconnected');
|
|
|
|
this.connections = new OT.Collection();
|
|
this.streams = new OT.Collection();
|
|
|
|
|
|
//--------------------------------------
|
|
// MESSAGE HANDLERS
|
|
//--------------------------------------
|
|
|
|
var
|
|
// The duplication of this and sessionConnectionFailed will go away when
|
|
// session and messenger are refactored
|
|
sessionConnectFailed = function(reason, code) {
|
|
setState('disconnected');
|
|
|
|
OT.error(reason);
|
|
|
|
this.trigger('sessionConnectFailed', reason);
|
|
|
|
TB.handleJsException(reason, code || OT.ExceptionCodes.CONNECT_FAILED, {
|
|
session: this
|
|
});
|
|
},
|
|
|
|
sessionDisconnectedHandler = function(event) {
|
|
var reason = event.reason;
|
|
if(reason == "networkTimedout") {
|
|
reason = "networkDisconnected";
|
|
this.logEvent('Connect', 'TimeOutDisconnect', "reason", event.reason);
|
|
} else {
|
|
this.logEvent('Connect', 'Disconnected', "reason", event.reason);
|
|
}
|
|
|
|
var publicEvent = new OT.SessionDisconnectEvent('sessionDisconnected', reason);
|
|
|
|
reset.call(this);
|
|
disconnectComponents.call(this);
|
|
|
|
var defaultAction = function() {
|
|
if (!publicEvent.isDefaultPrevented()) destroyComponents.call(this, publicEvent.reason);
|
|
}.bind(this);
|
|
|
|
this.dispatchEvent(publicEvent, defaultAction);
|
|
},
|
|
|
|
connectionCreatedHandler = function(connection) {
|
|
// We don't broadcast events for the symphony connection
|
|
if (connection.id.match(/^symphony\./)) return;
|
|
|
|
this.dispatchEvent(new OT.ConnectionEvent(
|
|
OT.Event.names.CONNECTION_CREATED,
|
|
[connection]
|
|
));
|
|
},
|
|
|
|
connectionDestroyedHandler = function(connection, reason) {
|
|
// We don't broadcast events for the symphony connection
|
|
if (connection.id.match(/^symphony\./)) return;
|
|
|
|
// Don't delete the connection if it's ours. This only happens when
|
|
// we're about to receive a session disconnected and session disconnected
|
|
// will also clean up our connection.
|
|
if (connection.id === _socket.id) return;
|
|
|
|
// Handle success callbacks
|
|
if (_callbacks.forceDisconnect[connection.id]) {
|
|
var callback = _callbacks.forceDisconnect[connection.id];
|
|
delete _callbacks.forceDisconnect[connection.id];
|
|
|
|
|
|
if (reason !== 'forceDisconnected') {
|
|
OT.warn("Expected a forceDisconnect for connection " + connection.id + ", but a " + reason + " was received instead.");
|
|
}
|
|
|
|
callback.succeeded();
|
|
}
|
|
|
|
this.dispatchEvent(
|
|
new OT.ConnectionEvent(
|
|
OT.Event.names.CONNECTION_DESTROYED,
|
|
[connection],
|
|
reason
|
|
)
|
|
);
|
|
},
|
|
|
|
streamCreatedHandler = function(stream) {
|
|
this.dispatchEvent(new OT.StreamEvent(
|
|
OT.Event.names.STREAM_CREATED,
|
|
[stream]
|
|
));
|
|
},
|
|
|
|
streamPropertyModifiedHandler = function(event) {
|
|
var stream = event.target,
|
|
propertyName = event.changedProperty,
|
|
newValue = event.newValue;
|
|
|
|
if (propertyName === 'orientation') {
|
|
propertyName = 'videoDimensions';
|
|
newValue = {width: newValue.width, height: newValue.height};
|
|
}
|
|
|
|
this.dispatchEvent(new OT.StreamPropertyChangedEvent(
|
|
OT.Event.names.STREAM_PROPERTY_CHANGED,
|
|
stream,
|
|
propertyName,
|
|
event.oldValue,
|
|
newValue
|
|
));
|
|
},
|
|
|
|
streamDestroyedHandler = function(stream, reason) {
|
|
// Handle success callbacks
|
|
if (_callbacks.forceUnpublish[stream.id]) {
|
|
var callback = _callbacks.forceUnpublish[stream.id];
|
|
delete _callbacks.forceUnpublish[stream.id];
|
|
|
|
if (reason !== 'forceUnpublished') {
|
|
OT.warn("Expected a forceUnpublish for stream " + stream.id + ", but a " + reason + " destroyed was received instead.");
|
|
}
|
|
|
|
callback.succeeded();
|
|
}
|
|
|
|
var event = new OT.StreamEvent('streamDestroyed', [stream], reason);
|
|
|
|
var defaultAction = function() {
|
|
if (!event.isDefaultPrevented()) {
|
|
// If the stream is one of ours then we need to cleanup
|
|
// a publisher.
|
|
var publisher = OT.publishers.where({streamId: stream.id})[0];
|
|
if (publisher) {
|
|
publisher._.unpublishFromSession(this);
|
|
publisher.destroy();
|
|
}
|
|
|
|
// If we are subscribed to any of the streams we should unsubscribe
|
|
OT.subscribers.where({streamId: stream.id}).forEach(function(subscriber) {
|
|
if (subscriber.session.id === this.id) {
|
|
this.unsubscribe(subscriber);
|
|
}
|
|
}, this);
|
|
}
|
|
}.bind(this);
|
|
|
|
this.dispatchEvent(event, defaultAction);
|
|
},
|
|
|
|
|
|
// Put ourselves into a pristine state
|
|
reset = function() {
|
|
_apiKey = null;
|
|
_token = null;
|
|
setState('disconnected');
|
|
|
|
this.connections.destroy();
|
|
this.streams.destroy();
|
|
},
|
|
|
|
disconnectComponents = function() {
|
|
OT.publishers.where({session: this}).forEach(function(publisher) {
|
|
publisher.disconnect();
|
|
});
|
|
|
|
OT.subscribers.where({session: this}).forEach(function(subscriber) {
|
|
subscriber.disconnect();
|
|
});
|
|
},
|
|
|
|
destroyComponents = function(reason) {
|
|
OT.publishers.where({session: this}).forEach(function(publisher) {
|
|
publisher.destroy(reason);
|
|
});
|
|
|
|
OT.subscribers.where({session: this}).forEach(function(subscriber) {
|
|
subscriber.destroy(reason);
|
|
});
|
|
},
|
|
|
|
connectMessenger = function() {
|
|
TB.debug("OT.Session: connecting to Raptor");
|
|
|
|
_socket = new OT.Raptor.Socket(_widgetId, this.sessionInfo.messagingServer);
|
|
_socket.connect(_token, this.sessionInfo, function(error, sessionState) {
|
|
if (error) {
|
|
sessionConnectFailed.call(this, error.reason, error.code);
|
|
return;
|
|
}
|
|
|
|
OT.debug("OT.Session: Received session state from Raptor", sessionState);
|
|
|
|
_connectionId = this.connection.id;
|
|
setState('connected');
|
|
|
|
// Listen for our own connection's destroyed event so we know when we've been disconnected.
|
|
this.connection.on('destroyed', sessionDisconnectedHandler, this);
|
|
|
|
// Listen for connection updates
|
|
this.connections.on({
|
|
add: connectionCreatedHandler,
|
|
remove: connectionDestroyedHandler
|
|
}, this);
|
|
|
|
// Listen for stream updates
|
|
this.streams.on({
|
|
add: streamCreatedHandler,
|
|
remove: streamDestroyedHandler,
|
|
update: streamPropertyModifiedHandler
|
|
}, this);
|
|
|
|
this.dispatchEvent(new OT.SessionConnectEvent(
|
|
OT.Event.names.SESSION_CONNECTED,
|
|
sessionState.connections,
|
|
sessionState.streams,
|
|
sessionState.archives
|
|
));
|
|
}.bind(this));
|
|
},
|
|
|
|
getSessionInfo = function() {
|
|
if (this.is('connecting')) {
|
|
OT.SessionInfo.get(
|
|
this,
|
|
onSessionInfoResponse.bind(this),
|
|
function(error) {
|
|
sessionConnectFailed.call(this, error.message + (error.code ? ' (' + error.code + ')' : ''));
|
|
}.bind(this)
|
|
);
|
|
}
|
|
},
|
|
|
|
onSessionInfoResponse = function(sessionInfo) {
|
|
if (this.is('connecting')) {
|
|
this.sessionInfo = sessionInfo;
|
|
if (this.sessionInfo.partnerId && this.sessionInfo.partnerId != _apiKey) {
|
|
_apiKey = this.sessionInfo.partnerId;
|
|
|
|
var reason = 'Authentication Error: The apiKey passed into the session.connect ' +
|
|
'method does not match the apiKey in the token or session you are trying to ' +
|
|
'connect to.';
|
|
|
|
this.logEvent('Connect', 'Failure', 'reason', 'GetSessionInfo:' +
|
|
OT.ExceptionCodes.AUTHENTICATION_ERROR + ':' + reason);
|
|
|
|
sessionConnectFailed.call(this, reason, OT.ExceptionCodes.AUTHENTICATION_ERROR);
|
|
} else {
|
|
connectMessenger.call(this);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Check whether we have permissions to perform the action.
|
|
permittedTo = function(action) {
|
|
return _socket && _socket.permittedTo(action);
|
|
};
|
|
|
|
this.logEvent = function(action, variation, payload_type, payload) {
|
|
var event = {
|
|
action: action,
|
|
variation: variation,
|
|
payload_type: payload_type,
|
|
payload: payload,
|
|
session_id: _sessionId,
|
|
partner_id: _apiKey,
|
|
widget_id: _widgetId,
|
|
widget_type: 'Controller'
|
|
};
|
|
if (this.connection && this.connection.id) event.connection_id = this.connection.id;
|
|
else if (_connectionId) event.connection_id = _connectionId;
|
|
_analytics.logEvent(event);
|
|
};
|
|
|
|
/**
|
|
* Connects to an OpenTok session. Pass your API key as the <code>apiKey</code> parameter. You get an API key when you
|
|
* <a href="https://dashboard.tokbox.com/users/sign_in">sign up</a> for an OpenTok account. Pass a token string as
|
|
* the <code>token</code> parameter. You generate a token using the
|
|
* <a href="/opentok/api/tools/documentation/api/server_side_libraries.html">OpenTok server-side libraries</a>
|
|
* or the <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page. For more information, see
|
|
* <a href="/opentok/tutorials/create-token/">Connection token creation</a>.
|
|
* <p>
|
|
* Upon a successful connection, the Session object dispatches a <code>sessionConnected</code> event. Call the
|
|
* <code>addEventListener()</code> method to set up an event listener to process this event before calling other methods of the Session object.
|
|
* </p>
|
|
* <p>
|
|
* The Session object dispatches a <code>connectionCreated</code> event when other clients create connections to the session.
|
|
* </p>
|
|
* <p>
|
|
* The TB object dispatches an <code>exception</code> event if the session ID,
|
|
* API key, or token string are invalid. See <a href="ExceptionEvent.html">ExceptionEvent</a>
|
|
* and <a href="TB.html#addEventListener">TB.addEventListener()</a>.
|
|
* </p>
|
|
* <p>
|
|
* The application throws an error if the system requirements are not met
|
|
* (see <a href="TB.html#checkSystemRequirements">TB.checkSystemRequirements()</a>).
|
|
* </p>
|
|
*
|
|
* <h5>
|
|
* Example
|
|
* </h5>
|
|
* <p>
|
|
* The following code initializes a session and sets up an event listener for when the session connects:
|
|
* </p>
|
|
* <pre>
|
|
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function sessionConnectHandler(sessionConnectEvent) {
|
|
* //
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
* <p>
|
|
* In this example, the <code>sessionConnectHandler()</code> function is passed an event object of type {@link SessionConnectEvent}.
|
|
* </p>
|
|
*
|
|
* <h5>
|
|
* Events dispatched:
|
|
* </h5>
|
|
*
|
|
* <p>
|
|
* <code>exception</code> (<a href="ExceptionEvent.html">ExceptionEvent</a>) — Dispatched by the TB class
|
|
* locally in the event of an error.
|
|
* </p>
|
|
* <p>
|
|
* <code>connectionCreated</code> (<a href="ConnectionEvent.html">ConnectionEvent</a>) —
|
|
* Dispatched by the Session object on all clients connected to the session.
|
|
* </p>
|
|
* <p>
|
|
* <code>sessionConnected</code> (<a href="SessionConnectEvent.html">SessionConnectEvent</a>) —
|
|
* Dispatched locally by the Session object when the connection is established.
|
|
* </p>
|
|
*
|
|
* @param {String} apiKey The API key that TokBox provided you when you registered for the OpenTok API.
|
|
*
|
|
* @param {String} token The session token. You generate a session token using our
|
|
* <a href="/opentok/libraries/server/">server-side libraries</a> or the
|
|
* <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page. For more information, see
|
|
* <a href="/opentok/tutorials/create-token/">Connection token creation</a>.
|
|
*
|
|
* @method #connect
|
|
* @memberOf Session
|
|
*/
|
|
this.connect = function(apiKey, token, completionHandler) {
|
|
if (this.is('connecting', 'connected')) {
|
|
OT.warn("OT.Session: Cannot connect, the session is already " + this.state);
|
|
return;
|
|
}
|
|
|
|
reset.call(this);
|
|
setState('connecting');
|
|
_token = token;
|
|
|
|
// Get a new widget ID when reconnecting.
|
|
if (_initialConnection) {
|
|
_initialConnection = false;
|
|
} else {
|
|
_widgetId = OT.$.uuid();
|
|
}
|
|
|
|
_apiKey = apiKey.toString();
|
|
|
|
// Ugly hack, make sure OT.APIKEY is set
|
|
if (OT.APIKEY.length === 0) {
|
|
OT.APIKEY = _apiKey;
|
|
}
|
|
|
|
if (completionHandler && OT.$.isFunction(completionHandler)) {
|
|
this.once(OT.Event.names.SESSION_CONNECTED, completionHandler.bind(null, null));
|
|
this.once('sessionConnectFailed', completionHandler);
|
|
}
|
|
|
|
var analyticsPayload = [
|
|
navigator.userAgent, OT.properties.version,
|
|
window.externalHost ? 'yes' : 'no'
|
|
];
|
|
this.logEvent( 'Connect', 'Attempt',
|
|
'userAgent|sdkVersion|chromeFrame',
|
|
analyticsPayload.map(function(e) { return e.replace('|', '\\|'); }).join('|')
|
|
);
|
|
|
|
getSessionInfo.call(this);
|
|
};
|
|
|
|
/**
|
|
* Disconnects from the OpenTok session.
|
|
*
|
|
* <p>
|
|
* Calling the <code>disconnect()</code> method ends your connection with the session. In the course of terminating your connection,
|
|
* it also ceases publishing any stream(s) you were publishing.
|
|
* </p>
|
|
* <p>
|
|
* Session objects on remote clients dispatch <code>streamDestroyed</code> events for any stream you were publishing.
|
|
* The Session object dispatches a <code>sessionDisconnected</code> event locally. The Session objects on remote clients dispatch
|
|
* <code>connectionDestroyed</code> events, letting other connections know you have left the session. The {@link SessionDisconnectEvent}
|
|
* and {@link StreamEvent} objects that define the <code>sessionDisconnect</code> and <code>connectionDestroyed</code> events each have
|
|
* a <code>reason</code> property. The <code>reason</code> property lets the developer determine whether the connection is being
|
|
* terminated voluntarily and whether any streams are being destroyed as a byproduct of the underlying connection's voluntary destruction.
|
|
* </p>
|
|
* <p>
|
|
* If the session is not currently connected, calling this method causes a warning to be logged.
|
|
* See <a "href=TB.html#setLogLevel">TB.setLogLevel()</a>.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* <i>Note:</i> If you intend to reuse a Publisher object created using <code>TB.initPublisher()</code>
|
|
* to publish to different sessions sequentially, call either <code>Session.disconnect()</code> or
|
|
* <code>Session.unpublish()</code>. Do not call both. Then call the <code>preventDefault()</code> method
|
|
* of the <code>streamDestroyed</code> or <code>sessionDisconnected</code> event object to prevent the
|
|
* Publisher object from being removed from the page. Be sure to call <code>preventDefault()</code> only
|
|
* if the <code>connection.connectionId</code> property of the Stream object in the event matches the
|
|
* <code>connection.connectionId</code> property of your Session object (to ensure that you are preventing
|
|
* the default behavior for your published streams, not for other streams that you subscribe to).
|
|
* </p>
|
|
*
|
|
* <h5>
|
|
* Events dispatched:
|
|
* </h5>
|
|
* <p>
|
|
* <code>sessionDisconnected</code> (<a href="SessionDisconnectEvent.html">SessionDisconnectEvent</a>)
|
|
* — Dispatched locally when the connection is disconnected.
|
|
* </p>
|
|
* <p>
|
|
* <code>connectionDestroyed</code> (<a href="ConnectionEvent.html">ConnectionEvent</a>) —
|
|
* Dispatched on other connections, along with the <code>streamDestroyed</code> event (as warranted).
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* <code>streamDestroyed</code> (<a href="StreamEvent.html">StreamEvent</a>) —
|
|
* Dispatched if streams are lost as a result of the session disconnecting.
|
|
* </p>
|
|
*
|
|
* @method #disconnect
|
|
* @memberOf Session
|
|
*/
|
|
this.disconnect = function() {
|
|
if (_socket && _socket.isNot('disconnected')) {
|
|
setState('disconnecting');
|
|
_socket.disconnect();
|
|
}
|
|
else {
|
|
reset.call(this);
|
|
}
|
|
};
|
|
|
|
this.destroy = function(reason, quiet) {
|
|
this.streams.destroy();
|
|
this.connections.destroy();
|
|
this.disconnect();
|
|
};
|
|
|
|
/**
|
|
* The <code>publish()</code> method starts publishing an audio-video stream to the session.
|
|
* The audio-video stream is captured from a local microphone and webcam. Upon successful publishing,
|
|
* the Session objects on all connected clients dispatch the <code>streamCreated</code> event.
|
|
* </p>
|
|
*
|
|
* <!--JS-ONLY-->
|
|
* <p>You pass a Publisher object as the one parameter of the method. You can initialize a Publisher object by calling the
|
|
* <a href="TB.html#initPublisher">TB.initPublisher()</a> method. Before calling <code>Session.publish()</code>.
|
|
* </p>
|
|
*
|
|
* <p>This method takes an alternate form: <code>publish([targetElement:String, properties:Object]):Publisher</code> —
|
|
* In this form, you do <i>not</i> pass a Publisher object into the function. Instead, you pass in a <code>targetElement</code>
|
|
* (the ID of the DOM element that the Publisher will replace) and a <code>properties</code> object that defines
|
|
* options for the Publisher (see <a href="TB.html#initPublisher">TB.initPublisher()</a>.) The method
|
|
* returns a new Publisher object, which starts sending an audio-video stream to the session.
|
|
* The remainder of this documentation describes the form that takes a single Publisher object as a parameter.
|
|
*
|
|
* <p>
|
|
* A local display of the published stream is created on the web page by replacing
|
|
* the specified element in the DOM with a streaming video display. The video stream
|
|
* is automatically mirrored horizontally so that users see themselves and movement
|
|
* in their stream in a natural way. If the width and height of the display do not match
|
|
* the 4:3 aspect ratio of the video signal, the video stream is cropped to fit the display.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* If calling this method creates a new Publisher object and the OpenTok library does not have access to
|
|
* the camera or microphone, the web page alerts the user to grant access to the camera and microphone.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* The TB object dispatches an <code>exception</code> event if the user's role does not
|
|
* include permissions required to publish. For example, if the user's role is set to subscriber,
|
|
* then they cannot publish. You define a user's role when you create the user token using the <code>generate_token()</code> method
|
|
* of our <a href="/opentok/libraries/server/">server-side libraries</a> or the <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page.
|
|
* You pass the token string as a parameter of the <code>connect()</code> method of the Session object.
|
|
* See <a href="ExceptionEvent.html">ExceptionEvent</a> and <a href="TB.html#addEventListener">TB.addEventListener()</a>.
|
|
* </p>
|
|
* <p>
|
|
* The application throws an error if the session is not connected.
|
|
* </p>
|
|
*
|
|
* <h5>Events dispatched:</h5>
|
|
* <p>
|
|
* <code>exception</code> (<a href="ExceptionEvent.html">ExceptionEvent</a>) — Dispatched by the TB object. This
|
|
* can occur when user's role does not allow publishing (the <code>code</code> property of event object is set to 1500);
|
|
* it can also occur if the connection fails to connect (the <code>code</code> property of event object is set to 1013).
|
|
* WebRTC is a peer-to-peer protocol, and it is possible that connections will fail to connect. The most common cause
|
|
* for failure is a firewall that the protocol cannot traverse.</li>
|
|
* </p>
|
|
* <p>
|
|
* <code>streamCreated</code> (<a href="StreamEvent.html">StreamEvent</a>) —
|
|
* The stream has been published. The Session object dispatches this on all clients
|
|
* subscribed to the stream, as well as on the publisher's client.
|
|
* </p>
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* <p>
|
|
* The following example publishes a video once the session connects:
|
|
* </p>
|
|
* <pre>
|
|
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionId = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = ""; // Replace with a generated token that has been assigned the moderator role.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function sessionConnectHandler(event) {
|
|
* var divProps = {width: 400, height:300, name:"Bob's stream"};
|
|
* publisher = TB.initPublisher(apiKey, 'publisher', divProps);
|
|
* // This assumes that there is a DOM element with the ID 'publisher'.
|
|
* session.publish(publisher);
|
|
* }
|
|
* </pre>
|
|
*
|
|
* @param publisher A Publisher object, which you initialize by calling the <a href="TB.html#initPublisher">TB.initPublisher()</a>
|
|
* method.
|
|
*
|
|
* @returns The Publisher object for this stream.
|
|
*
|
|
* @method #publish
|
|
* @memberOf Session
|
|
*/
|
|
this.publish = function(publisher, properties, completionHandler) {
|
|
var errorMsg;
|
|
|
|
if (this.isNot('connected')) {
|
|
_analytics.logError(1010, 'tb.exception', "We need to be connected before you can publish", null, {
|
|
action: 'publish',
|
|
variation: 'Failure',
|
|
payload_type: "reason",
|
|
payload: "We need to be connected before you can publish",
|
|
session_id: _sessionId,
|
|
partner_id: _apiKey,
|
|
widgetId: _widgetId,
|
|
widget_type: 'Controller'
|
|
});
|
|
|
|
if (completionHandler && OT.$.isFunction(completionHandler)) {
|
|
errorMsg = "We need to be connected before you can publish";
|
|
OT.$.callAsync(completionHandler, new OT.Error(OT.ExceptionCodes.NOT_CONNECTED, errorMsg));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!permittedTo("publish")) {
|
|
this.logEvent('publish', 'Failure', 'reason', 'This token does not allow publishing. The role must be at least `publisher` to enable this functionality');
|
|
|
|
TB.handleJsException("This token does not allow publishing. The role must be at least `publisher` to enable this functionality", OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
|
|
session: this
|
|
});
|
|
return null;
|
|
}
|
|
|
|
// If the user has passed in an ID of a element then we create a new publisher.
|
|
if (!publisher || typeof(publisher)==='string' || publisher.nodeType == Node.ELEMENT_NODE){
|
|
// Initiate a new Publisher with the new session credentials
|
|
publisher = OT.initPublisher(this.apiKey, publisher, properties);
|
|
}
|
|
else if (publisher instanceof OT.Publisher){
|
|
|
|
// If the publisher already has a session attached to it we can
|
|
if( "session" in publisher && publisher.session && "sessionId" in publisher.session ){
|
|
// send a warning message that we can't publish again.
|
|
if( publisher.session.sessionId === this.sessionId){
|
|
OT.warn("Cannot publish " + publisher.guid + " again to " + this.sessionId + ". Please call session.unpublish(publisher) first.");
|
|
}
|
|
else {
|
|
OT.warn("Cannot publish " + publisher.guid + " publisher already attached to " + publisher.session.sessionId+ ". Please call session.unpublish(publisher) first.");
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
errorMsg = "Session.publish :: First parameter passed in is neither a string nor an instance of the Publisher";
|
|
OT.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
if (completionHandler && OT.$.isFunction(completionHandler)) publisher.once('publishComplete', completionHandler);
|
|
|
|
// Add publisher reference to the session
|
|
publisher._.publishToSession(this);
|
|
|
|
// return the embed publisher
|
|
return publisher;
|
|
};
|
|
|
|
/**
|
|
* Ceases publishing the specified publisher's audio-video stream
|
|
* to the session. By default, the local representation of the audio-video stream is removed from the
|
|
* web page.< Upon successful termination, the Session object on every connected
|
|
* web page dispatches
|
|
* a <code>streamDestroyed</code> event.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* To prevent the Publisher from being removed from the DOM, add an event listener for the
|
|
* <code>streamDestroyed</code> event and call the <code>preventDefault()</code> method of the event object.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* <i>Note:</i> If you intend to reuse a Publisher object created using <code>TB.initPublisher()</code>
|
|
* to publish to different sessions sequentially, call either <code>Session.disconnect()</code> or
|
|
* <code>Session.unpublish()</code>. Do not call both. Then call the <code>preventDefault()</code> method
|
|
* of the <code>streamDestroyed</code> or <code>sessionDisconnected</code> event object to prevent the
|
|
* Publisher object from being removed from the page. Be sure to call <code>preventDefault()</code> only
|
|
* if the <code>connection.connectionId</code> property of the Stream object in the event matches the
|
|
* <code>connection.connectionId</code> property of your Session object (to ensure that you are preventing
|
|
* the default behavior for your published streams, not for other streams that you subscribe to).
|
|
* </p>
|
|
*
|
|
* <h5>Events dispatched:</h5>
|
|
*
|
|
* <p>
|
|
* <code>streamDestroyed</code> (<a href="StreamEvent.html">StreamEvent</a>) —
|
|
* The stream associated with the Publisher has been destroyed. Dispatched on
|
|
* the Publisher's browser and on the browser for all connections subscribing
|
|
* to the publisher's stream.
|
|
* </p>
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* The following example publishes a stream to a session and adds a Disconnect link to the web page. Clicking this link causes the stream to stop being published.
|
|
*
|
|
* <pre>
|
|
* <script>
|
|
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var token = "Replace with the TokBox token string provided to you."
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectHandler);
|
|
* session.connect(apiKey, token);
|
|
* var publisher;
|
|
*
|
|
* function sessionConnectHandler(event) {
|
|
* publisher = TB.initPublisher(apiKey, 'publisher');
|
|
* // This assumes that there is a DOM element with the ID 'publisher'.
|
|
* session.publish(publisher);
|
|
* }
|
|
* function unpublsh() {
|
|
* session.unpublish(publisher);
|
|
* }
|
|
* </script>
|
|
*
|
|
* <body>
|
|
*
|
|
* <div id="publisherContainer/>
|
|
* <br/>
|
|
*
|
|
* <a href="javascript:unpublish()">Stop Publishing</a>
|
|
*
|
|
* </body>
|
|
*
|
|
* </pre>
|
|
*
|
|
* @see <a href="#publish">publish()</a>
|
|
*
|
|
* @see <a href="StreamEvent.html">streamDestroyed event</a>
|
|
*
|
|
* @param {Publisher} publisher</span> The Publisher object to stop streaming.
|
|
*
|
|
* @method #unpublish
|
|
* @memberOf Session
|
|
*/
|
|
this.unpublish = function(publisher) {
|
|
if (!publisher) {
|
|
OT.error('OT.Session.unpublish: publisher parameter missing.');
|
|
return;
|
|
}
|
|
|
|
// Unpublish the localMedia publisher
|
|
publisher._.unpublishFromSession(this);
|
|
};
|
|
|
|
|
|
/**
|
|
* Subscribes to a stream that is available to the session. You can get an array of
|
|
* available streams from the <code>streams</code> property of the <code>sessionConnected</code>
|
|
* and <code>streamCreated</code> events (see <a href="SessionConnectEvent.html">SessionConnectEvent</a> and
|
|
* <a href="StreamEvent.html">StreamEvent</a>).
|
|
* </p>
|
|
* <p>
|
|
* The subscribed stream is displayed on the local web page by replacing the specified element in the DOM with a streaming video display.
|
|
* If the width and height of the display do not match the 4:3 aspect ratio of the video signal, the video stream is cropped to fit
|
|
* the display. If the stream lacks a video component, a blank screen with an audio indicator is displayed in place of the video stream.
|
|
* </p>
|
|
*
|
|
* <p>
|
|
* The application throws an error if the session is not connected<!--JS-ONLY--> or if the
|
|
* <code>targetElement</code> does not exist in the HTML DOM<!--/JS-ONLY-->.
|
|
* </p>
|
|
*
|
|
* <h5>Example</h5>
|
|
*
|
|
* The following code subscribes to available streams at the time that a session is connected
|
|
*
|
|
* <pre>
|
|
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function sessionConnectHandler(event) {
|
|
* for (var i = 0; i < event.streams.length; i++) {
|
|
* var stream = event.streams[i];
|
|
* displayStream(stream);
|
|
* }
|
|
* }
|
|
*
|
|
* function displayStream(stream) {
|
|
* var div = document.createElement('div');
|
|
* div.setAttribute('id', 'stream' + stream.streamId);
|
|
* var streamsContainer = document.getElementById('streamsContainer');
|
|
* streamsContainer.appendChild(div);
|
|
* subscriber = session.subscribe(stream, 'stream' + stream.streamId);
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
* You can also add an event listener for the <code>streamCreated</code> event. The Session object dispatches this event when a new stream is created.
|
|
* </p>
|
|
* <pre>
|
|
* session.addEventListener("streamCreated", streamCreatedHandler);
|
|
*
|
|
* function streamCreatedHandler(event) {
|
|
* for (var i = 0; i < event.streams.length; i++) {
|
|
* var stream = event.streams[i];
|
|
* displayStream(stream);
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*
|
|
* @param {Stream} stream The Stream object representing the stream to which we are trying to subscribe.
|
|
*
|
|
* @param {String} targetElement The ID of the existing DOM element
|
|
* that the Subscriber replaces. If you do not specify a <code>targetElement</code>, the application
|
|
* appends a new DOM element to the HTML <code>body</code>.
|
|
*
|
|
* @param {Object} properties This is an object that contains the following properties:
|
|
* <ul>
|
|
* <li><code>audioVolume</code> (Number) — The desired audio volume, between 0 and 100, when the Subscriber
|
|
* is first opened (default: 50). After you subscribe to the stream, you can adjust the volume by calling
|
|
* the <a href="Subscriber.html#setAudioVolume"><code>setAudioVolume()</code> method</a> of the Subscriber
|
|
* object. This volume setting affects local playback only; it does not affect the stream's volume on other
|
|
* clients.</li>
|
|
*
|
|
* <li><code>height</code> (Number) — The desired height, in pixels, of the
|
|
* displayed Subscriber video stream (default: 198). <i>Note:</i> Use the
|
|
* <code>height</code> and <code>width</code> properties to set the dimensions
|
|
* of the Subscriber video; do not set the height and width of the DOM element
|
|
* (using CSS).</li>
|
|
*
|
|
* <li><code>style.nameDisplayMode</code> (String) — This property determines whether to display the
|
|
* stream name. Possible values are: <code>"auto"</code> (the name is displayed when the stream is first
|
|
* displayed and when the user mouses over the display), <code>"off"</code> (the name is not displayed),
|
|
* and <code>"on"</code> (the name is displayed).</li>
|
|
*
|
|
* <li><code>subscribeToAudio</code> (Boolean) — Whether to initially subscribe to audio
|
|
* (if available) for the stream (default: <code>true</code>).</li>
|
|
*
|
|
* <li><code>subscribeToVideo</code> (Boolean) — Whether to initially subscribe to video
|
|
* (if available) for the stream (default: <code>true</code>).</li>
|
|
*
|
|
* <li><code>width</code> (Number) — The desired width, in pixels, of the
|
|
* displayed Subscriber video stream (default: 264). <i>Note:</i> Use the
|
|
* <code>height</code> and <code>width</code> properties to set the dimensions
|
|
* of the Subscriber video; do not set the height and width of the DOM element
|
|
* (using CSS).</li>
|
|
*
|
|
* </ul>
|
|
*
|
|
* @signature subscribe(stream, targetElement, properties)
|
|
* @returns {Subscriber} The Subscriber object for this stream. Stream control functions are exposed through the Subscriber object.
|
|
* @method #subscribe
|
|
* @memberOf Session
|
|
*/
|
|
this.subscribe = function(stream, targetElement, properties, completionHandler) {
|
|
var errorMsg;
|
|
|
|
if (!this.connection || !this.connection.connectionId) {
|
|
errorMsg = "Session.subscribe :: Connection required to subscribe";
|
|
OT.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
if (!stream) {
|
|
errorMsg = "Session.subscribe :: stream cannot be null";
|
|
OT.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
if (!stream.hasOwnProperty("streamId")) {
|
|
errorMsg = "Session.subscribe :: invalid stream object";
|
|
OT.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
var subscriber = new OT.Subscriber(targetElement, OT.$.extend(properties || {}, {
|
|
session: this
|
|
}));
|
|
|
|
if (completionHandler && OT.$.isFunction(completionHandler)) subscriber.once('subscribeComplete', completionHandler);
|
|
|
|
OT.subscribers.add(subscriber);
|
|
subscriber.subscribe(stream);
|
|
|
|
return subscriber;
|
|
};
|
|
|
|
/**
|
|
* Stops subscribing to a stream in the session. the display of the audio-video stream is removed from the local web page.
|
|
*
|
|
* <h5>Example</h5>
|
|
* <p>
|
|
* The following code subscribes to available streams at the time that a session is connected. For each stream, the code also
|
|
* adds an Unsubscribe link.
|
|
* </p>
|
|
* <pre>
|
|
* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
|
|
* var sessionID = ""; // Replace with your own session ID.
|
|
* // See https://dashboard.tokbox.com/projects
|
|
* var streams = [];
|
|
*
|
|
* var session = TB.initSession(sessionID);
|
|
* session.addEventListener("sessionConnected", sessionConnectHandler);
|
|
* session.connect(apiKey, token);
|
|
*
|
|
* function sessionConnectHandler(event) {
|
|
* for (var i = 0; i < event.streams.length; i++) {
|
|
* var stream = event.streams[i];
|
|
* displayStream(stream);
|
|
* }
|
|
* }
|
|
*
|
|
* function displayStream(stream) {
|
|
* var div = document.createElement('div');
|
|
* div.setAttribute('id', 'stream' + stream.streamId);
|
|
*
|
|
*
|
|
*
|
|
* var subscriber = session.subscribe(stream, 'stream' + stream.streamId);
|
|
* subscribers.push(subscriber);
|
|
*
|
|
*
|
|
* var aLink = document.createElement('a');
|
|
* aLink.setAttribute('href', 'javascript: unsubscribe("' + subscriber.id + '")');
|
|
* aLink.innerHTML = "Unsubscribe";
|
|
*
|
|
* var streamsContainer = document.getElementById('streamsContainer');
|
|
* streamsContainer.appendChild(div);
|
|
* streamsContainer.appendChild(aLink);
|
|
*
|
|
* streams = event.streams;
|
|
* }
|
|
*
|
|
* function unsubscribe(subscriberId) {
|
|
* console.log("unsubscribe called");
|
|
* for (var i = 0; i < subscribers.length; i++) {
|
|
* var subscriber = subscribers[i];
|
|
* if (subscriber.id == subscriberId) {
|
|
* session.unsubscribe(subscriber);
|
|
* }
|
|
* }
|
|
* }
|
|
* </pre>
|
|
*
|
|
* @param {Subscriber} subscriber The Subscriber object to unsubcribe.
|
|
*
|
|
* @see <a href="#subscribe">subscribe()</a>
|
|
*
|
|
* @method #unsubscribe
|
|
* @memberOf Session
|
|
*/
|
|
this.unsubscribe = function(subscriber) {
|
|
if (!subscriber) {
|
|
var errorMsg = "OT.Session.unsubscribe: subscriber cannot be null";
|
|
OT.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
if (!subscriber.stream) {
|
|
OT.warn("OT.Session.unsubscribe:: tried to unsubscribe a subscriber that had no stream");
|
|
return false;
|
|
}
|
|
|
|
OT.debug("OT.Session.unsubscribe: subscriber " + subscriber.id);
|
|
|
|
subscriber.destroy();
|
|
|
|
return true;
|
|
};
|
|
|
|
/**
|
|
* Returns an array of local Subscriber objects for a given stream.
|
|
*
|
|
* @param {Stream} stream The stream for which you want to find subscribers.
|
|
*
|
|
* @returns {Array} An array of {@link Subscriber} objects for the specified stream.
|
|
*
|
|
* @see <a href="#unsubscribe">unsubscribe()</a>
|
|
* @see <a href="Subscriber.html">Subscriber</a>
|
|
* @see <a href="StreamEvent.html">StreamEvent</a>
|
|
* @method #getSubscribersForStream
|
|
* @memberOf Session
|
|
*/
|
|
this.getSubscribersForStream = function(stream) {
|
|
return OT.subscribers.where({streamId: stream.id});
|
|
};
|
|
|
|
/**
|
|
* Returns the local Publisher object for a given stream.
|
|
*
|
|
* @param {Stream} stream The stream for which you want to find the Publisher.
|
|
*
|
|
* @returns {Publisher} A Publisher object for the specified stream. Returns <code>null</code> if there is no local Publisher object
|
|
* for the specified stream.
|
|
*
|
|
* @see <a href="#forceUnpublish">forceUnpublish()</a>
|
|
* @see <a href="Subscriber.html">Subscriber</a>
|
|
* @see <a href="StreamEvent.html">StreamEvent</a>
|
|
*
|
|
* @method #getPublisherForStream
|
|
* @memberOf Session
|
|
*/
|
|
this.getPublisherForStream = function(stream) {
|
|
var streamId;
|
|
|
|
if (typeof(stream) == "string") {
|
|
streamId = stream;
|
|
} else if (typeof(stream) == "object" && stream && stream.hasOwnProperty("id")) {
|
|
streamId = stream.id;
|
|
} else {
|
|
errorMsg = "Session.getPublisherForStream :: Invalid stream type";
|
|
OT.error(errorMsg);
|
|
throw new Error(errorMsg);
|
|
}
|
|
|
|
return OT.publishers.where({streamId: streamId})[0];
|
|
};
|
|
|
|
|
|
// Private Session API: for internal OT use only
|
|
this._ = {
|
|
jsepSubscribe: function(stream, subscribeToVideo, subscribeToAudio) {
|
|
return _socket.jsepSubscribe(stream.connection.id, stream.id, subscribeToVideo, subscribeToAudio);
|
|
}.bind(this),
|
|
|
|
jsepUnsubscribe: function(stream) {
|
|
return _socket.jsepUnsubscribe(stream.connection.id, stream.id);
|
|
}.bind(this),
|
|
|
|
jsepCandidate: function(toConnectionId, stream, candidate) {
|
|
return _socket.jsepCandidate(toConnectionId, stream.id, candidate);
|
|
}.bind(this),
|
|
|
|
jsepOffer: function(toConnectionId, stream, offerSDP) {
|
|
return _socket.jsepOffer(toConnectionId, stream.id, offerSDP);
|
|
}.bind(this),
|
|
|
|
jsepAnswer: function(toConnectionId, stream, answerSDP) {
|
|
return _socket.jsepAnswer(toConnectionId, stream.id, answerSDP);
|
|
}.bind(this),
|
|
|
|
// session.on("signal", function(SignalEvent))
|
|
// session.on("signal:{type}", function(SignalEvent))
|
|
dispatchSignal: function(fromConnection, type, data) {
|
|
var event = new OT.SignalEvent(type, data, fromConnection);
|
|
event.target = this;
|
|
|
|
// signal a "signal" event
|
|
// NOTE: trigger doesn't support defaultAction, and therefore preventDefault.
|
|
this.trigger(OT.Event.names.SIGNAL, event);
|
|
|
|
// signal an "signal:{type}" event" if there was a custom type
|
|
if (type) this.dispatchEvent(event);
|
|
}.bind(this),
|
|
|
|
modifySubscriber: function(subscriber, key, value) {
|
|
return _socket.modifySubscriber(subscriber, key, value);
|
|
}.bind(this),
|
|
|
|
createStream: function(publisherId, name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo) {
|
|
_socket.createStream(publisherId, name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo);
|
|
}.bind(this),
|
|
|
|
modifyStream: function(streamId, key, value) {
|
|
if (!streamId || !key || value === void 0) {
|
|
OT.error('OT.Session.modifyStream: must provide streamId, key and value to modify a stream property.');
|
|
return;
|
|
}
|
|
|
|
_socket.updateStream(streamId, key, value);
|
|
}.bind(this),
|
|
|
|
destroyStream: function(streamId) {
|
|
_socket.destroyStream(streamId);
|
|
}.bind(this)
|
|
};
|
|
|
|
|
|
/**
|
|
* Sends a signal to each client or specified clients in the session. Specify a <code>connections</code> property
|
|
* of the <code>signal</code> parameter to limit the recipients of the signal; otherwise the signal is sent to
|
|
* each client connected to the session.
|
|
* <p>
|
|
* The following example sends a signal of type "foo" with a specified data payload to all clients connected to
|
|
* the session:
|
|
* <pre>
|
|
* session.signal({
|
|
* type: "foo",
|
|
* data: {messageString:"hello", scores:[7, 14, 23]},
|
|
* },
|
|
* function(error) {
|
|
* if (error) {
|
|
* console.log("signal error: " + error.reason);
|
|
* } else {
|
|
* console.log("signal sent");
|
|
* }
|
|
* }
|
|
* );
|
|
* </pre>
|
|
* <p>
|
|
* Calling this method without limiting the set of recipient clients will result in multiple signals sent (one to
|
|
* each client in the session). For information on charges for signaling, see the <a href="http://tokbox.com/pricing">OpenTok
|
|
* pricing</a> page.
|
|
* <p>
|
|
* The following example sends a signal of type "foo" with a specified data payload to two specific clients
|
|
* connected to the session:
|
|
* <pre>
|
|
* session.signal({
|
|
* type: "foo",
|
|
* to: [connection1, connection2]; // connection1 and 2 are Connection objects
|
|
* data: {messageString:"hello"}
|
|
* },
|
|
* function(error) {
|
|
* if (error) {
|
|
* console.log("signal error: " + error.reason);
|
|
* } else {
|
|
* console.log("signal sent");
|
|
* }
|
|
* }
|
|
* );
|
|
* </pre>
|
|
* <p>
|
|
* Each of the properties of the object you pass into the <code>signal()</code> method is optional.
|
|
* <p>
|
|
* Add an event handler for the <code>signal</code> event to listen for all signals sent in the session.
|
|
* Add an event handler for the <code>signal:type</code> event to listen for signals of a specified type
|
|
* only (replace <code>type</code>, in <code>signal:type</code>, with the type of signal to listen for). The
|
|
* Session object dispatches these events. (See <a href="#events">events</a>.)
|
|
* <p>
|
|
* Each property is optional. If you set none of the properties, you will send a signal with no data or type to
|
|
* each client connected to the session.</p>
|
|
*
|
|
* @param {Object} signal An object that contains the following properties defining the signal:
|
|
* <ul>
|
|
* <li><code>to</code> — (Array) An array of <a href="Connection.html">Connection</a> objects, corresponding
|
|
* to clients that the message is to be sent to. If you do not specify this property, the signal is sent to all
|
|
* clients connected to the session.</li>
|
|
* <li><code>data</code> — (Object) The data to send. Valid types for data are a JSON-parsable object,
|
|
* a JSON-parsable array, a string, a number, a Boolean value, or <code>null</code>. Numbers cannot be NaN or Infinity.
|
|
* The limit to the size of data is 8kB.</li>
|
|
* <li><code>type</code> — (String) The type of the signal. You can use the type to filter signals when setting an
|
|
* event handler for the <code>signal:type</code> event (where you replace <code>type</code> with the type string).
|
|
* The maximum length of the <code>type</code> string is 128 characters, and it must contain only letters (A-Z and a-z),
|
|
* numbers (0-9), '-', '_', and '~'.</li>
|
|
* </li>
|
|
* </ul>
|
|
*
|
|
* <p>Each property is optional. If you set none of the properties, you will send a signal with no data or type to
|
|
* each client connected to the session.</p>
|
|
*
|
|
* @param {Function} completionHandler A function that is called when sending the signal succeeds or fails. This
|
|
* function takes one parameter, <code>error</code>, which is set to <code>null</code> if sending the signal succeeds;
|
|
* otherwise the <code>error</code> object has the following properties:
|
|
*
|
|
* <ul>
|
|
* <li><code>code</code> — (Number) An error code, which can be one of the following:
|
|
* <table style="width:100%">
|
|
* <tr>
|
|
* <td>400</td> <td>One of the signal properties — data, type, or to — is invalid.
|
|
* Or the data cannot be parsed as JSON.</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>404</td> <td>The to connection does not exist.</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>413</td> <td>The type string exceeds the maximum length (128 characters),
|
|
* or the data property exceeds the maximum size (8kB).</td>
|
|
* </tr>
|
|
* <tr>
|
|
* <td>500</td> <td>The WebSocket connection is down.</td>
|
|
* </tr>
|
|
* </table>
|
|
* </li>
|
|
* <li><code>reason</code> — (String) A description of the error.</li>
|
|
* <li><code>signal</code> — (Object) An object with properties corresponding to the values passed
|
|
* into the <code>signal()</code> method — <code>data</code>, <code>to</code>, and <code>type</code>.
|
|
* </li>
|
|
* </ul>
|
|
*
|
|
* <p>Note that the <code>completionHandler</code> success result (<code>error == null</code>) indicates that the
|
|
* options passed into the <code>Session.signal()</code> method are valid and the signal was sent. It does
|
|
* <i>not</i> indicate that the signal was successfully received by any of the intended recipients.
|
|
*
|
|
* @method #signal
|
|
* @memberOf Session
|
|
* @see <a href="#event:signal">signal</a> and <a href="#event:signal:type">signal:type</a> events
|
|
*/
|
|
this.signal = function(options, completion) {
|
|
_socket.signal(options, completion);
|
|
};
|
|
|
|
/**
|
|
* Forces a remote connection to leave the session.
|
|
*
|
|
* <p>
|
|
* The <code>forceDisconnect()</code> method is normally used as a moderation tool
|
|
* to remove users from an ongoing session.
|
|
* </p>
|
|
* <p>
|
|
* When a connection is terminated using the <code>forceDisconnect()</code>,
|
|
* <code>sessionDisconnected</code>, <code>connectionDestroyed</code> and
|
|
* <code>streamDestroyed</code> events are dispatched in the same way as they
|
|
* would be if the connection had terminated itself using the <code>disconnect()</code> method. However,
|
|
* the <code>reason</code> property of a {@link ConnectionEvent} or {@link StreamEvent} object specifies
|
|
* <code>"forceDisconnected"</code> as the reason for the destruction of the connection and stream(s).
|
|
* </p>
|
|
* <p>
|
|
* While you can use the <code>forceDisconnect()</code> method to terminate your own connection,
|
|
* calling the <code>disconnect()</code> method is simpler.
|
|
* </p>
|
|
* <p>
|
|
* The TB object dispatches an <code>exception</code> event if the user's role
|
|
* does not include permissions required to force other users to disconnect.
|
|
* You define a user's role when you create the user token using the <code>generate_token()</code>
|
|
* method using our <a href="/opentok/libraries/server/">server-side libraries</a> or the <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page.
|
|
* See <a href="ExceptionEvent.html">ExceptionEvent</a> and <a href="TB.html#addEventListener">TB.addEventListener()</a>.
|
|
* </p>
|
|
* <p>
|
|
* The application throws an error if the session is not connected.
|
|
* </p>
|
|
*
|
|
* <h5>Events dispatched:</h5>
|
|
*
|
|
* <p>
|
|
* <code>connectionDestroyed</code> (<a href="ConnectionEvent.html">ConnectionEvent</a>) —
|
|
* On clients other than which had the connection terminated.
|
|
* </p>
|
|
* <p>
|
|
* <code>exception</code> (<a href="ExceptionEvent.html">ExceptionEvent</a>) —
|
|
* The user's role does not allow forcing other user's to disconnect (<code>event.code = 1530</code>),
|
|
* or the specified stream is not publishing to the session (<code>event.code = 1535</code>).
|
|
* </p>
|
|
* <p>
|
|
* <code>sessionDisconnected</code> (<a href="SessionDisconnectEvent.html">SessionDisconnectEvent</a>) —
|
|
* On the client which has the connection terminated.
|
|
* </p>
|
|
* <p>
|
|
* <code>streamDestroyed</code> (<a href="StreamEvent.html">StreamEvent</a>) —
|
|
* If streams are stopped as a result of the connection ending.
|
|
* </p>
|
|
*
|
|
* @param {Connection} connection The connection to be disconnected from the session.
|
|
* This value can either be a <a href="Connection.html">Connection</a> object or a connection ID (which can be
|
|
* obtained from the <code>connectionId</code> property of the Connection object).
|
|
*
|
|
* @method #forceDisconnect
|
|
* @memberOf Session
|
|
*/
|
|
|
|
this.forceDisconnect = function(connectionOrConnectionId, completionHandler) {
|
|
var notPermittedErrorMsg = "This token does not allow forceDisconnect. The role must be at least `moderator` to enable this functionality";
|
|
|
|
if (permittedTo("forceDisconnect")) {
|
|
var connectionId = typeof(connectionOrConnectionId) === 'string' ? connectionOrConnectionId : connectionOrConnectionId.id;
|
|
|
|
if (completionHandler) {
|
|
var work = new RemoteWork(this, completionHandler, {
|
|
timeoutMessage: "Timed out while waiting for connection " + connectionId + " to be force Disconnected."
|
|
});
|
|
|
|
work.failsOnExceptionCodes({
|
|
1520: notPermittedErrorMsg
|
|
});
|
|
|
|
_callbacks.forceDisconnect[connectionId] = work;
|
|
}
|
|
|
|
_socket.forceDisconnect(connectionId);
|
|
} else {
|
|
// if this throws an error the handleJsException won't occur
|
|
if (completionHandler) OT.$.callAsync(completionHandler, new OT.Error(null, notPermittedErrorMsg));
|
|
|
|
TB.handleJsException(notPermittedErrorMsg, OT.ExceptionCodes.UNABLE_TO_FORCE_DISCONNECT, {
|
|
session: this
|
|
});
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Forces the publisher of the specified stream to stop publishing the stream.
|
|
*
|
|
* <p>
|
|
* Calling this method causes the Session object to dispatch a <code>streamDestroyed</code>
|
|
* event on all clients that are subscribed to the stream (including the client that is
|
|
* publishing the stream). The <code>reason</code> property of the StreamEvent object is
|
|
* set to <code>"forceUnpublished"</code>.
|
|
* </p>
|
|
* <p>
|
|
* The TB object dispatches an <code>exception</code> event if the user's role
|
|
* does not include permissions required to force other users to unpublish.
|
|
* You define a user's role when you create the user token using the <code>generate_token()</code>
|
|
* method using our <a href="/opentok/libraries/server/">server-side libraries</a> or the <a href="https://dashboard.tokbox.com/projects">Dashboard</a> page page.
|
|
* You pass the token string as a parameter of the <code>connect()</code> method of the Session object.
|
|
* See <a href="ExceptionEvent.html">ExceptionEvent</a> and <a href="TB.html#addEventListener">TB.addEventListener()</a>.
|
|
* </p>
|
|
*
|
|
* <h5>Events dispatched:</h5>
|
|
*
|
|
* <p>
|
|
* <code>exception</code> (<a href="ExceptionEvent.html">ExceptionEvent</a>) —
|
|
* The user's role does not allow forcing other users to unpublish.
|
|
* </p>
|
|
* <p>
|
|
* <code>streamDestroyed</code> (<a href="StreamEvent.html">StreamEvent</a>) —
|
|
* The stream has been unpublished. The Session object dispatches this on all clients
|
|
* subscribed to the stream, as well as on the publisher's client.
|
|
* </p>
|
|
*
|
|
* @param {Stream} stream The stream to be unpublished.
|
|
*
|
|
* @method #forceUnpublish
|
|
* @memberOf Session
|
|
*/
|
|
this.forceUnpublish = function(streamOrStreamId, completionHandler) {
|
|
var notPermittedErrorMsg = "This token does not allow forceUnpublish. The role must be at least `moderator` to enable this functionality";
|
|
|
|
if (permittedTo("forceUnpublish")) {
|
|
var stream = typeof(streamOrStreamId) === 'string' ? this.streams.get(streamOrStreamId) : streamOrStreamId;
|
|
|
|
if (completionHandler) {
|
|
var work = new RemoteWork(this, completionHandler, {
|
|
timeoutMessage: "Timed out while waiting for stream " + stream.id + " to be force unpublished."
|
|
});
|
|
|
|
work.failsOnExceptionCodes({
|
|
1530: notPermittedErrorMsg
|
|
});
|
|
|
|
_callbacks.forceUnpublish[stream.id] = work;
|
|
}
|
|
|
|
_socket.forceUnpublish(stream.id);
|
|
} else {
|
|
// if this throws an error the handleJsException won't occur
|
|
if (completionHandler) OT.$.callAsync(completionHandler, new OT.Error(null, notPermittedErrorMsg));
|
|
|
|
TB.handleJsException(notPermittedErrorMsg, OT.ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH, {
|
|
session: this
|
|
});
|
|
}
|
|
};
|
|
|
|
this.getStateManager = function() {
|
|
OT.warn("Fixme: Have not implemented session.getStateManager");
|
|
};
|
|
|
|
OT.$.defineGetters(this, {
|
|
apiKey: function() { return _apiKey; },
|
|
token: function() { return _token; },
|
|
connected: function() { return this.is('connected'); },
|
|
connection: function() { return _socket && _socket.id ? this.connections.get(_socket.id) : null; },
|
|
capabilities: function() { return _socket ? _socket.capabilities : new OT.Capabilities([]); },
|
|
sessionId: function() { return _sessionId; },
|
|
id: function() { return _sessionId; }
|
|
}, true);
|
|
|
|
|
|
/**
|
|
* A new connection to this session has been created.
|
|
* @name connectionCreated
|
|
* @event
|
|
* @memberof Session
|
|
* @see ConnectionEvent
|
|
* @see <a href="TB.html#initSession">TB.initSession()</a>
|
|
*/
|
|
|
|
/**
|
|
* A connection to this session has ended.
|
|
* @name connectionDestroyed
|
|
* @event
|
|
* @memberof Session
|
|
* @see ConnectionEvent
|
|
*/
|
|
|
|
/**
|
|
* The page has connected to an OpenTok session. This event is dispatched asynchronously in response to
|
|
* a successful call to the <code>connect()</code> method of a Session object. Before calling the <code>connect()</code>
|
|
* method, initialize the session by calling the <code>TB.initSession()</code> method. For a code example and more details,
|
|
* see <a href="#connect">Session.connect()</a>.
|
|
* @name sessionConnected
|
|
* @event
|
|
* @memberof Session
|
|
* @see SessionConnectEvent
|
|
* @see <a href="#connect">Session.connect()</a>
|
|
* @see <a href="TB.html#initSession">TB.initSession()</a>
|
|
*/
|
|
|
|
/**
|
|
* The session has disconnected. This event may be dispatched asynchronously in response to a successful call to the
|
|
* <code>disconnect()</code> method of the Session object. The event may also be disptached if a session connection is lost
|
|
* inadvertantly, as in the case of a lost network connection.
|
|
* @name sessionDisconnected
|
|
* @event
|
|
* @memberof Session
|
|
* @see <a href="#disconnect">Session.disconnect()</a>
|
|
* @see <a href="#disconnect">Session.forceDisconnect()</a>
|
|
* @see SessionDisconnectEvent
|
|
*/
|
|
|
|
/**
|
|
* A new stream has been created on this session. For a code example and more details, see {@link StreamEvent}.
|
|
* @name streamCreated
|
|
* @event
|
|
* @memberof Session
|
|
* @see StreamEvent
|
|
* @see <a href="Session.html#publish">Session.publish()</a>
|
|
*/
|
|
|
|
/**
|
|
* A stream has closed on this connection. For a code example and more details, see {@link StreamEvent}.
|
|
* @name streamDestroyed
|
|
* @event
|
|
* @memberof Session
|
|
* @see StreamEvent
|
|
*/
|
|
|
|
/**
|
|
* A stream has started or stopped publishing audio or video (see <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a> and
|
|
* <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a>); or the <code>videoDimensions</code> property of the Stream
|
|
* object has changed (see <a href="Stream.html#"videoDimensions>Stream.videoDimensions</a>).
|
|
* @name streamPropertyChanged
|
|
* @event
|
|
* @memberof Session
|
|
* @see StreamPropertyChangedEvent
|
|
* @see <a href="Publisher.html#publishAudio">Publisher.publishAudio()</a>
|
|
* @see <a href="Publisher.html#publishVideo">Publisher.publishVideo()</a>
|
|
* @see <a href="Stream.html#"hasAudio>Stream.hasAudio</a>
|
|
* @see <a href="Stream.html#"hasVideo>Stream.hasVideo</a>
|
|
* @see <a href="Stream.html#"videoDimensions>Stream.videoDimensions</a>
|
|
*/
|
|
|
|
/**
|
|
* A signal was received from the session. The <a href="SignalEvent.html">SignalEvent</a> class defines this event object.
|
|
* It includes the following properties:
|
|
* <ul>
|
|
* <li><code>data</code> — (Object) The data payload sent with the signal (if there is one).</li>
|
|
* <li><code>from</code> — (<a href="Connection.html">Connection</a>) The Connection corresponding to the
|
|
* client that sent with the signal.</li>
|
|
* <li><code>type</code> — (String) The type assigned to the signal (if there is one).</li>
|
|
* </ul>
|
|
* <p>
|
|
* You can register to receive all signals sent in the session, by adding an event handler for the <code>signal</code>
|
|
* event. For example, the following code adds an event handler to process all signals sent in the session:
|
|
* <pre>
|
|
* session.addEventListener("signal", function(event) {
|
|
* console.log("Signal sent from connection " + event.from.id);
|
|
* });
|
|
* </pre>
|
|
* <p>You can register for signals of a specfied type by adding an event handler for the <code>signal:type</code>
|
|
* event (replacing <code>type</code> with the actual type string to filter on).
|
|
*
|
|
* @name signal
|
|
* @event
|
|
* @memberof Session
|
|
* @see <a href="Session.html#signal">Session.signal()</a>
|
|
* @see SignalEvent
|
|
* @see <a href="#event:signal:type">signal:type</a> event
|
|
*/
|
|
|
|
/**
|
|
* A signal of the specified type was received from the session. The <a href="SignalEvent.html">SignalEvent</a> class
|
|
* defines this event object.
|
|
* It includes the following properties:
|
|
* <ul>
|
|
* <li><code>data</code> — (Object) The data payload sent with the signal.</li>
|
|
* <li><code>from</code> — (<a href="Connection.html">Connection</a>) The Connection corresponding to the
|
|
* client that sent with the signal.</li>
|
|
* <li><code>type</code> — (String) The type assigned to the signal (if there is one).</li>
|
|
* </ul>
|
|
* <p>
|
|
* You can register for signals of a specfied type by adding an event handler for the <code>signal:type</code>
|
|
* event (replacing <code>type</code> with the actual type string to filter on). For example, the following code adds
|
|
* an event handler for signals of type "foo":
|
|
* <pre>
|
|
* session.addEventListener("signal:foo", function(event) {
|
|
* console.log("foo signal sent from connection " + event.from.id);
|
|
* });
|
|
* </pre>
|
|
* <p>
|
|
* You can register to receive <i>all</i> signals sent in the session, by adding an event handler for the
|
|
* <code>signal</code> event.
|
|
*
|
|
* @name signal:type
|
|
* @event
|
|
* @memberof Session
|
|
* @see <a href="Session.html#signal">Session.signal()</a>
|
|
* @see SignalEvent
|
|
* @see <a href="#event:signal">signal</a> event
|
|
*/
|
|
|
|
|
|
};
|
|
|
|
})(window);
|
|
(function(window) {
|
|
var style = document.createElement('link');
|
|
style.type = 'text/css';
|
|
style.media = 'screen';
|
|
style.rel = 'stylesheet';
|
|
style.href = OT.properties.cssURL;
|
|
var head = document.head || document.getElementsByTagName('head')[0];
|
|
head.appendChild(style);
|
|
})(window);
|
|
(function(window){
|
|
|
|
// Register as a named AMD module, since TokBox could be concatenated with other
|
|
// files that may use define, but not via a proper concatenation script that
|
|
// understands anonymous AMD modules. A named AMD is safest and most robust
|
|
// way to register. Uppercase TB is used because AMD module names are
|
|
// derived from file names, and OpenTok is normally delivered in an uppercase
|
|
// file name.
|
|
if ( typeof define === "function" && define.amd ) {
|
|
define( "TB", [], function () { return TB; } );
|
|
}
|
|
|
|
})(window);
|