/** * @license OpenTok JavaScript Library v2.2.9.1 * http://www.tokbox.com/ * * Copyright (c) 2014 TokBox, Inc. * Released under the MIT license * http://opensource.org/licenses/MIT * * Date: September 08 10:17:05 2014 */ (function(window) { if (!window.OT) window.OT = {}; OT.properties = { version: 'v2.2.9.1', // The current version (eg. v2.0.4) (This is replaced by gradle) build: '72b534e', // The current build hash (This is replaced by gradle) // Whether or not to turn on debug logging by default debug: 'false', // The URL of the tokbox website websiteURL: 'http://www.tokbox.com', // The URL of the CDN cdnURL: 'http://static.opentok.com', // The URL to use for logging loggingURL: 'http://hlg.tokbox.com/prod', // The anvil API URL apiURL: 'http://anvil.opentok.com', // What protocol to use when connecting to the rumor web socket messagingProtocol: 'wss', // What port to use when connection to the rumor web socket messagingPort: 443, // If this environment supports SSL supportSSL: 'true', // The CDN to use if we're using SSL cdnURLSSL: 'https://static.opentok.com', // The URL to use for logging loggingURLSSL: 'https://hlg.tokbox.com/prod', // The anvil API URL to use if we're using SSL apiURLSSL: 'https://anvil.opentok.com', minimumVersion: { firefox: parseFloat('29'), chrome: parseFloat('34') } }; })(window); /** * @license Common JS Helpers on OpenTok 0.2.0 3fa583f master * http://www.tokbox.com/ * * Copyright (c) 2014 TokBox, Inc. * Released under the MIT license * http://opensource.org/licenses/MIT * * Date: August 08 12:31:42 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'); // // /*jshint browser:true, smarttabs:true*/ !(function(window, undefined) { var OTHelpers = function(domId) { return document.getElementById(domId); }; var previousOTHelpers = window.OTHelpers; window.OTHelpers = OTHelpers; // A guard to detect when IE has performed cleans on unload window.___othelpers = true; OTHelpers.keys = Object.keys || function(object) { var keys = [], hasOwnProperty = Object.prototype.hasOwnProperty; for(var key in object) { if(hasOwnProperty.call(object, key)) { keys.push(key); } } return keys; }; var _each = Array.prototype.forEach || function(iter, ctx) { for(var idx = 0, count = this.length || 0; idx < count; ++idx) { if(idx in this) { iter.call(ctx, this[idx], idx); } } }; OTHelpers.forEach = function(array, iter, ctx) { return _each.call(array, iter, ctx); }; var _map = Array.prototype.map || function(iter, ctx) { var collect = []; _each.call(this, function(item, idx) { collect.push(iter.call(ctx, item, idx)); }); return collect; }; OTHelpers.map = function(array, iter) { return _map.call(array, iter); }; var _filter = Array.prototype.filter || function(iter, ctx) { var collect = []; _each.call(this, function(item, idx) { if(iter.call(ctx, item, idx)) { collect.push(item); } }); return collect; }; OTHelpers.filter = function(array, iter, ctx) { return _filter.call(array, iter, ctx); }; var _some = Array.prototype.some || function(iter, ctx) { var any = false; for(var idx = 0, count = this.length || 0; idx < count; ++idx) { if(idx in this) { if(iter.call(ctx, this[idx], idx)) { any = true; break; } } } return any; }; OTHelpers.some = function(array, iter, ctx) { return _some.call(array, iter, ctx); }; var _indexOf = Array.prototype.indexOf || function(searchElement, fromIndex) { var i, pivot = (fromIndex) ? fromIndex : 0, length; if (!this) { throw new TypeError(); } length = this.length; if (length === 0 || pivot >= length) { return -1; } if (pivot < 0) { pivot = length - Math.abs(pivot); } for (i = pivot; i < length; i++) { if (this[i] === searchElement) { return i; } } return -1; }; OTHelpers.arrayIndexOf = function(array, searchElement, fromIndex) { return _indexOf.call(array, searchElement, fromIndex); }; var _bind = Function.prototype.bind || function() { var args = Array.prototype.slice.call(arguments), ctx = args.shift(), fn = this; return function() { return fn.apply(ctx, args.concat(Array.prototype.slice.call(arguments))); }; }; OTHelpers.bind = function() { var args = Array.prototype.slice.call(arguments), fn = args.shift(); return _bind.apply(fn, args); }; var _trim = String.prototype.trim || function() { return this.replace(/^\s+|\s+$/g, ''); }; OTHelpers.trim = function(str) { return _trim.call(str); }; OTHelpers.noConflict = function() { OTHelpers.noConflict = function() { return OTHelpers; }; window.OTHelpers = previousOTHelpers; return OTHelpers; }; OTHelpers.isNone = function(obj) { return obj === undefined || obj === null; }; OTHelpers.isObject = function(obj) { return obj === Object(obj); }; OTHelpers.isFunction = function(obj) { return !!obj && (obj.toString().indexOf('()') !== -1 || Object.prototype.toString.call(obj) === '[object Function]'); }; OTHelpers.isArray = OTHelpers.isFunction(Array.isArray) && Array.isArray || function (vArg) { return Object.prototype.toString.call(vArg) === '[object Array]'; }; OTHelpers.isEmpty = function(obj) { if (obj === null || obj === undefined) return true; if (OTHelpers.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; }; // 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(); OTHelpers.forEach(sources, 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(); OTHelpers.forEach(sources, 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 OTHelpers.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj); }; // Handy do nothing function OTHelpers.noop = function() {}; // 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 = OTHelpers.bind(now, performance); navigationStart = performance.timing.navigationStart; return function() { return navigationStart + now(); }; } else { return function() { return new Date().getTime(); }; } })(); var _browser = function() { var userAgent = window.navigator.userAgent.toLowerCase(), appName = window.navigator.appName, navigatorVendor, browser = 'unknown', version = -1; if (userAgent.indexOf('opera') > -1 || userAgent.indexOf('opr') > -1) { browser = 'Opera'; if (/opr\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { version = parseFloat( RegExp.$1 ); } } else if (userAgent.indexOf('firefox') > -1) { browser = 'Firefox'; if (/firefox\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { version = parseFloat( RegExp.$1 ); } } else if (appName === 'Microsoft Internet Explorer') { // IE 10 and below browser = 'IE'; if (/msie ([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { version = parseFloat( RegExp.$1 ); } } else if (appName === 'Netscape' && userAgent.indexOf('trident') > -1) { // IE 11+ browser = 'IE'; if (/trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { version = parseFloat( RegExp.$1 ); } } else if (userAgent.indexOf('chrome') > -1) { browser = 'Chrome'; if (/chrome\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { version = parseFloat( RegExp.$1 ); } } else if ((navigatorVendor = window.navigator.vendor) && navigatorVendor.toLowerCase().indexOf('apple') > -1) { browser = 'Safari'; if (/version\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { version = parseFloat( RegExp.$1 ); } } return { browser: browser, version: version, iframeNeedsLoad: userAgent.indexOf('webkit') < 0 }; }(); OTHelpers.browser = function() { return _browser.browser; }; OTHelpers.browserVersion = function() { 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'); }, // 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 }; } OTHelpers.defineProperties(self, propsDefinition); }; var generatePropertyFunction = function(object, getter, setter) { if(getter && !setter) { return function() { return getter.call(object); }; } else if(getter && setter) { return function(value) { if(value !== void 0) { setter.call(object, value); } return getter.call(object); }; } else { return function(value) { if(value !== void 0) { setter.call(object, value); } }; } }; OTHelpers.defineProperties = function(object, getterSetters) { for (var key in getterSetters) { object[key] = generatePropertyFunction(object, getterSetters[key].get, getterSetters[key].set); } }; // 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; }; // These next bits are included from Underscore.js. The original copyright // notice is below. // // http://underscorejs.org // (c) 2009-2011 Jeremy Ashkenas, DocumentCloud Inc. // (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. // 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('[' + OTHelpers.keys(entityMap.escape).join('') + ']', 'g'), unescape: new RegExp('(' + OTHelpers.keys(entityMap.unescape).join('|') + ')', 'g') }; // Functions for escaping and unescaping strings to/from HTML interpolation. OTHelpers.forEach(['escape', 'unescape'], 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 = self.currentState = initialState; var setState = function(state) { if (currentState !== state) { if (OTHelpers.arrayIndexOf(possibleStates, state) === -1) { if (stateChangedFailed && OTHelpers.isFunction(stateChangedFailed)) { stateChangedFailed('invalidState', state); } return; } self.previousState = previousState = currentState; self.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 OTHelpers.arrayIndexOf(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 OTHelpers.arrayIndexOf(arguments, currentState) === -1; }; 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)); /*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 = [], _canApplyConsole = true; try { Function.prototype.bind.call(window.console.log, window.console); } catch (err) { _canApplyConsole = false; } // Some objects can't be logged in the console, mostly these are certain // types of native objects that are exposed to JS. This is only really a // problem with IE, hence only the IE version does anything. var makeLogArgumentsSafe = function(args) { return args; }; if (OTHelpers.browser() === 'IE') { makeLogArgumentsSafe = function(args) { return [toDebugString(Array.prototype.slice.apply(args))]; }; } // 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, args = makeLogArgumentsSafe(arguments); // In IE, window.console may not exist if the developer tools aren't open // This also means that cons and cons[method] can appear at any moment // hence why we retest this every time. 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 || _canApplyConsole) { if (!cons[method].apply) { cons[method] = Function.prototype.bind.call(cons[method], cons); } cons[method].apply(cons, args); } else { // This isn't the same result as the above, but it's better // than nothing. cons[method](args); } } else if (fallback) { fallback.apply(on, args); // Skip appendToLogs, we delegate entirely to the fallback return; } appendToLogs(method, makeLogArgumentsSafe(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(); } function toJson(object) { try { return JSON.stringify(object); } catch(e) { return object.toString(); } } function toDebugString(object) { var components = []; if (typeof(object) === 'undefined') { // noop } else if (object === null) { components.push('NULL'); } else if (OTHelpers.isArray(object)) { for (var i=0; i 0) { var args = timeouts.shift(), fn = args.shift(); fn.apply(null, args); } } }; // Ensure that we don't receive messages after unload // Yes, this seems to really happen in IE sometimes, usually // when iFrames are involved. OTHelpers.on(window, 'unload', removeMessageHandler); if(window.addEventListener) { window.addEventListener('message', handleMessage, true); } else if(window.attachEvent) { window.attachEvent('onmessage', handleMessage); } _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) { timeouts.push(Array.prototype.slice.call(arguments)); window.postMessage(messageName, '*'); }; } else { _callAsync = function (/* fn, [arg1, arg2, ..., argN] */) { var args = Array.prototype.slice.call(arguments), fn = args.shift(); setTimeout(function() { fn.apply(null, args); }, 0); }; } // 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. // // It does this using window.postMessage, although if postMessage won't // work it will fallback to setTimeout. // OTHelpers.callAsync = _callAsync; // 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 on, once, and off * 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; OTHelpers.forEach(listeners, 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] && OTHelpers.some(_events[name], 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; OTHelpers.forEach(listeners, 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] = OTHelpers.filter(_events[eventName], function(listener){ return listener.context !== context; }); } else { delete _events[eventName]; } } }; var addListeners = OTHelpers.bind(function (eventNames, handler, context, closure) { var listener = {handler: handler}; if (context) listener.context = context; if (closure) listener.closure = closure; OTHelpers.forEach(eventNames, function(name) { if (!_events[name]) _events[name] = []; _events[name].push(listener); var addedListener = name + ':added'; if (_events[addedListener]) { executeListeners(addedListener, [_events[name].length]); } }); }, self); var removeListeners = function (eventNames, handler, context) { function filterHandlerAndContext(listener) { return !(listener.handler === handler && listener.context === context); } OTHelpers.forEach(eventNames, OTHelpers.bind(function(name) { if (_events[name]) { _events[name] = OTHelpers.filter(_events[name], filterHandlerAndContext); if (_events[name].length === 0) delete _events[name]; var removedListener = name + ':removed'; if (_events[ removedListener]) { executeListeners(removedListener, [_events[name] ? _events[name].length : 0]); } } }, 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; }; /** * Adds an event handler function for one or more events. * *

* The following code adds an event handler for one event: *

* *
    * obj.on("eventName", function (event) {
    *     // This is the event handler.
    * });
    * 
* *

If you pass in multiple event names and a handler method, the handler is * registered for each of those events:

* *
    * obj.on("eventName1 eventName2",
    *        function (event) {
    *            // This is the event handler.
    *        });
    * 
* *

You can also pass in a third context parameter (which is optional) to * define the value of this in the handler method:

* *
obj.on("eventName",
    *        function (event) {
    *            // This is the event handler.
    *        },
    *        obj);
    * 
* *

* The method also supports an alternate syntax, in which the first parameter is an object * that is a hash map of event names and handler functions and the second parameter (optional) * is the context for this in each handler: *

*
    * obj.on(
    *    {
    *       eventName1: function (event) {
    *               // This is the handler for eventName1.
    *           },
    *       eventName2:  function (event) {
    *               // This is the handler for eventName2.
    *           }
    *    },
    *    obj);
    * 
* *

* If you do not add a handler for an event, the event is ignored locally. *

* * @param {String} type The string identifying the type of event. You can specify multiple event * names in this string, separating them with a space. The event handler will process each of * the events. * @param {Function} handler The handler function to process the event. This function takes * the event object as a parameter. * @param {Object} context (Optional) Defines the value of this in the event * handler function. * * @returns {EventDispatcher} The EventDispatcher object. * * @memberOf EventDispatcher * @method #on * @see off() * @see once() * @see Events */ self.on = function(eventNames, handlerOrContext, context) { if (typeof(eventNames) === 'string' && handlerOrContext) { addListeners(eventNames.split(' '), handlerOrContext, context); } else { for (var name in eventNames) { if (eventNames.hasOwnProperty(name)) { addListeners([name], eventNames[name], handlerOrContext); } } } return this; }; /** * Removes an event handler or handlers. * *

If you pass in one event name and a handler method, the handler is removed for that * event:

* *
obj.off("eventName", eventHandler);
* *

If you pass in multiple event names and a handler method, the handler is removed for * those events:

* *
obj.off("eventName1 eventName2", eventHandler);
* *

If you pass in an event name (or names) and no handler method, all handlers are * removed for those events:

* *
obj.off("event1Name event2Name");
* *

If you pass in no arguments, all event handlers are removed for all events * dispatched by the object:

* *
obj.off();
* *

* The method also supports an alternate syntax, in which the first parameter is an object that * is a hash map of event names and handler functions and the second parameter (optional) is * the context for this in each handler: *

*
    * obj.off(
    *    {
    *       eventName1: event1Handler,
    *       eventName2: event2Handler
    *    });
    * 
* * @param {String} type (Optional) The string identifying the type of event. You can * use a space to specify multiple events, as in "accessAllowed accessDenied * accessDialogClosed". If you pass in no type value (or other arguments), * all event handlers are removed for the object. * @param {Function} handler (Optional) The event handler function to remove. The handler * must be the same function object as was passed into on(). Be careful with * helpers like bind() that return a new function when called. If you pass in * no handler, all event handlers are removed for the specified event * type. * @param {Object} context (Optional) If you specify a context, the event handler * is removed for all specified events and handlers that use the specified context. (The * context must match the context passed into on().) * * @returns {Object} The object that dispatched the event. * * @memberOf EventDispatcher * @method #off * @see on() * @see once() * @see Events */ self.off = function(eventNames, handlerOrContext, context) { if (typeof eventNames === 'string') { if (handlerOrContext && OTHelpers.isFunction(handlerOrContext)) { removeListeners(eventNames.split(' '), handlerOrContext, context); } else { OTHelpers.forEach(eventNames.split(' '), function(name) { removeAllListenersNamed(name, handlerOrContext); }, this); } } else if (!eventNames) { // remove all bound events _events = {}; } else { for (var name in eventNames) { if (eventNames.hasOwnProperty(name)) { removeListeners([name], eventNames[name], handlerOrContext); } } } return this; }; /** * Adds an event handler function for one or more events. Once the handler is called, * the specified handler method is removed as a handler for this event. (When you use * the on() method to add an event handler, the handler is not * removed when it is called.) The once() method is the equivilent of * calling the on() * method and calling off() the first time the handler is invoked. * *

* The following code adds a one-time event handler for the accessAllowed event: *

* *
    * obj.once("eventName", function (event) {
    *    // This is the event handler.
    * });
    * 
* *

If you pass in multiple event names and a handler method, the handler is registered * for each of those events:

* *
obj.once("eventName1 eventName2"
    *          function (event) {
    *              // This is the event handler.
    *          });
    * 
* *

You can also pass in a third context parameter (which is optional) to define * the value of * this in the handler method:

* *
obj.once("eventName",
    *          function (event) {
    *              // This is the event handler.
    *          },
    *          obj);
    * 
* *

* The method also supports an alternate syntax, in which the first parameter is an object that * is a hash map of event names and handler functions and the second parameter (optional) is the * context for this in each handler: *

*
    * obj.once(
    *    {
    *       eventName1: function (event) {
    *                  // This is the event handler for eventName1.
    *           },
    *       eventName2:  function (event) {
    *                  // This is the event handler for eventName1.
    *           }
    *    },
    *    obj);
    * 
* * @param {String} type The string identifying the type of event. You can specify multiple * event names in this string, separating them with a space. The event handler will process * the first occurence of the events. After the first event, the handler is removed (for * all specified events). * @param {Function} handler The handler function to process the event. This function takes * the event object as a parameter. * @param {Object} context (Optional) Defines the value of this in the event * handler function. * * @returns {Object} The object that dispatched the event. * * @memberOf EventDispatcher * @method #once * @see on() * @see off() * @see Events */ self.once = function(eventNames, handler, context) { var names = eventNames.split(' '), fun = OTHelpers.bind(function() { var result = handler.apply(context || null, arguments); removeListeners(names, handler, context); return result; }, this); addListeners(names, handler, context, fun); return this; }; /** * Deprecated; use on() or once() instead. *

* This method registers a method as an event listener for a specific event. *

* *

* 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. *

*

* Throws an exception if the listener name is invalid. *

* * @param {String} type The string identifying the type of event. * * @param {Function} listener The function to be invoked when the object dispatches the event. * * @param {Object} context (Optional) Defines the value of this in the event * handler function. * * @memberOf EventDispatcher * @method #addEventListener * @see on() * @see once() * @see Events */ // See 'on' for usage. // @depreciated will become a private helper function in the future. self.addEventListener = function(eventName, handler, context) { OTHelpers.warn('The addEventListener() method is deprecated. Use on() or once() instead.'); addListeners([eventName], handler, context); }; /** * Deprecated; use on() or once() instead. *

* Removes an event listener for a specific event. *

* *

* Throws an exception if the listener name is invalid. *

* * @param {String} type The string identifying the type of event. * * @param {Function} listener The event listener function to remove. * * @param {Object} context (Optional) If you specify a context, the event * handler is removed for all specified events and event listeners that use the specified context. (The context must match the context passed into * addEventListener().) * * @memberOf EventDispatcher * @method #removeEventListener * @see off() * @see Events */ // See 'off' for usage. // @depreciated will become a private helper function in the future. self.removeEventListener = function(eventName, handler, context) { OTHelpers.warn('The removeEventListener() method is deprecated. Use off() instead.'); 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; 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; }; }; }; })(window, window.OTHelpers); /*jshint browser:true, smarttabs:true*/ // tb_require('../helpers.js') // tb_require('./callbacks.js') // DOM helpers (function(window, OTHelpers, undefined) { OTHelpers.isElementNode = function(node) { return node && typeof node === 'object' && node.nodeType === 1; }; // 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) { /*jshint newcap:false */ 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, children, doc) { var element = (doc || 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]); } } } var setChildren = function(child) { if(typeof child === 'string') { element.innerHTML = element.innerHTML + child; } else { element.appendChild(child); } }; if(OTHelpers.isArray(children)) { OTHelpers.forEach(children, setChildren); } else if(children) { setChildren(children); } 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; }; // 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 OTHelpers.forEach(stylesToObserve, function(style) { oldStyles[style] = getStyle(style); }); var observer = new MutationObserver(function(mutations) { var changeSet = {}; OTHelpers.forEach(mutations, function(mutation) { if (mutation.attributeName !== 'style') return; var isHidden = OTHelpers.isDisplayNone(element); OTHelpers.forEach(stylesToObserve, function(style) { if(isHidden && (style === 'width' || style === 'height')) return; var newValue = getStyle(style); if (newValue !== oldStyles[style]) { 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"); // OTHelpers.forEach(removedNodes, 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 = []; OTHelpers.forEach(mutations, 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(options) { OTHelpers.eventing(this, true); var callback = arguments[arguments.length - 1]; if(!OTHelpers.isFunction(callback)) { throw new Error('OTHelpers.Modal2 must be given a callback'); } if(arguments.length < 2) { options = {}; } var domElement = document.createElement('iframe'); domElement.id = options.id || OTHelpers.uuid(); domElement.style.position = 'absolute'; domElement.style.position = 'fixed'; domElement.style.height = '100%'; domElement.style.width = '100%'; domElement.style.top = '0px'; domElement.style.left = '0px'; domElement.style.right = '0px'; domElement.style.bottom = '0px'; domElement.style.zIndex = 1000; domElement.style.border = '0'; try { domElement.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. domElement.style.backgroundColor = 'transparent'; domElement.setAttribute('allowTransparency', 'true'); } domElement.scrolling = 'no'; domElement.setAttribute('scrolling', 'no'); // This is necessary for IE, as it will not inherit it's doctype from // the parent frame. var frameContent = '' + '' + '' + ''; var wrappedCallback = function() { var doc = domElement.contentDocument || domElement.contentWindow.document; if (OTHelpers.browserVersion().iframeNeedsLoad) { doc.body.style.backgroundColor = 'transparent'; doc.body.style.border = 'none'; if (OTHelpers.browser() !== 'IE') { // Skip this for IE as we use the bookmarklet workaround // for THAT browser. doc.open(); doc.write(frameContent); doc.close(); } } callback( domElement.contentWindow, doc ); }; document.body.appendChild(domElement); if(OTHelpers.browserVersion().iframeNeedsLoad) { if (OTHelpers.browser() === 'IE') { // This works around some issues with IE and document.write. // Basically this works by slightly abusing the bookmarklet/scriptlet // functionality that all browsers support. domElement.contentWindow.contents = frameContent; /*jshint scripturl:true*/ domElement.src = 'javascript:window["contents"]'; /*jshint scripturl:false*/ } OTHelpers.on(domElement, 'load', wrappedCallback); } else { setTimeout(wrappedCallback); } this.close = function() { OTHelpers.removeElement(domElement); this.trigger('closed'); this.element = domElement = null; return this; }; this.element = domElement; }; })(window, window.OTHelpers); /* * getComputedStyle from * https://github.com/jonathantneal/Polyfills-for-IE8/blob/master/getComputedStyle.js // tb_require('../helpers.js') // tb_require('./dom.js') /*jshint strict: false, eqnull: true, browser:true, smarttabs:true*/ (function(window, OTHelpers, undefined) { /*jshint eqnull: true, browser: true */ function getPixelSize(element, style, property, fontSize) { var sizeWithSuffix = style[property], size = parseFloat(sizeWithSuffix), suffix = sizeWithSuffix.split(/\d/)[0], rootSize; fontSize = fontSize != null ? fontSize : /%|em/.test(suffix) && element.parentElement ? getPixelSize(element.parentElement, element.parentElement.currentStyle, 'fontSize', null) : 16; rootSize = property === 'fontSize' ? fontSize : /width/i.test(property) ? element.clientWidth : element.clientHeight; return (suffix === 'em') ? size * fontSize : (suffix === 'in') ? size * 96 : (suffix === 'pt') ? size * 96 / 72 : (suffix === '%') ? size / 100 * rootSize : size; } function setShortStyleProperty(style, property) { var borderSuffix = property === 'border' ? 'Width' : '', t = property + 'Top' + borderSuffix, r = property + 'Right' + borderSuffix, b = property + 'Bottom' + borderSuffix, l = property + 'Left' + borderSuffix; style[property] = (style[t] === style[r] === style[b] === style[l] ? [style[t]] : style[t] === style[b] && style[l] === style[r] ? [style[t], style[r]] : style[l] === style[r] ? [style[t], style[r], style[b]] : [style[t], style[r], style[b], style[l]]).join(' '); } function CSSStyleDeclaration(element) { var currentStyle = element.currentStyle, style = this, fontSize = getPixelSize(element, currentStyle, 'fontSize', null), property; for (property in currentStyle) { if (/width|height|margin.|padding.|border.+W/.test(property) && style[property] !== 'auto') { style[property] = getPixelSize(element, currentStyle, property, fontSize) + 'px'; } else if (property === 'styleFloat') { /*jshint -W069 */ style['float'] = currentStyle[property]; } else { style[property] = currentStyle[property]; } } setShortStyleProperty(style, 'margin'); setShortStyleProperty(style, 'padding'); setShortStyleProperty(style, 'border'); style.fontSize = fontSize + 'px'; return style; } CSSStyleDeclaration.prototype = { constructor: CSSStyleDeclaration, getPropertyPriority: function () {}, getPropertyValue: function ( prop ) { return this[prop] || ''; }, item: function () {}, removeProperty: function () {}, setProperty: function () {}, getPropertyCSSValue: function () {} }; function getComputedStyle(element) { return new CSSStyleDeclaration(element); } OTHelpers.getComputedStyle = function(element) { if(element && element.ownerDocument && element.ownerDocument.defaultView && element.ownerDocument.defaultView.getComputedStyle) { return element.ownerDocument.defaultView.getComputedStyle(element); } else { return getComputedStyle(element); } }; })(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 = OTHelpers.trim(value).split(/\s+/), i, l; if (OTHelpers.supportsClassList()) { for (i=0, l=classNames.length; i 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') // tb_require('./getcomputedstyle.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 = OTHelpers.getComputedStyle(element); return computedStyle.getPropertyValue('display') === 'none'; }; OTHelpers.show = function(element) { var display = element.style.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) { if (nameOrHash.hasOwnProperty(cssName)) { 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 = OTHelpers.getComputedStyle(element), 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') (function(window, OTHelpers, undefined) { var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; if (requestAnimationFrame) { requestAnimationFrame = OTHelpers.bind(requestAnimationFrame, window); } else { var lastTime = 0; var startTime = OTHelpers.now(); requestAnimationFrame = function(callback){ var currTime = OTHelpers.now(); var timeToCall = Math.max(0, 16 - (currTime - lastTime)); var id = window.setTimeout(function() { callback(currTime - startTime); }, timeToCall); lastTime = currTime + timeToCall; return id; }; } OTHelpers.requestAnimationFrame = requestAnimationFrame; })(window, window.OTHelpers); // AJAX helpers /*jshint browser:true, smarttabs:true*/ // tb_require('../helpers.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.getJSON = function(url, options, callback) { options = options || {}; var done = function(error, event) { if(error) { callback(error, event && event.target && event.target.responseText); } else { var response; try { response = JSON.parse(event.target.responseText); } catch(e) { // Badly formed JSON callback(e, event && event.target && event.target.responseText); return; } callback(null, response, event); } }; if(options.xdomainrequest) { OTHelpers.xdomainRequest(url, { method: 'GET' }, done); } else { var extendedHeaders = OTHelpers.extend({ 'Accept': 'application/json' }, options.headers || {}); OTHelpers.get(url, OTHelpers.extend(options || {}, { headers: extendedHeaders }), done); } }; OTHelpers.xdomainRequest = function(url, options, callback) { /*global XDomainRequest*/ var xdr = new XDomainRequest(), _options = options || {}, _method = _options.method; if(!_method) { callback(new Error('No HTTP method specified in options')); return; } _method = _method.toUpperCase(); if(!(_method === 'GET' || _method === 'POST')) { callback(new Error('HTTP method can only be ')); return; } function done(err, event) { xdr.onload = xdr.onerror = xdr.ontimeout = function() {}; xdr = void 0; callback(err, event); } xdr.onload = function() { done(null, { target: { responseText: xdr.responseText, headers: { 'content-type': xdr.contentType } } }); }; xdr.onerror = function() { done(new Error('XDomainRequest of ' + url + ' failed')); }; xdr.ontimeout = function() { done(new Error('XDomainRequest of ' + url + ' timed out')); }; xdr.open(_method, url); xdr.send(options.body && formatPostData(options.body)); }; OTHelpers.request = function(url, options, callback) { var request = new XMLHttpRequest(), _options = options || {}, _method = _options.method; if(!_method) { callback(new Error('No HTTP method specified in options')); return; } // Setup callbacks to correctly respond to success and error callbacks. This includes // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore // by default. if(callback) { OTHelpers.on(request, '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 ) { callback(null, event); } else { callback(event); } }); OTHelpers.on(request, 'error', callback); } request.open(options.method, url, true); if (!_options.headers) _options.headers = {}; for (var name in _options.headers) { request.setRequestHeader(name, _options.headers[name]); } request.send(options.body && formatPostData(options.body)); }; OTHelpers.get = function(url, options, callback) { var _options = OTHelpers.extend(options || {}, { method: 'GET' }); OTHelpers.request(url, _options, callback); }; OTHelpers.post = function(url, options, callback) { var _options = OTHelpers.extend(options || {}, { method: 'POST' }); if(_options.xdomainrequest) { OTHelpers.xdomainRequest(url, _options, callback); } else { OTHelpers.request(url, _options, callback); } }; })(window, window.OTHelpers); !(function(window) { /* global OTHelpers */ if (!window.OT) window.OT = {}; // Bring OTHelpers in as OT.$ OT.$ = OTHelpers.noConflict(); // Allow events to be bound on OT OT.$.eventing(OT); // REMOVE THIS POST IE MERGE OT.$.defineGetters = function(self, getters, enumerable) { var propsDefinition = {}; if (enumerable === void 0) enumerable = false; for (var key in getters) { if(!getters.hasOwnProperty(key)) { continue; } propsDefinition[key] = { get: getters[key], enumerable: enumerable }; } Object.defineProperties(self, propsDefinition); }; // STOP REMOVING HERE // 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, _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('OT.setLogLevel(' + retVal + ')'); return retVal; }; var debugTrue = OT.properties.debug === 'true' || OT.properties.debug === true; OT.setLogLevel(debugTrue ? OT.DEBUG : OT.ERROR); OT.$.userAgent = function() { var userAgent = navigator.userAgent; if (TBPlugin.isInstalled()) userAgent += '; TBPlugin ' + TBPlugin.version(); return userAgent; }; /** * Sets the API log level. *

* Calling OT.setLogLevel() sets the log level for runtime log messages that * are the OpenTok library generates. The default value for the log level is OT.ERROR. *

*

* The OpenTok JavaScript library displays log messages in the debugger console (such as * Firebug), if one exists. *

*

* The following example logs the session ID to the console, by calling OT.log(). * The code also logs an error message when it attempts to publish a stream before the Session * object dispatches a sessionConnected event. *

*
  * OT.setLogLevel(OT.LOG);
  * session = OT.initSession(sessionId);
  * OT.log(sessionId);
  * publisher = OT.initPublisher("publishContainer");
  * session.publish(publisher);
  * 
* * @param {Number} logLevel The degree of logging desired by the developer: * *

*

*

* * @name OT.setLogLevel * @memberof OT * @function * @see OT.log() */ /** * 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 OT.LOG or OT.DEBUG, * by calling OT.setLogLevel(OT.LOG) or OT.setLogLevel(OT.DEBUG). * * @param {String} message The string to log. * * @name OT.log * @memberof OT * @function * @see OT.setLogLevel() */ })(window); !(function() { var addCss = function(document, url, callback) { var head = document.head || document.getElementsByTagName('head')[0]; var cssTag = OT.$.createElement('link', { type: 'text/css', media: 'screen', rel: 'stylesheet', href: url }); head.appendChild(cssTag); OT.$.on(cssTag, 'error', function(error) { OT.error('Could not load CSS for dialog', url, error && error.message || error); }); OT.$.on(cssTag, 'load', callback); }; var addDialogCSS = function(document, urls, callback) { var allURLs = [ '//fonts.googleapis.com/css?family=Didact+Gothic', OT.properties.cssURL ].concat(urls); var remainingStylesheets = allURLs.length; OT.$.forEach(allURLs, function(stylesheetUrl) { addCss(document, stylesheetUrl, function() { if(--remainingStylesheets <= 0) { callback(); } }); }); }; var templateElement = function(classes, children, tagName) { var el = OT.$.createElement(tagName || 'div', { 'class': classes }, children, this); el.on = OT.$.bind(OT.$.on, OT.$, el); el.off = OT.$.bind(OT.$.off, OT.$, el); return el; }; var checkBoxElement = function (classes, nameAndId, onChange) { var checkbox = templateElement.call(this, '', null, 'input').on('change', onChange); if (OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 8) { // Fix for IE8 not triggering the change event checkbox.on('click', function() { checkbox.blur(); checkbox.focus(); }); } checkbox.setAttribute('name', nameAndId); checkbox.setAttribute('id', nameAndId); checkbox.setAttribute('type', 'checkbox'); return checkbox; }; var linkElement = function(children, href, classes) { var link = templateElement.call(this, classes || '', children, 'a'); link.setAttribute('href', href); return link; }; OT.Dialogs = {}; OT.Dialogs.AllowDeny = { Chrome: {}, Firefox: {} }; OT.Dialogs.AllowDeny.Chrome.initialPrompt = function() { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), close, root; close = el('OT_closeButton', '×') .on('click', function() { modal.trigger('closeButtonClicked'); modal.close(); }); root = el('OT_root OT_dialog OT_dialog-allow-deny-chrome-first', [ close, el('OT_dialog-messages', [ el('OT_dialog-messages-main', 'Allow camera and mic access'), el('OT_dialog-messages-minor', 'Click the Allow button in the upper-right corner ' + 'of your browser to enable real-time communication.'), el('OT_dialog-allow-highlight-chrome') ]) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.AllowDeny.Chrome.previouslyDenied = function(website) { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), close, root; close = el('OT_closeButton', '×') .on('click', function() { modal.trigger('closeButtonClicked'); modal.close(); }); root = el('OT_root OT_dialog OT_dialog-allow-deny-chrome-pre-denied', [ close, el('OT_dialog-messages', [ el('OT_dialog-messages-main', 'Allow camera and mic access'), el('OT_dialog-messages-minor', [ 'To interact with this app, follow these 3 steps:', el('OT_dialog-3steps', [ el('OT_dialog-3steps-step', [ el('OT_dialog-3steps-step-num', '1'), 'Find this icon in the URL bar and click it', el('OT_dialog-allow-camera-icon') ]), el('OT_dialog-3steps-seperator'), el('OT_dialog-3steps-step', [ el('OT_dialog-3steps-step-num', '2'), 'Select "Ask if ' + website + ' wants to access your camera and mic" ' + 'and then click Done.' ]), el('OT_dialog-3steps-seperator'), el('OT_dialog-3steps-step', [ el('OT_dialog-3steps-step-num', '3'), 'Refresh your browser.' ]) ]) ]) ]) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.AllowDeny.Chrome.deniedNow = function() { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), root; root = el('OT_root OT_dialog-blackout', el('OT_dialog OT_dialog-allow-deny-chrome-now-denied', [ el('OT_dialog-messages', [ el('OT_dialog-messages-main ', el('OT_dialog-allow-camera-icon') ), el('OT_dialog-messages-minor', 'Find & click this icon to allow camera and mic access.' ) ]) ]) ); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.AllowDeny.Firefox.maybeDenied = function() { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), close, root; close = el('OT_closeButton', '×') .on('click', function() { modal.trigger('closeButtonClicked'); modal.close(); }); root = el('OT_root OT_dialog OT_dialog-allow-deny-firefox-maybe-denied', [ close, el('OT_dialog-messages', [ el('OT_dialog-messages-main', 'Please allow camera & mic access'), el('OT_dialog-messages-minor', [ 'To interact with this app, follow these 3 steps:', el('OT_dialog-3steps', [ el('OT_dialog-3steps-step', [ el('OT_dialog-3steps-step-num', '1'), 'Reload the page, or click the camera icon ' + 'in the browser URL bar.' ]), el('OT_dialog-3steps-seperator'), el('OT_dialog-3steps-step', [ el('OT_dialog-3steps-step-num', '2'), 'In the menu, select your camera & mic.' ]), el('OT_dialog-3steps-seperator'), el('OT_dialog-3steps-step', [ el('OT_dialog-3steps-step-num', '3'), 'Click "Share Selected Devices."' ]) ]) ]) ]) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.AllowDeny.Firefox.denied = function() { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), btn = OT.$.bind(templateElement, document, 'OT_dialog-button OT_dialog-button-large'), root, refreshButton; refreshButton = btn('Reload') .on('click', function() { modal.trigger('refresh'); }); root = el('OT_root OT_dialog-blackout', el('OT_dialog OT_dialog-allow-deny-firefox-denied', [ el('OT_dialog-messages', [ el('OT_dialog-messages-minor', 'Access to camera and microphone has been denied. ' + 'Click the button to reload page.' ) ]), el('OT_dialog-single-button', refreshButton) ]) ); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.Plugin = {}; OT.Dialogs.Plugin.promptToInstall = function() { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), btn = function(children, size) { var classes = 'OT_dialog-button ' + (size ? 'OT_dialog-button-' + size : 'OT_dialog-button-large'), b = el(classes, children); b.enable = function() { OT.$.removeClass(this, 'OT_dialog-button-disabled'); return this; }; b.disable = function() { OT.$.addClass(this, 'OT_dialog-button-disabled'); return this; }; return b; }, downloadButton = btn('Download plugin'), cancelButton = btn('cancel', 'small'), refreshButton = btn('Refresh browser'), acceptEULA, checkbox, close, root; function onDownload() { modal.trigger('download'); setTimeout(function() { root.querySelector('.OT_dialog-messages-main').innerHTML = 'Plugin installation successful'; var sections = root.querySelectorAll('.OT_dialog-single-button-wide'); OT.$.addClass(sections[0], 'OT_dialog-hidden'); OT.$.removeClass(sections[1], 'OT_dialog-hidden'); }, 3000); } function onRefresh() { modal.trigger('refresh'); } function onToggleEULA() { if (checkbox.checked) { enableButtons(); } else { disableButtons(); } } function enableButtons() { downloadButton.enable(); downloadButton.on('click', onDownload); refreshButton.enable(); refreshButton.on('click', onRefresh); } function disableButtons() { downloadButton.disable(); downloadButton.off('click', onDownload); refreshButton.disable(); refreshButton.off('click', onRefresh); } downloadButton.disable(); refreshButton.disable(); cancelButton.on('click', function() { modal.trigger('cancelButtonClicked'); modal.close(); }); close = el('OT_closeButton', '×') .on('click', function() { modal.trigger('closeButtonClicked'); modal.close(); }); acceptEULA = linkElement.call(document, 'end-user license agreement', 'http://tokbox.com/support/ie-eula'); checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA); root = el('OT_root OT_dialog OT_dialog-plugin-prompt', [ close, el('OT_dialog-messages', [ el('OT_dialog-messages-main', 'This app requires real-time communication') ]), el('OT_dialog-single-button-wide', [ el('OT_dialog-single-button-with-title', [ el('OT_dialog-button-title', [ checkbox, (function() { var x = el('', 'accept', 'label'); x.setAttribute('for', checkbox.id); x.style.margin = '0 5px'; return x; })(), acceptEULA ]), downloadButton, cancelButton ]) ]), el('OT_dialog-single-button-wide OT_dialog-hidden', [ el('OT_dialog-single-button-with-title', [ el('OT_dialog-button-title', [ 'You can now enjoy webRTC enabled video via Internet Explorer.' ]), refreshButton ]) ]) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.Plugin.promptToReinstall = function() { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), close, okayButton, root; close = el('OT_closeButton', '×'); okayButton = el('OT_dialog-button', 'Okay'); OT.$.on(okayButton, 'click', function() { modal.trigger('okay'); }); OT.$.on(close, 'click', function() { modal.trigger('closeButtonClicked'); modal.close(); }); root = el('OT_ROOT OT_dialog OT_dialog-plugin-reinstall', [ close, el('OT_dialog-messages', [ el('OT_dialog-messages-main', 'Reinstall Opentok Plugin'), el('OT_dialog-messages-minor', 'Uh oh! Try reinstalling the OpenTok plugin again to ' + 'enable real-time video communication for Internet Explorer.') ]), el('OT_dialog-single-button', okayButton) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; OT.Dialogs.Plugin.updateInProgress = function() { var progressBar, progressText, progressValue = 0; var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), root; progressText = el('OT_dialog-plugin-upgrade-percentage', '0%', 'strong'); progressBar = el('OT_dialog-progress-bar-fill'); root = el('OT_ROOT OT_dialog OT_dialog-plugin-upgrading', [ el('OT_dialog-messages', [ el('OT_dialog-messages-main', [ 'One moment please... ', progressText ]), el('OT_dialog-progress-bar', progressBar), el('OT_dialog-messages-minor', 'Please wait while the OpenTok plugin is updated') ]) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); if(progressValue != null) { modal.setUpdateProgress(progressValue); } }); }); modal.setUpdateProgress = function(newProgress) { if(progressBar && progressText) { if(newProgress > 99) { OT.$.css(progressBar, 'width', ''); progressText.innerHTML = '100%'; } else if(newProgress < 1) { OT.$.css(progressBar, 'width', '0%'); progressText.innerHTML = '0%'; } else { OT.$.css(progressBar, 'width', newProgress + '%'); progressText.innerHTML = newProgress + '%'; } } else { progressValue = newProgress; } }; return modal; }; OT.Dialogs.Plugin.updateComplete = function(error) { var modal = new OT.$.Modal(function(window, document) { var el = OT.$.bind(templateElement, document), reloadButton, root; reloadButton = el('OT_dialog-button', 'Reload').on('click', function() { modal.trigger('reload'); }); var msgs; if(error) { msgs = ['Update Failed.', error + '' || 'NO ERROR']; } else { msgs = ['Update Complete.', 'The OpenTok plugin has been succesfully updated. ' + 'Please reload your browser.']; } root = el('OT_root OT_dialog OT_dialog-plugin-upgraded', [ el('OT_dialog-messages', [ el('OT_dialog-messages-main', msgs[0]), el('OT_dialog-messages-minor', msgs[1]) ]), el('OT_dialog-single-button', reloadButton) ]); addDialogCSS(document, [], function() { document.body.appendChild(root); }); }); return modal; }; })(); !(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.'); } OT.useSSL = function () { return OT.properties.supportSSL && (window.location.protocol.indexOf('https') >= 0 || window.location.protocol.indexOf('chrome-extension') >= 0); }; // 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 (window.OTProperties) { // Allow window.OTProperties to override cdnURL, configURL, assetURL and cssURL if (window.OTProperties.cdnURL) props.cdnURL = window.OTProperties.cdnURL; if (window.OTProperties.cdnURLSSL) props.cdnURLSSL = window.OTProperties.cdnURLSSL; if (window.OTProperties.configURL) props.configURL = window.OTProperties.configURL; if (window.OTProperties.assetURL) props.assetURL = window.OTProperties.assetURL; if (window.OTProperties.cssURL) props.cssURL = window.OTProperties.cssURL; } if (!props.assetURL) { if (OT.useSSL()) { props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version; } else { props.assetURL = props.cdnURL + '/webrtc/' + props.version; } } var isIE89 = OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 9; if (!(isIE89 && window.location.protocol.indexOf('https') < 0)) { props.apiURL = props.apiURLSSL; props.loggingURL = props.loggingURLSSL; } if (!props.configURL) props.configURL = props.assetURL + '/js/dynamic_config.min.js'; if (!props.cssURL) props.cssURL = props.assetURL + '/css/ot.min.css'; return props; }(OT.properties); })(window); !(function() { //-------------------------------------- // 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; _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 = OT.$.bind(_onLoad, 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); /** * @license TB Plugin 0.4.0.8 72b534e HEAD * http://www.tokbox.com/ * * Copyright (c) 2014 TokBox, Inc. * Released under the MIT license * http://opensource.org/licenses/MIT * * Date: September 08 10:17:49 2014 * */ /* jshint globalstrict: true, strict: false, undef: true, unused: false, trailing: true, browser: true, smarttabs:true */ /* global scope:true, OT:true */ /* exported TBPlugin */ /* jshint ignore:start */ (function(scope) { /* jshint ignore:end */ // If we've already be setup, bail if (scope.TBPlugin !== void 0) return; // TB must exist first, otherwise we can't do anything if (scope.OT === void 0) return; // Establish the environment that we're running in var env = OT.$.browserVersion(), isSupported = env.browser === 'IE' && env.version >= 8, pluginReady = false; var TBPlugin = { isSupported: function () { return isSupported; }, isReady: function() { return pluginReady; } }; scope.TBPlugin = TBPlugin; // We only support IE, version 10 or above right now if (!TBPlugin.isSupported()) { TBPlugin.isInstalled = function isInstalled () { return false; }; return; } // tb_require('./header.js') /* exported shim */ // Shims for various missing things from JS // Applied only after init is called to prevent unnecessary polution var shim = function shim () { if (!Function.prototype.bind) { Function.prototype.bind = function (oThis) { if (typeof this !== 'function') { // closest thing possible to the ECMAScript 5 internal IsCallable function throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable'); } var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, FNOP = function () {}, fBound = function () { return fToBind.apply(this instanceof FNOP && oThis ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments))); }; FNOP.prototype = this.prototype; fBound.prototype = new FNOP(); return fBound; }; } if(!Array.isArray) { Array.isArray = function (vArg) { return Object.prototype.toString.call(vArg) === '[object Array]'; }; } if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (searchElement, fromIndex) { var i, pivot = (fromIndex) ? fromIndex : 0, length; if (!this) { throw new TypeError(); } length = this.length; if (length === 0 || pivot >= length) { return -1; } if (pivot < 0) { pivot = length - Math.abs(pivot); } for (i = pivot; i < length; i++) { if (this[i] === searchElement) { return i; } } return -1; }; } if (!Array.prototype.map) { Array.prototype.map = function(fun /*, thisArg */) { 'use strict'; if (this === void 0 || this === null) throw new TypeError(); var t = Object(this); var len = t.length >>> 0; if (typeof fun !== 'function') { throw new TypeError(); } var res = new Array(len); var thisArg = arguments.length >= 2 ? arguments[1] : void 0; for (var i = 0; i < len; i++) { // NOTE: Absolute correctness would demand Object.defineProperty // be used. But this method is fairly new, and failure is // possible only if Object.prototype or Array.prototype // has a property |i| (very unlikely), so use a less-correct // but more portable alternative. if (i in t) res[i] = fun.call(thisArg, t[i], i, t); } return res; }; } }; // tb_require('./header.js') // tb_require('./shims.js') /* jshint globalstrict: true, strict: false, undef: true, unused: true, trailing: true, browser: true, smarttabs:true */ /* global OT:true, TBPlugin:true, pluginInfo:true, debug:true, scope:true, _document:true */ /* exported createMediaCaptureController:true, createPeerController:true, injectObject:true, plugins:true, mediaCaptureObject:true, removeAllObjects:true, curryCallAsync:true */ var objectTimeouts = {}, mediaCaptureObject, plugins = {}; var curryCallAsync = function curryCallAsync (fn) { return function() { var args = Array.prototype.slice.call(arguments); args.unshift(fn); OT.$.callAsync.apply(OT.$, args); }; }; var generatePluginUuid = function generatePluginUuid () { return OT.$.uuid().replace(/\-+/g, ''); }; var clearObjectLoadTimeout = function clearObjectLoadTimeout (callbackId) { if (!callbackId) return; if (objectTimeouts[callbackId]) { clearTimeout(objectTimeouts[callbackId]); delete objectTimeouts[callbackId]; } if (scope[callbackId]) { try { delete scope[callbackId]; } catch (err) { scope[callbackId] = void 0; } } }; var removeObjectFromDom = function removeObjectFromDom (object) { clearObjectLoadTimeout(object.getAttribute('tb_callbackId')); if (mediaCaptureObject && mediaCaptureObject.id === object.id) { mediaCaptureObject = null; } else if (plugins.hasOwnProperty(object.id)) { delete plugins[object.id]; } object.parentNode.removeChild(object); }; // @todo bind destroy to unload, may need to coordinate with TB // jshint -W098 var removeAllObjects = function removeAllObjects () { if (mediaCaptureObject) mediaCaptureObject.destroy(); for (var id in plugins) { if (plugins.hasOwnProperty(id)) { plugins[id].destroy(); } } }; // Reference counted wrapper for a plugin object var PluginObject = function PluginObject (plugin) { var _plugin = plugin, _liveObjects = []; this._ = _plugin; this.addRef = function(ref) { _liveObjects.push(ref); return this; }; this.removeRef = function(ref) { if (_liveObjects.length === 0) return; var index = _liveObjects.indexOf(ref); if (index !== -1) { _liveObjects.splice(index, 1); } if (_liveObjects.length === 0) { this.destroy(); } return this; }; this.isValid = function() { return _plugin.valid; }; // Event Handling Mechanisms var eventHandlers = {}; var onCustomEvent = OT.$.bind(curryCallAsync(function onCustomEvent() { var args = Array.prototype.slice.call(arguments), name = args.shift(); if (!eventHandlers.hasOwnProperty(name) && eventHandlers[name].length) { return; } OT.$.forEach(eventHandlers[name], function(handler) { handler[0].apply(handler[1], args); }); }), this); this.on = function (name, callback, context) { if (!eventHandlers.hasOwnProperty(name)) { eventHandlers[name] = []; } eventHandlers[name].push([callback, context]); return this; }; this.off = function (name, callback, context) { if (!eventHandlers.hasOwnProperty(name) || eventHandlers[name].length === 0) { return; } OT.$.filter(eventHandlers[name], function(listener) { return listener[0] === callback && listener[1] === context; }); return this; }; this.once = function (name, callback, context) { var fn = function () { this.off(name, fn, this); return callback.apply(context, arguments); }; this.on(name, fn, this); return this; }; this.onReady = function(readyCallback) { if (_plugin.on) { // If the plugin supports custom events we'll use them _plugin.on(-1, {customEvent: curryCallAsync(onCustomEvent, this)}); } // Only the main plugin has an initialise method if (_plugin.initialise) { this.on('ready', OT.$.bind(curryCallAsync(readyCallback), this)); _plugin.initialise(); } else { readyCallback.call(null); } }; this.destroy = function() { while (_liveObjects.length) { _liveObjects.shift().destroy(); } removeObjectFromDom(_plugin); _plugin = null; }; this.setStream = function(stream, completion) { if (completion) { if (stream.hasVideo()) { // FIX ME renderingStarted currently doesn't first // this.once('renderingStarted', completion); var verifyStream = function() { if (_plugin.videoWidth > 0) { // This fires a little too soon. setTimeout(completion, 200); } else { setTimeout(verifyStream, 500); } }; setTimeout(verifyStream, 500); } else { // TODO Investigate whether there is a good way to detect // when the audio is ready. Does it even matter? completion(); } } _plugin.setStream(stream); }; }; // Stops and cleans up after the plugin object load timeout. var injectObject = function injectObject (mimeType, isVisible, params, completion) { var callbackId = 'TBPlugin_loaded_' + generatePluginUuid(); params.onload = callbackId; params.userAgent = window.navigator.userAgent.toLowerCase(); scope[callbackId] = function() { clearObjectLoadTimeout(callbackId); o.setAttribute('id', 'tb_plugin_' + o.uuid); o.removeAttribute('tb_callbackId'); pluginRefCounted.uuid = o.uuid; pluginRefCounted.id = o.id; pluginRefCounted.onReady(function(err) { if (err) { OT.error('Error while starting up plugin ' + o.uuid + ': ' + err); return; } debug('Plugin ' + o.id + ' is loaded'); if (completion && OT.$.isFunction(completion)) { completion.call(TBPlugin, null, pluginRefCounted); } }); }; var tmpContainer = document.createElement('div'), objBits = [], extraAttributes = ['width="0" height="0"'], pluginRefCounted, o; if (isVisible !== true) { extraAttributes.push('visibility="hidden"'); } objBits.push(''); for (var name in params) { if (params.hasOwnProperty(name)) { objBits.push(''); } } objBits.push(''); tmpContainer.innerHTML = objBits.join(''); _document.body.appendChild(tmpContainer); function firstElementChild(element) { if(element.firstElementChild) { return element.firstElementChild; } for(var i = 0, len = element.childNodes.length; i < len; ++i) { if(element.childNodes[i].nodeType === 1) { return element.childNodes[i]; } } return null; } o = firstElementChild(tmpContainer); o.setAttribute('tb_callbackId', callbackId); pluginRefCounted = new PluginObject(o); _document.body.appendChild(o); _document.body.removeChild(tmpContainer); objectTimeouts[callbackId] = setTimeout(function() { clearObjectLoadTimeout(callbackId); completion.call(TBPlugin, 'The object with the mimeType of ' + mimeType + ' timed out while loading.'); _document.body.removeChild(o); }, 3000); return pluginRefCounted; }; // Creates the Media Capture controller. This exposes selectSources and is // used in the private API. // // Only one Media Capture controller can exist at once, calling this method // more than once will raise an exception. // var createMediaCaptureController = function createMediaCaptureController (completion) { if (mediaCaptureObject) { throw new Error('TBPlugin.createMediaCaptureController called multiple times!'); } mediaCaptureObject = injectObject(pluginInfo.mimeType, false, {windowless: false}, completion); mediaCaptureObject.selectSources = function() { return this._.selectSources.apply(this._, arguments); }; return mediaCaptureObject; }; // Create an instance of the publisher/subscriber/peerconnection object. // Many of these can exist at once, but the +id+ of each must be unique // within a single instance of scope (window or window-like thing). // var createPeerController = function createPeerController (completion) { var o = injectObject(pluginInfo.mimeType, true, {windowless: true}, function(err, plugin) { if (err) { completion.call(TBPlugin, err); return; } plugins[plugin.id] = plugin; completion.call(TBPlugin, null, plugin); }); return o; }; // tb_require('./header.js') // tb_require('./shims.js') // tb_require('./plugin_object.js') /* jshint globalstrict: true, strict: false, undef: true, unused: true, trailing: true, browser: true, smarttabs:true */ /* global OT:true, debug:true */ /* exported VideoContainer */ var VideoContainer = function VideoContainer (plugin, stream) { this.domElement = plugin._; this.parentElement = plugin._.parentNode; plugin.addRef(this); this.appendTo = function (parentDomElement) { if (parentDomElement && plugin._.parentNode !== parentDomElement) { debug('VideoContainer appendTo', parentDomElement); parentDomElement.appendChild(plugin._); this.parentElement = parentDomElement; } }; this.show = function (completion) { debug('VideoContainer show'); plugin._.removeAttribute('width'); plugin._.removeAttribute('height'); plugin.setStream(stream, completion); OT.$.show(plugin._); }; this.setWidth = function (width) { debug('VideoContainer setWidth to ' + width); plugin._.setAttribute('width', width); }; this.setHeight = function (height) { debug('VideoContainer setHeight to ' + height); plugin._.setAttribute('height', height); }; this.setVolume = function (value) { // TODO debug('VideoContainer setVolume not implemented: called with ' + value); }; this.getVolume = function () { // TODO debug('VideoContainer getVolume not implemented'); return 0.5; }; this.getImgData = function () { return plugin._.getImgData('image/png'); }; this.getVideoWidth = function () { return plugin._.videoWidth; }; this.getVideoHeight = function () { return plugin._.videoHeight; }; this.destroy = function () { plugin._.setStream(null); plugin.removeRef(this); }; }; // tb_require('./header.js') // tb_require('./shims.js') // tb_require('./plugin_object.js') /* jshint globalstrict: true, strict: false, undef: true, unused: true, trailing: true, browser: true, smarttabs:true */ /* global OT:true, TBPlugin:true, pluginInfo:true, ActiveXObject:true, injectObject:true, curryCallAsync:true */ /* exported AutoUpdater:true */ var AutoUpdater; (function() { var autoUpdaterController, updaterMimeType, // <- cached version, use getInstallerMimeType instead installedVersion = -1; // <- cached version, use getInstallerMimeType instead var versionGreaterThan = function versionGreaterThan (version1,version2) { if (version1 === version2) return false; var v1 = version1.split('.'), v2 = version2.split('.'); v1 = parseFloat(parseInt(v1.shift(), 10) + '.' + v1.map(function(vcomp) { return parseInt(vcomp, 10); }).join('')); v2 = parseFloat(parseInt(v2.shift(), 10) + '.' + v2.map(function(vcomp) { return parseInt(vcomp, 10); }).join('')); return v1 > v2; }; // Work out the full mimeType (including the currently installed version) // of the installer. var findMimeTypeAndVersion = function findMimeTypeAndVersion () { if (updaterMimeType !== void 0) { return updaterMimeType; } var activeXControlId = 'TokBox.otiePluginInstaller', unversionedMimeType = 'application/x-otieplugininstaller', plugin = navigator.plugins[activeXControlId]; installedVersion = -1; if (plugin) { // Look through the supported mime-types for the version // There should only be one mime-type in our use case, and // if there's more than one they should all have the same // version. var numMimeTypes = plugin.length, extractVersion = new RegExp(unversionedMimeType.replace('-', '\\-') + ',version=([0-9]+)', 'i'), mimeType, bits; for (var i=0; i ', object); } else { scope.OT.info('TB Plugin - ' + message); } }; /// Private API var isDomReady = function isDomReady () { return (_document.readyState === 'complete' || (_document.readyState === 'interactive' && _document.body)); }, onDomReady = function onDomReady () { var callCompletionHandlers = function(err) { var callback; while ( (callback = readyCallbacks.pop()) && OT.$.isFunction(callback) ) { callback.call(TBPlugin, err); } }; AutoUpdater.get(function(err, updater) { if (err) { OT.error('Error while loading the AutoUpdater: ' + err); callCompletionHandlers('Error while loading the AutoUpdater: ' + err); return; } // If the plugin is out of date then we kick off the // auto update process and then bail out. if (updater.isOutOfDate()) { updater.autoUpdate(); return; } // Inject the controller object into the page, wait for it to load or timeout... createMediaCaptureController(function(err) { if (!err && (mediaCaptureObject && !mediaCaptureObject.isValid())) { err = 'The TB Plugin failed to load properly'; } pluginReady = true; callCompletionHandlers(err); OT.onUnload(destroy); }); }); }, waitForDomReady = function waitForDomReady () { if (isDomReady()) { onDomReady(); } else if (_document.addEventListener) { _document.addEventListener('DOMContentLoaded', onDomReady, false); } else if (_document.attachEvent) { _document.attachEvent('onreadystatechange', function() { if (_document.readyState === 'complete') onDomReady(); }); } }, // @todo bind destroy to unload, may need to coordinate with TB // jshint -W098 destroy = function destroy () { removeAllObjects(); }; /// Public API TBPlugin.isInstalled = function isInstalled () { if (!this.isSupported()) return false; return AutoUpdater.isinstalled(); }; TBPlugin.version = function version () { return pluginInfo.version; }; TBPlugin.installedVersion = function installedVersion () { return AutoUpdater.installedVersion(); }; // Returns a URI to the TBPlugin installer that is paired with // this version of TBPlugin.js. TBPlugin.pathToInstaller = function pathToInstaller () { return 'https://s3.amazonaws.com/otplugin.tokbox.com/v' + pluginInfo.version + '/otiePluginMain.msi'; }; // Trigger +callback+ when the plugin is ready // // Most of the public API cannot be called until // the plugin is ready. // TBPlugin.ready = function ready (callback) { if (TBPlugin.isReady()) { var err; if (!mediaCaptureObject || !mediaCaptureObject.isValid()) { err = 'The TB Plugin failed to load properly'; } callback.call(TBPlugin, err); } else { readyCallbacks.push(callback); } }; // Helper function for TBPlugin.getUserMedia var _getUserMedia = function _getUserMedia(mediaConstraints, success, error) { createPeerController(function(err, plugin) { if (err) { error.call(TBPlugin, err); return; } plugin._.getUserMedia(mediaConstraints.toHash(), function(streamJson) { success.call(TBPlugin, MediaStream.fromJson(streamJson, plugin)); }, error); }); }; // Equivalent to: window.getUserMedia(constraints, success, error); // // Except that the constraints won't be identical TBPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) { var constraints = new MediaConstraints(userConstraints); if (constraints.screenSharing) { _getUserMedia(constraints, success, error); } else { var sources = []; if (constraints.hasVideo) sources.push('video'); if (constraints.hasAudio) sources.push('audio'); mediaCaptureObject.selectSources(sources, function(captureDevices) { for (var key in captureDevices) { if (captureDevices.hasOwnProperty(key)) { OT.debug(key + ' Capture Device: ' + captureDevices[key]); } } // Use the sources to acquire the hardware and start rendering constraints.setVideoSource(captureDevices.video); constraints.setAudioSource(captureDevices.audio); _getUserMedia(constraints, success, error); }, error); } }; TBPlugin.initRumorSocket = function(messagingURL, completion) { TBPlugin.ready(function(error) { if(error) { completion(error); } else { completion(null, new PluginRumorSocket(mediaCaptureObject, messagingURL)); } }); }; // Equivalent to: var pc = new window.RTCPeerConnection(iceServers, options); // // Except that it is async and takes a completion handler TBPlugin.initPeerConnection = function initPeerConnection (iceServers, options, localStream, completion) { var gotPeerObject = function(err, plugin) { if (err) { completion.call(TBPlugin, err); return; } debug('Got PeerConnection for ' + plugin.id); var peerConnection = new PeerConnection(iceServers, options, plugin); completion.call(TBPlugin, null, peerConnection); }; // @fixme this is nasty and brittle. We need some way to use the same Object // for the PeerConnection that was used for the getUserMedia call (in the case // of publishers). We don't really have a way of implicitly associating them though. // Hence, publishers will have to pass through their localStream (if they have one) // and we will look up the original Object and use that. Otherwise we generate // a new one. if (localStream && localStream._.plugin) { gotPeerObject(null, localStream._.plugin); } else { createPeerController(gotPeerObject); } }; // A RTCSessionDescription like object exposed for native WebRTC compatability TBPlugin.RTCSessionDescription = function RTCSessionDescription (options) { this.type = options.type; this.sdp = options.sdp; }; // A RTCIceCandidate like object exposed for native WebRTC compatability TBPlugin.RTCIceCandidate = function RTCIceCandidate (options) { this.sdpMid = options.sdpMid; this.sdpMLineIndex = parseInt(options.sdpMLineIndex, 10); this.candidate = options.candidate; }; // Make this available for now TBPlugin.debug = debug; shim(); waitForDomReady(); // tb_require('./tb_plugin.js') /* jshint ignore:start */ })(this); /* jshint ignore:end */ !(function() { /*global OT:true */ var defaultAspectRatio = 4.0/3.0, miniWidth = 128, miniHeight = 128, microWidth = 64, microHeight = 64; // 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 (TBPlugin.isInstalled()) { // The plugin will sort out it's own aspect ratio, so we // only need to tell the container to expand to fit it's parent. OT.$.css(element, { width: '100%', height: '100%', left: 0, top: 0 }); return; } 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; 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, diff = w - h; props = { width: h + 'px', height: w + 'px', marginTop: -(diff / 2) + 'px', marginLeft: (diff / 2) + 'px' }; OT.$.css(video, props); } else { OT.$.css(video, { width: '', height: '', marginTop: '', marginLeft: ''}); } } } function fixMini(container, width, height) { var w = parseInt(width, 10), h = parseInt(height, 10); if(w < microWidth || h < microHeight) { OT.$.addClass(container, 'OT_micro'); } else { OT.$.removeClass(container, 'OT_micro'); } if(w < miniWidth || h < miniHeight) { OT.$.addClass(container, 'OT_mini'); } else { OT.$.removeClass(container, 'OT_mini'); } } 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 { 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; } } else { OT.$.emptyElement(container); } } 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, width, height, loading = true, audioOnly = false; 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'; fixMini(container, width || '264px', height || '198px'); 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; if (!TBPlugin.isInstalled()) { // 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; fixMini(container, width, height); 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 = OT.$.some(removedNodes, 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; } }; this.setBackgroundImageURI = function(bgImgURI) { if (bgImgURI.substr(0, 5) !== 'http:' && bgImgURI.substr(0, 6) !== 'https:') { if (bgImgURI.substr(0, 22) !== 'data:image/png;base64,') { bgImgURI = 'data:image/png;base64,' + bgImgURI; } } OT.$.css(posterContainer, 'backgroundImage', 'url(' + bgImgURI + ')'); OT.$.css(posterContainer, 'backgroundSize', 'contain'); OT.$.css(posterContainer, 'opacity', '1.0'); }; if (properties && properties.style && properties.style.backgroundImageURI) { this.setBackgroundImageURI(properties.style.backgroundImageURI); } this.bindVideo = function(webRTCStream, options, completion) { // remove the old video element if it exists // @todo this might not be safe, publishers/subscribers use this as well... if (videoElement) { videoElement.destroy(); videoElement = null; } var onError = options && options.error ? options.error : void 0; delete options.error; var video = new OT.VideoElement({ attributes: options }, onError); // Initialize the audio volume if (options.audioVolume) video.setAudioVolume(options.audioVolume); // makes the incoming audio streams take priority (will impact only FF OS for now) video.audioChannelType('telephony'); video.appendTo(videoContainer).bindToStream(webRTCStream, function(err) { if (err) { video.destroy(); completion(err); return; } videoElement = video; videoElement.on({ orientationChanged: function(){ fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight, videoElement.aspectRatio(), videoElement.isRotated()); } }); var fix = function() { fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight, videoElement ? videoElement.aspectRatio() : null, videoElement ? videoElement.isRotated() : null); }; if(isNaN(videoElement.aspectRatio())) { videoElement.on('streamBound', fix); } else { fix(); } completion(null, video); }); return video; }; this.video = function() { return videoElement; }; OT.$.defineProperties(this, { showPoster: { get: function() { return !OT.$.isDisplayNone(posterContainer); }, set: function(newValue) { if(newValue) { 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'); } } }, audioOnly: { get: function() { return audioOnly; }, set: function(a) { audioOnly = a; if (audioOnly) { OT.$.addClass(container, 'OT_audio-only'); } else { OT.$.removeClass(container, 'OT_audio-only'); } } }, domId: { get: function() { return container.getAttribute('id'); } } }); this.domElement = container; this.addError = function(errorMsg, helpMsg, classNames) { container.innerHTML = '

' + errorMsg + (helpMsg ? ' ' + helpMsg + '' : '') + '

'; OT.$.addClass(container, classNames || 'OT_subscriber_error'); if(container.querySelector('p').offsetHeight > container.offsetHeight) { container.querySelector('span').style.display = 'none'; } }; }; })(window); // Web OT Helpers !(function(window) { var NativeRTCPeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection); if (navigator.webkitGetUserMedia) { /*global webkitMediaStream, webkitRTCPeerConnection*/ // 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. /* global MediaStream */ 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; // }; // } } // The setEnabled method on MediaStreamTracks is a TBPlugin // construct. In this particular instance it's easier to bring // all the good browsers down to IE's level than bootstrap it up. if (typeof window.MediaStreamTrack !== 'undefined') { if (!window.MediaStreamTrack.prototype.setEnabled) { window.MediaStreamTrack.prototype.setEnabled = function (enabled) { this.enabled = OT.$.castToBoolean(enabled); }; } } OT.$.createPeerConnection = function (config, options, publishersWebRtcStream, completion) { if (TBPlugin.isInstalled()) { TBPlugin.initPeerConnection(config, options, publishersWebRtcStream, completion); } else { var pc; try { pc = new NativeRTCPeerConnection(config, options); } catch(e) { completion(e.message); return; } completion(null, pc); } }; // 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.$.hasCapabilities('webrtc')) return 'NONE'; var chromeVersion = window.navigator.userAgent.toLowerCase().match(/chrome\/([0-9\.]+)/i); return chromeVersion && parseFloat(chromeVersion[1], 10) < 25 ? 'SDES_SRTP' : 'DTLS_SRTP'; }; })(window); // Web OT Helpers !(function(window) { /* jshint globalstrict: true, strict: false, undef: true, unused: true, trailing: true, browser: true, smarttabs:true */ /* global TBPlugin, OT */ /// // Capabilities // // Support functions to query browser/client Media capabilities. // // Indicates whether this client supports the getUserMedia // API. // OT.$.registerCapability('getUserMedia', function() { return !!(navigator.webkitGetUserMedia || navigator.mozGetUserMedia || TBPlugin.isInstalled()); }); // TODO Remove all PeerConnection stuff, that belongs to the messaging layer not the Media layer. // Indicates whether this client supports the PeerConnection // API. // // 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.$.registerCapability('PeerConnection', function() { var browser = OT.$.browserVersion(); if (navigator.webkitGetUserMedia) { return typeof(window.webkitRTCPeerConnection) === 'function' && !!window.webkitRTCPeerConnection.prototype.addStream; } else if (navigator.mozGetUserMedia) { if (typeof(window.mozRTCPeerConnection) === 'function' && browser.version > 20.0) { try { new window.mozRTCPeerConnection(); return true; } catch (err) { return false; } } } else { return TBPlugin.isInstalled(); } }); // Indicates whether this client supports WebRTC // // This is defined as: getUserMedia + PeerConnection + exceeds min browser version // OT.$.registerCapability('webrtc', function() { var browser = OT.$.browserVersion(), minimumVersions = OT.properties.minimumVersion || {}, minimumVersion = minimumVersions[browser.browser.toLowerCase()]; if(minimumVersion && minimumVersion > browser.version) { OT.debug('Support for', browser.browser, 'is disabled because we require', minimumVersion, 'but this is', browser.version); return false; } return OT.$.hasCapabilities('getUserMedia', 'PeerConnection'); }); // TODO Remove all transport stuff, that belongs to the messaging layer not the Media layer. // Indicates if the browser supports bundle // // Broadly: // * Firefox doesn't support bundle // * Chrome support bundle // * OT Plugin supports bundle // OT.$.registerCapability('bundle', function() { return OT.$.hasCapabilities('webrtc') && (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()); }); // Indicates 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 rtcp mux // * OT Plugin supports rtcp mux // OT.$.registerCapability('RTCPMux', function() { return OT.$.hasCapabilities('webrtc') && (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()); }); // Indicates whether this browser supports the getMediaDevices (getSources) API. // OT.$.registerCapability('getMediaDevices', function() { return OT.$.isFunction(window.MediaStreamTrack) && OT.$.isFunction(window.MediaStreamTrack.getSources); }); })(window); // Web OT Helpers !(function() { var nativeGetUserMedia, vendorToW3CErrors, gumNamesToMessages, mapVendorErrorName, parseErrorEvent, areInvalidConstraints; // Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth nativeGetUserMedia = (function() { if (navigator.getUserMedia) { return OT.$.bind(navigator.getUserMedia, navigator); } else if (navigator.mozGetUserMedia) { return OT.$.bind(navigator.mozGetUserMedia, navigator); } else if (navigator.webkitGetUserMedia) { return OT.$.bind(navigator.webkitGetUserMedia, navigator); } else if (TBPlugin.isInstalled()) { return OT.$.bind(TBPlugin.getUserMedia, TBPlugin); } })(); // 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. // Chrome TrackStartError is triggered when the camera is already used by another app (Windows) vendorToW3CErrors = { PERMISSION_DENIED: 'PermissionDeniedError', NOT_SUPPORTED_ERROR: 'NotSupportedError', MANDATORY_UNSATISFIED_ERROR: ' ConstraintNotSatisfiedError', NO_DEVICES_FOUND: 'NoDevicesFoundError', HARDWARE_UNAVAILABLE: 'HardwareUnavailableError', TrackStartError: 'HardwareUnavailableError' }; gumNamesToMessages = { PermissionDeniedError: 'End-user denied permission to hardware devices', PermissionDismissedError: 'End-user dismissed permission to hardware devices', NotSupportedError: 'A constraint specified is not supported by the browser.', ConstraintNotSatisfiedError: 'It\'s not possible to satisfy one or more constraints ' + 'passed into the getUserMedia function', OverconstrainedError: 'Due to changes in the environment, one or more mandatory ' + 'constraints can no longer be satisfied.', NoDevicesFoundError: 'No voice or video input devices are available on this machine.', HardwareUnavailableError: 'The selected voice or video devices are unavailable. Verify ' + 'that the chosen devices are not in use by another application.' }; // Map vendor error strings to names and messages if possible mapVendorErrorName = function mapVendorErrorName(vendorErrorName, vendorErrors) { var errorName, errorMessage; if(vendorErrors.hasOwnProperty(vendorErrorName)) { errorName = vendorErrors[vendorErrorName]; } else { // This doesn't map to a known error from the Media Capture spec, it's // probably a custom vendor error message. errorName = vendorErrorName; } if(gumNamesToMessages.hasOwnProperty(errorName)) { errorMessage = gumNamesToMessages[errorName]; } else { errorMessage = 'Unknown Error while getting user media'; } 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 parseErrorEvent = function parseErrorObject(event) { var error; if (OT.$.isObject(event) && event.name) { error = mapVendorErrorName(event.name, vendorToW3CErrors); error.constraintName = event.constraintName; } else if (typeof event === 'string') { error = mapVendorErrorName(event, vendorToW3CErrors); } else { error = { message: 'Unknown Error type while getting media' }; } return error; }; // Validates a Hash of getUserMedia constraints. Currently we only // check to see if there is at least one non-false constraint. areInvalidConstraints = function(constraints) { if (!constraints || !OT.$.isObject(constraints)) return true; for (var key in constraints) { if(!constraints.hasOwnProperty(key)) { continue; } if (constraints[key]) return false; } return true; }; // 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 OT.$ // @private // // @param {Object} constraints // A dictionary of constraints to pass to getUserMedia. See // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints // 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, customGetUserMedia) { var getUserMedia = nativeGetUserMedia; if(OT.$.isFunction(customGetUserMedia)) { getUserMedia = customGetUserMedia; } // 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); // The error name 'PERMISSION_DENIED' is from an earlier version of the spec if (error.name === 'PermissionDeniedError' || error.name === 'PermissionDismissedError') { 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); } }; })(); // Web OT Helpers !(function(window) { /* jshint globalstrict: true, strict: false, undef: true, unused: true, trailing: true, browser: true, smarttabs:true */ /* global OT */ /// // Device Helpers // // Support functions to enumerating and guerying device info // var chromeToW3CDeviceKinds = { audio: 'audioInput', video: 'videoInput' }; OT.$.shouldAskForDevices = function(callback) { var MST = window.MediaStreamTrack; if(MST != null && OT.$.isFunction(MST.getSources)) { window.MediaStreamTrack.getSources(function(sources) { var hasAudio = sources.some(function(src) { return src.kind === 'audio'; }); var hasVideo = sources.some(function(src) { return src.kind === 'video'; }); callback.call(null, { video: hasVideo, audio: hasAudio }); }); } else { // This environment can't enumerate devices anyway, so we'll memorise this result. OT.$.shouldAskForDevices = function(callback) { setTimeout(OT.$.bind(callback, null, { video: true, audio: true })); }; OT.$.shouldAskForDevices(callback); } }; OT.$.getMediaDevices = function(callback) { if(OT.$.hasCapabilities('getMediaDevices')) { window.MediaStreamTrack.getSources(function(sources) { var filteredSources = OT.$.filter(sources, function(source) { return chromeToW3CDeviceKinds[source.kind] != null; }); callback(void 0, OT.$.map(filteredSources, function(source) { return { deviceId: source.id, label: source.label, kind: chromeToW3CDeviceKinds[source.kind] }; })); }); } else { callback(new Error('This browser does not support getMediaDevices APIs')); } }; })(window); (function(window) { var VideoOrientationTransforms = { 0: 'rotate(0deg)', 270: 'rotate(90deg)', 90: 'rotate(-90deg)', 180: 'rotate(180deg)' }; OT.VideoOrientation = { ROTATED_NORMAL: 0, ROTATED_LEFT: 270, ROTATED_RIGHT: 90, ROTATED_UPSIDE_DOWN: 180 }; var DefaultAudioVolume = 50; var DEGREE_TO_RADIANS = Math.PI * 2 / 360; // // // var _videoElement = new OT.VideoElement({ // fallbackText: 'blah' // }, errorHandler); // // _videoElement.bindToStream(webRtcStream, completion); // => VideoElement // _videoElement.appendTo(DOMElement) // => VideoElement // // _videoElement.domElement // => 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(/* optional */ options/*, optional errorHandler*/) { var _options = OT.$.defaults( options && !OT.$.isFunction(options) ? options : {}, { fallbackText: 'Sorry, Web RTC is not available in your browser' }), errorHandler = OT.$.isFunction(arguments[arguments.length-1]) ? arguments[arguments.length-1] : void 0, orientationHandler = OT.$.bind(function(orientation) { this.trigger('orientationChanged', orientation); }, this), _videoElement = TBPlugin.isInstalled() ? new PluginVideoElement(_options, errorHandler, orientationHandler) : new NativeDOMVideoElement(_options, errorHandler, orientationHandler), _streamBound = false, _stream, _preInitialisedVolue; OT.$.eventing(this); // Public Properties OT.$.defineProperties(this, { domElement: { get: function() { return _videoElement.domElement(); } }, videoWidth: { get: function() { return _videoElement['video' + (this.isRotated() ? 'Height' : 'Width')](); } }, videoHeight: { get: function() { return _videoElement['video' + (this.isRotated() ? 'Width' : 'Height')](); } }, aspectRatio: { get: function() { return (this.videoWidth() + 0.0) / this.videoHeight(); } }, isRotated: { get: function() { return _videoElement.isRotated(); } }, orientation: { get: function() { return _videoElement.orientation(); }, set: function(orientation) { _videoElement.orientation(orientation); } }, audioChannelType: { get: function() { return _videoElement.audioChannelType(); }, set: function(type) { _videoElement.audioChannelType(type); } } }); // Public Methods this.imgData = function() { return _videoElement.imgData(); }; this.appendTo = function(parentDomElement) { _videoElement.appendTo(parentDomElement); return this; }; this.bindToStream = function(webRtcStream, completion) { _streamBound = false; _stream = webRtcStream; _videoElement.bindToStream(webRtcStream, OT.$.bind(function(err) { if (err) { completion(err); return; } _streamBound = true; if (_preInitialisedVolue) { this.setAudioVolume(_preInitialisedVolue); _preInitialisedVolue = null; } completion(null); }, this)); return this; }; this.unbindStream = function() { if (!_stream) return this; _stream = null; _videoElement.unbindStream(); return this; }; this.setAudioVolume = function (value) { if (_streamBound) _videoElement.setAudioVolume( OT.$.roundFloat(value / 100, 2) ); else _preInitialisedVolue = value; return this; }; this.getAudioVolume = function () { if (_streamBound) return parseInt(_videoElement.getAudioVolume() * 100, 10); else return _preInitialisedVolue || 50; }; this.whenTimeIncrements = function (callback, context) { _videoElement.whenTimeIncrements(callback, context); return this; }; this.destroy = function () { // unbind all events so they don't fire after the object is dead this.off(); _videoElement.destroy(); return void 0; }; }; var PluginVideoElement = function PluginVideoElement (options, errorHandler, orientationChangedHandler) { var _videoProxy, _parentDomElement; canBeOrientatedMixin(this, function() { return _videoProxy.domElement; }, orientationChangedHandler); /// Public methods this.domElement = function() { return _videoProxy ? _videoProxy.domElement : void 0; }; this.videoWidth = function() { return _videoProxy ? _videoProxy.getVideoWidth() : void 0; }; this.videoHeight = function() { return _videoProxy ? _videoProxy.getVideoHeight() : void 0; }; this.imgData = function() { return _videoProxy ? _videoProxy.getImgData() : null; }; // Append the Video DOM element to a parent node this.appendTo = function(parentDomElement) { _parentDomElement = parentDomElement; return this; }; // Bind a stream to the video element. this.bindToStream = function(webRtcStream, completion) { if (!_parentDomElement) { completion('The VideoElement must attached to a DOM node before a stream can be bound'); return; } _videoProxy = webRtcStream._.render(); _videoProxy.appendTo(_parentDomElement); _videoProxy.show(completion); return this; }; // Unbind the currently bound stream from the video element. this.unbindStream = function() { // TODO: some way to tell TBPlugin to release that stream and controller if (_videoProxy) { _videoProxy.destroy(); _parentDomElement = null; _videoProxy = null; } return this; }; this.setAudioVolume = function(value) { if (_videoProxy) _videoProxy.setVolume(value); }; this.getAudioVolume = function() { // Return the actual volume of the DOM element if (_videoProxy) return _videoProxy.getVolume(); return DefaultAudioVolume; }; // see https://wiki.mozilla.org/WebAPI/AudioChannels // The audioChannelType is not currently supported in the plugin. this.audioChannelType = function(/* type */) { return 'unknown'; }; this.whenTimeIncrements = function(callback, context) { // exists for compatibility with NativeVideoElement OT.$.callAsync(OT.$.bind(callback, context)); }; this.destroy = function() { this.unbindStream(); return void 0; }; }; var NativeDOMVideoElement = function NativeDOMVideoElement (options, errorHandler, orientationChangedHandler) { var _domElement, _videoElementMovedWarning = false; /// Private API var _onVideoError = OT.$.bind(function(event) { var reason = 'There was an unexpected problem with the Video Stream: ' + videoElementErrorCodeToStr(event.target.error.code); errorHandler(reason, this, 'VideoElement'); }, 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 = createNativeVideoElement(options.fallbackText, options.attributes); _domElement.addEventListener('pause', _playVideoOnPause); canBeOrientatedMixin(this, function() { return _domElement; }, orientationChangedHandler); /// Public methods this.domElement = function() { return _domElement; }; this.videoWidth = function() { return _domElement.videoWidth; }; this.videoHeight = function() { return _domElement.videoHeight; }; this.imgData = function() { var canvas = OT.$.createElement('canvas', { width: _domElement.videoWidth, height: _domElement.videoHeight, style: { display: 'none' } }); document.body.appendChild(canvas); try { canvas.getContext('2d').drawImage(_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 OT.$.trim(imgData.replace('data:image/png;base64,', '')); }; // Append the Video DOM element to a parent node this.appendTo = function(parentDomElement) { parentDomElement.appendChild(_domElement); return this; }; // Bind a stream to the video element. this.bindToStream = function(webRtcStream, completion) { bindStreamToNativeVideoElement(_domElement, webRtcStream, function(err) { if (err) { completion(err); return; } _domElement.addEventListener('error', _onVideoError, false); completion(null); }); return this; }; // Unbind the currently bound stream from the video element. this.unbindStream = function() { if (_domElement) { unbindNativeStream(_domElement); } return this; }; this.setAudioVolume = function(value) { if (_domElement) _domElement.volume = value; }; this.getAudioVolume = function() { // Return the actual volume of the DOM element if (_domElement) return _domElement.volume; return DefaultAudioVolume; }; // see https://wiki.mozilla.org/WebAPI/AudioChannels // The audioChannelType is currently only available in Firefox. This property returns // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel" this.audioChannelType = function(type) { if (type !== void 0) { _domElement.mozAudioChannelType = type; } if ('mozAudioChannelType' in _domElement) { return _domElement.mozAudioChannelType; } else { return 'unknown'; } }; this.whenTimeIncrements = function(callback, context) { if(_domElement) { var lastTime, handler; handler = OT.$.bind(function() { if(!lastTime || lastTime >= _domElement.currentTime) { lastTime = _domElement.currentTime; } else { _domElement.removeEventListener('timeupdate', handler, false); callback.call(context, this); } }, this); _domElement.addEventListener('timeupdate', handler, false); } }; this.destroy = function() { 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; } return void 0; }; }; /// Private Helper functions // A mixin to create the orientation API implementation on +self+ // +getDomElementCallback+ is a function that the mixin will call when it wants to // get the native Dom element for +self+. // // +initialOrientation+ sets the initial orientation (shockingly), it's currently unused // so the initial value is actually undefined. // var canBeOrientatedMixin = function canBeOrientatedMixin (self, getDomElementCallback, orientationChangedHandler, initialOrientation) { var _orientation = initialOrientation; OT.$.defineProperties(self, { isRotated: { get: function() { return this.orientation() && (this.orientation().videoOrientation === 270 || this.orientation().videoOrientation === 90); } }, orientation: { get: function() { return _orientation; }, set: function(orientation) { _orientation = orientation; var transform = VideoOrientationTransforms[orientation.videoOrientation] || VideoOrientationTransforms.ROTATED_NORMAL; switch(OT.$.browser()) { case 'Chrome': case 'Safari': getDomElementCallback().style.webkitTransform = transform; break; case 'IE': if (OT.$.browserVersion().version >= 9) { getDomElementCallback().style.msTransform = transform; } else { // So this basically defines matrix that represents a rotation // of a single vector in a 2d basis. // // R = [cos(Theta) -sin(Theta)] // [sin(Theta) cos(Theta)] // // Where Theta is the number of radians to rotate by // // Then to rotate the vector v: // v' = Rv // // We then use IE8 Matrix filter property, which takes // a 2x2 rotation matrix, to rotate our DOM element. // var radians = orientation.videoOrientation * DEGREE_TO_RADIANS, element = getDomElementCallback(), costheta = Math.cos(radians), sintheta = Math.sin(radians); // element.filters.item(0).M11 = costheta; // element.filters.item(0).M12 = -sintheta; // element.filters.item(0).M21 = sintheta; // element.filters.item(0).M22 = costheta; element.style.filter = 'progid:DXImageTransform.Microsoft.Matrix(' + 'M11='+costheta+',' + 'M12='+(-sintheta)+',' + 'M21='+sintheta+',' + 'M22='+costheta+',SizingMethod=\'auto expand\')'; } break; default: // The standard version, just Firefox, Opera, and IE > 9 getDomElementCallback().style.transform = transform; } orientationChangedHandler(_orientation); } }, // see https://wiki.mozilla.org/WebAPI/AudioChannels // The audioChannelType is currently only available in Firefox. This property returns // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel" audioChannelType: { get: function() { if ('mozAudioChannelType' in this.domElement) { return this.domElement.mozAudioChannelType; } else { return 'unknown'; } }, set: function(type) { if ('mozAudioChannelType' in this.domElement) { this.domElement.mozAudioChannelType = type; } } } }); }; function createNativeVideoElement(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) { if(!attributes.hasOwnProperty(key)) { continue; } 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 bindStreamToNativeVideoElement(videoElement, webRtcStream, completion) { var cleanup, onLoad, onError, onStoppedLoading, timeout; // Note: onloadedmetadata doesn't fire in Chrome for audio only crbug.com/110938 // After version 36 it will fire if the video track is disabled. var browser = OT.$.browserVersion(), needsDisabledAudioProtection = browser.browser === 'Chrome' && browser.version < 36; if (navigator.mozGetUserMedia || !(needsDisabledAudioProtection && (webRtcStream.getVideoTracks().length > 0 && webRtcStream.getVideoTracks()[0].enabled))) { cleanup = function cleanup () { clearTimeout(timeout); videoElement.removeEventListener('loadedmetadata', onLoad, false); videoElement.removeEventListener('error', onError, false); webRtcStream.onended = null; }; onLoad = function onLoad () { cleanup(); completion(null); }; onError = function onError (event) { cleanup(); unbindNativeStream(videoElement); completion('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(); unbindNativeStream(videoElement); completion('Stream ended while trying to bind it to a video element.'); }; // Timeout if it takes too long timeout = setTimeout(OT.$.bind(function() { if (videoElement.currentTime === 0) { cleanup(); completion('The video stream failed to connect. Please notify the site ' + 'owner if this continues to happen.'); } else if (webRtcStream.ended === true) { // The ended event should have fired by here, but support for it isn't // always so awesome. onStoppedLoading(); } else { OT.warn('Never got the loadedmetadata event but currentTime > 0'); onLoad(null); } }, this), 30000); videoElement.addEventListener('loadedmetadata', onLoad, false); videoElement.addEventListener('error', onError, false); webRtcStream.onended = onStoppedLoading; } else { OT.$.callAsync(completion, null); } // 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(); } function unbindNativeStream(videoElement) { if (videoElement.srcObject !== void 0) { videoElement.srcObject = null; } else if (videoElement.mozSrcObject !== void 0) { videoElement.mozSrcObject = null; } else { window.URL.revokeObjectURL(videoElement.src); } } })(window); // tb_require('../helpers/helpers.js') !(function() { /* jshint globalstrict: true, strict: false, undef: true, unused: true, trailing: true, browser: true, smarttabs:true */ /* global OT */ var currentGuidStorage, currentGuid; var isInvalidStorage = function isInvalidStorage (storageInterface) { return !(OT.$.isFunction(storageInterface.get) && OT.$.isFunction(storageInterface.set)); }; var getClientGuid = function getClientGuid (completion) { if (currentGuid) { completion(null, currentGuid); return; } // It's the first time that getClientGuid has been called // in this page lifetime. Attempt to load any existing Guid // from the storage currentGuidStorage.get(completion); }; OT.overrideGuidStorage = function (storageInterface) { if (isInvalidStorage(storageInterface)) { throw new Error('The storageInterface argument does not seem to be valid, ' + 'it must implement get and set methods'); } if (currentGuidStorage === storageInterface) { return; } currentGuidStorage = storageInterface; // If a client Guid has already been assigned to this client then // let the new storage know about it so that it's in sync. if (currentGuid) { currentGuidStorage.set(currentGuid, function(error) { if (error) { OT.error('Failed to send initial Guid value (' + currentGuid + ') to the newly assigned Guid Storage. The error was: ' + error); // @todo error } }); } }; if (!OT._) OT._ = {}; OT._.getClientGuid = function (completion) { getClientGuid(function(error, guid) { if (error) { completion(error); return; } if (!guid) { // Nothing came back, this client is entirely new. // generate a new Guid and persist it guid = OT.$.uuid(); currentGuidStorage.set(guid, function(error) { if (error) { completion(error); return; } currentGuid = guid; }); } else if (!currentGuid) { currentGuid = guid; } completion(null, currentGuid); }); }; // Implement our default storage mechanism, which sets/gets a cookie // called 'opentok_client_id' OT.overrideGuidStorage({ get: function(completion) { completion(null, OT.$.getCookie('opentok_client_id')); }, set: function(guid, completion) { OT.$.setCookie('opentok_client_id', guid); completion(null); } }); })(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, browser = OT.$.browserVersion(), send = function(data, isQos, callback) { OT.$.post((isQos ? endPointQos : endPoint) + '?_=' + OT.$.uuid.v4(), { body: data, xdomainrequest: (browser.browser === 'IE' & browser.version < 10), headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }, callback); }, 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(err) { if(err) { OT.debug('Failed to send ClientEvent, moving on to the next item.'); // There was an error, move onto the next item } else { curr.onComplete(); } 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; }; 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' }; // 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: OT.$.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 = {}; OT._.getClientGuid(function(error, guid) { if (error) { // @todo return; } // Set a bunch of defaults var data = OT.$.extend({ 'variation' : '', 'guid' : guid, '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 = {}; OT._.getClientGuid(function(error, guid) { if (error) { // @todo return; } // Set a bunch of defaults var data = OT.$.extend({ 'guid' : guid, '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)) { if(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('|') ]; }; }; })(window); !(function() { OT.$.registerCapability('audioOutputLevelStat', function() { return OT.$.browserVersion().browser === 'Chrome'; }); OT.$.registerCapability('webAudioCapableRemoteStream', function() { return OT.$.browserVersion().browser === 'Firefox'; }); OT.$.registerCapability('getStatsWithSingleParameter', function() { return OT.$.browserVersion().browser === 'Chrome'; }); OT.$.registerCapability('webAudio', function() { return 'AudioContext' in 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:') { /*global alert*/ 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 _analytics = new OT.Analytics(); var // Global parameters used by upgradeSystemRequirements _intervalId, _lastHash = document.location.hash; /** * The first step in using the OpenTok API is to call the OT.initSession() * method. Other methods of the OT object check for system requirements and set up error logging. * * @class OT */ /** *

* Initializes and returns the local session object for a specified session ID. *

*

* You connect to an OpenTok session using the connect() method * of the Session object returned by the OT.initSession() method. * Note that calling OT.initSession() 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). *

* *

* For an example, see Session.connect(). *

* * @method OT.initSession * @memberof OT * @param {String} apiKey Your OpenTok API key (see the * OpenTok dashboard). * @param {String} sessionId The session ID identifying the OpenTok session. For more * information, see Session creation. * @returns {Session} The session object through which all further interactions with * the session will occur. */ OT.initSession = function(apiKey, sessionId) { if(sessionId == null) { sessionId = apiKey; apiKey = null; } var session = OT.sessions.get(sessionId); if (!session) { session = new OT.Session(apiKey, sessionId); OT.sessions.add(session); } return session; }; /** *

* Initializes and returns a Publisher object. You can then pass this Publisher * object to Session.publish() to publish a stream to a session. *

*

* Note: If you intend to reuse a Publisher object created using * OT.initPublisher() to publish to different sessions sequentially, * call either Session.disconnect() or Session.unpublish(). * Do not call both. Then call the preventDefault() method of the * streamDestroyed or sessionDisconnected event object to prevent the * Publisher object from being removed from the page. *

* * @param {Object} targetElement (Optional) The DOM element or the id attribute of the * existing DOM element used to determine the location of the Publisher video in the HTML DOM. See * the insertMode property of the properties parameter. If you do not * specify a targetElement, the application appends a new DOM element to the HTML * body. * *

* The application throws an error if an element with an ID set to the * targetElement value does not exist in the HTML DOM. *

* * @param {Object} properties (Optional) This object contains the following properties (each of which * are optional): *

*
    *
  • * audioSource (String) — The ID of the audio input device (such as a * microphone) to be used by the publisher. You can obtain a list of available devices, including * audio input devices, by calling the OT.getDevices() method. Each * device listed by the method has a unique device ID. If you pass in a device ID that does not * match an existing audio input device, the call to OT.initPublisher() fails with an * error (error code 1500, "Unable to Publish") passed to the completion handler function. *
  • *
  • * frameRate (Number) — The desired frame rate, in frames per second, * of the video. Valid values are 30, 15, 7, and 1. The published stream will use the closest * value supported on the publishing client. The frame rate can differ slightly from the value * you set, depending on the browser of the client. And the video will only use the desired * frame rate if the client configuration supports it. *

    If the publisher specifies a frame rate, the actual frame rate of the video stream * is set as the frameRate property of the Stream object, though the actual frame rate * will vary based on changing network and system conditions. If the developer does not specify a * frame rate, this property is undefined. *

    * For sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed, lowering the frame rate or lowering the resolution reduces * the maximum bandwidth the stream can use. However, in sessions with the media mode set to * relayed, lowering the frame rate or resolution may not reduce the stream's bandwidth. *

    *

    * You can also restrict the frame rate of a Subscriber's video stream. To restrict the frame rate * a Subscriber, call the restrictFrameRate() method of the subscriber, passing in * true. * (See Subscriber.restrictFrameRate().) *

    *
  • *
  • * height (Number) — The desired height, in pixels, of the * displayed Publisher video stream (default: 198). Note: Use the * height and width properties to set the dimensions * of the publisher video; do not set the height and width of the DOM element * (using CSS). *
  • *
  • * insertMode (String) — Specifies how the Publisher object will be * inserted in the HTML DOM. See the targetElement parameter. This string can * have the following values: *
      *
    • "replace" — The Publisher object replaces contents of the * targetElement. This is the default.
    • *
    • "after" — The Publisher object is a new element inserted after * the targetElement in the HTML DOM. (Both the Publisher and targetElement have the * same parent element.)
    • *
    • "before" — The Publisher object is a new element inserted before * the targetElement in the HTML DOM. (Both the Publisher and targetElement have the same * parent element.)
    • *
    • "append" — The Publisher object is a new element added as a child * of the targetElement. If there are other child elements, the Publisher is appended as * the last child element of the targetElement.
    • *
    *
  • *
  • * mirror (Boolean) — Whether the publisher's video image * is mirrored in the publisher's page<. The default value is true * (the video image is mirrored). This property does not affect the display * on other subscribers' web pages. *
  • *
  • * name (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. *
  • *
  • * publishAudio (Boolean) — Whether to initially publish audio * for the stream (default: true). This setting applies when you pass * the Publisher object in a call to the Session.publish() method. *
  • *
  • * publishVideo (Boolean) — Whether to initially publish video * for the stream (default: true). This setting applies when you pass * the Publisher object in a call to the Session.publish() method. *
  • *
  • * resolution (String) — The desired resolution of the video. The format * of the string is "widthxheight", where the width and height are represented in * pixels. Valid values are "1280x720", "640x480", and * "320x240". The published video will only use the desired resolution if the * client configuration supports it. *

    * The requested resolution of a video stream is set as the videoDimensions.width and * videoDimensions.height properties of the Stream object. *

    *

    * The default resolution for a stream (if you do not specify a resolution) is 640x480 pixels. * If the client system cannot support the resolution you requested, the the stream will use the * next largest setting supported. *

    *

    * For sessions that use the OpenTok Media Router (sessions with the * media mode * set to routed, lowering the frame rate or lowering the resolution reduces the maximum bandwidth * the stream can use. However, in sessions that have the media mode set to relayed, lowering the * frame rate or resolution may not reduce the stream's bandwidth. *

    *
  • *
  • * style (Object) — An object containing properties that define the initial * appearance of user interface controls of the Publisher. The style object includes * the following properties: *
      *
    • audioLevelDisplayMode (String) — How to display the audio level * indicator. Possible values are: "auto" (the indicator is displayed when the * video is disabled), "off" (the indicator is not displayed), and * "on" (the indicator is always displayed).
    • * *
    • backgroundImageURI (String) — A URI for an image to display as * the background image when a video is not displayed. (A video may not be displayed if * you call publishVideo(false) on the Publisher object). You can pass an http * or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the * data URI scheme (instead of http or https) and pass in base-64-encrypted * PNG data, such as that obtained from the * Publisher.getImgData() method. For example, * you could set the property to "data:VBORw0KGgoAA...", where the portion of the * string after "data:" is the result of a call to * Publisher.getImgData(). If the URL or the image data is invalid, the property * is ignored (the attempt to set the image fails silently). *

      * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer), * you cannot set the backgroundImageURI style to a string larger than 32 kB. * This is due to an IE 8 limitation on the size of URI strings. Due to this limitation, * you cannot set the backgroundImageURI style to a string obtained with the * getImgData() method. *

    • * *
    • buttonDisplayMode (String) — How to display the microphone controls * Possible values are: "auto" (controls are displayed when the stream is first * displayed and when the user mouses over the display), "off" (controls are not * displayed), and "on" (controls are always displayed).
    • * *
    • nameDisplayMode (String) — Whether to display the stream 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 always displayed).
    • *
    *
  • *
  • * videoSource (String) — The ID of the video input device (such as a * camera) to be used by the publisher. You can obtain a list of available devices, including * video input devices, by calling the OT.getDevices() method. Each * device listed by the method has a unique device ID. If you pass in a device ID that does not * match an existing video input device, the call to OT.initPublisher() fails with an * error (error code 1500, "Unable to Publish") passed to the completion handler function. *
  • *
  • * width (Number) — The desired width, in pixels, of the * displayed Publisher video stream (default: 264). Note: Use the * height and width properties to set the dimensions * of the publisher video; do not set the height and width of the DOM element * (using CSS). *
  • *
* @param {Function} completionHandler (Optional) A function to be called when the method succeeds * or fails in initializing a Publisher object. This function takes one parameter — * error. On success, the error object is set to null. On * failure, the error object has two properties: code (an integer) and * message (a string), which identify the cause of the failure. The method succeeds * when the user grants access to the camera and microphone. The method fails if the user denies * access to the camera and microphone. The completionHandler function is called * before the Publisher dispatches an accessAllowed (success) event or an * accessDenied (failure) event. *

* The following code adds a completionHandler when calling the * OT.initPublisher() method: *

*
* var publisher = OT.initPublisher('publisher', null, function (error) {
*   if (error) {
*     console.log(error);
*   } else {
*     console.log("Publisher initialized.");
*   }
* });
* 
* * @returns {Publisher} The Publisher object. * @see for audio input * devices or "videoInput" for video input devices. *

* The deviceId property is a unique ID for the device. You can pass * the deviceId in as the audioSource or videoSource * property of the the options parameter of the * OT.initPublisher() method. *

* The label property identifies the device. The label * property is set to an empty string if the user has not previously granted access to * a camera and microphone. In HTTP, the user must have granted access to a camera and * microphone in the current page (for example, in response to a call to * OT.initPublisher()). In HTTPS, the user must have previously granted access * to the camera and microphone in the current page or in a page previously loaded from the * domain. * * * @see OT.initPublisher() * @method OT.getDevices * @memberof OT */ OT.getDevices = function(callback) { OT.$.getMediaDevices(callback); }; /** * Checks if the system supports OpenTok for WebRTC. * @return {Number} Whether the system supports OpenTok for WebRTC (1) or not (0). * @see OT.upgradeSystemRequirements() * @method OT.checkSystemRequirements * @memberof OT */ OT.checkSystemRequirements = function() { OT.debug('OT.checkSystemRequirements()'); // Try native support first, then TBPlugin... var systemRequirementsMet = OT.$.hasCapabilities('websockets', 'webrtc') || TBPlugin.isInstalled(); systemRequirementsMet = systemRequirementsMet ? this.HAS_REQUIREMENTS : this.NOT_HAS_REQUIREMENTS; OT.checkSystemRequirements = function() { OT.debug('OT.checkSystemRequirements()'); return systemRequirementsMet; }; if(systemRequirementsMet === this.NOT_HAS_REQUIREMENTS) { _analytics.logEvent({ action: 'checkSystemRequirements', variation: 'notHasRequirements', 'payload_type': 'userAgent', 'partner_id': OT.APIKEY, payload: OT.$.userAgent() }); } return systemRequirementsMet; }; /** * Displays information about system requirments for OpenTok for WebRTC. This * information is displayed in an iframe element that fills the browser window. *

* Note: this information is displayed automatically when you call the * OT.initSession() or the OT.initPublisher() method * if the client does not support OpenTok for WebRTC. *

* @see OT.checkSystemRequirements() * @method OT.upgradeSystemRequirements * @memberof OT */ OT.upgradeSystemRequirements = function(){ // trigger after the OT environment has loaded OT.onLoad( function() { if(TBPlugin.isSupported()) { OT.Dialogs.Plugin.promptToInstall().on({ download: function() { window.location = TBPlugin.pathToInstaller(); }, refresh: function() { location.reload(); }, closed: function() {} }); return; } 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'); var browser = OT.$.browserVersion(), minimumBrowserVersion = OT.properties.minimumVersion[browser.browser.toLowerCase()], isSupportedButOld = minimumBrowserVersion > browser.version; d.src = OT.properties.assetURL + '/html/upgrade.html#' + encodeURIComponent(isSupportedButOld ? 'true' : 'false') + ',' + encodeURIComponent(JSON.stringify(OT.properties.minimumVersion)) + '|' + 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 OT.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 scriptSrc = (function(){ var s = document.getElementsByTagName('script'); s = s[s.length - 1]; s = s.getAttribute('src') || s.src; return s; })(); var m = scriptSrc.match(/[\?\&]apikey=([^&]+)/i); return m ? m[1] : ''; })(); OT.HAS_REQUIREMENTS = 1; OT.NOT_HAS_REQUIREMENTS = 0; /** * This method is deprecated. Use on() or once() instead. * *

* Registers a method as an event listener for a specific event. *

* *

* The OT object dispatches one type of event — an exception event. The * following code adds an event listener for the exception event: *

* *
* OT.addEventListener("exception", exceptionHandler);
*
* function exceptionHandler(event) {
*    alert("exception event. \n  code == " + event.code + "\n  message == " + event.message);
* }
* 
* *

* 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. *

*

* Throws an exception if the listener name is invalid. *

* * @param {String} type The string identifying the type of event. * * @param {Function} listener The function to be invoked when the OT object dispatches the event. * @see on() * @see once() * @memberof OT * @method addEventListener */ /** * This method is deprecated. Use off() instead. * *

* Removes an event listener for a specific event. *

* *

* Throws an exception if the listener name is invalid. *

* * @param {String} type The string identifying the type of event. * * @param {Function} listener The event listener function to remove. * * @see off() * @memberof OT * @method removeEventListener */ /** * Adds an event handler function for one or more events. * *

* The OT object dispatches one type of event — an exception event. The following * code adds an event * listener for the exception event: *

* *
* OT.on("exception", function (event) {
*   // This is the event handler.
* });
* 
* *

You can also pass in a third context parameter (which is optional) to define the * value of * this in the handler method:

* *
* OT.on("exception",
*   function (event) {
*     // This is the event handler.
*   }),
*   session
* );
* 
* *

* If you do not add a handler for an event, the event is ignored locally. *

* * @param {String} type The string identifying the type of event. * @param {Function} handler The handler function to process the event. This function takes the event * object as a parameter. * @param {Object} context (Optional) Defines the value of this in the event handler * function. * * @memberof OT * @method on * @see off() * @see once() * @see Events */ /** * Adds an event handler function for an event. Once the handler is called, the specified handler * method is * removed as a handler for this event. (When you use the OT.on() method to add an event * handler, the handler * is not removed when it is called.) The OT.once() method is the equivilent of * calling the OT.on() * method and calling OT.off() the first time the handler is invoked. * *

* The following code adds a one-time event handler for the exception event: *

* *
* OT.once("exception", function (event) {
*   console.log(event);
* }
* 
* *

You can also pass in a third context parameter (which is optional) to define the * value of * this in the handler method:

* *
* OT.once("exception",
*   function (event) {
*     // This is the event handler.
*   },
*   session
* );
* 
* *

* The method also supports an alternate syntax, in which the first parameter is an object that is a * hash map of * event names and handler functions and the second parameter (optional) is the context for this in * each handler: *

*
* OT.once(
*   {exeption: function (event) {
*     // This is the event handler.
*     }
*   },
*   session
* );
* 
* * @param {String} type The string identifying the type of event. You can specify multiple event * names in this string, * separating them with a space. The event handler will process the first occurence of the events. * After the first event, * the handler is removed (for all specified events). * @param {Function} handler The handler function to process the event. This function takes the event * object as a parameter. * @param {Object} context (Optional) Defines the value of this in the event handler * function. * * @memberof OT * @method once * @see on() * @see once() * @see Events */ /** * Removes an event handler. * *

Pass in an event name and a handler method, the handler is removed for that event:

* *
OT.off("exceptionEvent", exceptionEventHandler);
* *

If you pass in an event name and no handler method, all handlers are removed for that * events:

* *
OT.off("exceptionEvent");
* *

* The method also supports an alternate syntax, in which the first parameter is an object that is a * hash map of * event names and handler functions and the second parameter (optional) is the context for matching * handlers: *

*
* OT.off(
*   {
*     exceptionEvent: exceptionEventHandler
*   },
*   this
* );
* 
* * @param {String} type (Optional) The string identifying the type of event. You can use a space to * specify multiple events, as in "eventName1 eventName2 eventName3". If you pass in no * type value (or other arguments), all event handlers are removed for the object. * @param {Function} handler (Optional) The event handler function to remove. If you pass in no * handler, all event handlers are removed for the specified event type. * @param {Object} context (Optional) If you specify a context, the event handler is * removed for all specified events and handlers that use the specified context. * * @memberof OT * @method off * @see on() * @see once() * @see Events */ /** * Dispatched by the OT class when the app encounters an exception. * Note that you set up an event handler for the exception event by calling the * OT.on() method. * * @name exception * @event * @borrows ExceptionEvent#message as this.message * @memberof OT * @see ExceptionEvent */ if (!window.OT) window.OT = OT; if (!window.TB) window.TB = OT; })(window); !(function() { OT.Collection = function(idField) { var _models = [], _byId = {}, _idField = idField || 'id'; OT.$.eventing(this, true); var modelProperty = function(model, property) { if(OT.$.isFunction(model[property])) { return model[property](); } else { return model[property]; } }; var onModelUpdate = OT.$.bind(function onModelUpdate (event) { this.trigger('update', event); this.trigger('update:'+event.target.id, event); }, this), onModelDestroy = OT.$.bind(function onModelDestroyed (event) { this.remove(event.target, event.reason); }, this); this.reset = function() { // Stop listening on the models, they are no longer our problem OT.$.forEach(_models, function(model) { model.off('updated', onModelUpdate, this); model.off('destroyed', onModelDestroy, this); }, this); _models = []; _byId = {}; }; this.destroy = function(reason) { OT.$.forEach(_models, function(model) { if(model && typeof model.destroy === 'function') { model.destroy(reason, 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 OT.$.filter(_models, attrsOrFilterFn, context); return OT.$.filter(_models, function(model) { for (var key in attrsOrFilterFn) { if(!attrsOrFilterFn.hasOwnProperty(key)) { continue; } if (modelProperty(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(!attrsOrFilterFn.hasOwnProperty(key)) { continue; } if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false; } return true; }; } filterFn = OT.$.bind(filterFn, 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 = modelProperty(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); this.trigger('add', model); this.trigger('add:'+id, model); return this; }; this.remove = function(model, reason) { var id = modelProperty(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); this.trigger('remove', model, reason); this.trigger('remove:'+id, model, reason); return this; }; // Used by session connecto fire add events after adding listeners this._triggerAddEvents = function() { var models = this.where.apply(this, arguments); OT.$.forEach(models, function(model) { this.trigger('add', model); this.trigger('add:' + modelProperty(model, _idField), model); }, this); }; this.length = function() { return _models.length; }; }; }(this)); !(function() { /** * 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.

* *

For example, the Stream object dispatches a streamPropertyChanged event when * the stream's properties are updated. You add a callback for an event using the * on() method of the Stream object:

* *
   * stream.on("streamPropertyChanged", function (event) {
   *     alert("Properties changed for stream " + event.target.streamId);
   * });
* * @class Event * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable * (true) or not (false). You can cancel the default behavior by * calling the preventDefault() method of the Event object in the callback * function. (See preventDefault().) * * @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. * *

To see whether an event has a default behavior, check the cancelable property * of the event object.

* *

Call the preventDefault() method in the callback function for the event.

* *

The following events have default behaviors:

* * * * @method #preventDefault * @memberof Event */ /** * Whether the default event behavior has been prevented via a call to * preventDefault() (true) or not (false). * See preventDefault(). * @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', // OT 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', ENV_UNLOADED: 'envUnloaded', // Audio activity Events AUDIO_LEVEL_UPDATED: 'audioLevelUpdated' }; 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_SUBSCRIBE: 1501, UNABLE_TO_FORCE_DISCONNECT: 1520, UNABLE_TO_FORCE_UNPUBLISH: 1530 }; /** * The {@link OT} class dispatches exception events when the OpenTok API encounters * an exception (error). The ExceptionEvent object defines the properties of the event * object that is dispatched. * *

Note that you set up a callback for the exception event by calling the * OT.on() method.

* * @class ExceptionEvent * @property {Number} code The error code. The following is a list of error codes:

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* code * * * title *
* 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 *
* 1013 * * Connection Failed *
* 1014 * * API Response Failure *
* 1500 * * Unable to Publish *
* 1520 * * Unable to Force Disconnect *
* 1530 * * Unable to Force Unpublish *
* 1535 * * Force Unpublish on Invalid Stream *
* 2000 * * * Internal Error *
* 2010 * * * Report Issue Failure *
* *

Check the message property for more details about the error.

* * @property {String} message The error message. * * @property {Object} target The object that the event pertains to. For an * exception event, this will be an object other than the OT object * (such as a Session object or a Publisher object). * * @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); }; /** * Dispatched by the Session object when a client connects to or disconnects from a {@link Session}. * For the local client, the Session object dispatches a "sessionConnected" or "sessionDisconnected" * event, defined by the {@link SessionConnectEvent} and {@link SessionDisconnectEvent} classes. * *
Example
* *

The following code keeps a running total of the number of connections to a session * by monitoring the connections property of the sessionConnect, * connectionCreated and connectionDestroyed events:

* *
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 = OT.initSession(apiKey, sessionID);
 * session.on("connectionCreated", function(event) {
 *    connectionCount++;
 *    displayConnectionCount();
 * });
 * session.on("connectionDestroyed", function(event) {
 *    connectionCount--;
 *    displayConnectionCount();
 * });
 * session.connect(token);
 *
 * function displayConnectionCount() {
 *     document.getElementById("connectionCountField").value = connectionCount.toString();
 * }
* *

This example assumes that there is an input text field in the HTML DOM * with the id set to "connectionCountField":

* *
<input type="text" id="connectionCountField" value="0"></input>
* * * @property {Connection} connection A Connection objects for the connections that was * created or deleted. * * @property {Array} connections Deprecated. Use the connection property. A * connectionCreated or connectionDestroyed event is dispatched * for each connection created and destroyed in the session. * * @property {String} reason For a connectionDestroyed event, * a description of why the connection ended. This property can have two values: *

*
    *
  • "clientDisconnected" — A client disconnected from the session by calling * the disconnect() method of the Session object or by closing the browser. * (See Session.disconnect().)
  • * *
  • "forceDisconnected" — A moderator has disconnected the publisher * from the session, by calling the forceDisconnect() method of the Session * object. (See Session.forceDisconnect().)
  • * *
  • "networkDisconnected" — The network connection terminated abruptly * (for example, the client lost their internet connection).
  • *
* *

Depending on the context, this description may allow the developer to refine * the course of action they take in response to an event.

* *

For a connectionCreated event, this string is undefined.

* * @class ConnectionEvent * @augments Event */ var connectionEventPluralDeprecationWarningShown = false; OT.ConnectionEvent = function (type, connection, reason) { OT.Event.call(this, type, false); if (OT.$.canDefineProperty) { Object.defineProperty(this, 'connections', { get: function() { if(!connectionEventPluralDeprecationWarningShown) { OT.warn('OT.ConnectionEvent connections property is deprecated, ' + 'use connection instead.'); connectionEventPluralDeprecationWarningShown = true; } return [connection]; } }); } else { this.connections = [connection]; } this.connection = connection; this.reason = reason; }; /** * StreamEvent is an event that can have the type "streamCreated" or "streamDestroyed". * These events are dispatched by the Session object when another client starts or * stops publishing a stream to a {@link Session}. For a local client's stream, the * Publisher object dispatches the event. * *

Example — streamCreated event dispatched * by the Session object

*

The following code initializes a session and sets up an event listener for when * a stream published by another client is created:

* *
 * session.on("streamCreated", function(event) {
 *   // streamContainer is a DOM element
 *   subscriber = session.subscribe(event.stream, targetElement);
 * }).connect(token);
 * 
* *

Example — streamDestroyed event dispatched * by the Session object

* *

The following code initializes a session and sets up an event listener for when * other clients' streams end:

* *
 * session.on("streamDestroyed", function(event) {
 *     console.log("Stream " + event.stream.name + " ended. " + event.reason);
 * }).connect(token);
 * 
* *

Example — streamCreated event dispatched * by a Publisher object

*

The following code publishes a stream and adds an event listener for when the streaming * starts

* *
 * var publisher = session.publish(targetElement)
 *   .on("streamCreated", function(event) {
 *     console.log("Publisher started streaming.");
 *   );
 * 
* *

Example — streamDestroyed event * dispatched by a Publisher object

* *

The following code publishes a stream, and leaves the Publisher in the HTML DOM * when the streaming stops:

* *
 * var publisher = session.publish(targetElement)
 *   .on("streamDestroyed", function(event) {
 *     event.preventDefault();
 *     console.log("Publisher stopped streaming.");
 *   );
 * 
* * @class StreamEvent * * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable * (true) or not (false). You can cancel the default behavior by calling * the preventDefault() method of the StreamEvent object in the event listener * function. The streamDestroyed * event is cancelable. (See preventDefault().) * * @property {String} reason For a streamDestroyed event, * a description of why the session disconnected. This property can have one of the following * values: *

*
    *
  • "clientDisconnected" — A client disconnected from the session by calling * the disconnect() method of the Session object or by closing the browser. * (See Session.disconnect().)
  • * *
  • "forceDisconnected" — A moderator has disconnected the publisher of the * stream from the session, by calling the forceDisconnect() method of the Session * object. (See Session.forceDisconnect().)
  • * *
  • "forceUnpublished" — A moderator has forced the publisher of the stream * to stop publishing the stream, by calling the forceUnpublish() method of the * Session object. (See Session.forceUnpublish().)
  • * *
  • "networkDisconnected" — The network connection terminated abruptly (for * example, the client lost their internet connection).
  • * *
* *

Depending on the context, this description may allow the developer to refine * the course of action they take in response to an event.

* *

For a streamCreated event, this string is undefined.

* * @property {Stream} stream A Stream object corresponding to the stream that was added (in the * case of a streamCreated event) or deleted (in the case of a * streamDestroyed event). * * @property {Array} streams Deprecated. Use the stream property. A * streamCreated or streamDestroyed event is dispatched for * each stream added or destroyed. * * @augments Event */ var streamEventPluralDeprecationWarningShown = false; OT.StreamEvent = function (type, stream, reason, cancelable) { OT.Event.call(this, type, cancelable); if (OT.$.canDefineProperty) { Object.defineProperty(this, 'streams', { get: function() { if(!streamEventPluralDeprecationWarningShown) { OT.warn('OT.StreamEvent streams property is deprecated, use stream instead.'); streamEventPluralDeprecationWarningShown = true; } return [stream]; } }); } else { this.streams = [stream]; } this.stream = stream; this.reason = reason; }; /** * Prevents the default behavior associated with the event from taking place. * *

For the streamDestroyed event dispatched by the Session object, * the default behavior is that all Subscriber objects that are subscribed to the stream are * unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a * destroyed event when the element is removed from the HTML DOM. If you call the * preventDefault() method in the event listener for the streamDestroyed * event, the default behavior is prevented and you can clean up Subscriber objects using your * own code. See * Session.getSubscribersForStream().

*

* For the streamDestroyed event dispatched by a Publisher object, the default * behavior is that the Publisher object is removed from the HTML DOM. The Publisher object * dispatches a destroyed event when the element is removed from the HTML DOM. * If you call the preventDefault() method in the event listener for the * streamDestroyed event, the default behavior is prevented, and you can * retain the Publisher for reuse or clean it up using your own code. *

*

To see whether an event has a default behavior, check the cancelable property of * the event object.

* *

Call the preventDefault() method in the event listener function for the event.

* * @method #preventDefault * @memberof StreamEvent */ /** * The Session object dispatches SessionConnectEvent object when a session has successfully * connected in response to a call to the connect() method of the Session object. *

* In version 2.2, the completionHandler of the Session.connect() method * indicates success or failure in connecting to the session. * * @class SessionConnectEvent * @property {Array} connections Deprecated in version 2.2 (and set to an empty array). In * version 2.2, listen for the connectionCreated event dispatched by the Session * object. In version 2.2, the Session object dispatches a connectionCreated event * for each connection (including your own). This includes connections present when you first * connect to the session. * * @property {Array} streams Deprecated in version 2.2 (and set to an empty array). In version * 2.2, listen for the streamCreated event dispatched by the Session object. In * version 2.2, the Session object dispatches a streamCreated event for each stream * other than those published by your client. This includes streams * present when you first connect to the session. * * @see Session.connect()

* @augments Event */ var sessionConnectedConnectionsDeprecationWarningShown = false; var sessionConnectedStreamsDeprecationWarningShown = false; var sessionConnectedArchivesDeprecationWarningShown = false; OT.SessionConnectEvent = function (type) { OT.Event.call(this, type, false); if (OT.$.canDefineProperty) { Object.defineProperties(this, { connections: { get: function() { if(!sessionConnectedConnectionsDeprecationWarningShown) { OT.warn('OT.SessionConnectedEvent no longer includes connections. Listen ' + 'for connectionCreated events instead.'); sessionConnectedConnectionsDeprecationWarningShown = true; } return []; } }, streams: { get: function() { if(!sessionConnectedStreamsDeprecationWarningShown) { OT.warn('OT.SessionConnectedEvent no longer includes streams. Listen for ' + 'streamCreated events instead.'); sessionConnectedConnectionsDeprecationWarningShown = true; } return []; } }, archives: { get: function() { if(!sessionConnectedArchivesDeprecationWarningShown) { OT.warn('OT.SessionConnectedEvent no longer includes archives. Listen for ' + 'archiveStarted events instead.'); sessionConnectedArchivesDeprecationWarningShown = true; } return []; } } }); } else { this.connections = []; this.streams = []; this.archives = []; } }; /** * 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 * disconnect() method of the session object. * *

* Example *

*

* The following code initializes a session and sets up an event listener for when a session is * disconnected. *

*
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 = OT.initSession(apiKey, sessionID);
 *  session.on("sessionDisconnected", function(event) {
 *      alert("The session disconnected. " + event.reason);
 *  });
 *  session.connect(token);
 *  
* * @property {String} reason A description of why the session disconnected. * This property can have two values: *

*
    *
  • "clientDisconnected" — A client disconnected from the session by calling * the disconnect() method of the Session object or by closing the browser. * ( See Session.disconnect().)
  • *
  • "forceDisconnected" — A moderator has disconnected you from the session * by calling the forceDisconnect() method of the Session object. (See * Session.forceDisconnect().)
  • *
  • "networkDisconnected" — The network connection terminated abruptly * (for example, the client lost their internet connection).
  • *
* * @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. * *

For the sessionDisconnectEvent, the default behavior is that all Subscriber * objects are unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a * destroyed event when the element is removed from the HTML DOM. If you call the * preventDefault() method in the event listener for the sessionDisconnect * event, the default behavior is prevented, and you can, optionally, clean up Subscriber objects * using your own code). * *

To see whether an event has a default behavior, check the cancelable property of * the event object.

* *

Call the preventDefault() method in the event listener function for the event.

* * @method #preventDefault * @memberof SessionDisconnectEvent */ /** * The Session object dispatches a streamPropertyChanged event in the * following circumstances: * *
    * *
  • When a publisher starts or stops publishing audio or video. This change causes * the hasAudio or hasVideo property of the Stream object to * change. This change results from a call to the publishAudio() or * publishVideo() methods of the Publish object.
  • * *
  • When the videoDimensions property of a stream changes. For more information, * see Stream.videoDimensions.
  • * *
* * @class StreamPropertyChangedEvent * @property {String} changedProperty The property of the stream that changed. This value * is either "hasAudio", "hasVideo", or "videoDimensions". * @property {Object} newValue The new value of the property (after the change). * @property {Object} oldValue The old value of the property (before the change). * @property {Stream} stream The Stream object for which a property has changed. * * @see Publisher.publishAudio()

* @see Publisher.publishVideo()

* @see Stream.videoDimensions

* @augments Event */ OT.StreamPropertyChangedEvent = function (type, stream, changedProperty, oldValue, newValue) { OT.Event.call(this, type, false); this.type = type; this.stream = stream; this.changedProperty = changedProperty; this.oldValue = oldValue; this.newValue = newValue; }; /** * Defines event objects for the archiveStarted and archiveStopped events. * The Session object dispatches these events when an archive recording of the session starts and * stops. * * @property {String} id The archive ID. * @property {String} name The name of the archive. You can assign an archive a name when you create * it, using the OpenTok REST API or one of the * OpenTok server SDKs. * * @class ArchiveEvent * @augments Event */ OT.ArchiveEvent = function (type, archive) { OT.Event.call(this, type, false); this.type = type; this.id = archive.id; this.name = archive.name; this.status = archive.status; this.archive = archive; }; OT.ArchiveUpdatedEvent = function (stream, key, oldValue, newValue) { OT.Event.call(this, 'updated', false); this.target = stream; this.changedProperty = key; this.oldValue = oldValue; this.newValue = newValue; }; /** * 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 {String} data The data string sent with the signal (if there is one). * @property {Connection} from The Connection corresponding to the client that sent with the signal. * * @see Session.signal()

* @see Session events (signal and signal:type)

* @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', false); 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; }; /** * Defines the event object for the videoDisabled and videoEnabled events * dispatched by the Subscriber. * * @class VideoEnabledChangedEvent * * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable * (true) or not (false). You can cancel the default behavior by * calling the preventDefault() method of the event object in the callback * function. (See preventDefault().) * * @property {String} reason The reason the video was disabled or enabled. This can be set to one of * the following values: * *
    * *
  • "publishVideo" — The publisher started or stopped publishing video, * by calling publishVideo(true) or publishVideo(false).
  • * *
  • "quality" — The OpenTok Media Router starts or stops sending video * to the subscriber based on stream quality changes. This feature of the OpenTok Media * Router has a subscriber drop the video stream when connectivity degrades. (The subscriber * continues to receive the audio stream, if there is one.) *

    * If connectivity improves to support video again, the Subscriber object dispatches * a videoEnabled event, and the Subscriber resumes receiving video. *

    * By default, the Subscriber displays a video disabled indicator when a * videoDisabled event with this reason is dispatched and removes the indicator * when the videoDisabled event with this reason is dispatched. You can control * the display of this icon by calling the setStyle() method of the Subscriber, * setting the videoDisabledDisplayMode property(or you can set the style when * calling the Session.subscribe() method, setting the style property * of the properties parameter). *

    * This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. *

  • * *
  • "subscribeToVideo" — The subscriber started or stopped subscribing to * video, by calling subscribeToVideo(true) or subscribeToVideo(false). *
  • * *
* * @property {Object} target The object that dispatched the event. * * @property {String} type The type of event: "videoDisabled" or * "videoEnabled". * * @see Subscriber videoDisabled event

* @see Subscriber videoEnabled event

* @augments Event */ OT.VideoEnabledChangedEvent = function(type, properties) { OT.Event.call(this, type, false); this.reason = properties.reason; }; OT.VideoDisableWarningEvent = function(type/*, properties*/) { OT.Event.call(this, type, false); }; /** * Dispatched periodically by a Subscriber or Publisher object to indicate the audio * level. This event is dispatched up to 60 times per second, depending on the browser. * * @property {String} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically * for use in adjusting a user interface element, such as a volume meter. Use a moving average * to smooth the data. * * @class AudioLevelUpdatedEvent * @augments Event */ OT.AudioLevelUpdatedEvent = function(audioLevel) { OT.Event.call(this, OT.Event.names.AUDIO_LEVEL_UPDATED, false); this.audioLevel = audioLevel; }; })(window); /* jshint ignore:start */ // https://code.google.com/p/stringencoding/ // An implementation of http://encoding.spec.whatwg.org/#api /** * @license Copyright 2014 Joshua Bell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Original source: https://github.com/inexorabletash/text-encoding ***/ (function(global) { 'use strict'; var browser = OT.$.browserVersion(); if(browser.browser === 'IE' && browser.version < 10) { return; // IE 8 doesn't do websockets. No websockets, no encoding. } 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.} 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.} 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.} Code points. */ function stringToCodePoints(string) { /** @type {Array.} */ 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.} */ 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.}} */ 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.}>, * 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.} 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.} 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.|Array.>)>} */ 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.>} */ 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.>} */ 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.} 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.} 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)); /* jshint ignore:end */ !(function() { // 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(OT) { var WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000, // Magic Connectivity Timeout Constant: We wait 9*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, wsCloseErrorCodes; // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Close_codes // http://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.CloseCodes.html 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(messagingURL, notifyDisconnectAddress, NativeSocket) { var states = ['disconnected', 'error', 'connected', 'connecting', 'disconnecting'], webSocket, id, onOpen, onError, onClose, onMessage, connectCallback, 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, states, '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 = OT.$.bind(function error (errorMessage) { OT.error('Rumor.Socket: ' + errorMessage); var socketError = new OT.Rumor.SocketError(null, errorMessage || 'Unknown Socket Error'); if (connectTimeout) clearTimeout(connectTimeout); setState('error'); if (this.previousState === 'connecting' && connectCallback) { connectCallback(socketError, null); connectCallback = null; } if (onError) onError(socketError); }, this), hasLostConnectivity = function hasLostConnectivity () { if (!lastMessageTimestamp) return false; return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT; }, sendKeepAlive = OT.$.bind(function() { if (!this.is('connected')) return; if ( hasLostConnectivity() ) { webSocketDisconnected({code: 4001}); } else { webSocket.send(OT.Rumor.Message.Ping()); keepAliveTimer = setTimeout(sendKeepAlive, WEB_SOCKET_KEEP_ALIVE_INTERVAL); } }, this), // Returns true if we think the DOM has been unloaded // It detects this by looking for the OT global, which // should always exist until the DOM is cleaned up. isDOMUnloaded = function isDOMUnloaded () { return !window.OT; }; //// Private Event Handlers var webSocketConnected = OT.$.bind(function webSocketConnected () { if (connectTimeout) clearTimeout(connectTimeout); if (this.isNot('connecting')) { OT.debug('webSocketConnected reached in state other than connecting'); return; } // 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)); setState('connected'); if (connectCallback) { connectCallback(null, id); connectCallback = null; } if (onOpen) onOpen(id); keepAliveTimer = setTimeout(function() { lastMessageTimestamp = OT.$.now(); sendKeepAlive(); }, WEB_SOCKET_KEEP_ALIVE_INTERVAL); }, this), webSocketConnectTimedOut = function webSocketConnectTimedOut () { var webSocketWas = webSocket; error('Timed out while waiting for the Rumor socket to connect.'); // This will prevent a socket eventually connecting // But call it _after_ the error just in case any of // the callbacks fire synchronously, breaking the error // handling code. try { webSocketWas.close(); } catch(x) {} }, webSocketError = function webSocketError () {}, // 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 = OT.$.bind(function webSocketDisconnected (closeEvent) { if (connectTimeout) clearTimeout(connectTimeout); if (keepAliveTimer) clearTimeout(keepAliveTimer); if (isDOMUnloaded()) { // Sometimes we receive the web socket close event after // the DOM has already been partially or fully unloaded // if that's the case here then it's not really safe, or // desirable, to continue. return; } 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'); }, this), webSocketReceivedMessage = function webSocketReceivedMessage (msg) { lastMessageTimestamp = OT.$.now(); if (onMessage) { if (msg.type !== OT.Rumor.MessageType.PONG) { onMessage(msg); } } }; //// Public API this.publish = function (topics, message, headers) { webSocket.send(OT.Rumor.Message.Publish(topics, message, headers)); }; this.subscribe = function(topics) { webSocket.send(OT.Rumor.Message.Subscribe(topics)); }; this.unsubscribe = function(topics) { webSocket.send(OT.Rumor.Message.Unsubscribe(topics)); }; 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; setState('connecting'); var TheWebSocket = NativeSocket || window.WebSocket; var events = { onOpen: webSocketConnected, onClose: webSocketDisconnected, onError: webSocketError, onMessage: webSocketReceivedMessage }; try { if(typeof TheWebSocket !== 'undefined') { webSocket = new OT.Rumor.NativeSocket(TheWebSocket, messagingURL, events); } else { webSocket = new OT.Rumor.PluginSocket(messagingURL, events); } 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(drainSocketBuffer) { if (connectTimeout) clearTimeout(connectTimeout); if (keepAliveTimer) clearTimeout(keepAliveTimer); if (!webSocket) { if (this.isNot('error')) setState('disconnected'); return; } if (webSocket.isClosed()) { 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()); } // Wait until the socket is ready to close webSocket.close(drainSocketBuffer); } }; OT.$.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; }(window.OT, this)); !(function() { var BUFFER_DRAIN_INTERVAL = 100, // The total number of times to retest the websocket's send buffer BUFFER_DRAIN_MAX_RETRIES = 10; OT.Rumor.NativeSocket = function(TheWebSocket, messagingURL, events) { var webSocket, disconnectWhenSendBufferIsDrained, bufferDrainTimeout, // Timer to poll whether th send buffer has been drained close; webSocket = new TheWebSocket(messagingURL); webSocket.binaryType = 'arraybuffer'; webSocket.onopen = events.onOpen; webSocket.onclose = events.onClose; webSocket.onerror = events.onError; webSocket.onmessage = function(message) { if (!OT) { // In IE 10/11, This can apparently be called after // the page is unloaded and OT is garbage-collected return; } var msg = OT.Rumor.Message.deserialize(message.data); events.onMessage(msg); }; // 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(); } }; close = function close() { webSocket.close(); }; this.close = function(drainBuffer) { if (drainBuffer) { disconnectWhenSendBufferIsDrained(); } else { close(); } }; this.send = function(msg) { webSocket.send(msg.serialize()); }; this.isClosed = function() { return webSocket.readyState === 3; }; }; }(this)); !(function() { OT.Rumor.PluginSocket = function(messagingURL, events) { var webSocket, state = 'initializing'; TBPlugin.initRumorSocket(messagingURL, OT.$.bind(function(err, rumorSocket) { if(err) { state = 'closed'; events.onClose({ code: 4999 }); } else if(state === 'initializing') { webSocket = rumorSocket; webSocket.onOpen(function() { state = 'open'; events.onOpen(); }); webSocket.onClose(function(error) { state = 'closed'; /* CLOSED */ events.onClose({ code: error }); }); webSocket.onError(function(error) { state = 'closed'; /* CLOSED */ events.onError(error); /* native websockets seem to do this, so should we */ events.onClose({ code: error }); }); webSocket.onMessage(function(type, addresses, headers, payload) { var msg = new OT.Rumor.Message(type, addresses, headers, payload); events.onMessage(msg); }); webSocket.open(); } else { this.close(); } }, this)); this.close = function() { if(state === 'initializing' || state === 'closed') { state = 'closed'; return; } webSocket.close(1000, ''); }; this.send = function(msg) { if(state === 'open') { webSocket.send(msg); } }; this.isClosed = function() { return state === 'closed'; }; }; }(this)); !(function() { /*global TextEncoder, TextDecoder */ // // // @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; this.transactionId = this.headers['TRANSACTION-ID']; this.status = this.headers.STATUS; this.isError = !(this.status && this.status[0] === '2'); }; OT.Rumor.Message.prototype.serialize = function () { var offset = 8, cBuf = 7, address = [], headerKey = [], headerVal = [], strArray, dataView, i, j; // The number of addresses cBuf++; // Write out the address. for (i = 0; i < this.toAddress.length; i++) { /*jshint newcap:false */ address.push(new TextEncoder('utf-8').encode(this.toAddress[i])); cBuf += 2; cBuf += address[i].length; } // The number of parameters cBuf++; // Write out the params i = 0; for (var key in this.headers) { if(!this.headers.hasOwnProperty(key)) { continue; } headerKey.push(new TextEncoder('utf-8').encode(key)); headerVal.push(new TextEncoder('utf-8').encode(this.headers[key])); cBuf += 4; cBuf += headerKey[i].length; cBuf += headerVal[i].length; i++; } 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 (i = 0; i < address.length; i++) { strArray = address[i]; uint8View[offset++] = strArray.length >> 8 & 0xFF; uint8View[offset++] = strArray.length >> 0 & 0xFF; for (j = 0; j < strArray.length; j++) { uint8View[offset++] = strArray[j]; } } uint8View[offset++] = headerKey.length; // Write out the params for (i = 0; i < headerKey.length; i++) { strArray = headerKey[i]; uint8View[offset++] = strArray.length >> 8 & 0xFF; uint8View[offset++] = strArray.length >> 0 & 0xFF; for (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 (j = 0; j < strArray.length; j++) { uint8View[offset++] = strArray[j]; } } // And finally the data for (i = 0; i < dataView.length; i++) { uint8View[offset++] = dataView[i]; } return buffer; }; function toArrayBuffer(buffer) { var ab = new ArrayBuffer(buffer.length); var view = new Uint8Array(ab); for (var i = 0; i < buffer.length; ++i) { view[i] = buffer[i]; } return ab; } OT.Rumor.Message.deserialize = function (buffer) { if(typeof Buffer !== 'undefined' && Buffer.isBuffer(buffer)) { buffer = toArrayBuffer(buffer); } var cBuf = 0, type, offset = 8, uint8View = new Uint8Array(buffer), strView, headerlen, headers, keyStr, valStr, length, i; // 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 (i = 0; i < uint8View[7]; i++) { length = uint8View[offset++] << 8; length += uint8View[offset++]; strView = new Uint8Array(buffer, offset, length); /*jshint newcap:false */ address[i] = new TextDecoder('utf-8').decode(strView); offset += length; } headerlen = uint8View[offset++]; headers = {}; for (i = 0; i < headerlen; i++) { length = uint8View[offset++] << 8; length += uint8View[offset++]; strView = new Uint8Array(buffer, offset, length); keyStr = new TextDecoder('utf-8').decode(strView); offset += length; length = uint8View[offset++] << 8; length += uint8View[offset++]; strView = new Uint8Array(buffer, offset, length); valStr = new TextDecoder('utf-8').decode(strView); headers[keyStr] = 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 = { uniqueId: uniqueId, notifyDisconnectAddress: 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, headers||{}, 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() { // 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 // 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() { 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) { if (msg.length === 0) return {}; var message = JSON.parse(msg), bits = message.uri.substr(1).split('/'); // Remove the Raptor protocol version bits.shift(); if (bits[bits.length-1] === '') bits.pop(); message.params = {}; for (var i=0, numBits=bits.length ; i 6) { message.resource = bits[bits.length-4] + '_' + bits[bits.length-2]; } else { message.resource = bits[bits.length-2]; } } else { if (bits[bits.length-1] === 'channel' && bits.length > 5) { message.resource = bits[bits.length-3] + '_' + bits[bits.length-1]; } else { message.resource = bits[bits.length-1]; } } message.signature = message.resource + '#' + message.method; return message; }; OT.Raptor.unboxFromRumorMessage = function (rumorMessage) { var message = OT.Raptor.deserializeMessage(rumorMessage.data); message.transactionId = rumorMessage.transactionId; message.fromAddress = rumorMessage.headers['X-TB-FROM-ADDRESS']; return message; }; OT.Raptor.parseIceServers = function (message) { try { return JSON.parse(message.data).content.iceServers; } catch (e) { return []; } }; OT.Raptor.Message = {}; OT.Raptor.Message.connections = {}; OT.Raptor.Message.connections.create = function (apiKey, sessionId, connectionId) { return OT.Raptor.serializeMessage({ method: 'create', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId, content: { userAgent: OT.$.userAgent() } }); }; OT.Raptor.Message.connections.destroy = function (apiKey, sessionId, connectionId) { return OT.Raptor.serializeMessage({ method: 'delete', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId, content: {} }); }; OT.Raptor.Message.sessions = {}; OT.Raptor.Message.sessions.get = function (apiKey, sessionId) { return OT.Raptor.serializeMessage({ method: 'read', uri: '/v2/partner/' + apiKey + '/session/' + sessionId, content: {} }); }; OT.Raptor.Message.streams = {}; OT.Raptor.Message.streams.get = function (apiKey, sessionId, streamId) { return OT.Raptor.serializeMessage({ method: 'read', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, content: {} }); }; OT.Raptor.Message.streams.create = function (apiKey, sessionId, streamId, name, videoOrientation, videoWidth, videoHeight, hasAudio, hasVideo, frameRate, minBitrate, maxBitrate) { var channels = []; if (hasAudio !== void 0) { channels.push({ id: 'audio1', type: 'audio', active: hasAudio }); } if (hasVideo !== void 0) { var channel = { id: 'video1', type: 'video', active: hasVideo, width: videoWidth, height: videoHeight, orientation: videoOrientation }; if (frameRate) channel.frameRate = frameRate; channels.push(channel); } var messageContent = { id: streamId, name: name, channel: channels }; if (minBitrate) messageContent.minBitrate = minBitrate; if (maxBitrate) messageContent.maxBitrate = maxBitrate; return OT.Raptor.serializeMessage({ method: 'create', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, content: messageContent }); }; OT.Raptor.Message.streams.destroy = function (apiKey, sessionId, streamId) { return OT.Raptor.serializeMessage({ method: 'delete', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, content: {} }); }; OT.Raptor.Message.streams.offer = function (apiKey, sessionId, streamId, offerSdp) { return OT.Raptor.serializeMessage({ method: 'offer', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, content: { sdp: offerSdp } }); }; OT.Raptor.Message.streams.answer = function (apiKey, sessionId, streamId, answerSdp) { return OT.Raptor.serializeMessage({ method: 'answer', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, content: { sdp: answerSdp } }); }; OT.Raptor.Message.streams.candidate = function (apiKey, sessionId, streamId, candidate) { return OT.Raptor.serializeMessage({ method: 'candidate', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, content: candidate }); }; OT.Raptor.Message.streamChannels = {}; OT.Raptor.Message.streamChannels.update = function (apiKey, sessionId, streamId, channelId, attributes) { return OT.Raptor.serializeMessage({ method: 'update', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/channel/' + channelId, content: attributes }); }; OT.Raptor.Message.subscribers = {}; OT.Raptor.Message.subscribers.create = function (apiKey, sessionId, streamId, subscriberId, connectionId, channelsToSubscribeTo) { var content = { id: subscriberId, connection: connectionId, keyManagementMethod: OT.$.supportedCryptoScheme(), bundleSupport: OT.$.hasCapabilities('bundle'), rtcpMuxSupport: OT.$.hasCapabilities('RTCPMux') }; if (channelsToSubscribeTo) content.channel = channelsToSubscribeTo; return OT.Raptor.serializeMessage({ method: 'create', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId, content: content }); }; OT.Raptor.Message.subscribers.destroy = function (apiKey, sessionId, streamId, subscriberId) { return OT.Raptor.serializeMessage({ method: 'delete', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId, content: {} }); }; OT.Raptor.Message.subscribers.update = function (apiKey, sessionId, streamId, subscriberId, attributes) { return OT.Raptor.serializeMessage({ method: 'update', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId, content: attributes }); }; OT.Raptor.Message.subscribers.candidate = function (apiKey, sessionId, streamId, subscriberId, candidate) { return OT.Raptor.serializeMessage({ method: 'candidate', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId, content: candidate }); }; OT.Raptor.Message.subscribers.offer = function (apiKey, sessionId, streamId, subscriberId, offerSdp) { return OT.Raptor.serializeMessage({ method: 'offer', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId, content: { sdp: offerSdp } }); }; OT.Raptor.Message.subscribers.answer = function (apiKey, sessionId, streamId, subscriberId, answerSdp) { return OT.Raptor.serializeMessage({ method: 'answer', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId, content: { sdp: answerSdp } }); }; OT.Raptor.Message.subscriberChannels = {}; OT.Raptor.Message.subscriberChannels.update = function (apiKey, sessionId, streamId, subscriberId, channelId, attributes) { return OT.Raptor.serializeMessage({ method: 'update', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId + '/subscriber/' + subscriberId + '/channel/' + channelId, content: attributes }); }; OT.Raptor.Message.signals = {}; OT.Raptor.Message.signals.create = function (apiKey, sessionId, toAddress, type, data) { var content = {}; if (type !== void 0) content.type = type; if (data !== void 0) content.data = data; return OT.Raptor.serializeMessage({ method: 'signal', uri: '/v2/partner/' + apiKey + '/session/' + sessionId + (toAddress !== void 0 ? '/connection/' + toAddress : '') + '/signal/' + OT.$.uuid(), content: content }); }; }(this)); !(function() { var MAX_SIGNAL_DATA_LENGTH = 8192, 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' }; } if ( !(toAddress instanceof OT.Connection || toAddress 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 = this.to; if (to && typeof(to) !== 'string') { to = to.id; } return OT.Raptor.Message.signals.create(OT.APIKEY, sessionId, to, this.type, this.data); }; this.toHash = function() { return options; }; this.error = null; if (options) { if (options.hasOwnProperty('data')) { this.data = OT.$.clone(options.data); this.error = validateData(this.data); } if (options.hasOwnProperty('to')) { this.to = 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() { function SignalError(code, reason) { this.code = code; this.reason = reason; } // The Dispatcher bit is purely to make testing simpler, it defaults to a new OT.Raptor.Dispatcher // so in normal operation you would omit it. OT.Raptor.Socket = function(widgetId, messagingSocketUrl, symphonyUrl, dispatcher) { var _states = ['disconnected', 'connecting', 'connected', 'error', 'disconnecting'], _sessionId, _token, _rumor, _dispatcher, _completion; //// Private API var setState = OT.$.statable(this, _states, 'disconnected'), onConnectComplete = function onConnectComplete(error) { if (error) { setState('error'); } else { setState('connected'); } _completion.apply(null, arguments); }, onClose = OT.$.bind(function onClose (err) { var reason = this.is('disconnecting') ? 'clientDisconnected' : 'networkDisconnected'; if(err && err.code === 4001) { reason = 'networkTimedout'; } setState('disconnected'); _dispatcher.onClose(reason); }, this), onError = function onError () {}; // @todo what does having an error mean? Are they always fatal? Are we disconnected now? //// Public API 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; _token = token; _completion = completion; var connectionId = OT.$.uuid(), rumorChannel = '/v2/partner/' + OT.APIKEY + '/session/' + _sessionId; _rumor = new OT.Rumor.Socket(messagingSocketUrl, symphonyUrl); _rumor.onClose(onClose); _rumor.onMessage(OT.$.bind(_dispatcher.dispatch, _dispatcher)); _rumor.connect(connectionId, OT.$.bind(function(error) { if (error) { error.message = 'WebSocketConnection:' + error.code + ':' + error.message; onConnectComplete(error); return; } // we do this here to avoid getting connect errors twice _rumor.onError(onError); OT.debug('Raptor Socket connected. Subscribing to ' + rumorChannel + ' on ' + messagingSocketUrl); _rumor.subscribe([rumorChannel]); //connect to session var connectMessage = OT.Raptor.Message.connections.create(OT.APIKEY, _sessionId, _rumor.id()); this.publish(connectMessage, {'X-TB-TOKEN-AUTH': _token}, OT.$.bind(function(error) { if (error) { error.message = 'ConnectToSession:' + error.code + ':Received error response to connection create message.'; onConnectComplete(error); return; } this.publish( OT.Raptor.Message.sessions.get(OT.APIKEY, _sessionId), function (error) { if (error) { error.message = 'GetSessionState:' + error.code + ':Received error response to session read'; } onConnectComplete.apply(null, arguments); }); }, this)); }, this)); }; this.disconnect = function (drainSocketBuffer) { if (this.is('disconnected')) return; setState('disconnecting'); _rumor.disconnect(drainSocketBuffer); }; // Publishs +message+ to the Symphony app server. // // The completion handler is optional, as is the headers // dict, but if you provide the completion handler it must // be the last argument. // this.publish = function (message, headers, completion) { if (_rumor.isNot('connected')) { OT.error('OT.Raptor.Socket: cannot publish until the socket is connected.' + message); return; } var transactionId = OT.$.uuid(), _headers = {}, _completion; // Work out if which of the optional arguments (headers, completion) // have been provided. if (headers) { if (OT.$.isFunction(headers)) { _headers = {}; _completion = headers; } else { _headers = headers; } } if (!_completion && completion && OT.$.isFunction(completion)) _completion = completion; if (_completion) _dispatcher.registerCallback(transactionId, _completion); OT.debug('OT.Raptor.Socket Publish (ID:' + transactionId + ') '); OT.debug(message); _rumor.publish([symphonyUrl], message, OT.$.extend(_headers, { 'Content-Type': 'application/x-raptor+v2', 'TRANSACTION-ID': transactionId, 'X-TB-FROM-ADDRESS': _rumor.id() })); return transactionId; }; // Register a new stream against _sessionId this.streamCreate = function(name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo, frameRate, minBitrate, maxBitrate, completion) { var streamId = OT.$.uuid(), message = OT.Raptor.Message.streams.create( OT.APIKEY, _sessionId, streamId, name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo, frameRate, minBitrate, maxBitrate); this.publish(message, function(error, message) { completion(error, streamId, message); }); }; this.streamDestroy = function(streamId) { this.publish( OT.Raptor.Message.streams.destroy(OT.APIKEY, _sessionId, streamId) ); }; this.streamChannelUpdate = function(streamId, channelId, attributes) { this.publish( OT.Raptor.Message.streamChannels.update(OT.APIKEY, _sessionId, streamId, channelId, attributes) ); }; this.subscriberCreate = function(streamId, subscriberId, channelsToSubscribeTo, completion) { this.publish( OT.Raptor.Message.subscribers.create(OT.APIKEY, _sessionId, streamId, subscriberId, _rumor.id(), channelsToSubscribeTo), completion ); }; this.subscriberDestroy = function(streamId, subscriberId) { this.publish( OT.Raptor.Message.subscribers.destroy(OT.APIKEY, _sessionId, streamId, subscriberId) ); }; this.subscriberUpdate = function(streamId, subscriberId, attributes) { this.publish( OT.Raptor.Message.subscribers.update(OT.APIKEY, _sessionId, streamId, subscriberId, attributes) ); }; this.subscriberChannelUpdate = function(streamId, subscriberId, channelId, attributes) { this.publish( OT.Raptor.Message.subscriberChannels.update(OT.APIKEY, _sessionId, streamId, subscriberId, channelId, attributes) ); }; this.forceDisconnect = function(connectionIdToDisconnect, completion) { this.publish( OT.Raptor.Message.connections.destroy(OT.APIKEY, _sessionId, connectionIdToDisconnect), completion ); }; this.forceUnpublish = function(streamIdToUnpublish, completion) { this.publish( OT.Raptor.Message.streams.destroy(OT.APIKEY, _sessionId, streamIdToUnpublish), completion ); }; this.jsepCandidate = function(streamId, candidate) { this.publish( OT.Raptor.Message.streams.candidate(OT.APIKEY, _sessionId, streamId, candidate) ); }; this.jsepCandidateP2p = function(streamId, subscriberId, candidate) { this.publish( OT.Raptor.Message.subscribers.candidate(OT.APIKEY, _sessionId, streamId, subscriberId, candidate) ); }; this.jsepOffer = function(streamId, offerSdp) { this.publish( OT.Raptor.Message.streams.offer(OT.APIKEY, _sessionId, streamId, offerSdp) ); }; this.jsepOfferP2p = function(streamId, subscriberId, offerSdp) { this.publish( OT.Raptor.Message.subscribers.offer(OT.APIKEY, _sessionId, streamId, subscriberId, offerSdp) ); }; this.jsepAnswer = function(streamId, answerSdp) { this.publish( OT.Raptor.Message.streams.answer(OT.APIKEY, _sessionId, streamId, answerSdp) ); }; this.jsepAnswerP2p = function(streamId, subscriberId, answerSdp) { this.publish( OT.Raptor.Message.subscribers.answer(OT.APIKEY, _sessionId, streamId, subscriberId, 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(), function(err) { var error; if (err) error = new SignalError(err.code, err.message); if (completion && OT.$.isFunction(completion)) completion(error, signal.toHash()); }); }; this.id = function() { return _rumor && _rumor.id(); }; if(dispatcher == null) { dispatcher = new OT.Raptor.Dispatcher(); } _dispatcher = dispatcher; }; }(this)); !(function() { /*global EventEmitter, util*/ // Connect error codes and reasons that Raptor can return. var connectErrorReasons; connectErrorReasons = { 409: 'This P2P session already has 2 participants.', 410: 'The session already has four participants.', 1004: 'The token passed is invalid.' }; OT.Raptor.Dispatcher = function () { if(OT.isNodeModule) { EventEmitter.call(this); } else { OT.$.eventing(this, true); this.emit = this.trigger; } this.callbacks = {}; }; if(OT.isNodeModule) { util.inherits(OT.Raptor.Dispatcher, EventEmitter); } OT.Raptor.Dispatcher.prototype.registerCallback = function (transactionId, completion) { this.callbacks[transactionId] = completion; }; OT.Raptor.Dispatcher.prototype.triggerCallback = function (transactionId) { /*, arg1, arg2, argN-1, argN*/ if (!transactionId) return; var completion = this.callbacks[transactionId]; if (completion && OT.$.isFunction(completion)) { var args = Array.prototype.slice.call(arguments); args.shift(); completion.apply(null, args); } delete this.callbacks[transactionId]; }; OT.Raptor.Dispatcher.prototype.onClose = function(reason) { this.emit('close', reason); }; OT.Raptor.Dispatcher.prototype.dispatch = function(rumorMessage) { // The special casing of STATUS messages is ugly. Need to think about // how to better integrate this. if (rumorMessage.type === OT.Rumor.MessageType.STATUS) { OT.debug('OT.Raptor.dispatch: STATUS'); OT.debug(rumorMessage); var error; if (rumorMessage.isError) { error = new OT.Error(rumorMessage.status); } this.triggerCallback(rumorMessage.transactionId, error, rumorMessage); return; } var message = OT.Raptor.unboxFromRumorMessage(rumorMessage); OT.debug('OT.Raptor.dispatch ' + message.signature); OT.debug(rumorMessage.data); switch(message.resource) { case 'session': this.dispatchSession(message); break; case 'connection': this.dispatchConnection(message); break; case 'stream': this.dispatchStream(message); break; case 'stream_channel': this.dispatchStreamChannel(message); break; case 'subscriber': this.dispatchSubscriber(message); break; case 'subscriber_channel': this.dispatchSubscriberChannel(message); break; case 'signal': this.dispatchSignal(message); break; case 'archive': this.dispatchArchive(message); break; default: OT.warn('OT.Raptor.dispatch: Type ' + message.resource + ' is not currently implemented'); } }; OT.Raptor.Dispatcher.prototype.dispatchSession = function (message) { switch (message.method) { case 'read': this.emit('session#read', message.content, message.transactionId); break; default: OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); } }; OT.Raptor.Dispatcher.prototype.dispatchConnection = function (message) { switch (message.method) { case 'created': this.emit('connection#created', message.content); break; case 'deleted': this.emit('connection#deleted', message.params.connection, message.reason); break; default: OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); } }; OT.Raptor.Dispatcher.prototype.dispatchStream = function (message) { switch (message.method) { case 'created': this.emit('stream#created', message.content, message.transactionId); break; case 'deleted': this.emit('stream#deleted', message.params.stream, message.reason); break; case 'updated': this.emit('stream#updated', message.params.stream, message.content); break; // The JSEP process case 'generateoffer': case 'answer': case 'pranswer': case 'offer': case 'candidate': this.dispatchJsep(message.method, message); break; default: OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); } }; OT.Raptor.Dispatcher.prototype.dispatchStreamChannel = function (message) { switch (message.method) { case 'updated': this.emit('streamChannel#updated', message.params.stream, message.params.channel, message.content); break; default: OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); } }; // Dispatch JSEP messages // // generateoffer: // Request to generate a offer for another Peer (or Prism). This kicks // off the JSEP process. // // answer: // generate a response to another peers offer, this contains our constraints // and requirements. // // pranswer: // a provisional answer, i.e. not the final one. // // candidate // // OT.Raptor.Dispatcher.prototype.dispatchJsep = function (method, message) { this.emit('jsep#' + method, message.params.stream, message.fromAddress, message); }; OT.Raptor.Dispatcher.prototype.dispatchSubscriberChannel = function (message) { switch (message.method) { case 'updated': this.emit('subscriberChannel#updated', message.params.stream, message.params.channel, message.content); break; case 'update': // subscriberId, streamId, content this.emit('subscriberChannel#update', message.params.subscriber, message.params.stream, message.content); break; default: OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); } }; OT.Raptor.Dispatcher.prototype.dispatchSubscriber = function (message) { switch (message.method) { case 'created': this.emit('subscriber#created', message.params.stream, message.fromAddress, message.content.id); break; case 'deleted': this.dispatchJsep('unsubscribe', message); this.emit('subscriber#deleted', message.params.stream, message.fromAddress); break; // The JSEP process case 'generateoffer': case 'answer': case 'pranswer': case 'offer': case 'candidate': this.dispatchJsep(message.method, message); break; default: OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); } }; OT.Raptor.Dispatcher.prototype.dispatchSignal = function (message) { if (message.method !== 'signal') { OT.warn('OT.Raptor.dispatch: ' + message.signature + ' is not currently implemented'); return; } this.emit('signal', message.fromAddress, message.content.type, message.content.data); }; OT.Raptor.Dispatcher.prototype.dispatchArchive = function (message) { switch (message.method) { case 'created': this.emit('archive#created', message.content); break; case 'updated': this.emit('archive#updated', message.params.archive, message.content); break; } }; }(this)); (function(window) { // @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 channel = dict.channel.map(function(channel) { return new OT.StreamChannel(channel); }); var connectionId = dict.connectionId ? dict.connectionId : dict.connection.id; return new OT.Stream( dict.id, dict.name, dict.creationTime, session.connections.get(connectionId), session, channel ); } function parseAndAddStreamToSession(dict, session) { if (session.streams.has(dict.id)) return; var stream = parseStream(dict, session); session.streams.add( stream ); return stream; } function parseArchive(dict) { return new OT.Archive( dict.id, dict.name, dict.status ); } function parseAndAddArchiveToSession(dict, session) { if (session.archives.has(dict.id)) return; var archive = parseArchive(dict); session.archives.add(archive); return archive; } var sessionRead; var sessionReadQueue = []; function sessionReadQueuePush(type, args) { var triggerArgs = ['signal']; triggerArgs.push.apply(triggerArgs, args); sessionReadQueue.push(triggerArgs); } window.OT.SessionDispatcher = function(session) { var dispatcher = new OT.Raptor.Dispatcher(); dispatcher.on('close', function(reason) { var connection = session.connection; if (!connection) { return; } if (connection.destroyedReason()) { OT.debug('OT.Raptor.Socket: Socket was closed but the connection had already ' + 'been destroyed. Reason: ' + connection.destroyedReason()); return; } connection.destroy( reason ); }); dispatcher.on('session#read', function(content, transactionId) { var state = {}, connection; state.streams = []; state.connections = []; state.archives = []; OT.$.forEach(content.connection, function(connectionParams) { connection = OT.Connection.fromHash(connectionParams); state.connections.push(connection); session.connections.add(connection); }); OT.$.forEach(content.stream, function(streamParams) { state.streams.push( parseAndAddStreamToSession(streamParams, session) ); }); OT.$.forEach(content.archive || content.archives, function(archiveParams) { state.archives.push( parseAndAddArchiveToSession(archiveParams, session) ); }); session._.subscriberMap = {}; dispatcher.triggerCallback(transactionId, null, state); sessionRead = true; for (var i = 0; i < sessionReadQueue.length; ++i) { dispatcher.trigger.apply(dispatcher, sessionReadQueue[i]); } sessionReadQueue = []; }); dispatcher.on('connection#created', function(connection) { connection = OT.Connection.fromHash(connection); if (session.connection && connection.id !== session.connection.id) { session.connections.add( connection ); } }); dispatcher.on('connection#deleted', function(connection, reason) { connection = session.connections.get(connection); connection.destroy(reason); }); dispatcher.on('stream#created', function(stream, transactionId) { stream = parseAndAddStreamToSession(stream, session); if (stream.publisher) { stream.publisher.setStream(stream); } dispatcher.triggerCallback(transactionId, null, stream); }); dispatcher.on('stream#deleted', function(streamId, reason) { var stream = session.streams.get(streamId); if (!stream) { OT.error('OT.Raptor.dispatch: A stream does not exist with the id of ' + streamId + ', for stream#deleted message!'); // @todo error return; } stream.destroy(reason); }); dispatcher.on('stream#updated', function(streamId, content) { var stream = session.streams.get(streamId); if (!stream) { OT.error('OT.Raptor.dispatch: A stream does not exist with the id of ' + streamId + ', for stream#updated message!'); // @todo error return; } stream._.update(content); }); dispatcher.on('streamChannel#updated', function(streamId, channelId, content) { var stream; if (!(streamId && (stream = session.streams.get(streamId)))) { OT.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' + 'exist, for streamChannel message!'); // @todo error return; } stream._.updateChannel(channelId, content); }); // Dispatch JSEP messages // // generateoffer: // Request to generate a offer for another Peer (or Prism). This kicks // off the JSEP process. // // answer: // generate a response to another peers offer, this contains our constraints // and requirements. // // pranswer: // a provisional answer, i.e. not the final one. // // candidate // // var jsepHandler = function(method, streamId, fromAddress, message) { var fromConnection, actors; switch (method) { // Messages for Subscribers case 'offer': actors = []; var subscriber = OT.subscribers.find({streamId: streamId}); if (subscriber) actors.push(subscriber); break; // Messages for Publishers case 'answer': case 'pranswer': case 'generateoffer': case 'unsubscribe': actors = OT.publishers.where({streamId: streamId}); break; // Messages for Publishers and Subscribers case '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: jsep#' + method + ' is not currently implemented'); return; } if (actors.length === 0) return; // 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(fromAddress); if(!fromConnection && fromAddress.match(/^symphony\./)) { fromConnection = OT.Connection.fromHash({ id: fromAddress, creationTime: Math.floor(OT.$.now()) }); actors[0].session.connections.add(fromConnection); } else if(!fromConnection) { OT.warn('OT.Raptor.dispatch: Messsage comes from a connection (' + fromAddress + ') that we do not know about. The message was ignored.'); return; } OT.$.forEach(actors, function(actor) { actor.processMessage(method, fromConnection, message); }); }; dispatcher.on('jsep#offer', OT.$.bind(jsepHandler, null, 'offer')); dispatcher.on('jsep#answer', OT.$.bind(jsepHandler, null, 'answer')); dispatcher.on('jsep#pranswer', OT.$.bind(jsepHandler, null, 'pranswer')); dispatcher.on('jsep#generateoffer', OT.$.bind(jsepHandler, null, 'generateoffer')); dispatcher.on('jsep#unsubscribe', OT.$.bind(jsepHandler, null, 'unsubscribe')); dispatcher.on('jsep#candidate', OT.$.bind(jsepHandler, null, 'candidate')); dispatcher.on('subscriberChannel#updated', function(streamId, channelId, content) { if (!streamId || !session.streams.has(streamId)) { OT.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' + 'exist, for subscriberChannel#updated message!'); // @todo error return; } session.streams.get(streamId)._ .updateChannel(channelId, content); }); dispatcher.on('subscriberChannel#update', function(subscriberId, streamId, content) { if (!streamId || !session.streams.has(streamId)) { OT.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does not ' + 'exist, for subscriberChannel#update message!'); // @todo error return; } // Hint to update for congestion control from the Media Server if (!OT.subscribers.has(subscriberId)) { OT.error('OT.Raptor.dispatch: Unable to determine subscriberId, or the subscriber ' + 'does not exist, for subscriberChannel#update message!'); // @todo error return; } // We assume that an update on a Subscriber channel is to disableVideo // we may need to be more specific in the future OT.subscribers.get(subscriberId).disableVideo(content.active); }); dispatcher.on('subscriber#created', function(streamId, fromAddress, subscriberId) { var stream = streamId ? session.streams.get(streamId) : null; if (!stream) { OT.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does ' + 'not exist, for subscriber#created message!'); // @todo error return; } session._.subscriberMap[fromAddress + '_' + stream.id] = subscriberId; }); dispatcher.on('subscriber#deleted', function(streamId, fromAddress) { var stream = streamId ? session.streams.get(streamId) : null; if (!stream) { OT.error('OT.Raptor.dispatch: Unable to determine streamId, or the stream does ' + 'not exist, for subscriber#created message!'); // @todo error return; } delete session._.subscriberMap[fromAddress + '_' + stream.id]; }); dispatcher.on('signal', function(fromAddress, signalType, data) { if (sessionRead) { var fromConnection = session.connections.get(fromAddress); session._.dispatchSignal(fromConnection, signalType, data); } else { if (!sessionRead) { sessionReadQueuePush('signal', arguments); } } }); dispatcher.on('archive#created', function(archive) { parseAndAddArchiveToSession(archive, session); }); dispatcher.on('archive#updated', function(archiveId, update) { var archive = session.archives.get(archiveId); if (!archive) { OT.error('OT.Raptor.dispatch: An archive does not exist with the id of ' + archiveId + ', for archive#updated message!'); // @todo error return; } archive._.update(update); }); return dispatcher; }; })(window); !(function() { // Helper to synchronise several startup tasks and then dispatch a unified // 'envLoaded' event. // // This depends on: // * OT // * OT.Config // function EnvironmentLoader() { var _configReady = false, // If the plugin is installed, then we should wait for it to // be ready as well. _pluginSupported = TBPlugin.isSupported(), _pluginLoadAttemptComplete = _pluginSupported ? TBPlugin.isReady() : true, isReady = function() { return !OT.$.isDOMUnloaded() && OT.$.isReady() && _configReady && _pluginLoadAttemptComplete; }, onLoaded = function() { if (isReady()) { OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_LOADED)); } }, onDomReady = function() { OT.$.onDOMUnload(onDomUnload); // The Dynamic Config won't load until the DOM is ready OT.Config.load(OT.properties.configURL); onLoaded(); }, onDomUnload = function() { // Disconnect the session first, this will prevent the plugin // from locking up during browser unload. // if (_pluginSupported) { // var sessions = OT.sessions.where(); // for (var i=0; icompletionHandler parameter: * * * *

* The completionHandler parameter is a function that is called when the call to * the asynchronous method succeeds or fails. If the asynchronous call fails, the completion * handler function is passes an error object (defined by the Error class). The code * and message properties of the error object provide details about the error. * * @property {Number} code The error code, defining the error. * *

* In the event of an error, the code value of the error parameter can * have one of the following values: *

* *

Errors when calling Session.connect():

* * * * * * * * * * * * * * * * * * *
codeDescription
1004Authentication error. Check the error message for details. This error can result if you * in an expired token when trying to connect to a session. It can also occur if you pass * in an invalid token or API key. Make sure that you are generating the token using the * current version of one of the * OpenTok server SDKs.
1005Invalid Session ID. Make sure you generate the session ID using the current version of * one of the OpenTok server * SDKs.
1006Connect Failed. Unable to connect to the session. You may want to have the client check * the network connection.
* *

Errors when calling Session.forceDisconnect():

* * * * * * * * * * * * * * *
* code * Description
1010The client is not connected to the OpenTok session. Check that client connects * successfully and has not disconnected before calling forceDisconnect().
1520Unable to force disconnect. The client's token does not have the role set to moderator. * Once the client has connected to the session, the capabilities property of * the Session object lists the client's capabilities.
* *

Errors when calling Session.forceUnpublish():

* * * * * * * * * * * * * * * * * * *
codeDescription
1010The client is not connected to the OpenTok session. Check that client connects * successfully and has not disconnected before calling forceUnpublish().
1530Unable to force unpublish. The client's token does not have the role set to moderator. * Once the client has connected to the session, the capabilities property of * the Session object lists the client's capabilities.
1535Force Unpublish on an invalid stream. Make sure that the stream has not left the * session before you call the forceUnpublish() method.
* *

Errors when calling Session.publish():

* * * * * * * * * * * * * * * * * * *
codeDescription
1010The client is not connected to the OpenTok session. Check that the client connects * successfully before trying to publish. And check that the client has not disconnected * before trying to publish.
1500Unable to Publish. The client's token does not have the role set to to publish or * moderator. Once the client has connected to the session, the capabilities * property of the Session object lists the client's capabilities.
1601Internal error -- WebRTC publisher error. Try republishing or reconnecting to the * session.
* *

Errors when calling Session.signal():

* * * * * * * * * * * * * * * * * * * * *
codeDescription
400One of the signal properties — data, type, or to — * is invalid. Or the data cannot be parsed as JSON.
404 The to connection does not exist.
413 The type string exceeds the maximum length (128 bytes), * or the data string exceeds the maximum size (8 kB).
500The client is not connected to the OpenTok session. Check that the client connects * successfully before trying to signal. And check that the client has not disconnected before * trying to publish.
* *

Errors when calling Session.subscribe():

* * * * * * * * * * * * * *
* Errors when calling Session.subscribe(): *
* code * Description
1600Internal error -- WebRTC subscriber error. Try resubscribing to the stream or * reconnecting to the session.
* *

Errors when calling TB.initPublisher():

* * * * * * * * * * *
codeDescription
1004Authentication error. Check the error message for details. This error can result if you * pass in an expired token when trying to connect to a session. It can also occur if you * pass in an invalid token or API key. Make sure that you are generating the token using * the current version of one of the * OpenTok server SDKs.
* *

General errors that can occur when calling any method:

* * * * * * * * * * * * * * *
codeDescription
1011Invalid Parameter. Check that you have passed valid parameter values into the method * call.
2000Internal Error. Try reconnecting to the OpenTok session and trying the action again.
* * @property {String} message The message string provides details about the error. * * @class Error * @augments Event */ OT.Error = function(code, message) { this.code = code; this.message = message; }; var errorsCodesToTitle = { 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', 1520: 'Unable to Force Disconnect', 1530: 'Unable to Force Unpublish', 2000: 'Internal Error', 2001: 'Embed Failed', 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('OT.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('OT.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 // // OT.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.isConnected()) 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); }; // This is a placeholder until error handling can be rewritten OT.dispatchError = function (code, message, completionHandler, session) { OT.error(code, message); if (completionHandler && OT.$.isFunction(completionHandler)) { completionHandler.call(null, new OT.Error(code, message)); } OT.handleJsException(message, code, { session: session }); }; })(window); !(function() { 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() { /** * The Connection object represents a connection to an OpenTok session. Each client that connects * to a session has a unique connection, with a unique connection ID (represented by the * id property of the Connection object for the client). *

* The Session object has a connection 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 connect() method of the {@link Session} * object.) *

* The Session object dispatches a connectionCreated event when each client * (including your own) connects to a session (and for clients that are present in the * session when you connect). The connectionCreated event object has a * connection property, which is a Connection object corresponding to the client * the event pertains to. *

* The Stream object has a connection property that is a Connection object. * It represents the connection of the client that is publishing the stream. * * @class Connection * @property {String} connectionId 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 new Date(creationTime), * where creationTime * is the creationTime 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 * generate_token() method of our * server-side libraries. You can also generate a token * and define connection data on the * Dashboard page. */ OT.Connection = function(id, creationTime, data, capabilitiesHash, permissionsHash) { var destroyedReason; this.id = this.connectionId = id; this.creationTime = creationTime ? Number(creationTime) : null; this.data = data; this.capabilities = new OT.ConnectionCapabilities(capabilitiesHash); this.permissions = new OT.Capabilities(permissionsHash); this.quality = null; OT.$.eventing(this); this.destroy = OT.$.bind(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 ) ); } }, this); this.destroyed = function() { return destroyedReason !== void 0; }; this.destroyedReason = function() { return destroyedReason; }; }; OT.Connection.fromHash = function(hash) { return new OT.Connection(hash.id, hash.creationTime, hash.data, OT.$.extend(hash.capablities || {}, { supportsWebRTC: true }), hash.permissions || [] ); }; })(window); !(function() { // id: String | mandatory | immutable // type: String {video/audio/data/...} | mandatory | immutable // active: Boolean | mandatory | mutable // orientation: Integer? | optional | mutable // frameRate: Float | optional | mutable // height: Integer | optional | mutable // width: Integer | optional | mutable OT.StreamChannel = function(options) { this.id = options.id; this.type = options.type; this.active = OT.$.castToBoolean(options.active); this.orientation = options.orientation || OT.VideoOrientation.ROTATED_NORMAL; if (options.frameRate) this.frameRate = parseFloat(options.frameRate, 10); this.width = parseInt(options.width, 10); this.height = parseInt(options.height, 10); OT.$.eventing(this, true); // Returns true if a property was updated. this.update = function(attributes) { var videoDimensions = {}, oldVideoDimensions = {}; for (var key in attributes) { if(!attributes.hasOwnProperty(key)) { continue; } // we shouldn't really read this before we know the key is valid var oldValue = this[key]; switch(key) { case 'active': this.active = OT.$.castToBoolean(attributes[key]); break; case 'disableWarning': this.disableWarning = OT.$.castToBoolean(attributes[key]); break; case 'frameRate': this.frameRate = parseFloat(attributes[key], 10); break; case 'width': case 'height': this[key] = parseInt(attributes[key], 10); videoDimensions[key] = this[key]; oldVideoDimensions[key] = oldValue; break; case 'orientation': this[key] = attributes[key]; videoDimensions[key] = this[key]; oldVideoDimensions[key] = oldValue; break; default: OT.warn('Tried to update unknown key ' + key + ' on ' + this.type + ' channel ' + this.id); return; } this.trigger('update', this, key, oldValue, this[key]); } if (OT.$.keys(videoDimensions).length) { // To make things easier for the public API, we broadcast videoDimensions changes, // which is an aggregate of width, height, and orientation changes. this.trigger('update', this, 'videoDimensions', oldVideoDimensions, videoDimensions); } return true; }; }; })(window); !(function() { var validPropertyNames = ['name', 'archiving']; /** * Specifies a stream. A stream is a representation of a published stream in a session. When a * client calls the Session.publish() method, a new stream is * created. Properties of the Stream object provide information about the stream. * *

When a stream is added to a session, the Session object dispatches a * streamCreatedEvent. When a stream is destroyed, the Session object dispatches a * streamDestroyed event. The StreamEvent object, which defines these event objects, * has a stream property, which is an array of Stream object. For details and a code * example, see {@link StreamEvent}.

* *

When a connection to a session is made, the Session object dispatches a * sessionConnected event, defined by the SessionConnectEvent object. The * SessionConnectEvent object has a streams 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}.

* * @class Stream * @property {Connection} connection The Connection object corresponding * to the connection that is publishing the stream. You can compare this to to the * connection 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 new Date(creationTime), where creationTime is * the creationTime property of the Stream object. * * @property {Number} frameRate The frame rate of the video stream. This property is only set if the * publisher of the stream specifies a frame rate when calling the OT.initPublisher() * method; otherwise, this property is undefined. * * @property {Boolean} hasAudio Whether the stream has audio. This property can change if the * publisher turns on or off audio (by calling * Publisher.publishAudio()). When this occurs, the * {@link Session} object dispatches a streamPropertyChanged 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 * Publisher.publishVideo()). When this occurs, the * {@link Session} object dispatches a streamPropertyChanged event (see * {@link StreamPropertyChangedEvent}.) * * @property {String} name The name of the stream. Publishers can specify a name when publishing * a stream (using the publish() method of the publisher's Session object). * * @property {String} streamId The unique ID of the stream. * * @property {Object} videoDimensions This object has two properties: width and * height. Both are numbers. The width property is the width of the * encoded stream; the height 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 streamPropertyChanged event (see * {@link StreamPropertyChangedEvent}.) */ OT.Stream = function(id, name, creationTime, connection, session, channel) { var destroyedReason; this.id = this.streamId = id; this.name = name; this.creationTime = Number(creationTime); this.connection = connection; this.channel = channel; this.publisher = OT.publishers.find({streamId: this.id}); OT.$.eventing(this); var onChannelUpdate = OT.$.bind(function(channel, key, oldValue, newValue) { var _key = key; switch(_key) { case 'active': _key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo'; this[_key] = newValue; break; case 'disableWarning': _key = channel.type === 'audio' ? 'audioDisableWarning': 'videoDisableWarning'; this[_key] = newValue; if (!this[channel.type === 'audio' ? 'hasAudio' : 'hasVideo']) { return; // Do NOT event in this case. } break; case 'orientation': case 'width': case 'height': this.videoDimensions = { width: channel.width, height: channel.height, orientation: channel.orientation }; // We dispatch this via the videoDimensions key instead return; } this.dispatchEvent( new OT.StreamUpdatedEvent(this, _key, oldValue, newValue) ); }, this); var associatedWidget = OT.$.bind(function() { if(this.publisher) { return this.publisher; } else { return OT.subscribers.find(function(subscriber) { return subscriber.stream.id === this.id && subscriber.session.id === session.id; }); } }, this); // Returns all channels that have a type of +type+. this.getChannelsOfType = function (type) { return OT.$.filter(this.channel, function(channel) { return channel.type === type; }); }; this.getChannel = function (id) { for (var i=0; iRTCPeerConnection.getStats based audio level sampler. * * It uses the the getStats method to get the audioOutputLevel. * This implementation expects the single parameter version of the getStats method. * * Currently the audioOutputLevel stats is only supported in Chrome. * * @param {OT.SubscriberPeerConnection} peerConnection the peer connection to use to get the stats * @constructor */ OT.GetStatsAudioLevelSampler = function(peerConnection) { if (!OT.$.hasCapabilities('audioOutputLevelStat', 'getStatsWithSingleParameter')) { throw new Error('The current platform does not provide the required capabilities'); } var _peerConnection = peerConnection, _statsProperty = 'audioOutputLevel'; /* * Acquires the audio level. * * @param {function(?number)} done a callback to be called with the acquired value in the * [0, 1] range when available or null if no value could be acquired */ this.sample = function(done) { _peerConnection.getStatsWithSingleParameter(function(statsReport) { var results = statsReport.result(); for (var i = 0; i < results.length; i++) { var result = results[i]; if (result.local) { var audioOutputLevel = parseFloat(result.local.stat(_statsProperty)); if (!isNaN(audioOutputLevel)) { // the mex value delivered by getStats for audio levels is 2^15 done(audioOutputLevel / 32768); return; } } } done(null); }); }; }; /* * An AudioContext based audio level sampler. It returns the maximum value in the * last 1024 samples. * * It is worth noting that the remote MediaStream audio analysis is currently only * available in FF. * * This implementation gracefully handles the case where the MediaStream has not * been set yet by returning a null value until the stream is set. It is up to the * call site to decide what to do with this value (most likely ignore it and retry later). * * @constructor * @param {AudioContext} audioContext an audio context instance to get an analyser node */ OT.AnalyserAudioLevelSampler = function(audioContext) { var _sampler = this, _analyser = null, _timeDomainData = null; var _getAnalyser = function(stream) { var sourceNode = audioContext.createMediaStreamSource(stream); var analyser = audioContext.createAnalyser(); sourceNode.connect(analyser); return analyser; }; this.webOTStream = null; this.sample = function(done) { if (!_analyser && _sampler.webOTStream) { _analyser = _getAnalyser(_sampler.webOTStream); _timeDomainData = new Uint8Array(_analyser.frequencyBinCount); } if (_analyser) { _analyser.getByteTimeDomainData(_timeDomainData); // varies from 0 to 255 var max = 0; for (var idx = 0; idx < _timeDomainData.length; idx++) { max = Math.max(max, Math.abs(_timeDomainData[idx] - 128)); } // normalize the collected level to match the range delivered by // the getStats' audioOutputLevel done(max / 128); } else { done(null); } }; }; /* * Transforms a raw audio level to produce a "smoother" animation when using displaying the * audio level. This transformer is state-full because it needs to keep the previous average * value of the signal for filtering. * * It applies a low pass filter to get rid of level jumps and apply a log scale. * * @constructor */ OT.AudioLevelTransformer = function() { var _averageAudioLevel = null; /* * * @param {number} audioLevel a level in the [0,1] range * @returns {number} a level in the [0,1] range transformed */ this.transform = function(audioLevel) { if (_averageAudioLevel === null || audioLevel >= _averageAudioLevel) { _averageAudioLevel = audioLevel; } else { // a simple low pass filter with a smoothing of 70 _averageAudioLevel = audioLevel * 0.3 + _averageAudioLevel * 0.7; } // 1.5 scaling to map -30-0 dBm range to [0,1] var logScaled = (Math.log(_averageAudioLevel) / Math.LN10) / 1.5 + 1; return Math.min(Math.max(logScaled, 0), 1); }; }; })(window); !(function() { /* * Executes the provided callback thanks to window.setInterval. * * @param {function()} callback * @param {number} frequency how many times per second we want to execute the callback * @constructor */ OT.IntervalRunner = function(callback, frequency) { var _callback = callback, _frequency = frequency, _intervalId = null; this.start = function() { _intervalId = window.setInterval(_callback, 1000 / _frequency); }; this.stop = function() { window.clearInterval(_intervalId); _intervalId = null; }; }; })(window); !(function(window) { // Normalise these var NativeRTCSessionDescription, NativeRTCIceCandidate; if (!TBPlugin.isInstalled()) { // order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless NativeRTCSessionDescription = (window.mozRTCSessionDescription || window.RTCSessionDescription); NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate); } else { NativeRTCSessionDescription = TBPlugin.RTCSessionDescription; NativeRTCIceCandidate = TBPlugin.RTCIceCandidate; } // Helper function to forward Ice Candidates via +messageDelegate+ var iceCandidateForwarder = function(messageDelegate) { return function(event) { 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.setPeerConnection(peerConnection); // iceProcessor.processPending(); // var IceCandidateProcessor = function() { var _pendingIceCandidates = [], _peerConnection = null; this.setPeerConnection = function(peerConnection) { _peerConnection = peerConnection; }; this.process = function(message) { var iceCandidate = new NativeRTCIceCandidate(message.content); 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: / [/] 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 = OT.$.filter(sdp.split('\r\n'), 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'); }; var removeVideoCodec = function removeVideoCodec (sdp, codec) { var matcher = new RegExp('a=rtpmap:(\\d+) ' + codec + '\\/\\d+', 'i'), payloadTypes = [], videoMediaLineIndex, sdpLines, match; sdpLines = OT.$.filter(sdp.split('\r\n'), function(line, index) { if (line.indexOf('m=video') !== -1) videoMediaLineIndex = index; match = line.match(matcher); if (match !== null) { payloadTypes.push(match[1]); // remove this line as it contains the codec return false; } return true; }); if (payloadTypes.length && videoMediaLineIndex) { sdpLines[videoMediaLineIndex] = sdpLines[videoMediaLineIndex].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, setLocalDescription, createAnswer; generateErrorCallback = function(message, prefix) { return function(errorReason) { OT.error(message); OT.error(errorReason); if (failure) failure(message, errorReason, prefix); }; }; setLocalDescription = function(answer) { answer.sdp = removeComfortNoise(answer.sdp); answer.sdp = removeVideoCodec(answer.sdp, 'ulpfec'); answer.sdp = removeVideoCodec(answer.sdp, 'red'); peerConnection.setLocalDescription( answer, // Success function() { success(answer); }, // Failure generateErrorCallback('Error while setting LocalDescription', 'SetLocalDescription') ); }; createAnswer = function() { peerConnection.createAnswer( // Success setLocalDescription, // Failure generateErrorCallback('Error while setting createAnswer', '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 cryptoLine = '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'+cryptoLine); } if (offer.sdp.indexOf('a=rtcp-fb') === -1) { var rtcpFbLine = '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'+rtcpFbLine); } peerConnection.setRemoteDescription( offer, // Success createAnswer, // Failure generateErrorCallback('Error while setting RemoteDescription', 'SetRemoteDescription') ); }; // 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, generateErrorCallback, setLocalDescription; constraints = { mandatory: {}, optional: [] }, generateErrorCallback = function(message, prefix) { return function(errorReason) { OT.error(message); OT.error(errorReason); if (failure) failure(message, errorReason, prefix); }; }; setLocalDescription = function(offer) { offer.sdp = removeComfortNoise(offer.sdp); offer.sdp = removeVideoCodec(offer.sdp, 'ulpfec'); offer.sdp = removeVideoCodec(offer.sdp, 'red'); peerConnection.setLocalDescription( offer, // Success function() { success(offer); }, // Failure generateErrorCallback('Error while setting LocalDescription', 'SetLocalDescription') ); }; // For interop with FireFox. Disable Data Channel in createOffer. if (navigator.mozGetUserMedia) { constraints.mandatory.MozDontOfferDataChannel = true; } peerConnection.createOffer( // Success setLocalDescription, // Failure generateErrorCallback('Error while creating Offer', 'CreateOffer'), constraints ); }; /* * Negotiates a WebRTC PeerConnection. * * Responsible for: * * offer-answer exchange * * iceCandidates * * notification of remote streams being added/removed * */ OT.PeerConnection = function(config) { var _peerConnection, _peerConnectionCompletionHandlers = [], _iceProcessor = new IceCandidateProcessor(), _offer, _answer, _state = 'new', _messageDelegates = []; 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 = OT.$.bind(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); } }, this), // Create and initialise the PeerConnection object. This deals with // any differences between the various browser implementations and // our own TBPlugin version. // // +completion+ is the function is call once we've either successfully // created the PeerConnection or on failure. // // +localWebRtcStream+ will be null unless the callee is representing // a publisher. This is an unfortunate implementation limitation // of TBPlugin, it's not used for vanilla WebRTC. Hopefully this can // be tidied up later. // createPeerConnection = OT.$.bind(function (completion, localWebRtcStream) { if (_peerConnection) { completion.call(null, null, _peerConnection); return; } _peerConnectionCompletionHandlers.push(completion); if (_peerConnectionCompletionHandlers.length > 1) { // The PeerConnection is already being setup, just wait for // it to be ready. return; } var pcConstraints = { optional: [ {DtlsSrtpKeyAgreement: true} ] }; OT.debug('Creating peer connection config "' + JSON.stringify(config) + '".'); if (!config.iceServers || config.iceServers.length === 0) { // This should never happen unless something is misconfigured OT.error('No ice servers present'); } OT.$.createPeerConnection(config, pcConstraints, localWebRtcStream, OT.$.bind(attachEventsToPeerConnection, this)); }, this), // An auxiliary function to createPeerConnection. This binds the various event callbacks // once the peer connection is created. // // +err+ will be non-null if an err occured while creating the PeerConnection // +pc+ will be the PeerConnection object itself. // attachEventsToPeerConnection = OT.$.bind(function(err, pc) { if (err) { triggerError('Failed to create PeerConnection, exception: ' + err.toString(), 'NewPeerConnection'); _peerConnectionCompletionHandlers = []; return; } OT.debug('OT attachEventsToPeerConnection'); _peerConnection = pc; _peerConnection.onicecandidate = iceCandidateForwarder(delegateMessage); _peerConnection.onaddstream = OT.$.bind(onRemoteStreamAdded, this); _peerConnection.onremovestream = OT.$.bind(onRemoteStreamRemoved, this); if (_peerConnection.onsignalingstatechange !== undefined) { _peerConnection.onsignalingstatechange = OT.$.bind(routeStateChanged, this); } else if (_peerConnection.onstatechange !== undefined) { _peerConnection.onstatechange = OT.$.bind(routeStateChanged, this); } if (_peerConnection.oniceconnectionstatechange !== undefined) { var failedStateTimer; _peerConnection.oniceconnectionstatechange = function (event) { if (event.target.iceConnectionState === 'failed') { if (failedStateTimer) { clearTimeout(failedStateTimer); } // We wait 5 seconds and make sure that it's still in the failed state // before we trigger the error. This is because we sometimes see // 'failed' and then 'connected' afterwards. setTimeout(function () { if (event.target.iceConnectionState === 'failed') { triggerError('The stream was unable to connect due to a network error.' + ' Make sure your connection isn\'t blocked by a firewall.', 'ICEWorkflow'); } }, 5000); } }; } triggerPeerConnectionCompletion(null); }, this), triggerPeerConnectionCompletion = function () { while (_peerConnectionCompletionHandlers.length) { _peerConnectionCompletionHandlers.shift().call(null); } }, // 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.setPeerConnection(null); qos.stopCollecting(); if (_peerConnection !== null) { if (_peerConnection.destroy) { // OTPlugin defines a destroy method on PCs. This allows // the plugin to release any resources that it's holding. _peerConnection.destroy(); } _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; } if (newState && newState.toLowerCase() !== _state) { _state = newState.toLowerCase(); OT.debug('PeerConnection.stateChange: ' + _state); switch(_state) { case 'closed': tearDownPeerConnection.call(this); break; } } }, qosCallback = OT.$.bind(function(parsedStats) { this.trigger('qos', parsedStats); }, this), 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); }, /// 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({type: 'offer', sdp: message.content.sdp}), // Relays +answer+ Answer relayAnswer = function(answer) { _iceProcessor.setPeerConnection(_peerConnection); _iceProcessor.processPending(); relaySDP(OT.Raptor.Actions.ANSWER, answer); qos.startCollecting(_peerConnection); }, reportError = function(message, errorReason, prefix) { triggerError('PeerConnection.offerProcessor ' + message + ': ' + errorReason, prefix); }; createPeerConnection(function() { offerProcessor( _peerConnection, offer, relayAnswer, reportError ); }); }, processAnswer = function(message) { if (!message.content.sdp) { OT.error('PeerConnection.processMessage: Weird answer message, no SDP.'); return; } _answer = new NativeRTCSessionDescription({type: 'answer', sdp: message.content.sdp}); _peerConnection.setRemoteDescription(_answer, function () { OT.debug('setRemoteDescription Success'); }, function (errorReason) { triggerError('Error while setting RemoteDescription ' + errorReason, 'SetRemoteDescription'); }); _iceProcessor.setPeerConnection(_peerConnection); _iceProcessor.processPending(); qos.startCollecting(_peerConnection); }, processSubscribe = function() { OT.debug('PeerConnection.processSubscribe: Sending offer to subscriber.'); if (!_peerConnection) { // TODO(rolly) I need to examine whether this can // actually happen. If it does happen in the short // term, I want it to be noisy. throw new Error('PeerConnection broke!'); } createPeerConnection(function() { suscribeProcessor( _peerConnection, // Success: Relay Offer function(offer) { _offer = offer; relaySDP(OT.Raptor.Actions.OFFER, _offer); }, // Failure function(message, errorReason, prefix) { triggerError('PeerConnection.suscribeProcessor ' + message + ': ' + errorReason, prefix); } ); }); }, triggerError = OT.$.bind(function(errorReason, prefix) { OT.error(errorReason); this.trigger('error', errorReason, prefix); }, this); this.addLocalStream = function(webRTCStream) { createPeerConnection(function() { _peerConnection.addStream(webRTCStream); }, 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 'generateoffer': processSubscribe.call(this, message); break; case 'offer': processOffer.call(this, message); break; case 'answer': case 'pranswer': processAnswer.call(this, message); break; case '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.setIceServers = function (iceServers) { if (iceServers) { config.iceServers = iceServers; } }; this.registerMessageDelegate = function(delegateFn) { return _messageDelegates.push(delegateFn); }; this.unregisterMessageDelegate = function(delegateFn) { var index = OT.$.arrayIndexOf(_messageDelegates, delegateFn); if ( index !== -1 ) { _messageDelegates.splice(index, 1); } return _messageDelegates.length; }; this.remoteStreams = function() { return _peerConnection ? getRemoteStreams() : []; }; this.getStatsWithSingleParameter = function(callback) { if (OT.$.hasCapabilities('getStatsWithSingleParameter')) { createPeerConnection(function() { _peerConnection.getStats(callback); }); } }; var qos = new OT.PeerConnection.QOS(qosCallback); }; })(window); // // There are three implementations of stats parsing in this file. // 1. For Chrome: Chrome is currently using an older version of the API // 2. For OTPlugin: The plugin is using a newer version of the API that // exists in the latest WebRTC codebase // 3. For Firefox: FF is using a version that looks a lot closer to the // current spec. // // I've attempted to keep the three implementations from sharing any code, // accordingly you'll notice a bunch of duplication between the three. // // This is acceptable as the goal is to be able to remove each implementation // as it's no longer needed without any risk of affecting the others. If there // was shared code between them then each removal would require an audit of // all the others. // // !(function() { /// // Get Stats using the older API. Used by all current versions // of Chrome. // var parseStatsOldAPI = function parseStatsOldAPI (peerConnection, prevStats, currentStats, completion) { /* this parses a result if there it contains the video bitrate */ var parseAvgVideoBitrate = function (result) { if (result.stat('googFrameHeightSent')) { currentStats.videoBytesTransferred = result.stat('bytesSent'); } else if (result.stat('googFrameHeightReceived')) { currentStats.videoBytesTransferred = result.stat('bytesReceived'); } else { return NaN; } var transferDelta = currentStats.videoBytesTransferred - (prevStats.videoBytesTransferred || 0); return Math.round(transferDelta * 8 / currentStats.deltaSecs); }, /* this parses a result if there it contains the audio bitrate */ parseAvgAudioBitrate = function (result) { if (result.stat('audioInputLevel')) { currentStats.audioBytesTransferred = result.stat('bytesSent'); } else if (result.stat('audioOutputLevel')) { currentStats.audioBytesTransferred = result.stat('bytesReceived'); } else { return NaN; } var transferDelta = currentStats.audioBytesTransferred - (prevStats.audioBytesTransferred || 0); return Math.round(transferDelta * 8 / currentStats.deltaSecs); }, parseFrameRate = function (result) { if (result.stat('googFrameRateSent')) { return result.stat('googFrameRateSent'); } else if (result.stat('googFrameRateReceived')) { return result.stat('googFrameRateReceived'); } return null; }, parseStatsReports = function (stats) { if (stats.result) { var resultList = stats.result(); for (var resultIndex = 0; resultIndex < resultList.length; resultIndex++) { var result = resultList[resultIndex]; if (result.stat) { if(result.stat('googActiveConnection') === 'true') { currentStats.localCandidateType = result.stat('googLocalCandidateType'); currentStats.remoteCandidateType = result.stat('googRemoteCandidateType'); currentStats.transportType = result.stat('googTransportType'); } var avgVideoBitrate = parseAvgVideoBitrate(result); if (!isNaN(avgVideoBitrate)) { currentStats.avgVideoBitrate = avgVideoBitrate; } var avgAudioBitrate = parseAvgAudioBitrate(result); if (!isNaN(avgAudioBitrate)) { currentStats.avgAudioBitrate = avgAudioBitrate; } var frameRate = parseFrameRate(result); if (frameRate != null) { currentStats.frameRate = frameRate; } } } } completion(null, currentStats); }; peerConnection.getStats(parseStatsReports); }; /// // Get Stats for the OT Plugin, newer than Chromes version, but // still not in sync with the spec. // var parseStatsOTPlugin = function parseStatsOTPlugin (peerConnection, prevStats, currentStats, completion) { var onStatsError = function onStatsError (error) { completion(error); }, /// // From the Audio Tracks // * avgAudioBitrate // * audioBytesTransferred // parseAudioStats = function (statsReport) { var lastBytesSent = prevStats.audioBytesTransferred || 0, transferDelta; if (statsReport.audioInputLevel) { currentStats.audioBytesTransferred = statsReport.bytesSent; } else if (statsReport.audioOutputLevel) { currentStats.audioBytesTransferred = statsReport.bytesReceived; } if (currentStats.audioBytesTransferred) { transferDelta = currentStats.audioBytesTransferred - lastBytesSent; currentStats.avgAudioBitrate = Math.round(transferDelta * 8 / currentStats.deltaSecs); } }, /// // From the Video Tracks // * frameRate // * avgVideoBitrate // * videoBytesTransferred // parseVideoStats = function (statsReport) { var lastBytesSent = prevStats.videoBytesTransferred || 0, transferDelta; if (statsReport.googFrameHeightSent) { currentStats.videoBytesTransferred = statsReport.bytesSent; } else if (statsReport.googFrameHeightReceived) { currentStats.videoBytesTransferred = statsReport.bytesReceived; } if (currentStats.videoBytesTransferred) { transferDelta = currentStats.videoBytesTransferred - lastBytesSent; currentStats.avgVideoBitrate = Math.round(transferDelta * 8 / currentStats.deltaSecs); } if (statsReport.googFrameRateSent) { currentStats.frameRate = statsReport.googFrameRateSent; } else if (statsReport.googFrameRateReceived) { currentStats.frameRate = statsReport.googFrameRateReceived; } }, isStatsForVideoTrack = function(statsReport) { return statsReport.googFrameHeightSent !== void 0 || statsReport.googFrameHeightReceived !== void 0 || currentStats.videoBytesTransferred !== void 0 || statsReport.googFrameRateSent !== void 0; }, isStatsForIceCandidate = function(statsReport) { return statsReport.googActiveConnection === 'true'; }; peerConnection.getStats(null, function(statsReports) { statsReports.forEach(function(statsReport) { if (isStatsForIceCandidate(statsReport)) { currentStats.localCandidateType = statsReport.googLocalCandidateType; currentStats.remoteCandidateType = statsReport.googRemoteCandidateType; currentStats.transportType = statsReport.googTransportType; } else if (isStatsForVideoTrack(statsReport)) { parseVideoStats(statsReport); } else { parseAudioStats(statsReport); } }); completion(null, currentStats); }, onStatsError); }; /// // Get Stats using the newer API. // var parseStatsNewAPI = function parseStatsNewAPI (peerConnection, prevStats, currentStats, completion) { var onStatsError = function onStatsError (error) { completion(error); }, parseAvgVideoBitrate = function parseAvgVideoBitrate (result) { if (result.bytesSent || result.bytesReceived) { currentStats.videoBytesTransferred = result.bytesSent || result.bytesReceived; } else { return NaN; } var transferDelta = currentStats.videoBytesTransferred - (prevStats.videoBytesTransferred || 0); return Math.round(transferDelta * 8 / currentStats.deltaSecs); }, parseAvgAudioBitrate = function parseAvgAudioBitrate (result) { if (result.bytesSent || result.bytesReceived) { currentStats.audioBytesTransferred = result.bytesSent || result.bytesReceived; } else { return NaN; } var transferDelta = currentStats.audioBytesTransferred - (prevStats.audioBytesTransferred || 0); return Math.round(transferDelta * 8 / currentStats.deltaSecs); }; peerConnection.getStats(null, 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)) { currentStats.avgVideoBitrate = avgVideoBitrate; } } else if (res.id.indexOf('audio') !== -1) { var avgAudioBitrate = parseAvgAudioBitrate(res); if(!isNaN(avgAudioBitrate)) { currentStats.avgAudioBitrate = avgAudioBitrate; } } } } completion(null, currentStats); }, onStatsError); }; var parseQOS = function (peerConnection, prevStats, currentStats, completion) { var firefoxVersion = window.navigator.userAgent .toLowerCase().match(/Firefox\/([0-9\.]+)/i); if (TBPlugin.isInstalled()) { parseQOS = parseStatsOTPlugin; return parseStatsOTPlugin(peerConnection, prevStats, currentStats, completion); } else if (firefoxVersion !== null && parseFloat(firefoxVersion[1], 10) >= 27.0) { parseQOS = parseStatsNewAPI; return parseStatsNewAPI(peerConnection, prevStats, currentStats, completion); } else { parseQOS = parseStatsOldAPI; return parseStatsOldAPI(peerConnection, prevStats, currentStats, completion); } }; OT.PeerConnection.QOS = function (qosCallback) { var _creationTime = OT.$.now(), _peerConnection; var calculateQOS = OT.$.bind(function calculateQOS (prevStats) { if (!_peerConnection) { // We don't have a PeerConnection yet, or we did and // it's been closed. Either way we're done. return; } var now = OT.$.now(); var currentStats = { timeStamp: now, duration: Math.round(now - _creationTime), deltaSecs: (now - prevStats.timeStamp) / 1000 }; var onParsedStats = function (err, parsedStats) { if (err) { OT.error('Failed to Parse QOS Stats: ' + JSON.stringify(err)); return; } qosCallback(parsedStats, prevStats); // Recalculate the stats setTimeout(OT.$.bind(calculateQOS, null, parsedStats), OT.PeerConnection.QOS.INTERVAL); }; parseQOS(_peerConnection, prevStats, currentStats, onParsedStats); }, this); this.startCollecting = function (peerConnection) { if (!peerConnection || !peerConnection.getStats) { // It looks like this browser doesn't support getStats // Bail. return; } _peerConnection = peerConnection; calculateQOS({ timeStamp: OT.$.now() }); }; this.stopCollecting = function () { _peerConnection = null; }; }; // Recalculate the stats in 30 sec OT.PeerConnection.QOS.INTERVAL = 30000; })(); !(function() { var _peerConnections = {}; OT.PeerConnections = { add: function(remoteConnection, streamId, config) { var key = remoteConnection.id + '_' + streamId, 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, streamId) { var key = remoteConnection.id + '_' + streamId, ref = _peerConnections[key]; if (ref) { ref.count -= 1; if (ref.count === 0) { ref.pc.disconnect(); delete _peerConnections[key]; } } } }; })(window); !(function() { /* * 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, streamId, webRTCStream) { var _peerConnection, _hasRelayCandidates = false, _subscriberId = session._.subscriberMap[remoteConnection.id + '_' + streamId], _onPeerClosed, _onPeerError, _relayMessageToPeer, _onQOS; // Private _onPeerClosed = function() { this.destroy(); this.trigger('disconnected', this); }; // Note: All Peer errors are fatal right now. _onPeerError = function(errorReason, prefix) { this.trigger('error', null, errorReason, this, prefix); this.destroy(); }; _relayMessageToPeer = OT.$.bind(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: if (session.sessionInfo.p2pEnabled) { session._.jsepAnswerP2p(streamId, _subscriberId, payload.sdp); } else { session._.jsepAnswer(streamId, payload.sdp); } break; case OT.Raptor.Actions.OFFER: this.trigger('connected'); if (session.sessionInfo.p2pEnabled) { session._.jsepOfferP2p(streamId, _subscriberId, payload.sdp); } else { session._.jsepOffer(streamId, payload.sdp); } break; case OT.Raptor.Actions.CANDIDATE: if (session.sessionInfo.p2pEnabled) { session._.jsepCandidateP2p(streamId, _subscriberId, payload); } else { session._.jsepCandidate(streamId, payload); } } }, this); _onQOS = OT.$.bind(function _onQOS (parsedStats, prevStats) { this.trigger('qos', remoteConnection, parsedStats, prevStats); }, this); OT.$.eventing(this); // Public this.destroy = function() { // Clean up our PeerConnection if (_peerConnection) { _peerConnection.off(); OT.PeerConnections.remove(remoteConnection, streamId); } _peerConnection = null; }; this.processMessage = function(type, message) { _peerConnection.processMessage(type, message); }; // Init this.init = function(iceServers) { _peerConnection = OT.PeerConnections.add(remoteConnection, streamId, { iceServers: iceServers }); _peerConnection.on({ close: _onPeerClosed, error: _onPeerError, qos: _onQOS }, this); _peerConnection.registerMessageDelegate(_relayMessageToPeer); _peerConnection.addLocalStream(webRTCStream); this.remoteConnection = function() { return remoteConnection; }; this.hasRelayCandidates = function() { return _hasRelayCandidates; }; }; }; })(window); !(function() { /* * 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, subscriber, properties) { var _peerConnection, _destroyed = false, _hasRelayCandidates = false, _onPeerClosed, _onRemoteStreamAdded, _onRemoteStreamRemoved, _onPeerError, _relayMessageToPeer, _setEnabledOnStreamTracksCurry, _onQOS; // Private _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, prefix) { this.trigger('error', errorReason, this, prefix); }; _relayMessageToPeer = OT.$.bind(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._.jsepAnswerP2p(stream.id, subscriber.widgetId, payload.sdp); break; case OT.Raptor.Actions.OFFER: session._.jsepOfferP2p(stream.id, subscriber.widgetId, payload.sdp); break; case OT.Raptor.Actions.CANDIDATE: session._.jsepCandidateP2p(stream.id, subscriber.widgetId, payload); break; } }, 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 0) { OT.$.forEach(_peerConnection.remoteStreams(), _onRemoteStreamAdded, this); } else if (numDelegates === 1) { // We only bother with the PeerConnection negotiation if we don't already // have a remote stream. var channelsToSubscribeTo; if (properties.subscribeToVideo || properties.subscribeToAudio) { var audio = stream.getChannelsOfType('audio'), video = stream.getChannelsOfType('video'); channelsToSubscribeTo = OT.$.map(audio, function(channel) { return { id: channel.id, type: channel.type, active: properties.subscribeToAudio }; }).concat(OT.$.map(video, function(channel) { return { id: channel.id, type: channel.type, active: properties.subscribeToVideo, restrictFrameRate: properties.restrictFrameRate !== void 0 ? properties.restrictFrameRate : false }; })); } session._.subscriberCreate(stream, subscriber, channelsToSubscribeTo, OT.$.bind(function(err, message) { if (err) { this.trigger('error', err.message, this, 'Subscribe'); } _peerConnection.setIceServers(OT.Raptor.parseIceServers(message)); }, this)); } }; this.getStatsWithSingleParameter = function(callback) { if(_peerConnection) { _peerConnection.getStatsWithSingleParameter(callback); } }; }; })(window); !(function() { // 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; this[name] = widget; }; 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() { 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); widget.setDisplayMode(_options.mode); if (_options.mode === 'auto') { // if the mode is auto we hold the "on mode" for 2 seconds // this will let the proper widgets nicely fade away and help discoverability OT.$.addClass(widget.domElement, 'OT_mode-on-hold'); setTimeout(function() { OT.$.removeClass(widget.domElement, 'OT_mode-on-hold'); }, 2000); } // add the widget to the parent parent.appendChild(this.domElement); return widget; }; }; })(window); !(function() { // BackingBar Chrome Widget // // nameMode (String) // Whether or not the name panel is being displayed // 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). // // muteMode (String) // Whether or not the mute button is being displayed // Possible values are: "auto" (the mute button is displayed // when the stream is first displayed and when the user mouses over the display), // "off" (the mute button is not displayed), and "on" (the mute button is displayed). // // displays a backing bar // can be shown/hidden // can be destroyed OT.Chrome.BackingBar = function(options) { var _nameMode = options.nameMode, _muteMode = options.muteMode; function getDisplayMode() { if(_nameMode === 'on' || _muteMode === 'on') { return 'on'; } else if(_nameMode === 'mini' || _muteMode === 'mini') { return 'mini'; } else if(_nameMode === 'mini-auto' || _muteMode === 'mini-auto') { return 'mini-auto'; } else if(_nameMode === 'auto' || _muteMode === 'auto') { return 'auto'; } else { return 'off'; } } // Mixin common widget behaviour OT.Chrome.Behaviour.Widget(this, { mode: getDisplayMode(), nodeName: 'div', htmlContent: '', htmlAttributes: { className: 'OT_bar OT_edge-bar-item' } }); this.setNameMode = function(nameMode) { _nameMode = nameMode; this.setDisplayMode(getDisplayMode()); }; this.setMuteMode = function(muteMode) { _muteMode = muteMode; this.setDisplayMode(getDisplayMode()); }; }; })(window); !(function() { // 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, _bugMode = options.bugMode; if (!_name || OT.$.trim(_name).length === '') { _name = null; // THere's no name, just flip the mode off options.mode = 'off'; } this.setName = OT.$.bind(function(name) { if (!_name) this.setDisplayMode('auto'); _name = name; this.domElement.innerHTML = _name; }); this.setBugMode = OT.$.bind(function(bugMode) { _bugMode = bugMode; if(bugMode === 'off') { OT.$.addClass(this.domElement, 'OT_name-no-bug'); } else { OT.$.removeClass(this.domElement, 'OT_name-no-bug'); } }, this); // Mixin common widget behaviour OT.Chrome.Behaviour.Widget(this, { mode: options.mode, nodeName: 'h1', htmlContent: _name, htmlAttributes: { className: 'OT_name OT_edge-bar-item' }, onCreate: OT.$.bind(function() { this.setBugMode(_bugMode); }, this) }); }; })(window); !(function() { OT.Chrome.MuteButton = function(options) { var _onClickCb, _muted = options.muted || false, updateClasses, attachEvents, detachEvents, onClick; updateClasses = OT.$.bind(function() { if (_muted) { OT.$.addClass(this.domElement, 'OT_active'); } else { OT.$.removeClass(this.domElement, 'OT_active '); } }, this); // Private Event Callbacks attachEvents = function(elem) { _onClickCb = OT.$.bind(onClick, this); OT.$.on(elem, 'click', _onClickCb); }; detachEvents = function(elem) { _onClickCb = null; OT.$.off(elem, 'click', _onClickCb); }; onClick = function() { _muted = !_muted; updateClasses(); if (_muted) { this.parent.trigger('muted', this); } else { this.parent.trigger('unmuted', this); } return false; }; OT.$.defineProperties(this, { muted: { get: function() { return _muted; }, set: function(muted) { _muted = muted; updateClasses(); } } }); // Mixin common widget behaviour var classNames = _muted ? 'OT_edge-bar-item OT_mute OT_active' : 'OT_edge-bar-item OT_mute'; OT.Chrome.Behaviour.Widget(this, { mode: options.mode, nodeName: 'button', htmlContent: 'Mute', htmlAttributes: { className: classNames }, onCreate: OT.$.bind(attachEvents, this), onDestroy: OT.$.bind(detachEvents, this) }); }; })(window); !(function() { OT.Chrome.OpenTokButton = function(options) { // Mixin common widget behaviour OT.Chrome.Behaviour.Widget(this, { mode: options ? options.mode : null, nodeName: 'span', htmlContent: 'OpenTok', htmlAttributes: { className: 'OT_opentok OT_edge-bar-item' } }); }; })(window); !(function() { // Archving Chrome Widget // // mode (String) // Whether to display the archving widget. Possible values are: "on" (the status is displayed // when archiving and briefly when archving ends) and "off" (the status is not displayed) // Whether to display the archving widget. Possible values are: "auto" (the name is displayed // when the status 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.Archiving = function(options) { var _archiving = options.archiving, _archivingStarted = options.archivingStarted || 'Archiving on', _archivingEnded = options.archivingEnded || 'Archiving off', _initialState = true, _lightBox, _light, _text, _textNode, renderStageDelayedAction, renderText, renderStage; renderText = function(text) { _textNode.nodeValue = text; _lightBox.setAttribute('title', text); }; renderStage = OT.$.bind(function() { if(renderStageDelayedAction) { clearTimeout(renderStageDelayedAction); renderStageDelayedAction = null; } if(_archiving) { OT.$.addClass(_light, 'OT_active'); } else { OT.$.removeClass(_light, 'OT_active'); } OT.$.removeClass(this.domElement, 'OT_archiving-' + (!_archiving ? 'on' : 'off')); OT.$.addClass(this.domElement, 'OT_archiving-' + (_archiving ? 'on' : 'off')); if(options.show && _archiving) { renderText(_archivingStarted); OT.$.addClass(_text, 'OT_mode-on'); OT.$.removeClass(_text, 'OT_mode-auto'); this.setDisplayMode('on'); renderStageDelayedAction = setTimeout(function() { OT.$.addClass(_text, 'OT_mode-auto'); OT.$.removeClass(_text, 'OT_mode-on'); }, 5000); } else if(options.show && !_initialState) { OT.$.addClass(_text, 'OT_mode-on'); OT.$.removeClass(_text, 'OT_mode-auto'); this.setDisplayMode('on'); renderText(_archivingEnded); renderStageDelayedAction = setTimeout(OT.$.bind(function() { this.setDisplayMode('off'); }, this), 5000); } else { this.setDisplayMode('off'); } }, this); // Mixin common widget behaviour OT.Chrome.Behaviour.Widget(this, { mode: _archiving && options.show && 'on' || 'off', nodeName: 'h1', htmlAttributes: {className: 'OT_archiving OT_edge-bar-item OT_edge-bottom'}, onCreate: OT.$.bind(function() { _lightBox = OT.$.createElement('div', { className: 'OT_archiving-light-box' }, ''); _light = OT.$.createElement('div', { className: 'OT_archiving-light' }, ''); _lightBox.appendChild(_light); _text = OT.$.createElement('div', { className: 'OT_archiving-status OT_mode-on OT_edge-bar-item OT_edge-bottom' }, ''); _textNode = document.createTextNode(''); _text.appendChild(_textNode); this.domElement.appendChild(_lightBox); this.domElement.appendChild(_text); renderStage(); }, this) }); this.setShowArchiveStatus = OT.$.bind(function(show) { options.show = show; if(this.domElement) { renderStage.call(this); } }, this); this.setArchiving = OT.$.bind(function(status) { _archiving = status; _initialState = false; if(this.domElement) { renderStage.call(this); } }, this); }; })(window); !(function() { OT.Chrome.AudioLevelMeter = function(options) { var widget = this, _meterBarElement, _voiceOnlyIconElement, _meterValueElement, _value, _maxValue = options.maxValue || 1, _minValue = options.minValue || 0; // Mixin common widget behaviour OT.Chrome.Behaviour.Widget(this, { mode: options ? options.mode : 'auto', nodeName: 'div', htmlAttributes: { className: 'OT_audio-level-meter' }, onCreate: function() { _meterBarElement = OT.$.createElement('div', { className: 'OT_audio-level-meter__bar' }, ''); _meterValueElement = OT.$.createElement('div', { className: 'OT_audio-level-meter__value' }, ''); _voiceOnlyIconElement = OT.$.createElement('div', { className: 'OT_audio-level-meter__audio-only-img' }, ''); widget.domElement.appendChild(_meterBarElement); widget.domElement.appendChild(_voiceOnlyIconElement); widget.domElement.appendChild(_meterValueElement); } }); function updateView() { var percentSize = _value * 100 / (_maxValue - _minValue); _meterValueElement.style.width = _meterValueElement.style.height = 2 * percentSize + '%'; _meterValueElement.style.top = _meterValueElement.style.right = -percentSize + '%'; } widget.setValue = function(value) { _value = value; updateView(); }; }; })(window); !(function() { OT.Chrome.VideoDisabledIndicator = function(options) { var _mode, _videoDisabled = false, _warning = false, updateClasses; _mode = options.mode || 'auto'; updateClasses = function(domElement) { if (_videoDisabled) { OT.$.addClass(domElement, 'OT_video-disabled'); } else { OT.$.removeClass(domElement, 'OT_video-disabled'); } if(_warning) { OT.$.addClass(domElement, 'OT_video-disabled-warning'); } else { OT.$.removeClass(domElement, 'OT_video-disabled-warning'); } if ((_videoDisabled || _warning) && (_mode === 'auto' || _mode === 'on')) { OT.$.addClass(domElement, 'OT_active'); } else { OT.$.removeClass(domElement, 'OT_active'); } }; this.disableVideo = function(value) { _videoDisabled = value; if(value === true) { _warning = false; } updateClasses(this.domElement); }; this.setWarning = function(value) { _warning = value; updateClasses(this.domElement); }; // Mixin common widget behaviour OT.Chrome.Behaviour.Widget(this, { mode: _mode, nodeName: 'div', htmlAttributes: { className: 'OT_video-disabled-indicator' } }); this.setDisplayMode = function(mode) { _mode = mode; updateClasses(this.domElement); }; }; })(window); (function() { /* Stylable Notes * RTC doesn't need to wait until anything is loaded * 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 * setStyle() method of thePublisher object. (See the documentation for * setStyle() to see the styles that define this object.) * @return {Object} The object that defines the styles of the Publisher. * @see setStyle() * @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 * setStyle() method of the Subscriber object. (See the documentation for * setStyle() to see the styles that define this object.) * @return {Object} The object that defines the styles of the Subscriber. * @see setStyle() * @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. * *

You can either pass one parameter or two parameters to this method.

* *

If you pass one parameter, style, it is an object that has the following * properties: * *

    *
  • audioLevelDisplayMode (String) — How to display the audio level * indicator. Possible values are: "auto" (the indicator is displayed when the * video is disabled), "off" (the indicator is not displayed), and * "on" (the indicator is always displayed).
  • * *
  • backgroundImageURI (String) — A URI for an image to display as * the background image when a video is not displayed. (A video may not be displayed if * you call publishVideo(false) on the Publisher object). You can pass an http * or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the * data URI scheme (instead of http or https) and pass in base-64-encrypted * PNG data, such as that obtained from the * Publisher.getImgData() method. For example, * you could set the property to "data:VBORw0KGgoAA...", where the portion of * the string after "data:" is the result of a call to * Publisher.getImgData(). If the URL or the image data is invalid, the * property is ignored (the attempt to set the image fails silently). *

    * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer), * you cannot set the backgroundImageURI style to a string larger than * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this * limitation, you cannot set the backgroundImageURI style to a string obtained * with the getImgData() method. *

  • * *
  • buttonDisplayMode (String) — How to display the microphone * controls. Possible values are: "auto" (controls are displayed when the * stream is first displayed and when the user mouses over the display), "off" * (controls are not displayed), and "on" (controls are always displayed).
  • * *
  • nameDisplayMode (String) — Whether to display the stream 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 always displayed).
  • *
*

* *

For example, the following code passes one parameter to the method:

* *
myPublisher.setStyle({nameDisplayMode: "off"});
* *

If you pass two parameters, style and value, 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:

* *
myPublisher.setStyle("nameDisplayMode", "off");
* *

You can set the initial settings when you call the Session.publish() * or OT.initPublisher() method. Pass a style property as part of the * properties parameter of the method.

* *

The OT object dispatches an exception event if you pass in an invalid style * to the method. The code property of the ExceptionEvent object is set to 1011.

* * @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 style passed in. Pass a value * for this parameter only if the value of the style parameter is a String.

* * @see getStyle() * @return {Publisher} The Publisher object * @see setStyle() * * @see Session.publish() * @see OT.initPublisher() * @method #setStyle * @memberOf Publisher */ /** * Sets properties that define the appearance of some user interface controls of the Subscriber. * *

You can either pass one parameter or two parameters to this method.

* *

If you pass one parameter, style, it is an object that has the following * properties: * *

    *
  • audioLevelDisplayMode (String) — How to display the audio level * indicator. Possible values are: "auto" (the indicator is displayed when the * video is disabled), "off" (the indicator is not displayed), and * "on" (the indicator is always displayed).
  • * *
  • backgroundImageURI (String) — A URI for an image to display as * the background image when a video is not displayed. (A video may not be displayed if * you call subscribeToVideo(false) on the Publisher object). You can pass an * http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the * data URI scheme (instead of http or https) and pass in base-64-encrypted * PNG data, such as that obtained from the * Subscriber.getImgData() method. For example, * you could set the property to "data:VBORw0KGgoAA...", where the portion of * the string after "data:" is the result of a call to * Publisher.getImgData(). If the URL or the image data is invalid, the * property is ignored (the attempt to set the image fails silently). *

    * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer), * you cannot set the backgroundImageURI style to a string larger than * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this * limitation, you cannot set the backgroundImageURI style to a string obtained * with the getImgData() method. *

  • * *
  • buttonDisplayMode (String) — How to display the speaker * controls. Possible values are: "auto" (controls are displayed when the * stream is first displayed and when the user mouses over the display), "off" * (controls are not displayed), and "on" (controls are always displayed).
  • * *
  • nameDisplayMode (String) — Whether to display the stream 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 always displayed).
  • * *
  • videoDisabledDisplayMode (String) — Whether to display the video * disabled indicator and video disabled warning icons for a Subscriber. These icons * indicate that the video has been disabled (or is in risk of being disabled for * the warning icon) due to poor stream quality. Possible values are: "auto" * (the icons are automatically when the displayed video is disabled or in risk of being * disabled due to poor stream quality), "off" (do not display the icons), and * "on" (display the icons).
  • *
*

* *

For example, the following code passes one parameter to the method:

* *
mySubscriber.setStyle({nameDisplayMode: "off"});
* *

If you pass two parameters, style and value, 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:

* *
mySubscriber.setStyle("nameDisplayMode", "off");
* *

You can set the initial settings when you call the Session.subscribe() method. * Pass a style property as part of the properties parameter of the * method.

* *

The OT object dispatches an exception event if you pass in an invalid style * to the method. The code property of the ExceptionEvent object is set to 1011.

* * @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 style passed in. Pass a value * for this parameter only if the value of the style parameter is a String.

* * @returns {Subscriber} The Subscriber object. * * @see getStyle() * @see setStyle() * * @see Session.subscribe() * @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 _style = {}, _COMPONENT_STYLES, _validStyleValues, isValidStyle, castValue; _COMPONENT_STYLES = [ 'showMicButton', 'showSpeakerButton', 'nameDisplayMode', 'buttonDisplayMode', 'backgroundImageURI', 'bugDisplayMode' ]; _validStyleValues = { buttonDisplayMode: ['auto', 'mini', 'mini-auto', 'off', 'on'], nameDisplayMode: ['auto', 'off', 'on'], bugDisplayMode: ['auto', 'off', 'on'], audioLevelDisplayMode: ['auto', 'off', 'on'], showSettingsButton: [true, false], showMicButton: [true, false], backgroundImageURI: null, showControlBar: [true, false], showArchiveStatus: [true, false], videoDisabledDisplayMode: ['auto', 'off', 'on'] }; // Validates the style +key+ and also whether +value+ is valid for +key+ isValidStyle = function(key, value) { return key === 'backgroundImageURI' || (_validStyleValues.hasOwnProperty(key) && OT.$.arrayIndexOf(_validStyleValues[key], 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 key in style) { if(!style.hasOwnProperty(key)) { continue; } if (OT.$.arrayIndexOf(_COMPONENT_STYLES, key) < 0) { // Strip unnecessary properties out, should this happen on Set? delete style[key]; } } 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) { if(!newStyles.hasOwnProperty(key)) { continue; } 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('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() { /* * A Publishers Microphone. * * TODO * * bind to changes in mute/unmute/volume/etc and respond to them */ OT.Microphone = function(webRTCStream, muted) { var _muted; OT.$.defineProperties(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 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 // Destroyed (The subscriber has been cleaned up, terminal state // // // 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 -> // Destroyed // // Destroyed -> (terminal 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, validTransitions, initialState = 'NotSubscribing'; validStates = [ 'NotSubscribing', 'Init', 'ConnectingToPeer', 'BindingRemoteStream', 'Subscribing', 'Failed', 'Destroyed' ]; validTransitions = { NotSubscribing: ['NotSubscribing', 'Init', 'Destroyed'], Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream', 'Destroyed'], ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed', 'Destroyed'], BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed', 'Destroyed'], Subscribing: ['NotSubscribing', 'Failed', 'Destroyed'], Failed: ['Destroyed'], Destroyed: [] }; OT.SubscribingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions); OT.SubscribingState.prototype.isDestroyed = function() { return this.current === 'Destroyed'; }; OT.SubscribingState.prototype.isFailed = function() { return this.current === 'Failed'; }; OT.SubscribingState.prototype.isSubscribing = function() { return this.current === 'Subscribing'; }; OT.SubscribingState.prototype.isAttemptingToSubscribe = function() { return OT.$.arrayIndexOf( [ 'Init', 'ConnectingToPeer', 'BindingRemoteStream' ], this.current ) !== -1; }; })(window); !(function() { // Models a Publisher's publishing State // // Valid States: // NotPublishing // GetUserMedia // BindingMedia // MediaBound // PublishingToSession // Publishing // Failed // Destroyed // // // 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 -> // Destroyed // // Destroyed -> (Terminal state // // var validStates = [ 'NotPublishing', 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession', 'Publishing', 'Failed', 'Destroyed' ], validTransitions = { NotPublishing: ['NotPublishing', 'GetUserMedia', 'Destroyed'], GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing', 'Destroyed'], BindingMedia: ['MediaBound', 'Failed', 'NotPublishing', 'Destroyed'], MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed', 'Destroyed'], PublishingToSession: ['NotPublishing', 'Publishing', 'Failed', 'Destroyed'], Publishing: ['NotPublishing', 'MediaBound', 'Failed', 'Destroyed'], Failed: ['Destroyed'], Destroyed: [] }, initialState = 'NotPublishing'; OT.PublishingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions); OT.PublishingState.prototype.isDestroyed = function() { return this.current === 'Destroyed'; }; OT.PublishingState.prototype.isAttemptingToPublish = function() { return OT.$.arrayIndexOf( [ 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession' ], this.current) !== -1; }; OT.PublishingState.prototype.isPublishing = function() { return this.current === 'Publishing'; }; })(window); !(function() { // 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 OT.initPublisher() method * creates a Publisher object.

* *

The following code instantiates a session, and publishes an audio-video stream * upon connection to the session:

* *
 *  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 = OT.initSession(apiKey, sessionID);
 *  session.on("sessionConnected", function(event) {
 *    // This example assumes that a DOM element with the ID 'publisherElement' exists
 *    var publisherProperties = {width: 400, height:300, name:"Bob's stream"};
 *    publisher = OT.initPublisher('publisherElement', publisherProperties);
 *    session.publish(publisher);
 *  });
 *  session.connect(token);
 *  
* *

This example creates a Publisher object and adds its video to a DOM element * with the ID publisherElement by calling the OT.initPublisher() * method. It then publishes a stream to the session by calling * the publish() method of the Session object.

* * @property {Boolean} accessAllowed Whether the user has granted access to the camera * and microphone. The Publisher object dispatches an accessAllowed event when * the user grants access. The Publisher object dispatches an accessDenied event * when the user denies access. * @property {Element} element The HTML DOM element containing the Publisher. * @property {String} id The DOM ID of the Publisher. * @property {Stream} stream The {@link Stream} object corresponding the the stream of * the Publisher. * @property {Session} session The {@link Session} to which the Publisher belongs. * * @see OT.initPublisher * @see Session.publish() * * @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, _streamId, _webRTCStream, _session, _peerConnections = {}, _loaded = false, _publishProperties, _publishStartTime, _microphone, _chrome, _audioLevelMeter, _analytics = new OT.Analytics(), _validResolutions, _validFrameRates = [ 1, 7, 15, 30 ], _prevStats, _state, _iceServers, _audioLevelCapable = OT.$.hasCapabilities('webAudio'), _audioLevelSampler; _validResolutions = { '320x240': {width: 320, height: 240}, '640x480': {width: 640, height: 480}, '1280x720': {width: 1280, height: 720} }; _prevStats = { 'timeStamp' : OT.$.now() }; OT.$.eventing(this); if(_audioLevelCapable) { _audioLevelSampler = new OT.AnalyserAudioLevelSampler(new window.AudioContext()); var publisher = this; var audioLevelRunner = new OT.IntervalRunner(function() { _audioLevelSampler.sample(function(audioInputLevel) { OT.$.requestAnimationFrame(function() { publisher.dispatchEvent( new OT.AudioLevelUpdatedEvent(audioInputLevel)); }); }); }, 60); this.on({ 'audioLevelUpdated:added': function(count) { if (count === 1) { audioLevelRunner.start(); } }, 'audioLevelUpdated:removed': function(count) { if (count === 0) { audioLevelRunner.stop(); } } }); } OT.StylableComponent(this, { showArchiveStatus: true, nameDisplayMode: 'auto', buttonDisplayMode: 'auto', bugDisplayMode: 'auto', audioLevelDisplayMode: '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.isConnected() ? _session.connection.connectionId : null, 'partner_id': _session ? _session.apiKey : OT.APIKEY, streamId: _stream ? _stream.id : null, 'widget_id': _guid, 'widget_type': 'Publisher' }); }, recordQOS = OT.$.bind(function(connection, parsedStats) { if(!_state.isPublishing()) { return; } var QoSBlob = { 'widget_type': 'Publisher', 'stream_type': 'WebRTC', sessionId: _session ? _session.sessionId : null, connectionId: _session && _session.isConnected() ? _session.connection.connectionId : null, partnerId: _session ? _session.apiKey : OT.APIKEY, streamId: _stream ? _stream.id : null, width: _container ? OT.$.width(_container.domElement) : undefined, height: _container ? OT.$.height(_container.domElement) : undefined, widgetId: _guid, version: OT.properties.version, 'media_server_name': _session ? _session.sessionInfo.messagingServer : null, p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false, duration: _publishStartTime ? new Date().getTime() - _publishStartTime.getTime() : 0, 'remote_connection_id': connection.id }; _analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) ); this.trigger('qos', parsedStats); }, this), /// Private Events stateChangeFailed = function(changeFailed) { OT.error('Publisher State Change Failed: ', changeFailed.message); OT.debug(changeFailed); }, onLoaded = function() { if (_state.isDestroyed()) { // The publisher was destroyed before loading finished return; } OT.debug('OT.Publisher.onLoaded'); _state.set('MediaBound'); // If we have a session and we haven't created the stream yet then // wait until that is complete before hiding the loading spinner _container.loading(this.session ? !_stream : false); _loaded = true; _createChrome.call(this); this.trigger('initSuccess'); this.trigger('loaded', this); }, onLoadFailure = function(reason) { logAnalyticsEvent('publish', 'Failure', 'reason', 'Publisher PeerConnection Error: ' + reason); _state.set('Failed'); this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.P2P_CONNECTION_FAILED, '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 && _webRTCStream.getVideoTracks().length > 0); this.accessAllowed = true; this.dispatchEvent( new OT.Event(OT.Event.names.ACCESS_ALLOWED, false) ); var videoContainerOptions = { muted: true, error: OT.$.bind(onVideoError, this) }; _targetElement = _container.bindVideo(_webRTCStream, videoContainerOptions, OT.$.bind(function(err) { if (err) { onLoadFailure.call(this, err); return; } onLoaded.call(this); }, this)); if(_audioLevelSampler) { _audioLevelSampler.webOTStream = webOTStream; } }, onStreamAvailableError = function(error) { OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message); _state.set('Failed'); this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH, error.message)); if (_container) _container.destroy(); logAnalyticsEvent('publish', 'Failure', 'reason', 'GetUserMedia:Publisher failed to access camera/mic: ' + error.message); OT.handleJsException('Publisher failed to access camera/mic: ' + error.message, OT.ExceptionCodes.UNABLE_TO_PUBLISH, { 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('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH, 'Publisher Access Denied: Permission Denied' + (error.message ? ': ' + error.message : ''))); logAnalyticsEvent('publish', 'Failure', 'reason', 'GetUserMedia:Publisher Access Denied: Permission Denied'); var browser = OT.$.browserVersion(); var event = new OT.Event(OT.Event.names.ACCESS_DENIED), defaultAction = function() { if(!event.isDefaultPrevented()) { if(browser.browser === 'Chrome') { if (_container) { _container.addError('', null, 'OT_publisher-denied-chrome'); } if(!accessDialogWasOpened) { OT.Dialogs.AllowDeny.Chrome.previouslyDenied(window.location.hostname); } else { OT.Dialogs.AllowDeny.Chrome.deniedNow(); } } else if(browser.browser === 'Firefox') { if(_container) { _container.addError('', 'Click the reload button in the URL bar to change ' + 'camera & mic settings.', 'OT_publisher-denied-firefox'); } OT.Dialogs.AllowDeny.Firefox.denied().on({ refresh: function() { window.location.reload(); } }); } } }; this.dispatchEvent(event, defaultAction); }, accessDialogPrompt, accessDialogChromeTimeout, accessDialogFirefoxTimeout, accessDialogWasOpened = false, onAccessDialogOpened = function() { accessDialogWasOpened = true; logAnalyticsEvent('accessDialog', 'Opened', '', ''); var browser = OT.$.browserVersion(); this.dispatchEvent( new OT.Event(OT.Event.names.ACCESS_DIALOG_OPENED, true), function(event) { if(!event.isDefaultPrevented()) { if(browser.browser === 'Chrome') { accessDialogChromeTimeout = setTimeout(function() { accessDialogChromeTimeout = null; logAnalyticsEvent('allowDenyHelpers', 'show', 'version', 'Chrome'); accessDialogPrompt = OT.Dialogs.AllowDeny.Chrome.initialPrompt(); accessDialogPrompt.on('closeButtonClicked', function() { logAnalyticsEvent('allowDenyHelpers', 'dismissed', 'version', 'Chrome'); }); }, 5000); } else if(browser.browser === 'Firefox') { accessDialogFirefoxTimeout = setTimeout(function() { accessDialogFirefoxTimeout = null; logAnalyticsEvent('allowDenyHelpers', 'show', 'version', 'Firefox'); accessDialogPrompt = OT.Dialogs.AllowDeny.Firefox.maybeDenied(); accessDialogPrompt.on('closeButtonClicked', function() { logAnalyticsEvent('allowDenyHelpers', 'dismissed', 'version', 'Firefox'); }); }, 7000); } } else { logAnalyticsEvent('allowDenyHelpers', 'developerPrevented', '', ''); } } ); }, onAccessDialogClosed = function() { logAnalyticsEvent('accessDialog', 'Closed', '', ''); if(accessDialogChromeTimeout) { clearTimeout(accessDialogChromeTimeout); logAnalyticsEvent('allowDenyHelpers', 'notShown', 'version', 'Chrome'); accessDialogChromeTimeout = null; } if(accessDialogFirefoxTimeout) { clearTimeout(accessDialogFirefoxTimeout); logAnalyticsEvent('allowDenyHelpers', 'notShown', 'version', 'Firefox'); accessDialogFirefoxTimeout = null; } if(accessDialogPrompt) { accessDialogPrompt.close(); var browser = OT.$.browserVersion(); logAnalyticsEvent('allowDenyHelpers', 'closed', 'version', browser.browser); accessDialogPrompt = null; } 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.isAttemptingToPublish()) { this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH, message)); } else { this.trigger('error', message); } OT.handleJsException('Publisher error playing stream: ' + message, OT.ExceptionCodes.UNABLE_TO_PUBLISH, { 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, prefix) { logAnalyticsEvent('publish', 'Failure', 'reason|hasRelayCandidates', (prefix ? prefix : '') + [':Publisher PeerConnection with connection ' + (peerConnection && peerConnection.remoteConnection && peerConnection.remoteConnection().id) + ' failed: ' + reason, peerConnection.hasRelayCandidates() ].join('|')); OT.handleJsException('Publisher PeerConnection Error: ' + reason, OT.ExceptionCodes.UNABLE_TO_PUBLISH, { 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. delete _peerConnections[peerConnection.remoteConnection().id]; }, /// Private Helpers // Assigns +stream+ to this publisher. The publisher listens // for a bunch of events on the stream so it can respond to // changes. assignStream = OT.$.bind(function(stream) { this.stream = _stream = stream; _stream.on('destroyed', this.disconnect, this); _state.set('Publishing'); _container.loading(!_loaded); _publishStartTime = new Date(); this.trigger('publishComplete', null, this); this.dispatchEvent(new OT.StreamEvent('streamCreated', stream, null, false)); logAnalyticsEvent('publish', 'Success', 'streamType:streamId', 'WebRTC:' + _streamId); }, this), // 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', OT.$.bind(this.cleanupSubscriber, this, remoteConnection.id)); peerConnection = _peerConnections[remoteConnection.id] = new OT.PublisherPeerConnection( remoteConnection, _session, _streamId, _webRTCStream ); peerConnection.on({ connected: function() { logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [ parseInt(OT.$.now() - startConnectingTime, 10), peerConnection.hasRelayCandidates() ].join('|')); }, disconnected: onPeerDisconnected, error: onPeerConnectionFailure, qos: recordQOS }, this); peerConnection.init(_iceServers); } 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) { if (!_chrome) return; switch(key) { case 'nameDisplayMode': _chrome.name.setDisplayMode(value); _chrome.backingBar.setNameMode(value); break; case 'showArchiveStatus': logAnalyticsEvent('showArchiveStatus', 'styleChange', 'mode', value ? 'on': 'off'); _chrome.archive.setShowArchiveStatus(value); break; case 'buttonDisplayMode': _chrome.muteButton.setDisplayMode(value); _chrome.backingBar.setMuteMode(value); break; case 'audioLevelDisplayMode': _chrome.audioLevel.setDisplayMode(value); break; case 'bugDisplayMode': // bugDisplayMode can't be updated but is used by some partners case 'backgroundImageURI': _container.setBackgroundImageURI(value); } }, _createChrome = function() { if(this.getStyle('bugDisplayMode') === 'off') { logAnalyticsEvent('bugDisplayMode', 'createChrome', 'mode', 'off'); } if(!this.getStyle('showArchiveStatus')) { logAnalyticsEvent('showArchiveStatus', 'createChrome', 'mode', 'off'); } var widgets = { backingBar: new OT.Chrome.BackingBar({ nameMode: !_publishProperties.name ? 'off' : this.getStyle('nameDisplayMode'), muteMode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode')) }), name: new OT.Chrome.NamePanel({ name: _publishProperties.name, mode: this.getStyle('nameDisplayMode'), bugMode: this.getStyle('bugDisplayMode') }), muteButton: new OT.Chrome.MuteButton({ muted: _publishProperties.publishAudio === false, mode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode')) }), opentokButton: new OT.Chrome.OpenTokButton({ mode: this.getStyle('bugDisplayMode') }), archive: new OT.Chrome.Archiving({ show: this.getStyle('showArchiveStatus'), archiving: false }) }; if(_audioLevelCapable) { _audioLevelMeter = new OT.Chrome.AudioLevelMeter({ mode: this.getStyle('audioLevelDisplayMode') }); var audioLevelTransformer = new OT.AudioLevelTransformer(); this.on('audioLevelUpdated', function(evt) { _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel)); }); widgets.audioLevel = _audioLevelMeter; } _chrome = new OT.Chrome({ parent: _container.domElement }).set(widgets).on({ muted: OT.$.bind(this.publishAudio, this, false), unmuted: OT.$.bind(this.publishAudio, this, true) }); }, reset = OT.$.bind(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 (_session) { this._.unpublishFromSession(_session, 'reset'); } this.id = _domId = null; this.stream = _stream = null; _loaded = false; this.session = _session = null; if (!_state.isDestroyed()) _state.set('NotPublishing'); }, this); this.publish = function(targetElement, properties) { OT.debug('OT.Publisher: publish'); if ( _state.isAttemptingToPublish() || _state.isPublishing() ) reset(); _state.set('GetUserMedia'); _publishProperties = OT.$.defaults(properties || {}, { publishAudio : true, publishVideo : true, mirror: true }); if (!_publishProperties.constraints) { _publishProperties.constraints = OT.$.clone(defaultConstraints); if(_publishProperties.audioSource === null || _publishProperties.audioSource === false) { _publishProperties.constraints.audio = false; _publishProperties.publishAudio = false; } else { if(typeof _publishProperties.audioSource === 'object') { if(_publishProperties.audioSource.deviceId != null) { _publishProperties.audioSource = _publishProperties.audioSource.deviceId; } else { OT.warn('Invalid audioSource passed to Publisher. Expected either a device ID'); } } if (_publishProperties.audioSource) { if (typeof _publishProperties.constraints.audio !== 'object') { _publishProperties.constraints.audio = {}; } if (!_publishProperties.constraints.audio.mandatory) { _publishProperties.constraints.audio.mandatory = {}; } if (!_publishProperties.constraints.audio.optional) { _publishProperties.constraints.audio.optional = []; } _publishProperties.constraints.audio.mandatory.sourceId = _publishProperties.audioSource; } } if(_publishProperties.videoSource === null || _publishProperties.videoSource === false) { _publishProperties.constraints.video = false; _publishProperties.publishVideo = false; } else { if(typeof _publishProperties.videoSource === 'object') { if(_publishProperties.videoSource.deviceId != null) { _publishProperties.videoSource = _publishProperties.videoSource.deviceId; } else { OT.warn('Invalid videoSource passed to Publisher. Expected either a device ID'); } } if (_publishProperties.videoSource) { if (typeof _publishProperties.constraints.video !== 'object') { _publishProperties.constraints.video = {}; } if (!_publishProperties.constraints.video.mandatory) { _publishProperties.constraints.video.mandatory = {}; } if (!_publishProperties.constraints.video.optional) { _publishProperties.constraints.video.optional = []; } _publishProperties.constraints.video.mandatory.sourceId = _publishProperties.videoSource; } if (_publishProperties.resolution) { if (_publishProperties.resolution !== void 0 && !_validResolutions.hasOwnProperty(_publishProperties.resolution)) { OT.warn('Invalid resolution passed to the Publisher. Got: ' + _publishProperties.resolution + ' expecting one of "' + OT.$.keys(_validResolutions).join('","') + '"'); } else { _publishProperties.videoDimensions = _validResolutions[_publishProperties.resolution]; if (typeof _publishProperties.constraints.video !== 'object') { _publishProperties.constraints.video = {}; } if (!_publishProperties.constraints.video.mandatory) { _publishProperties.constraints.video.mandatory = {}; } if (!_publishProperties.constraints.video.optional) { _publishProperties.constraints.video.optional = []; } _publishProperties.constraints.video.optional = _publishProperties.constraints.video.optional.concat([ {minWidth: _publishProperties.videoDimensions.width}, {maxWidth: _publishProperties.videoDimensions.width}, {minHeight: _publishProperties.videoDimensions.height}, {maxHeight: _publishProperties.videoDimensions.height} ]); } } if (_publishProperties.frameRate !== void 0 && OT.$.arrayIndexOf(_validFrameRates, _publishProperties.frameRate) === -1) { OT.warn('Invalid frameRate passed to the publisher got: ' + _publishProperties.frameRate + ' expecting one of ' + _validFrameRates.join(',')); delete _publishProperties.frameRate; } else if (_publishProperties.frameRate) { if (typeof _publishProperties.constraints.video !== 'object') { _publishProperties.constraints.video = {}; } if (!_publishProperties.constraints.video.mandatory) { _publishProperties.constraints.video.mandatory = {}; } if (!_publishProperties.constraints.video.optional) { _publishProperties.constraints.video.optional = []; } _publishProperties.constraints.video.optional = _publishProperties.constraints.video.optional.concat([ { minFrameRate: _publishProperties.frameRate }, { maxFrameRate: _publishProperties.frameRate } ]); } } } else { OT.warn('You have passed your own constraints not using ours'); } 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); this.id = _domId = _container.domId(); this.element = _container.domElement; OT.$.shouldAskForDevices(OT.$.bind(function(devices) { if(!devices.video) { OT.warn('Setting video constraint to false, there are no video sources'); _publishProperties.constraints.video = false; } if(!devices.audio) { OT.warn('Setting audio constraint to false, there are no audio sources'); _publishProperties.constraints.audio = false; } OT.$.getUserMedia( _publishProperties.constraints, OT.$.bind(onStreamAvailable, this), OT.$.bind(onStreamAvailableError, this), OT.$.bind(onAccessDialogOpened, this), OT.$.bind(onAccessDialogClosed, this), OT.$.bind(onAccessDenied, this) ); }, this)); }, this); return this; }; /** * Starts publishing audio (if it is currently not being published) * when the value is true; stops publishing audio * (if it is currently being published) when the value is false. * * @param {Boolean} value Whether to start publishing audio (true) * or not (false). * * @see OT.initPublisher() * @see Stream.hasAudio * @see StreamPropertyChangedEvent * @method #publishAudio * @memberOf Publisher */ this.publishAudio = function(value) { _publishProperties.publishAudio = value; if (_microphone) { _microphone.muted(!value); } if (_chrome) { _chrome.muteButton.muted(!value); } if (_session && _stream) { _stream.setChannelActiveState('audio', value); } return this; }; /** * Starts publishing video (if it is currently not being published) * when the value is true; stops publishing video * (if it is currently being published) when the value is false. * * @param {Boolean} value Whether to start publishing video (true) * or not (false). * * @see OT.initPublisher() * @see Stream.hasVideo * @see StreamPropertyChangedEvent * @method #publishVideo * @memberOf Publisher */ this.publishVideo = function(value) { var oldValue = _publishProperties.publishVideo; _publishProperties.publishVideo = value; if (_session && _stream && _publishProperties.publishVideo !== oldValue) { _stream.setChannelActiveState('video', 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 * The Publisher object dispatches a destroyed event when the DOM * element is removed. *

* @method #destroy * @memberOf Publisher * @return {Publisher} The Publisher. */ this.destroy = function(/* unused */ reason, quiet) { if (_state.isDestroyed()) return; _state.set('Destroyed'); reset(); if (quiet !== true) { this.dispatchEvent( new OT.DestroyedEvent( OT.Event.names.PUBLISHER_DESTROYED, this, reason ), OT.$.bind(this.off,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]; 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 'unsubscribe': this.cleanupSubscriber(message.content.connection.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. * *

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:

* *
    *  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);
    * 
* * @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: OT.$.bind(function(session) { // Add session property to Publisher this.session = _session = session; var createStream = function() { var streamWidth, streamHeight; // Bail if this.session is gone, it means we were unpublished // before createStream could finish. if (!_session) return; _state.set('PublishingToSession'); var onStreamRegistered = OT.$.bind(function(err, streamId, message) { if (err) { // @todo we should respect err.code here and translate it to the local // client equivalent. logAnalyticsEvent('publish', 'Failure', 'reason', 'Publish:' + OT.ExceptionCodes.UNABLE_TO_PUBLISH + ':' + err.message); if (_state.isAttemptingToPublish()) { this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH, err.message)); } return; } this.streamId = _streamId = streamId; _iceServers = OT.Raptor.parseIceServers(message); }, this); // We set the streamWidth and streamHeight to be the minimum of the requested // resolution and the actual resolution. if (_publishProperties.videoDimensions) { streamWidth = Math.min(_publishProperties.videoDimensions.width, _targetElement.videoWidth() || 640); streamHeight = Math.min(_publishProperties.videoDimensions.height, _targetElement.videoHeight() || 480); } else { streamWidth = _targetElement.videoWidth() || 640; streamHeight = _targetElement.videoHeight() || 480; } session._.streamCreate( _publishProperties && _publishProperties.name ? _publishProperties.name : '', OT.VideoOrientation.ROTATED_NORMAL, streamWidth, streamHeight, _publishProperties.publishAudio, _publishProperties.publishVideo, _publishProperties.frameRate, onStreamRegistered ); }; if (_loaded) createStream.call(this); else this.on('initSuccess', createStream, this); logAnalyticsEvent('publish', 'Attempt', 'streamType', 'WebRTC'); return this; }, this), unpublishFromSession: OT.$.bind(function(session, reason) { if (!_session || session.id !== _session.id) { OT.warn('The publisher ' + _guid + ' is trying to unpublish from a session ' + session.id + ' it is not attached to (it is attached to ' + (_session && _session.id || 'no session') + ')'); return this; } if (session.isConnected() && this.stream) { session._.streamDestroy(this.stream.id); } // Disconnect immediately, rather than wait for the WebSocket to // reply to our destroyStream message. this.disconnect(); this.session = _session = null; // We're back to being a stand-alone publisher again. if (!_state.isDestroyed()) _state.set('MediaBound'); logAnalyticsEvent('unpublish', 'Success', 'sessionId', session.id); this._.streamDestroyed(reason); return this; }, this), streamDestroyed: OT.$.bind(function(reason) { if(OT.$.arrayIndexOf(['reset'], reason) < 0) { var event = new OT.StreamEvent('streamDestroyed', _stream, reason, true); var defaultAction = OT.$.bind(function() { if(!event.isDefaultPrevented()) { this.destroy(); } }, this); this.dispatchEvent(event, defaultAction); } }, this), archivingStatus: OT.$.bind(function(status) { if(_chrome) { _chrome.archive.setArchiving(status); } return status; }, this), webRtcStream: function() { return _webRTCStream; } }; 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() { OT.warn('Fixme: Haven\'t implemented setMicrophoneGain'); }; this.getMicrophoneGain = function() { OT.warn('Fixme: Haven\'t implemented getMicrophoneGain'); return 0.5; }; this.setCamera = function() { OT.warn('Fixme: Haven\'t implemented setCamera'); }; this.setMicrophone = function() { OT.warn('Fixme: Haven\'t implemented setMicrophone'); }; // Platform methods: this.guid = function() { return _guid; }; this.videoElement = function() { return _targetElement.domElement(); }; this.setStream = assignStream; this.isWebRTC = true; this.isLoading = function() { return _container && _container.loading(); }; this.videoWidth = function() { return _targetElement.videoWidth(); }; this.videoHeight = function() { return _targetElement.videoHeight(); }; // Make read-only: element, guid, _.webRtcStream this.on('styleValueChanged', updateChromeForStyleChange, this); _state = new OT.PublishingState(stateChangeFailed); this.accessAllowed = false; /** * Dispatched when the user has clicked the Allow button, granting the * app access to the camera and microphone. The Publisher object has an * accessAllowed property which indicates whether the user * has granted access to the camera and microphone. * @see Event * @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. *

* The default behavior of this event is to display a user interface element * in the Publisher object, indicating that that user has denied access to * the camera and microphone. Call the preventDefault() method * method of the event object in the event listener to prevent this message * from being displayed. * @see Event * @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.) *

* The default behavior of this event is to display a message in the browser that instructs * the user how to enable the camera and microphone. Call the preventDefault() * method of the event object in the event listener to prevent this message from being displayed. * @see Event * @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.) * @see Event * @name accessDialogClosed * @event * @memberof Publisher */ /** * Dispatched periodically to indicate the publisher's audio level. The event is dispatched * up to 60 times per second, depending on the browser. The audioLevel property * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more * information. *

* The following example adjusts the value of a meter element that shows volume of the * publisher. Note that the audio level is adjusted logarithmically and a moving average * is applied: *

*

    * var movingAvg = null;
    * publisher.on('audioLevelUpdated', function(event) {
    *   if (movingAvg === null || movingAvg <= event.audioLevel) {
    *     movingAvg = event.audioLevel;
    *   } else {
    *     movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel;
    *   }
    *
    *   // 1.5 scaling to map the -30 - 0 dBm range to [0,1]
    *   var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1;
    *   logLevel = Math.min(Math.max(logLevel, 0), 1);
    *   document.getElementById('publisherMeter').value = logLevel;
    * });
    * 
*

This example shows the algorithm used by the default audio level indicator displayed * in an audio-only Publisher. * * @name audioLevelUpdated * @event * @memberof Publisher * @see AudioLevelUpdatedEvent */ /** * The publisher has started streaming to the session. * @name streamCreated * @event * @memberof Publisher * @see StreamEvent * @see Session.publish() */ /** * The publisher has stopped streaming to the session. The default behavior is that * the Publisher object is removed from the HTML DOM). The Publisher object dispatches a * destroyed event when the element is removed from the HTML DOM. If you call the * preventDefault() method of the event object in the event listener, the default * behavior is prevented, and you can, optionally, retain the Publisher for reuse or clean it up * using your own code. * @name streamDestroyed * @event * @memberof Publisher * @see StreamEvent */ /** * Dispatched when the Publisher element is removed from the HTML DOM. When this event * is dispatched, you may choose to adjust or remove HTML DOM elements related to the publisher. * @name destroyed * @event * @memberof Publisher */ }; // Helper function to generate unique publisher ids OT.Publisher.nextId = OT.$.uuid; })(window); !(function() { /** * 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 subscribe() method of the * {@link Session} object returns a Subscriber object. * * @property {Element} element The HTML DOM element containing the Subscriber. * @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, _audioLevelMeter, _stream, _fromConnectionId, _peerConnection, _session = options.session, _subscribeStartTime, _startConnectingTime, _properties = OT.$.clone(options), _analytics = new OT.Analytics(), _audioVolume = 100, _state, _prevStats, _lastSubscribeToVideoReason, _audioLevelCapable = OT.$.hasCapabilities('audioOutputLevelStat') || OT.$.hasCapabilities('webAudioCapableRemoteStream'), _audioLevelSampler, _audioLevelRunner, _frameRateRestricted = false; this.id = _domId; this.widgetId = _widgetId; this.session = _session; _prevStats = { timeStamp: OT.$.now() }; if (!_session) { OT.handleJsException('Subscriber must be passed a session option', 2000, { session: _session, target: this }); return; } OT.$.eventing(this, false); if(_audioLevelCapable) { this.on({ 'audioLevelUpdated:added': function(count) { if (count === 1 && _audioLevelRunner) { _audioLevelRunner.start(); } }, 'audioLevelUpdated:removed': function(count) { if (count === 0 && _audioLevelRunner) { _audioLevelRunner.stop(); } } }); } OT.StylableComponent(this, { nameDisplayMode: 'auto', buttonDisplayMode: 'auto', audioLevelDisplayMode: 'auto', videoDisabledIndicatorDisplayMode: 'auto', backgroundImageURI: null, showArchiveStatus: true, showMicButton: true, bugDisplayMode: 'auto' }); var logAnalyticsEvent = function(action, variation, payloadType, payload) { /* jshint camelcase:false*/ _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.isConnected() ? _session.connection.connectionId : null, partner_id: _session && _session.isConnected() ? _session.sessionInfo.partnerId : null, widget_id: _widgetId, widget_type: 'Subscriber' }); }, recordQOS = OT.$.bind(function(parsedStats) { if(_state.isSubscribing() && _session && _session.isConnected()) { /*jshint camelcase:false */ var QoSBlob = { widget_type: 'Subscriber', stream_type : 'WebRTC', width: _container ? OT.$.width(_container.domElement) : undefined, height: _container ? OT.$.height(_container.domElement) : undefined, 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 }; _analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) ); this.trigger('qos', parsedStats); } }, this), stateChangeFailed = function(changeFailed) { OT.error('Subscriber State Change Failed: ', changeFailed.message); OT.debug(changeFailed); }, onLoaded = function() { if (_state.isSubscribing() || !_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('|')); _container.loading(false); _createChrome.call(this); if(_frameRateRestricted) { _stream.setRestrictFrameRate(true); } this.trigger('subscribeComplete', null, 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.isAttemptingToSubscribe()) { // subscribing error _state.set('Failed'); this.trigger('subscribeComplete', new OT.Error(null, 'ClientDisconnected')); } else if (_state.isSubscribing()) { _state.set('Failed'); // we were disconnected after we were already subscribing // probably do nothing? } this.disconnect(); }, onPeerConnectionFailure = OT.$.bind(function(reason, peerConnection, prefix) { if (_state.isAttemptingToSubscribe()) { // 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('subscribeComplete', new OT.Error(null, reason)); } else if (_state.isSubscribing()) { // we were disconnected after we were already subscribing _state.set('Failed'); this.trigger('error', reason); } this.disconnect(); logAnalyticsEvent('subscribe', 'Failure', 'reason', (prefix ? prefix : '') + ':Subscriber PeerConnection Error: ' + reason); OT.handleJsException('Subscriber PeerConnection Error: ' + reason, OT.ExceptionCodes.P2P_CONNECTION_FAILED, { session: _session, target: this } ); _showError.call(this, reason); }, this), onRemoteStreamAdded = function(webOTStream) { OT.debug('OT.Subscriber.onRemoteStreamAdded'); _state.set('BindingRemoteStream'); // Disable the audio/video, if needed this.subscribeToAudio(_properties.subscribeToAudio); _lastSubscribeToVideoReason = 'loading'; this.subscribeToVideo(_properties.subscribeToVideo, 'loading'); var videoContainerOptions = { error: onPeerConnectionFailure, audioVolume: _audioVolume }; // This is a workaround for a bug in Chrome where a track disabled on // the remote end doesn't fire loadedmetadata causing the subscriber to timeout // https://jira.tokbox.com/browse/OPENTOK-15605 var browser = OT.$.browserVersion(), tracks, reenableVideoTrack = false; if (!_stream.hasVideo && browser.browser === 'Chrome' && browser.version >= 35) { tracks = webOTStream.getVideoTracks(); if(tracks.length > 0) { tracks[0].enabled = false; reenableVideoTrack = tracks[0]; } } _streamContainer = _container.bindVideo(webOTStream, videoContainerOptions, OT.$.bind(function(err) { if (err) { onPeerConnectionFailure(err.message || err, _peerConnection, 'VideoElement'); return; } // Continues workaround for https://jira.tokbox.com/browse/OPENTOK-15605 if (reenableVideoTrack != null && _properties.subscribeToVideo) { reenableVideoTrack.enabled = true; } _streamContainer.orientation({ width: _stream.videoDimensions.width, height: _stream.videoDimensions.height, videoOrientation: _stream.videoDimensions.orientation }); onLoaded.call(this, null); }, this)); if (OT.$.hasCapabilities('webAudioCapableRemoteStream') && _audioLevelSampler) { _audioLevelSampler.webOTStream = webOTStream; } 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); }, streamDestroyed = function () { this.disconnect(); }, streamUpdated = function(event) { switch(event.changedProperty) { case 'videoDimensions': _streamContainer.orientation({ width: event.newValue.width, height: event.newValue.height, videoOrientation: event.newValue.orientation }); break; case 'videoDisableWarning': _chrome.videoDisabledIndicator.setWarning(event.newValue); this.dispatchEvent(new OT.VideoDisableWarningEvent( event.newValue ? 'videoDisableWarning' : 'videoDisableWarningLifted' )); break; case 'hasVideo': if(_container) { var audioOnly = !(_stream.hasVideo && _properties.subscribeToVideo); _container.audioOnly(audioOnly); _container.showPoster(audioOnly); } this.dispatchEvent(new OT.VideoEnabledChangedEvent( _stream.hasVideo ? 'videoEnabled' : 'videoDisabled', { reason: 'publishVideo' })); break; case 'hasAudio': // noop } }, /// 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); _chrome.backingBar.setNameMode(value); break; case 'videoDisabledDisplayMode': _chrome.videoDisabledIndicator.setDisplayMode(value); break; case 'showArchiveStatus': _chrome.archive.setShowArchiveStatus(value); break; case 'buttonDisplayMode': _chrome.muteButton.setDisplayMode(value); _chrome.backingBar.setMuteMode(value); break; case 'audioLevelDisplayMode': _chrome.audioLevel.setDisplayMode(value); break; case 'bugDisplayMode': // bugDisplayMode can't be updated but is used by some partners case 'backgroundImageURI': _container.setBackgroundImageURI(value); } }, _createChrome = function() { if(this.getStyle('bugDisplayMode') === 'off') { logAnalyticsEvent('bugDisplayMode', 'createChrome', 'mode', 'off'); } var widgets = { backingBar: new OT.Chrome.BackingBar({ nameMode: !_properties.name ? 'off' : this.getStyle('nameDisplayMode'), muteMode: chromeButtonMode.call(this, this.getStyle('showMuteButton')) }), name: new OT.Chrome.NamePanel({ name: _properties.name, mode: this.getStyle('nameDisplayMode'), bugMode: this.getStyle('bugDisplayMode') }), muteButton: new OT.Chrome.MuteButton({ muted: _properties.muted, mode: chromeButtonMode.call(this, this.getStyle('showMuteButton')) }), opentokButton: new OT.Chrome.OpenTokButton({ mode: this.getStyle('bugDisplayMode') }), archive: new OT.Chrome.Archiving({ show: this.getStyle('showArchiveStatus'), archiving: false }) }; if(_audioLevelCapable) { _audioLevelMeter = new OT.Chrome.AudioLevelMeter({ mode: this.getStyle('audioLevelDisplayMode') }); var audioLevelTransformer = new OT.AudioLevelTransformer(); this.on('audioLevelUpdated', function(evt) { _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel)); }); widgets.audioLevel = _audioLevelMeter; } widgets.videoDisabledIndicator = new OT.Chrome.VideoDisabledIndicator({ mode: this.getStyle('videoDisabledDisplayMode') }); _chrome = new OT.Chrome({ parent: _container.domElement }).set(widgets).on({ muted: function() { muteAudio.call(this, true); }, unmuted: function() { muteAudio.call(this, false); } }, this); }, _showError = function() { // Display the error message inside the container, assuming it's // been created by now. if (_container) { _container.addError( 'The stream was unable to connect due to a network error.', 'Make sure your connection isn\'t blocked by a firewall.' ); } }; this.subscribe = function(stream) { OT.debug('OT.Subscriber: subscribe to ' + stream.id); if (_state.isSubscribing()) { // @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; } this.stream = _stream = stream; this.streamId = _stream.id; _stream.on({ updated: streamUpdated, destroyed: streamDestroyed }, this); _fromConnectionId = stream.connection.id; _properties.name = _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); this.id = _domId = _container.domId(); this.element = _container.domElement; _startConnectingTime = OT.$.now(); if (_stream.connection.id !== _session.connection.id) { logAnalyticsEvent('createPeerConnection', 'Attempt', '', ''); _state.set('ConnectingToPeer'); _peerConnection = new OT.SubscriberPeerConnection(_stream.connection, _session, _stream, this, _properties); _peerConnection.on({ disconnected: onDisconnected, error: onPeerConnectionFailure, remoteStreamAdded: onRemoteStreamAdded, remoteStreamRemoved: onRemoteStreamRemoved, qos: recordQOS }, this); // initialize the peer connection AFTER we've added the event listeners _peerConnection.init(); if (OT.$.hasCapabilities('audioOutputLevelStat')) { _audioLevelSampler = new OT.GetStatsAudioLevelSampler(_peerConnection, 'out'); } else if (OT.$.hasCapabilities('webAudioCapableRemoteStream')) { _audioLevelSampler = new OT.AnalyserAudioLevelSampler(new window.AudioContext()); } if(_audioLevelSampler) { var subscriber = this; // sample with interval to minimise disturbance on animation loop but dispatch the // event with RAF since the main purpose is animation of a meter _audioLevelRunner = new OT.IntervalRunner(function() { _audioLevelSampler.sample(function(audioOutputLevel) { if (audioOutputLevel !== null) { OT.$.requestAnimationFrame(function() { subscriber.dispatchEvent( new OT.AudioLevelUpdatedEvent(audioOutputLevel)); }); } }); }, 60); } } else { logAnalyticsEvent('createPeerConnection', 'Attempt', '', ''); var publisher = _session.getPublisherForStream(_stream); if(!(publisher && publisher._.webRtcStream())) { this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID')); return this; } // Subscribe to yourself edge-case onRemoteStreamAdded.call(this, publisher._.webRtcStream()); } logAnalyticsEvent('subscribe', 'Attempt', 'streamId', _stream.id); return this; }; this.destroy = function(reason, quiet) { if (_state.isDestroyed()) return; if(reason === 'streamDestroyed') { if (_state.isAttemptingToSubscribe()) { // We weren't subscribing yet so the stream was destroyed before we setup // the PeerConnection or receiving the initial stream. this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID')); } } _state.set('Destroyed'); if(_audioLevelRunner) { _audioLevelRunner.stop(); } this.disconnect(); if (_chrome) { _chrome.destroy(); _chrome = null; } if (_container) { _container.destroy(); _container = null; this.element = null; } if (_stream && !_stream.destroyed) { logAnalyticsEvent('unsubscribe', null, 'streamId', _stream.id); } this.id = _domId = null; this.stream = _stream = null; this.streamId = null; this.session =_session = null; _properties = null; if (quiet !== true) { this.dispatchEvent( new OT.DestroyedEvent( OT.Event.names.SUBSCRIBER_DESTROYED, this, reason ), OT.$.bind(this.off, this) ); } return this; }; this.disconnect = function() { if (!_state.isDestroyed() && !_state.isFailed()) { // If we are already in the destroyed state then disconnect // has been called after (or from within) destroy. _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.disableVideo = function(active) { if (!active) { OT.warn('Due to high packet loss and low bandwidth, video has been disabled'); } else { if (_lastSubscribeToVideoReason === 'auto') { OT.info('Video has been re-enabled'); _chrome.videoDisabledIndicator.disableVideo(false); } else { OT.info('Video was not re-enabled because it was manually disabled'); return; } } this.subscribeToVideo(active, 'auto'); if(!active) { _chrome.videoDisabledIndicator.disableVideo(true); } logAnalyticsEvent('updateQuality', 'video', active ? 'videoEnabled' : 'videoDisabled', true); }; /** * Return the base-64-encoded string of PNG data representing the Subscriber video. * *

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:

* *
     *  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);
     *  
* @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.isSubscribing()) { 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. * *

You can set the initial volume when you call the Session.subscribe() * method. Pass a audioVolume property of the properties parameter * of the method.

* * @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: * *
mySubscriber.setAudioVolume(50).setStyle(newStyle);
* * @see getAudioVolume() * @see Session.subscribe() * @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(_properties.muted && _audioVolume > 0) { _properties.premuteVolume = value; muteAudio.call(this, false); } if (_streamContainer) { _streamContainer.setAudioVolume(_audioVolume); } return this; }; /** * Returns the audio volume, between 0 and 100, of the Subscriber. * *

Generally you use this method in conjunction with the setAudioVolume() * method.

* * @return {Number} The audio volume, between 0 and 100, of the Subscriber. * @see setAudioVolume() * @method #getAudioVolume * @memberOf Subscriber */ this.getAudioVolume = function() { if(_properties.muted) { return 0; } 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 value is true; stops * subscribing to audio (if it is currently being subscribed to) when the value * is false. *

* Note: 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. *

* * @param {Boolean} value Whether to start subscribing to audio (true) or not * (false). * * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the * following: * *
mySubscriber.subscribeToAudio(true).subscribeToVideo(false);
* * @see subscribeToVideo() * @see Session.subscribe() * @see StreamPropertyChangedEvent * * @method #subscribeToAudio * @memberOf Subscriber */ this.subscribeToAudio = function(pValue) { var value = OT.$.castToBoolean(pValue, true); if (_peerConnection) { _peerConnection.subscribeToAudio(value && !_properties.subscribeMute); if (_session && _stream && value !== _properties.subscribeToAudio) { _stream.setChannelActiveState('audio', value && !_properties.subscribeMute); } } _properties.subscribeToAudio = value; return this; }; var muteAudio = function(_mute) { _chrome.muteButton.muted(_mute); if(_mute === _properties.mute) { return; } if(OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()) { _properties.subscribeMute = _properties.muted = _mute; this.subscribeToAudio(_properties.subscribeToAudio); } else { if(_mute) { _properties.premuteVolume = this.getAudioVolume(); _properties.muted = true; this.setAudioVolume(0); } else if(_properties.premuteVolume || _properties.audioVolume) { _properties.muted = false; this.setAudioVolume(_properties.premuteVolume || _properties.audioVolume); } } _properties.mute = _properties.mute; }; var reasonMap = { auto: 'quality', publishVideo: 'publishVideo', subscribeToVideo: 'subscribeToVideo' }; /** * Toggles video on and off. Starts subscribing to video (if it is available and * currently not being subscribed to) when the value is true; * stops subscribing to video (if it is currently being subscribed to) when the * value is false. *

* Note: 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. *

* * @param {Boolean} value Whether to start subscribing to video (true) or not * (false). * * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the * following: * *
mySubscriber.subscribeToVideo(true).subscribeToAudio(false);
* * @see subscribeToAudio() * @see Session.subscribe() * @see StreamPropertyChangedEvent * * @method #subscribeToVideo * @memberOf Subscriber */ this.subscribeToVideo = function(pValue, reason) { var value = OT.$.castToBoolean(pValue, true); if(_container) { var audioOnly = !(value && _stream.hasVideo); _container.audioOnly(audioOnly); _container.showPoster(audioOnly); if(value && _container.video()) { _container.loading(value); _container.video().whenTimeIncrements(function(){ _container.loading(false); }, this); } } if (_chrome && _chrome.videoDisabledIndicator) { _chrome.videoDisabledIndicator.disableVideo(false); } if (_peerConnection) { _peerConnection.subscribeToVideo(value); if (_session && _stream && (value !== _properties.subscribeToVideo || reason !== _lastSubscribeToVideoReason)) { _stream.setChannelActiveState('video', value, reason); } } _properties.subscribeToVideo = value; _lastSubscribeToVideoReason = reason; if (reason !== 'loading') { this.dispatchEvent(new OT.VideoEnabledChangedEvent( value ? 'videoEnabled' : 'videoDisabled', { reason: reasonMap[reason] || 'subscribeToVideo' } )); } return this; }; this.isSubscribing = function() { return _state.isSubscribing(); }; this.isWebRTC = true; this.isLoading = function() { return _container && _container.loading(); }; this.videoWidth = function() { return _streamContainer.videoWidth(); }; this.videoHeight = function() { return _streamContainer.videoHeight(); }; /** * Restricts the frame rate of the Subscriber's video stream, when you pass in * true. When you pass in false, the frame rate of the video stream * is not restricted. *

* When the frame rate is restricted, the Subscriber video frame will update once or less per * second. *

* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. In relayed sessions, * calling this method has no effect. *

* Restricting the subscriber frame rate has the following benefits: *

    *
  • It reduces CPU usage.
  • *
  • It reduces the network bandwidth consumed.
  • *
  • It lets you subscribe to more streams simultaneously.
  • *
*

* Reducing a subscriber's frame rate has no effect on the frame rate of the video in * other clients. * * @param {Boolean} value Whether to restrict the Subscriber's video frame rate * (true) or not (false). * * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the * following: * *

mySubscriber.restrictFrameRate(false).subscribeToAudio(true);
* * @method #restrictFrameRate * @memberOf Subscriber */ this.restrictFrameRate = function(val) { OT.debug('OT.Subscriber.restrictFrameRate(' + val + ')'); logAnalyticsEvent('restrictFrameRate', val.toString(), 'streamId', _stream.id); if (_session.sessionInfo.p2pEnabled) { OT.warn('OT.Subscriber.restrictFrameRate: Cannot restrictFrameRate on a P2P session'); } if (typeof val !== 'boolean') { OT.error('OT.Subscriber.restrictFrameRate: expected a boolean value got a ' + typeof val); } else { _frameRateRestricted = val; _stream.setRestrictFrameRate(val); } return this; }; this.on('styleValueChanged', updateChromeForStyleChange, this); this._ = { archivingStatus: function(status) { if(_chrome) { _chrome.archive.setArchiving(status); } } }; _state = new OT.SubscribingState(stateChangeFailed); /** * Dispatched periodically to indicate the subscriber's audio level. The event is dispatched * up to 60 times per second, depending on the browser. The audioLevel property * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more * information. *

* The following example adjusts the value of a meter element that shows volume of the * subscriber. Note that the audio level is adjusted logarithmically and a moving average * is applied: *

   * var movingAvg = null;
   * subscriber.on('audioLevelUpdated', function(event) {
   *   if (movingAvg === null || movingAvg <= event.audioLevel) {
   *     movingAvg = event.audioLevel;
   *   } else {
   *     movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel;
   *   }
   *
   *   // 1.5 scaling to map the -30 - 0 dBm range to [0,1]
   *   var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1;
   *   logLevel = Math.min(Math.max(logLevel, 0), 1);
   *   document.getElementById('subscriberMeter').value = logLevel;
   * });
   * 
*

This example shows the algorithm used by the default audio level indicator displayed * in an audio-only Subscriber. * * @name audioLevelUpdated * @event * @memberof Subscriber * @see AudioLevelUpdatedEvent */ /** * Dispatched when the video for the subscriber is disabled. *

* The reason property defines the reason the video was disabled. This can be set to * one of the following values: *

* *

    * *
  • "publishVideo" — The publisher stopped publishing video by calling * publishVideo(false).
  • * *
  • "quality" — The OpenTok Media Router stopped sending video * to the subscriber based on stream quality changes. This feature of the OpenTok Media * Router has a subscriber drop the video stream when connectivity degrades. (The subscriber * continues to receive the audio stream, if there is one.) *

    * Before sending this event, when the Subscriber's stream quality deteriorates to a level * that is low enough that the video stream is at risk of being disabled, the Subscriber * dispatches a videoDisableWarning event. *

    * If connectivity improves to support video again, the Subscriber object dispatches * a videoEnabled event, and the Subscriber resumes receiving video. *

    * By default, the Subscriber displays a video disabled indicator when a * videoDisabled event with this reason is dispatched and removes the indicator * when the videoDisabled event with this reason is dispatched. You can control * the display of this icon by calling the setStyle() method of the Subscriber, * setting the videoDisabledDisplayMode property(or you can set the style when * calling the Session.subscribe() method, setting the style property * of the properties parameter). *

    * This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. *

  • * *
  • "subscribeToVideo" — The subscriber started or stopped subscribing to * video, by calling subscribeToVideo(false). *
  • * *
* * @see VideoEnabledChangedEvent * @see event:videoDisableWarning * @see event:videoEnabled * @name videoDisabled * @event * @memberof Subscriber */ /** * Dispatched when the OpenTok Media Router determines that the stream quality has degraded * and the video will be disabled if the quality degrades more. If the quality degrades further, * the Subscriber disables the video and dispatches a videoDisabled event. *

* By default, the Subscriber displays a video disabled warning indicator when this event * is dispatched (and the video is disabled). You can control the display of this icon by * calling the setStyle() method and setting the * videoDisabledDisplayMode property (or you can set the style when calling * the Session.subscribe() method and setting the style property * of the properties parameter). *

* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. * * @see Event * @see event:videoDisabled * @see event:videoDisableWarningLifted * @name videoDisableWarning * @event * @memberof Subscriber */ /** * Dispatched when the OpenTok Media Router determines that the stream quality has improved * to the point at which the video being disabled is not an immediate risk. This event is * dispatched after the Subscriber object dispatches a videoDisableWarning event. *

* This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. * * @see Event * @see event:videoDisabled * @see event:videoDisableWarning * @name videoDisableWarningLifted * @event * @memberof Subscriber */ /** * Dispatched when the OpenTok Media Router resumes sending video to the subscriber * after video was previously disabled. *

* The reason property defines the reason the video was enabled. This can be set to * one of the following values: *

* *

    * *
  • "publishVideo" — The publisher started publishing video by calling * publishVideo(true).
  • * *
  • "quality" — The OpenTok Media Router resumed sending video * to the subscriber based on stream quality changes. This feature of the OpenTok Media * Router has a subscriber drop the video stream when connectivity degrades and then resume * the video stream if the stream quality improves. *

    * This feature is only available in sessions that use the OpenTok Media Router (sessions with * the media mode * set to routed), not in sessions with the media mode set to relayed. *

  • * *
  • "subscribeToVideo" — The subscriber started or stopped subscribing to * video, by calling subscribeToVideo(false). *
  • * *
* *

* To prevent video from resuming, in the videoEnabled event listener, * call subscribeToVideo(false) on the Subscriber object. * * @see VideoEnabledChangedEvent * @see event:videoDisabled * @name videoEnabled * @event * @memberof Subscriber */ /** * Dispatched when the Subscriber element is removed from the HTML DOM. When this event is * dispatched, you may choose to adjust or remove HTML DOM elements related to the subscriber. * @see Event * @name destroyed * @event * @memberof Subscriber */ }; })(window); !(function() { var parseErrorFromJSONDocument, onGetResponseCallback, onGetErrorCallback; OT.SessionInfo = function(jsonDocument) { var sessionJSON = jsonDocument[0]; OT.log('SessionInfo Response:'); OT.log(jsonDocument); /*jshint camelcase:false*/ this.sessionId = sessionJSON.session_id; this.partnerId = sessionJSON.partner_id; this.sessionStatus = sessionJSON.session_status; this.messagingServer = sessionJSON.messaging_server_url; this.messagingURL = sessionJSON.messaging_url; this.symphonyAddress = sessionJSON.symphony_address; this.p2pEnabled = !!(sessionJSON.properties && sessionJSON.properties.p2p && sessionJSON.properties.p2p.preference && sessionJSON.properties.p2p.preference.value === 'enabled'); }; // 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', browser = OT.$.browserVersion(), startTime = OT.$.now(), options, validateRawSessionInfo = function(sessionInfo) { session.logEvent('Instrumentation', null, 'gsi', OT.$.now() - startTime); var error = parseErrorFromJSONDocument(sessionInfo); if (error === false) { onGetResponseCallback(session, onSuccess, sessionInfo); } else { onGetErrorCallback(session, onFailure, error, JSON.stringify(sessionInfo)); } }; if(browser.browser === 'IE' && browser.version < 10) { sessionInfoURL = sessionInfoURL + '&format=json&token=' + encodeURIComponent(session.token) + '&version=1&cache=' + OT.$.uuid(); options = { xdomainrequest: true }; } else { options = { headers: { 'X-TB-TOKEN-AUTH': session.token, 'X-TB-VERSION': 1 } }; } session.logEvent('getSessionInfo', 'Attempt', 'api_url', OT.properties.apiURL); OT.$.getJSON(sessionInfoURL, options, function(error, sessionInfo) { if(error) { var responseText = sessionInfo; onGetErrorCallback(session, onFailure, new OT.Error(error.target && error.target.status || error.code, error.message || 'Could not connect to the OpenTok API Server.'), responseText); } else { validateRawSessionInfo(sessionInfo); } }); }; var messageServerToClientErrorCodes = {}; messageServerToClientErrorCodes['404'] = OT.ExceptionCodes.INVALID_SESSION_ID; messageServerToClientErrorCodes['409'] = OT.ExceptionCodes.INVALID_SESSION_ID; messageServerToClientErrorCodes['400'] = OT.ExceptionCodes.INVALID_SESSION_ID; messageServerToClientErrorCodes['403'] = OT.ExceptionCodes.AUTHENTICATION_ERROR; // Return the error in +jsonDocument+, if there is one. Otherwise it will return // false. parseErrorFromJSONDocument = function(jsonDocument) { if(OT.$.isArray(jsonDocument)) { var errors = OT.$.filter(jsonDocument, function(node) { return node.error != null; }); var numErrorNodes = errors.length; if(numErrorNodes === 0) { return false; } var errorCode = errors[0].error.code; if (messageServerToClientErrorCodes[errorCode.toString()]) { errorCode = messageServerToClientErrorCodes[errorCode]; } return { code: errorCode, message: errors[0].error.errorMessage && errors[0].error.errorMessage.message }; } else { return { code: null, message: 'Unknown error: getSessionInfo JSON response was badly formed' }; } }; onGetResponseCallback = function(session, onSuccess, rawSessionInfo) { session.logEvent('getSessionInfo', 'Success', 'api_url', OT.properties.apiURL); onSuccess( new OT.SessionInfo(rawSessionInfo) ); }; onGetErrorCallback = function(session, onFailure, error, responseText) { session.logEvent('Connect', 'Failure', 'errorMessage', 'GetSessionInfo:' + (error.code || 'No code') + ':' + error.message + ':' + (responseText || 'Empty responseText from API server')); onFailure(error, session); }; })(window); !(function() { /** * A class defining properties of the capabilities property of a * Session object. See Session.capabilities. *

* All Capabilities properties are undefined until you have connected to a session * and the Session object has dispatched the sessionConnected event. *

* For more information on token roles, see the * generate_token() * method of the OpenTok server-side libraries. * * @class Capabilities * * @property {Number} forceDisconnect Specifies whether you can call * the Session.forceDisconnect() method (1) or not (0). To call the * Session.forceDisconnect() method, * the user must have a token that is assigned the role of moderator. * @property {Number} forceUnpublish Specifies whether you can call * the Session.forceUnpublish() method (1) or not (0). To call the * Session.forceUnpublish() 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. */ OT.Capabilities = function(permissions) { this.publish = OT.$.arrayIndexOf(permissions, 'publish') !== -1 ? 1 : 0; this.subscribe = OT.$.arrayIndexOf(permissions, 'subscribe') !== -1 ? 1 : 0; this.forceUnpublish = OT.$.arrayIndexOf(permissions, 'forceunpublish') !== -1 ? 1 : 0; this.forceDisconnect = OT.$.arrayIndexOf(permissions, 'forcedisconnect') !== -1 ? 1 : 0; this.supportsWebRTC = OT.$.hasCapabilities('webrtc') ? 1 : 0; this.permittedTo = function(action) { return this.hasOwnProperty(action) && this[action] === 1; }; }; })(window); !(function(window) { /** * The Session object returned by the OT.initSession() 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 capabilities object * are undefined until you have connected to a session and the Session object has dispatched the * sessionConnected 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: connect and * {@link Connection}. * @property {String} sessionId The session ID for this session. You pass this value into the * OT.initSession() 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 OT.initSession} and * {@link connect}). * For more information on sessions and session IDs, see * Session creation. */ OT.Session = function(apiKey, sessionId) { OT.$.eventing(this); // Check that the client meets the minimum requirements, if they don't the upgrade // flow will be triggered. if (!OT.checkSystemRequirements()) { OT.upgradeSystemRequirements(); return; } if(sessionId == null) { sessionId = apiKey; apiKey = null; } this.id = this.sessionId = sessionId; var _initialConnection = true, _apiKey = apiKey, _token, _sessionId = sessionId, _socket, _widgetId = OT.$.uuid(), _connectionId, _analytics = new OT.Analytics(), sessionConnectFailed, sessionDisconnectedHandler, connectionCreatedHandler, connectionDestroyedHandler, streamCreatedHandler, streamPropertyModifiedHandler, streamDestroyedHandler, archiveCreatedHandler, archiveDestroyedHandler, archiveUpdatedHandler, reset, disconnectComponents, destroyPublishers, destroySubscribers, connectMessenger, getSessionInfo, onSessionInfoResponse, permittedTo, dispatchError; var setState = OT.$.statable(this, [ 'disconnected', 'connecting', 'connected', 'disconnecting' ], 'disconnected'); this.connection = null; this.connections = new OT.Collection(); this.streams = new OT.Collection(); this.archives = new OT.Collection(); //-------------------------------------- // MESSAGE HANDLERS //-------------------------------------- // 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', new OT.Error(code || OT.ExceptionCodes.CONNECT_FAILED, reason)); OT.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, reason); var defaultAction = OT.$.bind(function() { // Publishers handle preventDefault'ing themselves destroyPublishers.call(this, publicEvent.reason); // Subscriers don't, destroy 'em if needed if (!publicEvent.isDefaultPrevented()) destroySubscribers.call(this, publicEvent.reason); }, 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; this.dispatchEvent( new OT.ConnectionEvent( OT.Event.names.CONNECTION_DESTROYED, connection, reason ) ); }; streamCreatedHandler = function(stream) { if(stream.connection.id !== this.connection.id) { this.dispatchEvent(new OT.StreamEvent( OT.Event.names.STREAM_CREATED, stream, null, false )); } }; streamPropertyModifiedHandler = function(event) { var stream = event.target, propertyName = event.changedProperty, newValue = event.newValue; if (propertyName === 'videoDisableWarning' || propertyName === 'audioDisableWarning') { return; // These are not public properties, skip top level event for them. } 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) { // if the stream is one of ours we delegate handling // to the publisher itself. if(stream.connection.id === this.connection.id) { OT.$.forEach(OT.publishers.where({ streamId: stream.id }), OT.$.bind(function(publisher) { publisher._.unpublishFromSession(this, reason); }, this)); return; } var event = new OT.StreamEvent('streamDestroyed', stream, reason, true); var defaultAction = OT.$.bind(function() { if (!event.isDefaultPrevented()) { // If we are subscribed to any of the streams we should unsubscribe OT.$.forEach(OT.subscribers.where({streamId: stream.id}), function(subscriber) { if (subscriber.session.id === this.id) { if(subscriber.stream) { subscriber.destroy('streamDestroyed'); } } }, this); } else { // @TODO Add a one time warning that this no longer cleans up the publisher } }, this); this.dispatchEvent(event, defaultAction); }; archiveCreatedHandler = function(archive) { this.dispatchEvent(new OT.ArchiveEvent('archiveStarted', archive)); }; archiveDestroyedHandler = function(archive) { this.dispatchEvent(new OT.ArchiveEvent('archiveDestroyed', archive)); }; archiveUpdatedHandler = function(event) { var archive = event.target, propertyName = event.changedProperty, newValue = event.newValue; if(propertyName === 'status' && newValue === 'stopped') { this.dispatchEvent(new OT.ArchiveEvent('archiveStopped', archive)); } else { this.dispatchEvent(new OT.ArchiveEvent('archiveUpdated', archive)); } }; // Put ourselves into a pristine state reset = function() { this.token = _token = null; setState('disconnected'); this.connection = null; this.capabilities = new OT.Capabilities([]); this.connections.destroy(); this.streams.destroy(); this.archives.destroy(); }; disconnectComponents = function(reason) { OT.$.forEach(OT.publishers.where({session: this}), function(publisher) { publisher.disconnect(reason); }); OT.$.forEach(OT.subscribers.where({session: this}), function(subscriber) { subscriber.disconnect(); }); }; destroyPublishers = function(reason) { OT.$.forEach(OT.publishers.where({session: this}), function(publisher) { publisher._.streamDestroyed(reason); }); }; destroySubscribers = function(reason) { OT.$.forEach(OT.subscribers.where({session: this}), function(subscriber) { subscriber.destroy(reason); }); }; connectMessenger = function() { OT.debug('OT.Session: connecting to Raptor'); var socketUrl = this.sessionInfo.messagingURL, symphonyUrl = OT.properties.symphonyAddresss || this.sessionInfo.symphonyAddress; _socket = new OT.Raptor.Socket(_widgetId, socketUrl, symphonyUrl, OT.SessionDispatcher(this)); var analyticsPayload = [ socketUrl, OT.$.userAgent(), OT.properties.version, window.externalHost ? 'yes' : 'no' ]; _socket.connect(_token, this.sessionInfo, OT.$.bind(function(error, sessionState) { if (error) { _socket = void 0; analyticsPayload.splice(0,0,error.message); this.logEvent('Connect', 'Failure', 'reason|webSocketServerUrl|userAgent|sdkVersion|chromeFrame', analyticsPayload.map(function(e) { return e.replace('|', '\\|'); }).join('|')); sessionConnectFailed.call(this, error.message, error.code); return; } OT.debug('OT.Session: Received session state from Raptor', sessionState); this.connection = this.connections.get(_socket.id()); if(this.connection) { this.capabilities = this.connection.permissions; } setState('connected'); this.logEvent('Connect', 'Success', 'webSocketServerUrl|userAgent|sdkVersion|chromeFrame', OT.$.map(analyticsPayload, function(e) { return e.replace('|', '\\|'); }).join('|'), {connectionId: this.connection.id}); // 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.archives.on({ add: archiveCreatedHandler, remove: archiveDestroyedHandler, update: archiveUpdatedHandler }, this); this.dispatchEvent( new OT.SessionConnectEvent(OT.Event.names.SESSION_CONNECTED), OT.$.bind(function() { this.connections._triggerAddEvents(); // { id: this.connection.id } this.streams._triggerAddEvents(); // { id: this.stream.id } this.archives._triggerAddEvents(); }, this) ); }, this)); }; getSessionInfo = function() { if (this.is('connecting')) { OT.SessionInfo.get( this, OT.$.bind(onSessionInfoResponse, this), OT.$.bind(function(error) { sessionConnectFailed.call(this, error.message + (error.code ? ' (' + error.code + ')' : ''), error.code); }, this) ); } }; onSessionInfoResponse = function(sessionInfo) { if (this.is('connecting')) { var overrides = OT.properties.sessionInfoOverrides; this.sessionInfo = sessionInfo; if (overrides != null && typeof overrides === 'object') { this.sessionInfo = OT.$.defaults(overrides, this.sessionInfo); console.log('is', this.sessionInfo); } if (this.sessionInfo.partnerId && this.sessionInfo.partnerId !== _apiKey) { this.apiKey = _apiKey = this.sessionInfo.partnerId; var reason = 'Authentication Error: The API key does not match the token or session.'; 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 = OT.$.bind(function(action) { return this.capabilities.permittedTo(action); }, this); dispatchError = OT.$.bind(function(code, message, completionHandler) { OT.dispatchError(code, message, completionHandler, this); }, this); this.logEvent = function(action, variation, payloadType, payload, options) { /* jshint camelcase:false */ var event = { action: action, variation: variation, payload_type: payloadType, payload: payload, session_id: _sessionId, partner_id: _apiKey, widget_id: _widgetId, widget_type: 'Controller' }; if (this.connection && this.connection.id) _connectionId = event.connection_id = this.connection.id; else if (_connectionId) event.connection_id = _connectionId; if (options) event = OT.$.extend(options, event); _analytics.logEvent(event); }; /** * Connects to an OpenTok session. *

* Upon a successful connection, the completion handler (the second parameter of the method) is * invoked without an error object passed in. (If there is an error connecting, the completion * handler is invoked with an error object.) Make sure that you have successfully connected to the * session before calling other methods of the Session object. *

*

* The Session object dispatches a connectionCreated event when any client * (including your own) connects to to the session. *

* *
* Example *
*

* The following code initializes a session and sets up an event listener for when the session * connects: *

*
 *  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 = OT.initSession(apiKey, sessionID);
 *  session.on("sessionConnected", function(sessionConnectEvent) {
 *      //
 *  });
 *  session.connect(token);
 *  
*

*

* In this example, the sessionConnectHandler() function is passed an event * object of type {@link SessionConnectEvent}. *

* *
* Events dispatched: *
* *

* exception (ExceptionEvent) — Dispatched * by the OT class locally in the event of an error. *

*

* connectionCreated (ConnectionEvent) — * Dispatched by the Session object on all clients connected to the session. *

*

* sessionConnected (SessionConnectEvent) * — Dispatched locally by the Session object when the connection is established. *

* * @param {String} token The session token. You generate a session token using our * server-side libraries or the * Dashboard page. For more information, see * Connection token creation. * * @param {Function} completionHandler (Optional) A function to be called when the call to the * connect() method succeeds or fails. This function takes one parameter — * error (see the Error object). * On success, the completionHandler function is not passed any * arguments. On error, the function is passed an error object parameter * (see the Error object). The * error object has two properties: code (an integer) and * message (a string), which identify the cause of the failure. The following * code adds a completionHandler when calling the connect() method: *
  * session.connect(token, function (error) {
  *   if (error) {
  *       console.log(error.message);
  *   } else {
  *     console.log("Connected to session.");
  *   }
  * });
  * 
*

* Note that upon connecting to the session, the Session object dispatches a * sessionConnected event in addition to calling the completionHandler. * The SessionConnectEvent object, which defines the sessionConnected event, * includes connections and streams properties, which * list the connections and streams in the session when you connect. *

* * @see SessionConnectEvent * @method #connect * @memberOf Session */ this.connect = function(token) { if(apiKey == null && arguments.length > 1 && (typeof arguments[0] === 'string' || typeof arguments[0] === 'number') && typeof arguments[1] === 'string') { _apiKey = token.toString(); token = arguments[1]; } // The completion handler is always the last argument. var completionHandler = arguments[arguments.length - 1]; if (this.is('connecting', 'connected')) { OT.warn('OT.Session: Cannot connect, the session is already ' + this.state); return this; } reset.call(this); setState('connecting'); this.token = _token = !OT.$.isFunction(token) && token; // Get a new widget ID when reconnecting. if (_initialConnection) { _initialConnection = false; } else { _widgetId = OT.$.uuid(); } if (completionHandler && OT.$.isFunction(completionHandler)) { this.once('sessionConnected', OT.$.bind(completionHandler, null, null)); this.once('sessionConnectFailed', completionHandler); } if(_apiKey == null || OT.$.isFunction(_apiKey)) { setTimeout(OT.$.bind( sessionConnectFailed, this, 'API Key is undefined. You must pass an API Key to initSession.', OT.ExceptionCodes.AUTHENTICATION_ERROR )); return this; } if (!_sessionId) { setTimeout(OT.$.bind( sessionConnectFailed, this, 'SessionID is undefined. You must pass a sessionID to initSession.', OT.ExceptionCodes.INVALID_SESSION_ID )); return this; } this.apiKey = _apiKey = _apiKey.toString(); // Ugly hack, make sure OT.APIKEY is set if (OT.APIKEY.length === 0) { OT.APIKEY = _apiKey; } var analyticsPayload = [ OT.$.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); return this; }; /** * Disconnects from the OpenTok session. * *

* Calling the disconnect() method ends your connection with the session. In the * course of terminating your connection, it also ceases publishing any stream(s) you were * publishing. *

*

* Session objects on remote clients dispatch streamDestroyed events for any * stream you were publishing. The Session object dispatches a sessionDisconnected * event locally. The Session objects on remote clients dispatch connectionDestroyed * events, letting other connections know you have left the session. The * {@link SessionDisconnectEvent} and {@link StreamEvent} objects that define the * sessionDisconnect and connectionDestroyed events each have a * reason property. The reason 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. *

*

* If the session is not currently connected, calling this method causes a warning to be logged. * See OT.setLogLevel(). *

* *

* Note: If you intend to reuse a Publisher object created using * OT.initPublisher() to publish to different sessions sequentially, call either * Session.disconnect() or Session.unpublish(). Do not call both. * Then call the preventDefault() method of the streamDestroyed or * sessionDisconnected event object to prevent the Publisher object from being * removed from the page. Be sure to call preventDefault() only if the * connection.connectionId property of the Stream object in the event matches the * connection.connectionId 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). *

* *
* Events dispatched: *
*

* sessionDisconnected * (SessionDisconnectEvent) * — Dispatched locally when the connection is disconnected. *

*

* connectionDestroyed (ConnectionEvent) — * Dispatched on other clients, along with the streamDestroyed event (as warranted). *

* *

* streamDestroyed (StreamEvent) — * Dispatched on other clients if streams are lost as a result of the session disconnecting. *

* * @method #disconnect * @memberOf Session */ var disconnect = OT.$.bind(function disconnect(drainSocketBuffer) { if (_socket && _socket.isNot('disconnected')) { if (_socket.isNot('disconnecting')) { setState('disconnecting'); _socket.disconnect(drainSocketBuffer); } } else { reset.call(this); } }, this); this.disconnect = function(drainSocketBuffer) { disconnect(drainSocketBuffer !== void 0 ? drainSocketBuffer : true); }; this.destroy = function(reason) { this.streams.destroy(); this.connections.destroy(); this.archives.destroy(); disconnect(reason !== 'unloaded'); }; /** * The publish() 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 * streamCreated event. *

* * *

You pass a Publisher object as the one parameter of the method. You can initialize a * Publisher object by calling the OT.initPublisher() * method. Before calling Session.publish(). *

* *

This method takes an alternate form: publish([targetElement:String, * properties:Object]):Publisher — In this form, you do not pass a Publisher * object into the function. Instead, you pass in a targetElement (the ID of the * DOM element that the Publisher will replace) and a properties object that * defines options for the Publisher (see OT.initPublisher().) * 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. * *

* 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. *

* *

* 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. *

* *

* The OT object dispatches an exception 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 * generate_token() method of the * OpenTok server-side * libraries or the Dashboard page. * You pass the token string as a parameter of the connect() method of the Session * object. See ExceptionEvent and * OT.on(). *

*

* The application throws an error if the session is not connected. *

* *
Events dispatched:
*

* exception (ExceptionEvent) — Dispatched * by the OT object. This can occur when user's role does not allow publishing (the * code property of event object is set to 1500); it can also occur if the c * onnection fails to connect (the 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. *

*

* streamCreated (StreamEvent) — * 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. *

* *
Example
* *

* The following example publishes a video once the session connects: *

*
  * 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 = OT.initSession(apiKey, sessionID);
  * session.on("sessionConnected", function (event) {
  *     var publisherOptions = {width: 400, height:300, name:"Bob's stream"};
  *     // This assumes that there is a DOM element with the ID 'publisher':
  *     publisher = OT.initPublisher('publisher', publisherOptions);
  *     session.publish(publisher);
  * });
  * session.connect(token);
  * 
* * @param {Publisher} publisher A Publisher object, which you initialize by calling the * OT.initPublisher() method. * * @param {Function} completionHandler (Optional) A function to be called when the call to the * publish() method succeeds or fails. This function takes one parameter — * error. On success, the completionHandler function is not passed any * arguments. On error, the function is passed an error object parameter * (see the Error object). The * error object has two properties: code (an integer) and * message (a string), which identify the cause of the failure. Calling * publish() fails if the role assigned to your token is not "publisher" or * "moderator"; in this case error.code is set to 1500. Calling * publish() also fails the client fails to connect; in this case * error.code is set to 1013. The following code adds a * completionHandler when calling the publish() method: *
  * session.publish(publisher, null, function (error) {
  *   if (error) {
  *     console.log(error.message);
  *   } else {
  *     console.log("Publishing a stream.");
  *   }
  * });
  * 
* * @returns The Publisher object for this stream. * * @method #publish * @memberOf Session */ this.publish = function(publisher, properties, completionHandler) { if(typeof publisher === 'function') { completionHandler = publisher; publisher = undefined; } if(typeof properties === 'function') { completionHandler = properties; properties = undefined; } if (this.isNot('connected')) { /*jshint camelcase:false*/ _analytics.logError(1010, 'OT.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)) { dispatchError(OT.ExceptionCodes.NOT_CONNECTED, 'We need to be connected before you can publish', completionHandler); } 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'); dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, 'This token does not allow publishing. The role must be at least `publisher` ' + 'to enable this functionality', completionHandler); return null; } // If the user has passed in an ID of a element then we create a new publisher. if (!publisher || typeof(publisher)==='string' || OT.$.isElementNode(publisher)) { // Initiate a new Publisher with the new session credentials publisher = OT.initPublisher(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 { dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, 'Session.publish :: First parameter passed in is neither a ' + 'string nor an instance of the Publisher', completionHandler); return; } publisher.once('publishComplete', function(err) { if (err) { dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, 'Session.publish :: ' + err.message, completionHandler); return; } if (completionHandler && OT.$.isFunction(completionHandler)) { completionHandler.apply(null, arguments); } }); // 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 streamDestroyed event. *

* *

* To prevent the Publisher from being removed from the DOM, add an event listener for the * streamDestroyed event dispatched by the Publisher object and call the * preventDefault() method of the event object. *

* *

* Note: If you intend to reuse a Publisher object created using * OT.initPublisher() to publish to different sessions sequentially, call * either Session.disconnect() or Session.unpublish(). Do not call * both. Then call the preventDefault() method of the streamDestroyed * or sessionDisconnected event object to prevent the Publisher object from being * removed from the page. Be sure to call preventDefault() only if the * connection.connectionId property of the Stream object in the event matches the * connection.connectionId 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). *

* *
Events dispatched:
* *

* streamDestroyed (StreamEvent) — * The stream associated with the Publisher has been destroyed. Dispatched on by the * Publisher on on the Publisher's browser. Dispatched by the Session object on * all other connections subscribing to the publisher's stream. *

* *
Example
* * 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. * *
  * <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 = OT.initSession(apiKey, sessionID);
  *     session.on("sessionConnected", function sessionConnectHandler(event) {
  *         // This assumes that there is a DOM element with the ID 'publisher':
  *         publisher = OT.initPublisher('publisher');
  *         session.publish(publisher);
  *     });
  *     session.connect(token);
  *     var publisher;
  *
  *     function unpublish() {
  *         session.unpublish(publisher);
  *     }
  * </script>
  *
  * <body>
  *
  *     <div id="publisherContainer/>
  *     <br/>
  *
  *     <a href="javascript:unpublish()">Stop Publishing</a>
  *
  * </body>
  *
  * 
* * @see publish() * * @see streamDestroyed event * * @param {Publisher} publisher 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, 'unpublished'); }; /** * Subscribes to a stream that is available to the session. You can get an array of * available streams from the streams property of the sessionConnected * and streamCreated events (see * SessionConnectEvent and * StreamEvent). *

*

* 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. *

* *

* The application throws an error if the session is not connected or if the * targetElement does not exist in the HTML DOM. *

* *
Example
* * The following code subscribes to other clients' streams: * *
  * 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 = OT.initSession(apiKey, sessionID);
  * session.on("streamCreated", function(event) {
  *   subscriber = session.subscribe(event.stream, targetElement);
  * });
  * session.connect(token);
  * 
* * @param {Stream} stream The Stream object representing the stream to which we are trying to * subscribe. * * @param {Object} targetElement (Optional) The DOM element or the id attribute of * the existing DOM element used to determine the location of the Subscriber video in the HTML * DOM. See the insertMode property of the properties parameter. If * you do not specify a targetElement, the application appends a new DOM element * to the HTML body. * * @param {Object} properties This is an object that contains the following properties: *
    *
  • audioVolume (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 * setAudioVolume() method of the * Subscriber object. This volume setting affects local playback only; it does not affect * the stream's volume on other clients.
  • * *
  • height (Number) — The desired height, in pixels, of the * displayed Subscriber video stream (default: 198). Note: Use the * height and width properties to set the dimensions * of the Subscriber video; do not set the height and width of the DOM element * (using CSS).
  • * *
  • * insertMode (String) — Specifies how the Subscriber object will * be inserted in the HTML DOM. See the targetElement parameter. This * string can have the following values: *
      *
    • "replace" — The Subscriber object replaces contents of the * targetElement. This is the default.
    • *
    • "after" — The Subscriber object is a new element inserted * after the targetElement in the HTML DOM. (Both the Subscriber and targetElement * have the same parent element.)
    • *
    • "before" — The Subscriber object is a new element inserted * before the targetElement in the HTML DOM. (Both the Subsciber and targetElement * have the same parent element.)
    • *
    • "append" — The Subscriber object is a new element added as a * child of the targetElement. If there are other child elements, the Subscriber is * appended as the last child element of the targetElement.
    • *
    *
  • * *
  • * style (Object) — An object containing properties that define the initial * appearance of user interface controls of the Subscriber. The style object * includes the following properties: *
      *
    • audioLevelDisplayMode (String) — How to display the audio level * indicator. Possible values are: "auto" (the indicator is displayed when the * video is disabled), "off" (the indicator is not displayed), and * "on" (the indicator is always displayed).
    • * *
    • backgroundImageURI (String) — A URI for an image to display as * the background image when a video is not displayed. (A video may not be displayed if * you call subscribeToVideo(false) on the Subscriber object). You can pass an * http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the * data URI scheme (instead of http or https) and pass in base-64-encrypted * PNG data, such as that obtained from the * Subscriber.getImgData() method. For example, * you could set the property to "data:VBORw0KGgoAA...", where the portion of * the string after "data:" is the result of a call to * Subscriber.getImgData(). If the URL or the image data is invalid, the * property is ignored (the attempt to set the image fails silently). *

      * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer), * you cannot set the backgroundImageURI style to a string larger than * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this * limitation, you cannot set the backgroundImageURI style to a string obtained * with the getImgData() method. *

    • * *
    • buttonDisplayMode (String) — How to display the speaker controls * Possible values are: "auto" (controls are displayed when the stream is first * displayed and when the user mouses over the display), "off" (controls are not * displayed), and "on" (controls are always displayed).
    • * *
    • nameDisplayMode (String) — Whether to display the stream 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 always displayed).
    • * *
    • videoDisabledDisplayMode (String) — Whether to display the video * disabled indicator and video disabled warning icons for a Subscriber. These icons * indicate that the video has been disabled (or is in risk of being disabled for * the warning icon) due to poor stream quality. This style only applies to the Subscriber * object. Possible values are: "auto" (the icons are automatically when the * displayed video is disabled or in risk of being disabled due to poor stream quality), * "off" (do not display the icons), and "on" (display the * icons). The default setting is "auto"
    • *
    *
  • * *
  • subscribeToAudio (Boolean) — Whether to initially subscribe to audio * (if available) for the stream (default: true).
  • * *
  • subscribeToVideo (Boolean) — Whether to initially subscribe to video * (if available) for the stream (default: true).
  • * *
  • width (Number) — The desired width, in pixels, of the * displayed Subscriber video stream (default: 264). Note: Use the * height and width properties to set the dimensions * of the Subscriber video; do not set the height and width of the DOM element * (using CSS).
  • * *
* * @param {Function} completionHandler (Optional) A function to be called when the call to the * subscribe() method succeeds or fails. This function takes one parameter — * error. On success, the completionHandler function is not passed any * arguments. On error, the function is passed an error object, defined by the * Error class, has two properties: code (an integer) and * message (a string), which identify the cause of the failure. The following * code adds a completionHandler when calling the subscribe() method: *
  * session.subscribe(stream, "subscriber", null, function (error) {
  *   if (error) {
  *     console.log(error.message);
  *   } else {
  *     console.log("Subscribed to stream: " + stream.id);
  *   }
  * });
  * 
* * @signature subscribe(stream, targetElement, properties, completionHandler) * @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) { if (!this.connection || !this.connection.connectionId) { dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, 'Session.subscribe :: Connection required to subscribe', completionHandler); return; } if (!stream) { dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, 'Session.subscribe :: stream cannot be null', completionHandler); return; } if (!stream.hasOwnProperty('streamId')) { dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, 'Session.subscribe :: invalid stream object', completionHandler); return; } if(typeof targetElement === 'function') { completionHandler = targetElement; targetElement = undefined; } if(typeof properties === 'function') { completionHandler = properties; properties = undefined; } var subscriber = new OT.Subscriber(targetElement, OT.$.extend(properties || {}, { session: this })); subscriber.once('subscribeComplete', function(err) { if (err) { dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, 'Session.subscribe :: ' + err.message, completionHandler); return; } if (completionHandler && OT.$.isFunction(completionHandler)) { completionHandler.apply(null, arguments); } }); 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. * *
Example
*

* The following code subscribes to other clients' streams. For each stream, the code also * adds an Unsubscribe link. *

*
  * 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 = OT.initSession(apiKey, sessionID);
  * session.on("streamCreated", function(event) {
  *     var stream = event.stream;
  *     displayStream(stream);
  * });
  * session.connect(token);
  *
  * function displayStream(stream) {
  *     var div = document.createElement('div');
  *     div.setAttribute('id', 'stream' + stream.streamId);
  *
  *     var subscriber = session.subscribe(stream, div);
  *     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);
  *         }
  *     }
  * }
  * 
* * @param {Subscriber} subscriber The Subscriber object to unsubcribe. * * @see subscribe() * * @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 unsubscribe() * @see Subscriber * @see StreamEvent * @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 * null if there is no local Publisher object * for the specified stream. * * @see forceUnpublish() * @see Subscriber * @see StreamEvent * * @method #getPublisherForStream * @memberOf Session */ this.getPublisherForStream = function(stream) { var streamId, errorMsg; 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._ = { jsepCandidateP2p: function(streamId, subscriberId, candidate) { return _socket.jsepCandidateP2p(streamId, subscriberId, candidate); }, jsepCandidate: function(streamId, candidate) { return _socket.jsepCandidate(streamId, candidate); }, jsepOffer: function(streamId, offerSdp) { return _socket.jsepOffer(streamId, offerSdp); }, jsepOfferP2p: function(streamId, subscriberId, offerSdp) { return _socket.jsepOfferP2p(streamId, subscriberId, offerSdp); }, jsepAnswer: function(streamId, answerSdp) { return _socket.jsepAnswer(streamId, answerSdp); }, jsepAnswerP2p: function(streamId, subscriberId, answerSdp) { return _socket.jsepAnswerP2p(streamId, subscriberId, answerSdp); }, // session.on("signal", function(SignalEvent)) // session.on("signal:{type}", function(SignalEvent)) dispatchSignal: OT.$.bind(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); }, this), subscriberCreate: function(stream, subscriber, channelsToSubscribeTo, completion) { return _socket.subscriberCreate(stream.id, subscriber.widgetId, channelsToSubscribeTo, completion); }, subscriberDestroy: function(stream, subscriber) { return _socket.subscriberDestroy(stream.id, subscriber.widgetId); }, subscriberUpdate: function(stream, subscriber, attributes) { return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes); }, subscriberChannelUpdate: function(stream, subscriber, channel, attributes) { return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id, attributes); }, streamCreate: OT.$.bind(function(name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo, frameRate, completion) { _socket.streamCreate( name, orientation, encodedWidth, encodedHeight, hasAudio, hasVideo, frameRate, OT.Config.get('bitrates', 'min', OT.APIKEY), OT.Config.get('bitrates', 'max', OT.APIKEY), completion ); }, this), streamDestroy: function(streamId) { _socket.streamDestroy(streamId); }, streamChannelUpdate: function(stream, channel, attributes) { _socket.streamChannelUpdate(stream.id, channel.id, attributes); } }; /** * Sends a signal to each client or a specified client in the session. Specify a * to property of the signal parameter to limit the signal to * be sent to a specific client; otherwise the signal is sent to each client connected to * the session. *

* The following example sends a signal of type "foo" with a specified data payload ("hello") * to all clients connected to the session: *

  * session.signal({
  *     type: "foo",
  *     data: "hello"
  *   },
  *   function(error) {
  *     if (error) {
  *       console.log("signal error: " + error.message);
  *     } else {
  *       console.log("signal sent");
  *     }
  *   }
  * );
  * 
*

* Calling this method without specifying a recipient client (by setting the to * property of the signal parameter) results in multiple signals sent (one to each * client in the session). For information on charges for signaling, see the * OpenTok pricing page. *

* The following example sends a signal of type "foo" with a data payload ("hello") to a * specific client connected to the session: *

  * session.signal({
  *     type: "foo",
  *     to: recipientConnection; // a Connection object
  *     data: "hello"
  *   },
  *   function(error) {
  *     if (error) {
  *       console.log("signal error: " + error.message);
  *     } else {
  *       console.log("signal sent");
  *     }
  *   }
  * );
  * 
*

* Add an event handler for the signal event to listen for all signals sent in * the session. Add an event handler for the signal:type event to listen for * signals of a specified type only (replace type, in signal:type, * with the type of signal to listen for). The Session object dispatches these events. (See * events.) * * @param {Object} signal An object that contains the following properties defining the signal: *

    *
  • data — (String) The data to send. The limit to the length of data * string is 8kB. Do not set the data string to null or * undefined.
  • *
  • to — (Connection) A Connection * object corresponding to the client 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.
  • *
  • type — (String) The type of the signal. You can use the type to * filter signals when setting an event handler for the signal:type event * (where you replace type with the type string). The maximum length of the * type string is 128 characters, and it must contain only letters (A-Z and a-z), * numbers (0-9), '-', '_', and '~'.
  • * *
* *

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.

* * @param {Function} completionHandler A function that is called when sending the signal * succeeds or fails. This function takes one parameter — error. * On success, the completionHandler function is not passed any * arguments. On error, the function is passed an error object, defined by the * Error class. The error object has the following * properties: * *
    *
  • code — (Number) An error code, which can be one of the following: * * * * * * * * * * * * * *
    400 One of the signal properties — data, type, or to — * is invalid.
    404 The client specified by the to property is not connected to * the session.
    413 The type string exceeds the maximum length (128 bytes), * or the data string exceeds the maximum size (8 kB).
    500 You are not connected to the OpenTok session.
    *
  • *
  • message — (String) A description of the error.
  • *
* *

Note that the completionHandler success result (error == null) * indicates that the options passed into the Session.signal() method are valid * and the signal was sent. It does not indicate that the signal was successfully * received by any of the intended recipients. * * @method #signal * @memberOf Session * @see signal and signal:type events */ this.signal = function(options, completion) { var _options = options, _completion = completion; if (OT.$.isFunction(_options)) { _completion = _options; _options = null; } if (this.isNot('connected')) { var notConnectedErrorMsg = 'Unable to send signal - you are not connected to the session.'; dispatchError(500, notConnectedErrorMsg, _completion); return; } _socket.signal(_options, _completion); if (options && options.data && (typeof(options.data) !== 'string')) { OT.warn('Signaling of anything other than Strings is deprecated. ' + 'Please update the data property to be a string.'); } this.logEvent('signal', 'send', 'type', (options && options.data) ? typeof(options.data) : 'null'); }; /** * Forces a remote connection to leave the session. * *

* The forceDisconnect() method is normally used as a moderation tool * to remove users from an ongoing session. *

*

* When a connection is terminated using the forceDisconnect(), * sessionDisconnected, connectionDestroyed and * streamDestroyed events are dispatched in the same way as they * would be if the connection had terminated itself using the disconnect() * method. However, the reason property of a {@link ConnectionEvent} or * {@link StreamEvent} object specifies "forceDisconnected" as the reason * for the destruction of the connection and stream(s). *

*

* While you can use the forceDisconnect() method to terminate your own connection, * calling the disconnect() method is simpler. *

*

* The OT object dispatches an exception 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 * generate_token() method using * OpenTok * server-side libraries or the * Dashboard page. * See ExceptionEvent and OT.on(). *

*

* The application throws an error if the session is not connected. *

* *
Events dispatched:
* *

* connectionDestroyed (ConnectionEvent) — * On clients other than which had the connection terminated. *

*

* exception (ExceptionEvent) — * The user's role does not allow forcing other user's to disconnect (event.code = * 1530), * or the specified stream is not publishing to the session (event.code = 1535). *

*

* sessionDisconnected * (SessionDisconnectEvent) — * On the client which has the connection terminated. *

*

* streamDestroyed (StreamEvent) — * If streams are stopped as a result of the connection ending. *

* * @param {Connection} connection The connection to be disconnected from the session. * This value can either be a Connection object or a connection * ID (which can be obtained from the connectionId property of the Connection object). * * @param {Function} completionHandler (Optional) A function to be called when the call to the * forceDiscononnect() method succeeds or fails. This function takes one parameter * — error. On success, the completionHandler function is * not passed any arguments. On error, the function is passed an error object * parameter. The error object, defined by the Error * class, has two properties: code (an integer) * and message (a string), which identify the cause of the failure. * Calling forceDisconnect() fails if the role assigned to your * token is not "moderator"; in this case error.code is set to 1520. The following * code adds a completionHandler when calling the forceDisconnect() * method: *
  * session.forceDisconnect(connection, function (error) {
  *   if (error) {
  *       console.log(error);
  *     } else {
  *       console.log("Connection forced to disconnect: " + connection.id);
  *     }
  *   });
  * 
* * @method #forceDisconnect * @memberOf Session */ this.forceDisconnect = function(connectionOrConnectionId, completionHandler) { if (this.isNot('connected')) { var notConnectedErrorMsg = 'Cannot call forceDisconnect(). You are not ' + 'connected to the session.'; dispatchError(OT.ExceptionCodes.NOT_CONNECTED, notConnectedErrorMsg, completionHandler); return; } 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; _socket.forceDisconnect(connectionId, function(err) { if (err) { dispatchError(OT.ExceptionCodes.UNABLE_TO_FORCE_DISCONNECT, notPermittedErrorMsg, completionHandler); } else if (completionHandler && OT.$.isFunction(completionHandler)) { completionHandler.apply(null, arguments); } }); } else { // if this throws an error the handleJsException won't occur dispatchError(OT.ExceptionCodes.UNABLE_TO_FORCE_DISCONNECT, notPermittedErrorMsg, completionHandler); } }; /** * Forces the publisher of the specified stream to stop publishing the stream. * *

* Calling this method causes the Session object to dispatch a streamDestroyed * event on all clients that are subscribed to the stream (including the client that is * publishing the stream). The reason property of the StreamEvent object is * set to "forceUnpublished". *

*

* The OT object dispatches an exception 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 generate_token() * method using the * OpenTok * server-side libraries or the Dashboard * page. * You pass the token string as a parameter of the connect() method of the Session * object. See ExceptionEvent and * OT.on(). *

* *
Events dispatched:
* *

* exception (ExceptionEvent) — * The user's role does not allow forcing other users to unpublish. *

*

* streamDestroyed (StreamEvent) — * 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. *

* * @param {Stream} stream The stream to be unpublished. * * @param {Function} completionHandler (Optional) A function to be called when the call to the * forceUnpublish() method succeeds or fails. This function takes one parameter * — error. On success, the completionHandler function is * not passed any arguments. On error, the function is passed an error object * parameter. The error object, defined by the Error * class, has two properties: code (an integer) * and message (a string), which identify the cause of the failure. Calling * forceUnpublish() fails if the role assigned to your token is not "moderator"; * in this case error.code is set to 1530. The following code adds a * completionHandler when calling the forceUnpublish() method: *
  * session.forceUnpublish(stream, function (error) {
  *   if (error) {
  *       console.log(error);
  *     } else {
  *       console.log("Connection forced to disconnect: " + connection.id);
  *     }
  *   });
  * 
* * @method #forceUnpublish * @memberOf Session */ this.forceUnpublish = function(streamOrStreamId, completionHandler) { if (this.isNot('connected')) { var notConnectedErrorMsg = 'Cannot call forceUnpublish(). You are not ' + 'connected to the session.'; dispatchError(OT.ExceptionCodes.NOT_CONNECTED, notConnectedErrorMsg, completionHandler); return; } 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; _socket.forceUnpublish(stream.id, function(err) { if (err) { dispatchError(OT.ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH, notPermittedErrorMsg, completionHandler); } else if (completionHandler && OT.$.isFunction(completionHandler)) { completionHandler.apply(null, arguments); } }); } else { // if this throws an error the handleJsException won't occur dispatchError(OT.ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH, notPermittedErrorMsg, completionHandler); } }; this.getStateManager = function() { OT.warn('Fixme: Have not implemented session.getStateManager'); }; this.isConnected = function() { return this.is('connected'); }; this.capabilities = new OT.Capabilities([]); /** * Dispatched when an archive recording of the session starts. * * @name archiveStarted * @event * @memberof Session * @see ArchiveEvent * @see Archiving overview. */ /** * Dispatched when an archive recording of the session stops. * * @name archiveStopped * @event * @memberof Session * @see ArchiveEvent * @see Archiving overview. */ /** * A new client (including your own) has connected to the session. * @name connectionCreated * @event * @memberof Session * @see ConnectionEvent * @see OT.initSession() */ /** * A client, other than your own, has disconnected from the session. * @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 connect() method of a Session * object. Before calling the connect() method, initialize the session by * calling the OT.initSession() method. For a code example and more details, * see Session.connect(). * @name sessionConnected * @event * @memberof Session * @see SessionConnectEvent * @see Session.connect() * @see OT.initSession() */ /** * The client has disconnected from the session. This event may be dispatched asynchronously * in response to a successful call to the disconnect() 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. *

* The default behavior is that all Subscriber objects are unsubscribed and removed from the * HTML DOM. Each Subscriber object dispatches a destroyed event when the element is * removed from the HTML DOM. If you call the preventDefault() method in the event * listener for the sessionDisconnect event, the default behavior is prevented, and * you can, optionally, clean up Subscriber objects using your own code. * * @name sessionDisconnected * @event * @memberof Session * @see Session.disconnect() * @see Session.forceDisconnect() * @see SessionDisconnectEvent */ /** * A new stream, published by another client, has been created on this session. For streams * published by your own client, the Publisher object dispatches a streamCreated * event. For a code example and more details, see {@link StreamEvent}. * @name streamCreated * @event * @memberof Session * @see StreamEvent * @see Session.publish() */ /** * A stream from another client has stopped publishing to the session. *

* The default behavior is that all Subscriber objects that are subscribed to the stream are * unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a * destroyed event when the element is removed from the HTML DOM. If you call the * preventDefault() method in the event listener for the * streamDestroyed event, the default behavior is prevented and you can clean up * Subscriber objects using your own code. See * Session.getSubscribersForStream(). *

* For streams published by your own client, the Publisher object dispatches a * streamDestroyed event. *

* 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 * Publisher.publishAudio() and * Publisher.publishVideo()); or the * videoDimensions property of the Stream * object has changed (see Stream.videoDimensions). *

* Note that a subscriber's video can be disabled or enabled for reasons other than the * publisher disabling or enabling it. A Subscriber object dispatches videoDisabled * and videoEnabled events in all conditions that cause the subscriber's stream * to be disabled or enabled. * * @name streamPropertyChanged * @event * @memberof Session * @see StreamPropertyChangedEvent * @see Publisher.publishAudio() * @see Publisher.publishVideo() * @see Stream.hasAudio * @see Stream.hasVideo * @see Stream.videoDimensions * @see Subscriber videoDisabled event * @see Subscriber videoEnabled event */ /** * A signal was received from the session. The SignalEvent * class defines this event object. It includes the following properties: *

    *
  • data — (String) The data string sent with the signal (if there * is one).
  • *
  • from — (Connection) The Connection * corresponding to the client that sent with the signal.
  • *
  • type — (String) The type assigned to the signal (if there is * one).
  • *
*

* You can register to receive all signals sent in the session, by adding an event handler * for the signal event. For example, the following code adds an event handler * to process all signals sent in the session: *

	 * session.on("signal", function(event) {
	 *   console.log("Signal sent from connection: " + event.from.id);
	 *   console.log("Signal data: " + event.data);
	 * });
	 * 
*

You can register for signals of a specfied type by adding an event handler for the * signal:type event (replacing type with the actual type string * to filter on). * * @name signal * @event * @memberof Session * @see Session.signal() * @see SignalEvent * @see signal:type event */ /** * A signal of the specified type was received from the session. The * SignalEvent class defines this event object. * It includes the following properties: *

    *
  • data — (String) The data string sent with the signal.
  • *
  • from — (Connection) The Connection * corresponding to the client that sent with the signal.
  • *
  • type — (String) The type assigned to the signal (if there is one). *
  • *
*

* You can register for signals of a specfied type by adding an event handler for the * signal:type event (replacing type with the actual type string * to filter on). For example, the following code adds an event handler for signals of * type "foo": *

	 * session.on("signal:foo", function(event) {
	 *   console.log("foo signal sent from connection " + event.from.id);
	 *   console.log("Signal data: " + event.data);
	 * });
	 * 
*

* You can register to receive all signals sent in the session, by adding an event * handler for the signal event. * * @name signal:type * @event * @memberof Session * @see Session.signal() * @see SignalEvent * @see signal event */ }; })(window); (function() { var txt = function(text) { return document.createTextNode(text); }; var el = function(attr, children, tagName) { var el = OT.$.createElement(tagName || 'div', attr, children); el.on = OT.$.bind(OT.$.on, OT.$, el); return el; }; function DevicePickerController(opts) { var destroyExistingPublisher, publisher, devicesById; this.change = OT.$.bind(function() { destroyExistingPublisher(); var settings; this.pickedDevice = devicesById[opts.selectTag.value]; if(!this.pickedDevice) { console.log('No device for', opts.mode, opts.selectTag.value); return; } settings = { insertMode: 'append', name: this.pickedDevice.label, audioSource: null, videoSource: null, width: 220, height: 165 }; settings[opts.mode] = this.pickedDevice.deviceId; console.log('initPublisher', opts.previewTag, settings); var pub = OT.initPublisher(opts.previewTag, settings); pub.on({ accessDialogOpened: function(event) { event.preventDefault(); }, accessDialogClosed: function() { }, accessAllowed: function() { }, accessDenied: function(event) { event.preventDefault(); } }); publisher = pub; }, this); this.cleanup = destroyExistingPublisher = function() { if(publisher) { publisher.destroy(); publisher = void 0; } }; var disableSelector = function (opt, str) { opt.innerHTML = ''; opt.appendChild(el({}, txt(str), 'option')); opt.setAttribute('disabled', ''); }; var addDevice = function (device) { devicesById[device.deviceId] = device; return el({ value: device.deviceId }, txt(device.label), 'option'); }; this.setDeviceList = OT.$.bind(function (devices) { opts.selectTag.innerHTML = ''; devicesById = {}; if(devices.length > 0) { devices.map(addDevice).map(OT.$.bind(opts.selectTag.appendChild, opts.selectTag)); opts.selectTag.removeAttribute('disabled'); } else { disableSelector(opts.selectTag, 'No devices'); } this.change(); }, this); this.setLoading = function() { disableSelector(opts.selectTag, 'Loading...'); }; OT.$.on(opts.selectTag, 'change', this.change); } OT.HardwareSetup = function(targetElement, options, callback) { var camera, microphone, setupDOM, setState; setState = OT.$.statable(this, ['getDevices', 'chooseDevices', 'destroyed'], 'getDevices'); this.audioSource = function() { return microphone && microphone.pickedDevice; }; this.videoSource = function() { return camera && camera.pickedDevice; }; this.destroy = OT.$.bind(function() { if(this.is('destroyed')) { return; } if(camera) { camera.cleanup(); } if(microphone) { microphone.cleanup(); } if(this.is('chooseDevices')) { targetElement.parentNode.removeChild(targetElement); } setState('destroyed'); }, this); if(targetElement == null) { callback(new Error('You must provide a targetElement')); return; } if(!OT.$.hasCapabilities('getMediaDevices')) { callback(new Error('This browser does not support getMediaDevices APIs')); return; } var camSelector, camPreview, micSelector, micPreview, container; camSelector = el({ style: 'width: 100%' }, '', 'select'); camPreview = el({ style: 'background-color: #000; margin-left: 100px; width: 220px; height: 165px;' }, ''), micSelector = el({ style: 'width: 100%' }, '', 'select'), micPreview = el({ style: 'background-color: #000; margin-left: 100px; width: 220px; height: 165px;' }, ''); container = el({ id: 'OT_' + OT.$.uuid(), style: 'border: 1px solid #000; padding: 10px; width: 320px;' }, [ el({ style: 'padding: 0 0 10px; overflow: auto; text-align: right; ' }, [ el({ style: 'float: left; width: 90px; padding-right: 10px; line-height: 160%;' }, 'Camera'), el({ style: 'margin-left: 100px; ' }, camSelector), camPreview ]), el({ style: 'overflow: auto; text-align: right;' }, [ el({ style: 'float: left; width: 90px; padding-right: 10px; line-height: 160%;' }, 'Microphone'), el({ style: 'margin-left: 100px; ' }, micSelector), micPreview ]) ]); camera = new DevicePickerController({ selectTag: camSelector, previewTag: camPreview, mode: 'videoSource' }); microphone = new DevicePickerController({ selectTag: micSelector, previewTag: micPreview, mode: 'audioSource' }); camera.setLoading(); microphone.setLoading(); OT.getDevices(OT.$.bind(function(error, devices) { if (error) { callback(error); return; } if(this.is('destroyed')) { return; // They destroyed us before we got the devices, bail. } setupDOM(); camera.setDeviceList(devices.filter(function(device) { return device.kind === 'videoinput'; })); microphone.setDeviceList(devices.filter(function(device) { return device.kind === 'audioinput'; })); setState('chooseDevices'); }, this)); setupDOM = function() { var insertMode = options.insertMode; if(!(insertMode == null || insertMode === 'replace')) { if(insertMode === 'append') { targetElement.appendChild(container); targetElement = container; } else if(insertMode === 'before') { targetElement.parentNode.insertBefore(container, targetElement); targetElement = container; } else if(insertMode === 'after') { targetElement.parentNode.insertBefore(container, targetElement.nextSibling); targetElement = container; } } else { OT.$.emptyElement(targetElement); if(targetElement.getAttribute('id') == null) { targetElement.setAttribute('id', container.getAttribute('id')); } for(var key in container.style) { targetElement.style[key] = container.style[key]; } while(container.childNodes.length > 0) { targetElement.appendChild(container.firstChild); } } }; }; OT.initHardwareSetup = function(targetElement, options, callback) { return new OT.HardwareSetup(targetElement, options, callback); }; })(); !(function() { 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(){ /*global define*/ // 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);