/**
* @license OpenTok JavaScript Library v2.2.5
* http://www.tokbox.com/
*
* Copyright (c) 2014 TokBox, Inc.
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
* Date: May 22 07:14:18 2014
*/
(function(window) {
if (!window.OT) window.OT = {};
OT.properties = {
version: 'v2.2.5', // The current version (eg. v2.0.4) (This is replaced by gradle)
build: '12d9384', // 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 loggging URL to use if we're using SSL
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('26'),
chrome: parseFloat('32')
}
};
})(window);
/**
* @license Common JS Helpers on OpenTok 0.2.0 1f056b9 master
* http://www.tokbox.com/
*
* Copyright (c) 2014 TokBox, Inc.
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
* Date: May 19 04:04:43 2014
*
*/
// OT Helper Methods
//
// helpers.js <- the root file
// helpers/lib/{helper topic}.js <- specialised helpers for specific tasks/topics
// (i.e. video, dom, etc)
//
// @example Getting a DOM element by it's id
// var element = OTHelpers('domId');
//
// @example Testing for web socket support
// if (OT.supportsWebSockets()) {
// // do some stuff with websockets
// }
//
/*jshint browser:true, smarttabs:true*/
!(function(window, undefined) {
var OTHelpers = function(domId) {
return document.getElementById(domId);
};
var previousOTHelpers = window.OTHelpers;
window.OTHelpers = OTHelpers;
OTHelpers.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 true if the client supports WebSockets, false otherwise.
OTHelpers.supportsWebSockets = function() {
return 'WebSocket' in window;
};
// Returns the number of millisceonds since the the UNIX epoch, this is functionally
// equivalent to executing new Date().getTime().
//
// Where available, we use 'performance.now' which is more accurate and reliable,
// otherwise we default to new Date().getTime().
OTHelpers.now = (function() {
var performance = window.performance || {},
navigationStart,
now = performance.now ||
performance.mozNow ||
performance.msNow ||
performance.oNow ||
performance.webkitNow;
if (now) {
now = 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
* The following code adds an event handler for one event:
* If you pass in multiple event names and a handler method, the handler is
* registered for each of those events: You can also pass in a third
* 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:
*
* If you do not add a handler for an event, the event is ignored locally.
* If you pass in one event name and a handler method, the handler is removed for that
* event: If you pass in multiple event names and a handler method, the handler is removed for
* those events: If you pass in an event name (or names) and no handler method, all handlers are
* removed for those events: If you pass in no arguments, all event handlers are removed for all events
* dispatched by the object:
* 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:
*
* The following code adds a one-time event handler for the If you pass in multiple event names and a handler method, the handler is registered
* for each of those events: You can also pass in a third
* 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:
*
* 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
* Removes an event listener for a specific event.
*
*
*
* Throws an exception if the
* Calling
* 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
* 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);
});
}, 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];
}
}, 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.
*
*
* obj.on("eventName", function (event) {
* // This is the event handler.
* });
*
*
*
* obj.on("eventName1 eventName2",
* function (event) {
* // This is the event handler.
* });
*
*
* 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);
*
*
*
* obj.on(
* {
* eventName1: function (event) {
* // This is the handler for eventName1.
* },
* eventName2: function (event) {
* // This is the handler for eventName2.
* }
* },
* obj);
*
*
* 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.
*
* obj.off("eventName", eventHandler);
*
* obj.off("eventName1 eventName2", eventHandler);
*
* obj.off("event1Name event2Name");
*
* obj.off();
*
*
* 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.
*
* accessAllowed event:
*
* obj.once("eventName", function (event) {
* // This is the event handler.
* });
*
*
* obj.once("eventName1 eventName2"
* function (event) {
* // This is the event handler.
* });
*
*
* 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);
*
*
*
* 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.
* listener name is invalid.
* 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.
* listener name is invalid.
* 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) {
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;
};
// Helper function for adding event listeners to dom elements.
// WARNING: This doesn't preserve event types, your handler could be getting all kinds of different
// parameters depending on the browser. You also may have different scopes depending on the browser
// and bubbling and cancelable are not supported.
OTHelpers.on = function(element, eventName, handler) {
if (element.addEventListener) {
element.addEventListener(eventName, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + eventName, handler);
} else {
var oldHandler = element["on"+eventName];
element["on"+eventName] = function() {
handler.apply(this, arguments);
if (oldHandler) oldHandler.apply(this, arguments);
};
}
return element;
};
// Helper function for removing event listeners from dom elements.
OTHelpers.off = function(element, eventName, handler) {
if (element.removeEventListener) {
element.removeEventListener (eventName, handler,false);
}
else if (element.detachEvent) {
element.detachEvent("on" + eventName, handler);
}
};
// Detects when an element is not part of the document flow because it or one of it's ancesters has display:none.
OTHelpers.isDisplayNone = function(element) {
if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return true;
if (element.parentNode && element.parentNode.style) return OTHelpers.isDisplayNone(element.parentNode);
return false;
};
OTHelpers.findElementWithDisplayNone = function(element) {
if ( (element.offsetWidth === 0 || element.offsetHeight === 0) && OTHelpers.css(element, 'display') === 'none') return element;
if (element.parentNode && element.parentNode.style) return OTHelpers.findElementWithDisplayNone(element.parentNode);
return null;
};
function objectHasProperties(obj) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) return true;
}
return false;
}
// Allows an +onChange+ callback to be triggered when specific style properties
// of +element+ are notified. The callback accepts a single parameter, which is
// a hash where the keys are the style property that changed and the values are
// an array containing the old and new values ([oldValue, newValue]).
//
// Width and Height changes while the element is display: none will not be
// fired until such time as the element becomes visible again.
//
// This function returns the MutationObserver itself. Once you no longer wish
// to observe the element you should call disconnect on the observer.
//
// Observing changes:
// // observe changings to the width and height of object
// dimensionsObserver = OTHelpers.observeStyleChanges(object, ['width', 'height'], function(changeSet) {
// OT.debug("The new width and height are " + changeSet.width[1] + ',' + changeSet.height[1]);
// });
//
// Cleaning up
// // stop observing changes
// dimensionsObserver.disconnect();
// dimensionsObserver = null;
//
OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
var oldStyles = {};
var getStyle = function getStyle(style) {
switch (style) {
case 'width':
return OTHelpers.width(element);
case 'height':
return OTHelpers.height(element);
default:
return OTHelpers.css(element);
}
};
// get the inital values
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]) {
// OT.debug("CHANGED " + style + ": " + oldStyles[style] + " -> " + newValue);
changeSet[style] = [oldStyles[style], newValue];
oldStyles[style] = newValue;
}
});
});
if (objectHasProperties(changeSet)) {
// Do this after so as to help avoid infinite loops of mutations.
OTHelpers.callAsync(function() {
onChange.call(null, changeSet);
});
}
});
observer.observe(element, {
attributes:true,
attributeFilter: ['style'],
childList:false,
characterData:false,
subtree:false
});
return observer;
};
// trigger the +onChange+ callback whenever
// 1. +element+ is removed
// 2. or an immediate child of +element+ is removed.
//
// This function returns the MutationObserver itself. Once you no longer wish
// to observe the element you should call disconnect on the observer.
//
// Observing changes:
// // observe changings to the width and height of object
// nodeObserver = OTHelpers.observeNodeOrChildNodeRemoval(object, function(removedNodes) {
// OT.debug("Some child nodes were removed");
// 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');
var wrappedCallback = function() {
var doc = domElement.contentDocument || domElement.contentWindow.document;
doc.body.style.backgroundColor = 'transparent';
doc.body.style.border = 'none';
callback(
domElement.contentWindow,
doc
);
};
document.body.appendChild(domElement);
if(OTHelpers.browserVersion().iframeNeedsLoad) {
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; iOT.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.
* 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:
*
*
*
* OT.NONE API logging is disabled.
* OT.ERROR Logging of errors only.
* OT.WARN Logging of warnings and errors.
* OT.INFO Logging of other useful information, in addition to
* warnings and errors.
* OT.LOG Logging of OT.log() messages, in addition
* to OpenTok info, warning,
* and error messages.
* OT.DEBUG Fine-grained logging of all API actions, as well as
* OT.log() messages.
*
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(window) {
// IMPORTANT This file should be included straight after helpers.js
if (!window.OT) window.OT = {};
if (!OT.properties) {
throw new Error('OT.properties does not exist, please ensure that you include a valid ' +
'properties file.');
}
// Consumes and overwrites OT.properties. Makes it better and stronger!
OT.properties = function(properties) {
var props = OT.$.clone(properties);
props.debug = properties.debug === 'true' || properties.debug === true;
props.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true;
if (props.supportSSL && (window.location.protocol.indexOf('https') >= 0 ||
window.location.protocol.indexOf('chrome-extension') >= 0)) {
props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version;
props.loggingURL = props.loggingURLSSL;
} else {
props.assetURL = props.cdnURL + '/webrtc/' + props.version;
}
props.configURL = props.assetURL + '/js/dynamic_config.min.js';
props.cssURL = props.assetURL + '/css/ot.min.css';
return props;
}(OT.properties);
})(window);
!(function() {
//--------------------------------------
// 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 = _onLoad.bind(this);
_head.appendChild(_script);
},1);
_loadTimer = setTimeout(function() {
_this._onLoadTimeout();
}, this.loadTimeout);
},
_onLoadTimeout: function() {
_cleanup();
OT.warn('TB DynamicConfig failed to load in ' + _this.loadTimeout + ' ms');
this.trigger('dynamicConfigLoadFailed');
},
isLoaded: function() {
return _loaded;
},
reset: function() {
_cleanup();
_loaded = false;
_global = {};
_partners = {};
},
// This is public so that the dynamic config file can load itself.
// Using it for other purposes is discouraged, but not forbidden.
replaceWith: function(config) {
_cleanup();
if (!config) config = {};
_global = config.global || {};
_partners = config.partners || {};
if (!_loaded) _loaded = true;
this.trigger('dynamicConfigChanged');
},
// @example Get the value that indicates whether exceptionLogging is enabled
// OT.Config.get('exceptionLogging', 'enabled');
//
// @example Get a key for a specific partner, fallback to the default if there is
// no key for that partner
// OT.Config.get('exceptionLogging', 'enabled', 'apiKey');
//
get: function(moduleName, key, apiKey) {
var module = _getModule(moduleName, apiKey);
return module ? module[key] : null;
}
};
OT.$.eventing(_this);
return _this;
})();
})(window);
!(function() {
/*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 (!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;
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;
// 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 = removedNodes.some(function(node) {
return node === videoContainer || node.nodeName === 'VIDEO';
});
if (videoRemoved) {
videoElement.destroy();
videoElement = null;
}
if (videoContainer) {
OT.$.removeElement(videoContainer);
videoContainer = null;
}
if (dimensionsObserver) {
dimensionsObserver.disconnect();
dimensionsObserver = null;
}
if (videoObserver) {
videoObserver.disconnect();
videoObserver = null;
}
});
this.destroy = function() {
if (dimensionsObserver) {
dimensionsObserver.disconnect();
dimensionsObserver = null;
}
if (videoObserver) {
videoObserver.disconnect();
videoObserver = null;
}
if (videoElement) {
videoElement.destroy();
videoElement = null;
}
if (container) {
OT.$.removeElement(container);
container = null;
}
};
Object.defineProperties(this, {
showPoster: {
get: function() {
return !OT.$.isDisplayNone(posterContainer);
},
set: function(shown) {
if(shown) {
OT.$.show(posterContainer);
} else {
OT.$.hide(posterContainer);
}
}
},
poster: {
get: function() {
return OT.$.css(posterContainer, 'backgroundImage');
},
set: function(src) {
OT.$.css(posterContainer, 'backgroundImage', 'url(' + src + ')');
}
},
loading: {
get: function() { return loading; },
set: function(l) {
loading = l;
if (loading) {
OT.$.addClass(container, 'OT_loading');
} else {
OT.$.removeClass(container, 'OT_loading');
}
}
},
video: {
get: function() { return videoElement; },
set: function(video) {
// remove the old video element if it exists
// @todo this might not be safe, publishers/subscribers use this as well...
if (videoElement) videoElement.destroy();
video.appendTo(videoContainer);
videoElement = video;
videoElement.on({
orientationChanged: function(){
fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight,
videoElement.aspectRatio, videoElement.isRotated);
}
});
if (videoElement) {
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();
}
}
}
},
domElement: {
get: function() { return container; }
},
domId: {
get: function() { return container.getAttribute('id'); }
}
});
this.addError = function(errorMsg, helpMsg, classNames) {
container.innerHTML = '' + errorMsg + (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) { /* global mozRTCPeerConnection */ var nativeGetUserMedia, mozToW3CErrors, chromeToW3CErrors, gumNamesToMessages, mapVendorErrorName, parseErrorEvent, areInvalidConstraints; // Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth nativeGetUserMedia = (function() { if (navigator.getUserMedia) { return navigator.getUserMedia.bind(navigator); } else if (navigator.mozGetUserMedia) { return navigator.mozGetUserMedia.bind(navigator); } else if (navigator.webkitGetUserMedia) { return navigator.webkitGetUserMedia.bind(navigator); } })(); if (navigator.webkitGetUserMedia) { /*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; // }; // } } // 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. mozToW3CErrors = { PERMISSION_DENIED: 'PermissionDeniedError', NOT_SUPPORTED_ERROR: 'NotSupportedError', MANDATORY_UNSATISFIED_ERROR: ' ConstraintNotSatisfiedError', NO_DEVICES_FOUND: 'NoDevicesFoundError', HARDWARE_UNAVAILABLE: 'HardwareUnavailableError' }; // Chrome only seems to expose a single error with a code of 1 right now. chromeToW3CErrors = { 1: 'PermissionDeniedError' }; gumNamesToMessages = { PermissionDeniedError: 'End-user denied 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 mapVendorErrorName = function mapVendorErrorName (vendorErrorName, vendorErrors) { var errorName = vendorErrors[vendorErrorName], errorMessage = gumNamesToMessages[errorName]; if (!errorMessage) { // This doesn't map to a known error from the Media Capture spec, it's // probably a custom vendor error message. errorMessage = null; // This is undefined? errorName = vendorErrorName; } return { name: errorName, message: errorMessage }; }; // Parse and normalise a getUserMedia error event from Chrome or Mozilla // @ref http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError parseErrorEvent = function parseErrorObject (event) { var error; if (OT.$.isObject(event) && event.name) { error = { name: event.name, message: event.message || gumNamesToMessages[event.name], constraintName: event.constraintName }; } else if (OT.$.isObject(event)) { error = mapVendorErrorName(event.code, chromeToW3CErrors); // message and constraintName are probably missing if the // property is also omitted, but just in case they aren't. if (event.message) error.message = event.message; if (event.constraintName) error.constraintName = event.constraintName; } else if (event && mozToW3CErrors.hasOwnProperty(event)) { error = mapVendorErrorName(event, mozToW3CErrors); } else { error = { message: 'Unknown Error while getting user media' }; } return error; }; // Validates a Hash of getUserMedia constraints. Currently we only // check to see if there is at least one non-false constraint. areInvalidConstraints = function(constraints) { if (!constraints || !OT.$.isObject(constraints)) return true; for (var key in constraints) { if (constraints[key]) return false; } return true; }; // Returns true if the client supports Web RTC, false otherwise. // // Chrome Issues: // * The explicit prototype.addStream check is because webkitRTCPeerConnection was // partially implemented, but not functional, in Chrome 22. // // Firefox Issues: // * No real support before Firefox 19 // * Firefox 19 has issues with generating Offers. // * Firefox 20 doesn't interoperate with Chrome. // OT.$.supportsWebRTC = function() { var _supportsWebRTC = false; 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); _supportsWebRTC = false; } else if (navigator.webkitGetUserMedia) { _supportsWebRTC = typeof(webkitRTCPeerConnection) === 'function' && !!webkitRTCPeerConnection.prototype.addStream; } else if (navigator.mozGetUserMedia) { if (typeof(mozRTCPeerConnection) === 'function' && browser.version > 20.0) { try { new mozRTCPeerConnection(); _supportsWebRTC = true; } catch (err) { _supportsWebRTC = false; } } } OT.$.supportsWebRTC = function() { return _supportsWebRTC; }; return _supportsWebRTC; }; // Returns a String representing the supported WebRTC crypto scheme. The possible // values are SDES_SRTP, DTLS_SRTP, and NONE; // // Broadly: // * Firefox only supports DTLS // * Older versions of Chrome (<= 24) only support SDES // * Newer versions of Chrome (>= 25) support DTLS and SDES // OT.$.supportedCryptoScheme = function() { if (!OT.$.supportsWebRTC()) return 'NONE'; var chromeVersion = window.navigator.userAgent.toLowerCase().match(/chrome\/([0-9\.]+)/i); return chromeVersion && parseFloat(chromeVersion[1], 10) < 25 ? 'SDES_SRTP' : 'DTLS_SRTP'; }; // Returns true if the browser supports bundle // // Broadly: // * Firefox doesn't support bundle // * Chrome support bundle // OT.$.supportsBundle = function() { return OT.$.supportsWebRTC() && OT.$.browser() === 'Chrome'; }; // Returns true if the browser supports rtcp mux // // Broadly: // * Older versions of Firefox (<= 25) don't support rtcp mux // * Older versions of Firefox (>= 26) support rtcp mux (not tested yet) // * Chrome support bundle // OT.$.supportsRtcpMux = function() { return OT.$.supportsWebRTC() && OT.$.browser() === 'Chrome'; }; // A wrapper for the builtin navigator.getUserMedia. In addition to the usual // getUserMedia behaviour, this helper method also accepts a accessDialogOpened // and accessDialogClosed callback. // // @memberof 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') { var MST = window.MediaStreamTrack; if(MST != null && OT.$.isFunction(MST.getSources)) { window.MediaStreamTrack.getSources(function(sources) { if(sources.length > 0) { accessDenied.call(null, error); } else { failure.call(null, { name: 'NoDevicesFoundError', message: gumNamesToMessages.NoDevicesFoundError }); } }); } else { accessDenied.call(null, error); } } else { failure.call(null, error); } }; try { getUserMedia(constraints, onStream, onError); } catch (e) { OT.error('Couldn\'t get UserMedia: ' + e.toString()); onError(); return; } // The 'remember me' functionality of WebRTC only functions over HTTPS, if // we aren't on HTTPS then we should definitely be displaying the access // dialog. // // If we are on HTTPS, we'll wait 500ms to see if we get a stream // immediately. If we do then the user had clicked 'remember me'. Otherwise // we assume that the accessAllowed dialog is visible. // // @todo benchmark and see if 500ms is a reasonable number. It seems like // we should know a lot quicker. // if (location.protocol.indexOf('https') === -1) { // Execute after, this gives the client a chance to bind to the // accessDialogOpened event. triggerOpenedTimer = setTimeout(triggerOpened, 100); } else { // wait a second and then trigger accessDialogOpened triggerOpenedTimer = setTimeout(triggerOpened, 500); } }; OT.$.createPeerConnection = function (config, options) { var NativeRTCPeerConnection = (window.webkitRTCPeerConnection || window.mozRTCPeerConnection); return new NativeRTCPeerConnection(config, options); }; })(window); !(function(window) { var _videoErrorCodes = {}, VideoOrientationTransforms; 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 _videoElement = new OT.VideoElement({ // fallbackText: 'blah' // }); // // _videoElement.on({ // streamBound: function() {...}, // loadError: function() {...}, // error: function() {...} // }); // // _videoElement.bindToStream(webRtcStream); // => VideoElement // _videoElement.appendTo(DOMElement) // => VideoElement // // _videoElement.stream // => Web RTC stream // _videoElement.domElement // => DomNode // _videoElement.parentElement // => DomNode // // _videoElement.imgData // => PNG Data string // // _videoElement.orientation = OT.VideoOrientation.ROTATED_LEFT; // // _videoElement.unbindStream(); // _videoElement.destroy() // => Completely cleans up and // // removes the video element // // OT.VideoElement = function(options) { var _stream, _domElement, _parentElement, _streamBound = false, _videoElementMovedWarning = false, _options, _onVideoError, _onStreamBound, _onStreamBoundError, _playVideoOnPause; _options = OT.$.defaults(options || {}, { fallbackText: 'Sorry, Web RTC is not available in your browser' }); OT.$.eventing(this); /// Private API _onVideoError = function(event) { var reason = 'There was an unexpected problem with the Video Stream: ' + videoElementErrorCodeToStr(event.target.error.code); this.trigger('error', null, reason, this, 'VideoElement'); }.bind(this); _onStreamBound = function() { _streamBound = true; _domElement.addEventListener('error', _onVideoError, false); this.trigger('streamBound', this); }.bind(this); _onStreamBoundError = function(reason) { this.trigger('loadError', OT.ExceptionCodes.P2P_CONNECTION_FAILED, reason, this, 'VideoElement'); }.bind(this); // The video element pauses itself when it's reparented, this is // unfortunate. This function plays the video again and is triggered // on the pause event. _playVideoOnPause = function() { if(!_videoElementMovedWarning) { OT.warn('Video element paused, auto-resuming. If you intended to do this, use ' + 'publishVideo(false) or subscribeToVideo(false) instead.'); _videoElementMovedWarning = true; } _domElement.play(); }; _domElement = createVideoElement(_options.fallbackText, _options.attributes); _domElement.addEventListener('pause', _playVideoOnPause); /// Public Properties Object.defineProperties(this, { stream: { get: function() {return _stream; } }, domElement: { get: function() {return _domElement; } }, parentElement: { get: function() {return _parentElement; } }, isBoundToStream: { get: function() { return _streamBound; } }, poster: { get: function() { return _domElement.getAttribute('poster'); }, set: function(src) { _domElement.setAttribute('poster', src); } } }); /// Public methods // Append the Video DOM element to a parent node this.appendTo = function(parentDomElement) { _parentElement = parentDomElement; _parentElement.appendChild(_domElement); return this; }; // Bind a stream to the video element. this.bindToStream = function(webRtcStream) { _streamBound = false; _stream = webRtcStream; bindStreamToVideoElement(_domElement, _stream, _onStreamBound, _onStreamBoundError); return this; }; // Unbind the currently bound stream from the video element. this.unbindStream = function() { if (!_stream) return this; if (_domElement) { if (!navigator.mozGetUserMedia) { // The browser would have released this on unload anyway, but // we're being a good citizen. window.URL.revokeObjectURL(_domElement.src); } else { _domElement.mozSrcObject = null; } } _stream = null; return this; }; this.setAudioVolume = function(value) { if (_domElement) _domElement.volume = OT.$.roundFloat(value / 100, 2); }; this.getAudioVolume = function() { // Return the actual volume of the DOM element if (_domElement) return parseInt(_domElement.volume * 100, 10); return 50; }; this.whenTimeIncrements = function(callback, context) { if(_domElement) { var lastTime, handler; handler = function() { if(!lastTime || lastTime >= _domElement.currentTime) { lastTime = _domElement.currentTime; } else { _domElement.removeEventListener('timeupdate', handler, false); callback.call(context, this); } }.bind(this); _domElement.addEventListener('timeupdate', handler, false); } }; this.destroy = function() { // unbind all events so they don't fire after the object is dead this.off(); this.unbindStream(); if (_domElement) { // Unbind this first, otherwise it will trigger when the // video element is removed from the DOM. _domElement.removeEventListener('pause', _playVideoOnPause); OT.$.removeElement(_domElement); _domElement = null; } _parentElement = null; return undefined; }; }; // Checking for window.defineProperty for IE compatibility, // just so we don't throw exceptions when the script is included if (OT.$.canDefineProperty) { // Extracts a snapshot from a video element and returns it's as a PNG Data string. Object.defineProperties(OT.VideoElement.prototype, { imgData: { get: function() { var canvas, imgData; canvas = OT.$.createElement('canvas', { width: this.domElement.videoWidth, height: this.domElement.videoHeight, style: { display: 'none' } }); document.body.appendChild(canvas); try { canvas.getContext('2d').drawImage(this.domElement, 0, 0, canvas.width, canvas.height); } catch(err) { OT.warn('Cannot get image data yet'); return null; } imgData = canvas.toDataURL('image/png'); OT.$.removeElement(canvas); return imgData.replace('data:image/png;base64,', '').trim(); } }, videoWidth: { get: function() { return this.domElement['video' + (this.isRotated ? 'Height' : 'Width')]; } }, videoHeight: { get: function() { return this.domElement['video' + (this.isRotated ? 'Width' : 'Height')]; } }, aspectRatio: { get: function() { return (this.videoWidth + 0.0) / this.videoHeight; } }, isRotated: { get: function() { return this._orientation && ( this._orientation.videoOrientation === 270 || this._orientation.videoOrientation === 90 ); } }, orientation: { get: function() { return this._orientation; }, set: function(orientation) { var transform = VideoOrientationTransforms[orientation.videoOrientation] || VideoOrientationTransforms.ROTATED_NORMAL; this._orientation = orientation; switch(OT.$.browser()) { case 'Chrome': case 'Safari': this.domElement.style.webkitTransform = transform; break; case 'IE': this.domElement.style.msTransform = transform; break; default: // The standard version, just Firefox, Opera, and IE > 9 this.domElement.style.transform = transform; } this.trigger('orientationChanged'); } } }); } /// Private Helper functions function createVideoElement(fallbackText, attributes) { var videoElement = document.createElement('video'); videoElement.setAttribute('autoplay', ''); videoElement.innerHTML = fallbackText; if (attributes) { if (attributes.muted === true) { delete attributes.muted; videoElement.muted = 'true'; } for (var key in attributes) { videoElement.setAttribute(key, attributes[key]); } } return videoElement; } // See http://www.w3.org/TR/2010/WD-html5-20101019/video.html#error-codes // Checking for window.MediaError for IE compatibility, just so we don't throw // exceptions when the script is included if (window.MediaError) { _videoErrorCodes[window.MediaError.MEDIA_ERR_ABORTED] = 'The fetching process for the media ' + 'resource was aborted by the user agent at the user\'s request.'; _videoErrorCodes[window.MediaError.MEDIA_ERR_NETWORK] = 'A network error of some description ' + 'caused the user agent to stop fetching the media resource, after the resource was ' + 'established to be usable.'; _videoErrorCodes[window.MediaError.MEDIA_ERR_DECODE] = 'An error of some description ' + 'occurred while decoding the media resource, after the resource was established to be ' + ' usable.'; _videoErrorCodes[window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED] = 'The media resource ' + 'indicated by the src attribute was not suitable.'; } function videoElementErrorCodeToStr(errorCode) { return _videoErrorCodes[parseInt(errorCode, 10)] || 'An unknown error occurred.'; } function bindStreamToVideoElement(videoElement, webRTCStream, onStreamBound, onStreamBoundError) { var cleanup, onLoad, onError, onStoppedLoading, timeout; // Note: onloadedmetadata doesn't fire in Chrome for audio only crbug.com/110938 if (navigator.mozGetUserMedia || ( 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(); onStreamBound(); }; onError = function onError (event) { cleanup(); onStreamBoundError('There was an unexpected problem with the Video Stream: ' + videoElementErrorCodeToStr(event.target.error.code)); }; onStoppedLoading = function onStoppedLoading () { // The stream ended before we fully bound it. Maybe the other end called // stop on it or something else went wrong. cleanup(); onStreamBoundError('Stream ended while trying to bind it to a video element.'); }; // Timeout if it takes too long timeout = setTimeout(function() { if (videoElement.currentTime === 0) { onStreamBoundError('The video stream failed to connect. Please notify the site ' + 'owner if this continues to happen.'); } else { // This should never happen OT.warn('Never got the loadedmetadata event but currentTime > 0'); onStreamBound(); } }.bind(this), 30000); videoElement.addEventListener('loadedmetadata', onLoad, false); videoElement.addEventListener('error', onError, false); webRTCStream.onended = onStoppedLoading; } else { onStreamBound(); } // The official spec way is 'srcObject', we are slowly converging there. if (videoElement.srcObject !== void 0) { videoElement.srcObject = webRTCStream; } else if (videoElement.mozSrcObject !== void 0) { videoElement.mozSrcObject = webRTCStream; } else { videoElement.src = window.URL.createObjectURL(webRTCStream); } videoElement.play(); } })(window); !(function(window) { // Singleton interval var logQueue = [], queueRunning = false; OT.Analytics = function() { var endPoint = OT.properties.loggingURL + '/logging/ClientEvent', endPointQos = OT.properties.loggingURL + '/logging/ClientQos', reportedErrors = {}, // Map of camel-cased keys to underscored camelCasedKeys, send = function(data, isQos, callback) { OT.$.post(isQos ? endPointQos : endPoint, { body: data, 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.'); } 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: navigator.userAgent })); reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ? reportedErrors[errKey] + 1 : 1; return this.logEvent(OT.$.extend(options, { action: type + '.' + code, payloadType: payload[0], payload: payload[1] })); }; // Log a client event to the analytics backend. // // @example Logs a client event called 'foo' // OT.ClientEvents.log({ // action: 'foo', // payload_type: 'foo's payload', // payload: 'bar', // session_id: sessionId, // connection_id: connectionId // }) // // @param [Hash] options the options to log the client event with. // @option options [String] action The name of the Event that we are logging. // E.g. 'TokShowLoaded'. Required. // @option options [String] variation Usually used for Split A/B testing, when // you have multiple variations of the +_action+. // @option options [String] payloadType A text description of the payload. Required. // @option options [String] payload The payload. Required. // @option options [String] session_id The active OpenTok session, if there is one // @option options [String] connection_id The active OpenTok connectionId, if there is one // @option options [String] partner_id // @option options [String] guid ... // @option options [String] widget_id ... // @option options [String] stream_id ... // @option options [String] section ... // @option options [String] build ... // this.logEvent = function(options) { var partnerId = options.partnerId; if (!options) options = {}; // Set a bunch of defaults var data = OT.$.extend({ 'variation' : '', 'guid' : this.getClientGuid(), 'widget_id' : '', 'session_id': '', 'connection_id': '', 'stream_id' : '', 'partner_id' : partnerId, 'source' : window.location.href, 'section' : '', 'build' : '' }, options), onComplete = function(){ // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] // + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}'); }; // We camel-case our names, but the ClientEvents backend wants them // underscored... for (var key in camelCasedKeys) { if (camelCasedKeys.hasOwnProperty(key) && data[key]) { data[camelCasedKeys[key]] = data[key]; delete data[key]; } } post(data, onComplete, false); }; // Log a client QOS to the analytics backend. // this.logQOS = function(options) { var partnerId = options.partnerId; if (!options) options = {}; // Set a bunch of defaults var data = OT.$.extend({ 'guid' : this.getClientGuid(), 'widget_id' : '', 'session_id': '', 'connection_id': '', 'stream_id' : '', 'partner_id' : partnerId, 'source' : window.location.href, 'build' : '', 'duration' : 0 //in milliseconds }, options), onComplete = function(){ // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] // + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}'); }; // We camel-case our names, but the ClientEvents backend wants them // underscored... for (var key in camelCasedKeys) { if (camelCasedKeys.hasOwnProperty(key) && data[key]) { data[camelCasedKeys[key]] = data[key]; delete data[key]; } } post(data, onComplete, true); }; // Converts +payload+ to two pipe seperated strings. Doesn't currently handle // edgecases, e.g. escaping '\\|' will break stuff. // // *Note:* It strip any keys that have null values. this.escapePayload = function(payload) { var escapedPayload = [], escapedPayloadDesc = []; for (var key in payload) { if (payload.hasOwnProperty(key) && payload[key] !== null && payload[key] !== undefined) { escapedPayload.push( payload[key] ? payload[key].toString().replace('|', '\\|') : '' ); escapedPayloadDesc.push( key.toString().replace('|', '\\|') ); } } return [ escapedPayloadDesc.join('|'), escapedPayload.join('|') ]; }; // Uses HTML5 local storage to save a client ID. this.getClientGuid = function() { var guid = OT.$.getCookie('opentok_client_id'); if (!guid) { guid = OT.$.uuid(); OT.$.setCookie('opentok_client_id', guid); } // once we have a guid, memoise this function so if cookies & local storage are disabled // we still hand back the same guid each call within this page at least. OPENTOK-14015 this.getClientGuid = function() { return guid; }; return guid; }; }; })(window); !(function(window) { // This is not obvious, so to prevent end-user frustration we'll let them know // explicitly rather than failing with a bunch of permission errors. We don't // handle this using an OT Exception as it's really only a development thing. if (location.protocol === 'file:') { /*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 // Global parameters used by upgradeSystemRequirements _intervalId, _lastHash = document.location.hash; /** * The first step in using the OpenTok API is to call theOT.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.
*
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.
*
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 OpenTok cloud-enabled sessions, lowering the frame rate or lowering the resolution reduces * the maximum bandwidth the stream can use. However, in peer-to-peer sessions, 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 and width properties to set the dimensions
* of the publisher video; do not set the height and width of the DOM element
* (using CSS).
* 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.true
* (the video image is mirrored). This property does not affect the display
* on other subscribers' web pages.
* true). This setting applies when you pass
* the Publisher object in a call to the Session.publish() method.
* true). This setting applies when you pass
* the Publisher object in a call to the Session.publish() method.
* "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 OpenTok cloud-enabled sessions, lowering the frame rate or lowering the resolution reduces * the maximum bandwidth the stream can use. However, in peer-to-peer sessions, lowering the frame * rate or resolution may not reduce the stream's bandwidth. *
*style object includes
* the following properties:
* 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).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).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).
* 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 OT.upgradeSystemRequirements()
* @method OT.checkSystemRequirements
* @memberof OT
*/
OT.checkSystemRequirements = function() {
OT.debug('OT.checkSystemRequirements()');
var systemRequirementsMet = OT.$.supportsWebSockets() && OT.$.supportsWebRTC() ?
this.HAS_REQUIREMENTS : this.NOT_HAS_REQUIREMENTS;
OT.checkSystemRequirements = function() {
OT.debug('OT.checkSystemRequirements()');
return systemRequirementsMet;
};
return systemRequirementsMet;
};
/**
* Displays information about system requirments for OpenTok for WebRTC. This
* information is displayed in an iframe element that fills the browser window.
*
* 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.
*
* 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.
*
* Removes an event listener for a specific event. *
* *
* Throws an exception if the listener name is invalid.
*
* 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 ofthis 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 onModelUpdate = function onModelUpdate (event) {
this.trigger('update', event);
this.trigger('update:'+event.target.id, event);
}.bind(this),
onModelDestroy = function onModelDestroyed (event) {
this.remove(event.target, event.reason);
}.bind(this);
this.reset = function() {
// Stop listening on the models, they are no longer our problem
_models.forEach(function(model) {
model.off('updated', onModelUpdate, this);
model.off('destroyed', onModelDestroy, this);
}, this);
_models = [];
_byId = {};
};
this.destroy = function() {
_models.forEach(function(model) {
if(model && typeof model.destroy === 'function') {
model.destroy(void 0, true);
}
});
this.reset();
this.off();
};
this.get = function(id) { return id && _byId[id] !== void 0 ? _models[_byId[id]] : void 0; };
this.has = function(id) { return id && _byId[id] !== void 0; };
this.toString = function() { return _models.toString(); };
// Return only models filtered by either a dict of properties
// or a filter function.
//
// @example Return all publishers with a streamId of 1
// OT.publishers.where({streamId: 1})
//
// @example The same thing but filtering using a filter function
// OT.publishers.where(function(publisher) {
// return publisher.stream.id === 4;
// });
//
// @example The same thing but filtering using a filter function
// executed with a specific this
// OT.publishers.where(function(publisher) {
// return publisher.stream.id === 4;
// }, self);
//
this.where = function(attrsOrFilterFn, context) {
if (OT.$.isFunction(attrsOrFilterFn)) return _models.filter(attrsOrFilterFn, context);
return _models.filter(function(model) {
for (var key in attrsOrFilterFn) {
if (model[key] !== attrsOrFilterFn[key]) return false;
}
return true;
});
};
// Similar to where in behaviour, except that it only returns
// the first match.
this.find = function(attrsOrFilterFn, context) {
var filterFn;
if (OT.$.isFunction(attrsOrFilterFn)) {
filterFn = attrsOrFilterFn;
}
else {
filterFn = function(model) {
for (var key in attrsOrFilterFn) {
if (model[key] !== attrsOrFilterFn[key]) return false;
}
return true;
};
}
filterFn = filterFn.bind(context);
for (var i=0; i<_models.length; ++i) {
if (filterFn(_models[i]) === true) return _models[i];
}
return null;
};
this.add = function(model) {
var id = model[_idField];
if (this.has(id)) {
OT.warn('Model ' + id + ' is already in the collection', _models);
return this;
}
_byId[id] = _models.push(model) - 1;
model.on('updated', onModelUpdate, this);
model.on('destroyed', onModelDestroy, this);
this.trigger('add', model);
this.trigger('add:'+id, model);
return this;
};
this.remove = function(model, reason) {
var id = model[_idField];
_models.splice(_byId[id], 1);
// Shuffle everyone down one
for (var i=_byId[id]; i<_models.length; ++i) {
_byId[_models[i][_idField]] = i;
}
delete _byId[id];
model.off('updated', onModelUpdate, this);
model.off('destroyed', onModelDestroy, this);
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);
models.forEach(function(model) {
this.trigger('add', model);
this.trigger('add:' + model[_idField], model);
}, this);
};
OT.$.defineGetters(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:
* *sessionDisconnect See
*
* SessionDisconnectEvent.preventDefault().streamDestroyed See
* StreamEvent.preventDefault().accessDialogOpened See the
* accessDialogOpened event.accessDenied See the
* accessDenied event.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'
};
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.
| * 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.
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);
};
/**
* Connection event is an event that can have type "connectionCreated" or "connectionDestroyed".
* These events are dispatched by the Session object when another 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 SessionConnectEvent and
* SessionDisconnectEvent classes.
*
* 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("sessionConnected", function(event) {
* connectionCount = 1; // This represent's your client's connection to the session
* displayConnectionCount();
* });
* session.on("connectionCreated", function(event) {
* connectionCount += 1;
* displayConnectionCount();
* });
* session.on("connectionDestroyed", function(event) {
* connectionCount -= 1;
* 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.
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);
*
*
* 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);
*
*
* 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.");
* );
*
*
* 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 {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.
*
* @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.
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().)
* @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.
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 other than that of your client. 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()
disconnect() method of the session object.
*
* * 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).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.
streamPropertyChanged event in the
* following circumstances:
*
* 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.videoDimensions property of a stream changes. For more information,
* see Stream.videoDimensions."hasAudio", "hasVideo", or "videoDimensions".
* @property {Stream} stream The Stream object for which a property has changed.
* @property {Object} newValue The new value of the property (after the change).
* @property {Object} oldValue The old value of the property (before the change).
*
* @see 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;
};
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;
};
})(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';
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.completionHandler 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():
code |
* Description | *
| 1004 | *Authentication 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. | *
| 1005 | *Invalid Session ID. Make sure you generate the session ID using the current version of * one of the OpenTok server * SDKs. | *
| 1006 | *Connect 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 | *
| 1010 | *The client is not connected to the OpenTok session. Check that client connects * successfully and has not disconnected before calling forceDisconnect(). | *
| 1520 | *Unable 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():
code |
* Description | *
| 1010 | *The client is not connected to the OpenTok session. Check that client connects * successfully and has not disconnected before calling forceUnpublish(). | *
| 1530 | *Unable 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. |
*
| 1535 | *Force 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():
code |
* Description | *
| 1010 | *The 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. | *
| 1500 | *Unable 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. |
*
| 1601 | *Internal error -- WebRTC publisher error. Try republishing or reconnecting to the * session. | *
Errors when calling Session.signal():
code |
* Description | *
| 400 | *One 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). | *
| 500 | *The 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 | *
| 1600 | *Internal error -- WebRTC subscriber error. Try resubscribing to the stream or * reconnecting to the session. | *
Errors when calling TB.initPublisher():
code |
* Description | *
| 1004 | *Authentication 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:
* *code |
* Description | *
| 1011 | *Invalid Parameter. Check that you have passed valid parameter values into the method * call. | *
| 2000 | *Internal Error. Try reconnecting to the OpenTok session and trying the action again. | *
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 (other
* than 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 = function(reason, quiet) {
destroyedReason = reason || 'clientDisconnected';
if (quiet !== true) {
this.dispatchEvent(
new OT.DestroyedEvent(
'destroyed', // This should be OT.Event.names.CONNECTION_DESTROYED, but
// the value of that is currently shared with Session
this,
destroyedReason
)
);
}
}.bind(this);
Object.defineProperties(this, {
destroyed: {
get: function() { return destroyedReason !== void 0; },
enumerable: true
},
destroyedReason: {
get: function() { return destroyedReason; },
enumerable: true
}
});
};
OT.Connection.fromHash = function(hash) {
return new OT.Connection(hash.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) {
// 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 '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 (Object.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}.
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} fps The frame rate of the video stream.
*
* @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 {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}.)
*
* @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.
*/
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});
this.publisherId = this.publisher ? this.publisher.id : null;
OT.$.eventing(this);
var onChannelUpdate = function(channel, key, oldValue, newValue) {
var _key = key;
switch(_key) {
case 'active':
_key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo';
this[_key] = newValue;
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) );
}.bind(this);
var associatedWidget = function() {
if(this.publisher) {
return this.publisher;
} else {
return OT.subscribers.find(function(subscriber) {
return subscriber.streamId === this.id &&
subscriber.session.id === session.id;
});
}
}.bind(this);
// Returns all channels that have a type of +type+.
this.getChannelsOfType = function (type) {
return this.channel.filter(function(channel) {
return channel.type === type;
});
};
this.getChannel = function (id) {
for (var i=0; isetStyle() 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:
*
*
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).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.
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:
*
*
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).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).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.
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'],
showSettingsButton: [true, false],
showMicButton: [true, false],
backgroundImageURI: null,
showControlBar: [true, false],
showArchiveStatus: [true, false]
};
// Validates the style +key+ and also whether +value+ is valid for +key+
isValidStyle = function(key, value) {
return key === 'backgroundImageURI' ||
( _validStyleValues.hasOwnProperty(key) &&
_validStyleValues[key].indexOf(value) !== -1 );
};
castValue = function(value) {
switch(value) {
case 'true':
return true;
case 'false':
return false;
default:
return value;
}
};
// Returns a shallow copy of the styles.
this.getAll = function() {
var style = OT.$.clone(_style);
for (var i in style) {
if (_COMPONENT_STYLES.indexOf(i) < 0) {
// Strip unnecessary properties out, should this happen on Set?
delete style[i];
}
}
return style;
};
this.get = function(key) {
if (key) {
return _style[key];
}
// We haven't been asked for any specific key, just return the lot
return this.getAll();
};
// *note:* this will not trigger onStyleChange if +silent+ is truthy
this.setAll = function(newStyles, silent) {
var oldValue, newValue;
for (var key in newStyles) {
newValue = castValue(newStyles[key]);
if (isValidStyle(key, newValue)) {
oldValue = _style[key];
if (newValue !== oldValue) {
_style[key] = newValue;
if (!silent) onStyleChange(key, newValue, oldValue);
}
} else {
OT.warn('Style.setAll::Invalid style property passed ' + key + ' : ' + newValue);
}
}
return this;
};
this.set = function(key, value) {
OT.debug('Publisher.setStyle: ' + key.toString());
var newValue = castValue(value),
oldValue;
if (!isValidStyle(key, newValue)) {
OT.warn('Style.set::Invalid style property passed ' + key + ' : ' + newValue);
return this;
}
oldValue = _style[key];
if (newValue !== oldValue) {
_style[key] = newValue;
onStyleChange(key, value, oldValue);
}
return this;
};
if (initalStyles) this.setAll(initalStyles, true);
};
})(window);
!(function() {
/*
* A Publishers Microphone.
*
* TODO
* * bind to changes in mute/unmute/volume/etc and respond to them
*/
OT.Microphone = function(webRTCStream, muted) {
var _muted,
_gain = 50;
Object.defineProperty(this, 'muted', {
get: function() { return _muted; },
set: function(muted) {
if (_muted === muted) return;
_muted = muted;
var audioTracks = webRTCStream.getAudioTracks();
for (var i=0, num=audioTracks.length; iOT.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.
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,
_analytics = new OT.Analytics(),
_validResolutions,
_validFrameRates = [ 1, 7, 15, 30 ],
_qosIntervals = {},
_prevStats,
_state,
_iceServers;
_validResolutions = {
'320x240': {width: 320, height: 240},
'640x480': {width: 640, height: 480},
'1280x720': {width: 1280, height: 720}
};
_prevStats = {
'timeStamp' : OT.$.now()
};
OT.$.eventing(this);
OT.StylableComponent(this, {
showMicButton: true,
showArchiveStatus: true,
nameDisplayMode: 'auto',
buttonDisplayMode: 'auto',
bugDisplayMode: 'auto',
backgroundImageURI: null
});
/// Private Methods
var logAnalyticsEvent = function(action, variation, payloadType, payload) {
_analytics.logEvent({
action: action,
variation: variation,
'payload_type': payloadType,
payload: payload,
'session_id': _session ? _session.sessionId : null,
'connection_id': _session &&
_session.connected ? _session.connection.connectionId : null,
'partner_id': _session ? _session.apiKey : OT.APIKEY,
streamId: _stream ? _stream.id : null,
'widget_id': _guid,
'widget_type': 'Publisher'
});
},
recordQOS = function(connectionId) {
var QoSBlob = {
'widget_type': 'Publisher',
'stream_type': 'WebRTC',
sessionId: _session ? _session.sessionId : null,
connectionId: _session && _session.connected ? _session.connection.connectionId : null,
partnerId: _session ? _session.apiKey : OT.APIKEY,
streamId: _stream ? _stream.id : null,
widgetId: _guid,
version: OT.properties.version,
'media_server_name': _session ? _session.sessionInfo.messagingServer : null,
p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
duration: _publishStartTime ? new Date().getTime() - _publishStartTime.getTime() : 0,
'remote_connection_id': connectionId
};
// get stats for each connection id
_peerConnections[connectionId].getStats(_prevStats, function(stats) {
var statIndex;
if (stats) {
for (statIndex in stats) {
QoSBlob[statIndex] = stats[statIndex];
}
}
_analytics.logQOS(QoSBlob);
});
},
/// Private Events
stateChangeFailed = function(changeFailed) {
OT.error('Publisher State Change Failed: ', changeFailed.message);
OT.debug(changeFailed);
},
onLoaded = function() {
OT.debug('OT.Publisher.onLoaded');
_state.set('MediaBound');
_container.loading = false;
_loaded = true;
_createChrome.call(this);
this.trigger('initSuccess');
this.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.dispatchEvent(
new OT.Event(OT.Event.names.ACCESS_ALLOWED, false)
);
_targetElement = new OT.VideoElement({
attributes: {muted:true}
});
_targetElement.on({
streamBound: onLoaded,
loadError: onLoadFailure,
error: onVideoError
}, this)
.bindToStream(_webRTCStream);
_container.video = _targetElement;
},
onStreamAvailableError = function(error) {
OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
_state.set('Failed');
this.trigger('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,
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') {
accessDialogPrompt = OT.Dialogs.AllowDeny.Chrome.initialPrompt();
} else if(browser.browser === 'Firefox') {
accessDialogFirefoxTimeout = setTimeout(function() {
accessDialogPrompt = OT.Dialogs.AllowDeny.Firefox.maybeDenied();
}, 7000);
}
}
}
);
},
onAccessDialogClosed = function() {
logAnalyticsEvent('accessDialog', 'Closed', '', '');
if(accessDialogFirefoxTimeout) {
clearTimeout(accessDialogFirefoxTimeout);
accessDialogFirefoxTimeout = null;
}
if(accessDialogPrompt) {
accessDialogPrompt.close();
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.attemptingToPublish) {
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.
clearInterval(_qosIntervals[peerConnection.remoteConnection.id]);
delete _qosIntervals[peerConnection.remoteConnection.id];
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 = function(stream) {
_stream = stream;
_stream.on('destroyed', this.disconnect, this);
_state.set('Publishing');
_publishStartTime = new Date();
this.trigger('publishComplete', null, this);
this.dispatchEvent(new OT.StreamEvent('streamCreated', stream, null, false));
logAnalyticsEvent('publish', 'Success', 'streamType:streamId', 'WebRTC:' + _streamId);
}.bind(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',
this.cleanupSubscriber.bind(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('|'));
// start recording the QoS for this peer connection
_qosIntervals[remoteConnection.id] = setInterval(function() {
recordQOS(remoteConnection.id);
}, 30000);
},
disconnected: onPeerDisconnected,
error: onPeerConnectionFailure
}, 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.nameMode = value;
break;
case 'showArchiveStatus':
logAnalyticsEvent('showArchiveStatus', 'styleChange', 'mode', value ? 'on': 'off');
_chrome.archive.setShowArchiveStatus(value);
break;
case 'buttonDisplayMode':
case 'showMicButton':
// _chrome.muteButton.setDisplayMode(
// chromeButtonMode.call(this, this.getStyle('showMicButton'))
// );
case 'bugDisplayMode':
// _chrome.name.bugMode = value;
}
},
_createChrome = function() {
if(this.getStyle('bugDisplayMode') === 'off') {
logAnalyticsEvent('bugDisplayMode', 'createChrome', 'mode', 'off');
}
if(!this.getStyle('showArchiveStatus')) {
logAnalyticsEvent('showArchiveStatus', 'createChrome', 'mode', 'off');
}
_chrome = new OT.Chrome({
parent: _container.domElement
}).set({
backingBar: new OT.Chrome.BackingBar({
nameMode: this.getStyle('nameDisplayMode'),
muteMode: chromeButtonMode.call(this, this.getStyle('showMicButton'))
}),
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('showMicButton'))
}),
opentokButton: new OT.Chrome.OpenTokButton({
mode: this.getStyle('bugDisplayMode')
}),
archive: new OT.Chrome.Archiving({
show: this.getStyle('showArchiveStatus'),
archiving: false
})
}).on({
muted: this.publishAudio.bind(this, false),
unmuted: this.publishAudio.bind(this, true)
});
},
reset = function() {
if (_chrome) {
_chrome.destroy();
_chrome = null;
}
this.disconnect();
_microphone = null;
if (_targetElement) {
_targetElement.destroy();
_targetElement = null;
}
cleanupLocalStream();
if (_container) {
_container.destroy();
_container = null;
}
if (this.session) {
this._.unpublishFromSession(this.session, 'reset');
}
// clear all the intervals
for (var connId in _qosIntervals) {
clearInterval(_qosIntervals[connId]);
delete _qosIntervals[connId];
}
_domId = null;
_stream = null;
_loaded = false;
_session = null;
_state.set('NotPublishing');
}.bind(this);
this.publish = function(targetElement, properties) {
OT.debug('OT.Publisher: publish');
if ( _state.attemptingToPublish || _state.publishing ) reset();
_state.set('GetUserMedia');
_publishProperties = OT.$.defaults(properties || {}, {
publishAudio : true,
publishVideo : true,
mirror: true
});
if (!_publishProperties.constraints) {
_publishProperties.constraints = OT.$.clone(defaultConstraints);
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 "' +
Object.keys(_validResolutions).join('","') + '"');
} else {
_publishProperties.videoDimensions = _validResolutions[_publishProperties.resolution];
_publishProperties.constraints.video = {
mandatory: {},
optional: [
{minWidth: _publishProperties.videoDimensions.width},
{maxWidth: _publishProperties.videoDimensions.width},
{minHeight: _publishProperties.videoDimensions.height},
{maxHeight: _publishProperties.videoDimensions.height}
]
};
}
}
if (_publishProperties.frameRate !== void 0 &&
_validFrameRates.indexOf(_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 (!_publishProperties.constraints.video.optional) {
if (typeof _publishProperties.constraints.video !== 'object') {
_publishProperties.constraints.video = {};
}
_publishProperties.constraints.video.optional = [];
}
_publishProperties.constraints.video.optional.push({
minFrameRate: _publishProperties.frameRate
});
_publishProperties.constraints.video.optional.push({
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);
_domId = _container.domId;
OT.$.getUserMedia(
_publishProperties.constraints,
onStreamAvailable.bind(this),
onStreamAvailableError.bind(this),
onAccessDialogOpened.bind(this),
onAccessDialogClosed.bind(this),
onAccessDenied.bind(this)
);
}, this);
return this;
};
/**
* Starts publishing audio (if it is currently not being published)
* when the 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; idestroyed event when the DOM
* element is removed.
*
* @method #destroy
* @memberOf Publisher
* @return {Publisher} The Publisher.
*/
this.destroy = function(/* unused */ reason, quiet) {
reset();
if (quiet !== true) {
this.dispatchEvent(
new OT.DestroyedEvent(
OT.Event.names.PUBLISHER_DESTROYED,
this,
reason
),
this.off.bind(this)
);
}
return this;
};
/**
* @methodOf Publisher
* @private
*/
this.disconnect = function() {
// Close the connection to each of our subscribers
for (var fromConnectionId in _peerConnections) {
this.cleanupSubscriber(fromConnectionId);
}
};
this.cleanupSubscriber = function(fromConnectionId) {
var pc = _peerConnections[fromConnectionId];
clearInterval(_qosIntervals[fromConnectionId]);
delete _qosIntervals[fromConnectionId];
if (pc) {
pc.destroy();
delete _peerConnections[fromConnectionId];
logAnalyticsEvent('disconnect', 'PeerConnection',
'subscriberConnection', fromConnectionId);
}
};
this.processMessage = function(type, fromConnection, message) {
OT.debug('OT.Publisher.processMessage: Received ' + type + ' from ' + fromConnection.id);
OT.debug(message);
switch (type) {
case '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: function(session) {
// Add session property to Publisher
this.session = session;
var createStream = function() {
var streamWidth,
streamHeight;
// Bail if this.session is gone, it means we were unpublished
// before createStream could finish.
if (!this.session) return;
_state.set('PublishingToSession');
var onStreamRegistered = 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.attemptingToPublish) {
this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
err.message));
}
return;
}
_streamId = streamId;
_iceServers = OT.Raptor.parseIceServers(message);
}.bind(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;
}.bind(this),
unpublishFromSession: function(session, reason) {
if (!this.session || session.id !== this.session.id) {
OT.warn('The publisher ' + this.guid + ' is trying to unpublish from a session ' +
session.id + ' it is not attached to (' +
(this.session && this.session.id || 'no this.session') + ')');
return this;
}
if (session.connected && 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 = null;
// We're back to being a stand-alone publisher again.
_state.set('MediaBound');
logAnalyticsEvent('unpublish', 'Success', 'sessionId', session.id);
this._.streamDestroyed(reason);
return this;
}.bind(this),
streamDestroyed: function(reason) {
if(['reset'].indexOf(reason) < 0) {
var event = new OT.StreamEvent('streamDestroyed', _stream, reason, true);
var defaultAction = function() {
if(!event.isDefaultPrevented()) {
this.destroy();
}
}.bind(this);
this.dispatchEvent(event, defaultAction);
}
}.bind(this),
archivingStatus: function(status) {
if(_chrome) {
_chrome.archive.setArchiving(status);
}
return status;
}.bind(this)
};
this.detectDevices = function() {
OT.warn('Fixme: Haven\'t implemented detectDevices');
};
this.detectMicActivity = function() {
OT.warn('Fixme: Haven\'t implemented detectMicActivity');
};
this.getEchoCancellationMode = function() {
OT.warn('Fixme: Haven\'t implemented getEchoCancellationMode');
return 'fullDuplex';
};
this.setMicrophoneGain = function() {
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');
};
Object.defineProperties(this, {
id: {
get: function() { return _domId; },
enumerable: true
},
element: {
get: function() { return _container.domElement; },
enumerable: true
},
guid: {
get: function() { return _guid; },
enumerable: true
},
stream: {
get: function() { return _stream; },
set: function(stream) {
assignStream(stream);
},
enumerable: true
},
streamId: {
get: function() { return _streamId; },
enumerable: true
},
targetElement: {
get: function() { return _targetElement.domElement; }
},
domId: {
get: function() { return _domId; }
},
session: {
get: function() { return _session; },
set: function(session) {
_session = session;
},
enumerable: true
},
isWebRTC: {
get: function() { return true; }
},
loading: {
get: function(){
return _container && _container.loading;
}
},
videoWidth: {
get: function() { return _targetElement.videoWidth; },
enumerable: true
},
videoHeight: {
get: function() { return _targetElement.videoHeight; },
enumerable: true
}
});
Object.defineProperty(this._, 'webRtcStream', {
get: function() { return _webRTCStream; }
});
this.on('styleValueChanged', updateChromeForStyleChange, this);
_state = new OT.PublishingState(stateChangeFailed);
/**
* Dispatched when the user has clicked the Allow button, granting the
* app access to the camera and microphone. 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
*/
/**
* 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(window) {
/**
* The Subscriber object is a representation of the local video element that is playing back
* a remote stream. The Subscriber object includes methods that let you disable and enable
* local audio playback for the subscribed stream. The 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,
_stream,
_fromConnectionId,
_peerConnection,
_session = options.session,
_subscribeStartTime,
_startConnectingTime,
_qosInterval,
_properties = OT.$.clone(options),
_analytics = new OT.Analytics(),
_audioVolume = 50,
_state,
_subscribeAudioFalseWorkaround, // OPENTOK-6844
_prevStats,
_lastSubscribeToVideoReason;
_prevStats = {
timeStamp: OT.$.now()
};
if (!_session) {
OT.handleJsException('Subscriber must be passed a session option', 2000, {
session: _session,
target: this
});
return;
}
OT.$.eventing(this);
OT.StylableComponent(this, {
nameDisplayMode: 'auto',
buttonDisplayMode: 'auto',
backgroundImageURI: null,
showArchiveStatus: true,
showMicButton: true
});
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.connected ? _session.connection.connectionId : null,
partner_id: _session && _session.connected ? _session.sessionInfo.partnerId : null,
widget_id: _widgetId,
widget_type: 'Subscriber'
});
},
recordQOS = function() {
if(_state.subscribing && _session && _session.connected) {
/*jshint camelcase:false */
var QoSBlob = {
widget_type: 'Subscriber',
stream_type : 'WebRTC',
session_id: _session ? _session.sessionId : null,
connectionId: _session ? _session.connection.connectionId : null,
media_server_name: _session ? _session.sessionInfo.messagingServer : null,
p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
partner_id: _session ? _session.apiKey : null,
stream_id: _stream.id,
widget_id: _widgetId,
version: OT.properties.version,
duration: parseInt(OT.$.now() - _subscribeStartTime, 10),
remote_connection_id: _stream.connection.connectionId
};
// get stats for each connection id
_peerConnection.getStats(_prevStats, function(stats) {
var statIndex;
if (stats) {
for (statIndex in stats) {
QoSBlob[statIndex] = stats[statIndex];
}
}
_analytics.logQOS(QoSBlob);
});
}
},
stateChangeFailed = function(changeFailed) {
OT.error('Subscriber State Change Failed: ', changeFailed.message);
OT.debug(changeFailed);
},
onLoaded = function() {
if (_state.subscribing || !_streamContainer) return;
OT.debug('OT.Subscriber.onLoaded');
_state.set('Subscribing');
_subscribeStartTime = OT.$.now();
logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
parseInt(_subscribeStartTime - _startConnectingTime, 10),
_peerConnection && _peerConnection.hasRelayCandidates
].join('|'));
_qosInterval = setInterval(recordQOS, 30000);
if(_subscribeAudioFalseWorkaround) {
_subscribeAudioFalseWorkaround = null;
this.subscribeToVideo(false);
}
_container.loading = false;
_createChrome.call(this);
this.trigger('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.attemptingToSubscribe) {
// subscribing error
_state.set('Failed');
this.trigger('subscribeComplete', new OT.Error(null, 'ClientDisconnected'));
} else if (_state.subscribing) {
_state.set('Failed');
// we were disconnected after we were already subscribing
// probably do nothing?
}
this.disconnect();
},
onPeerConnectionFailure = function(code, reason, peerConnection, prefix) {
if (_state.attemptingToSubscribe) {
// We weren't subscribing yet so this was a failure in setting
// up the PeerConnection or receiving the initial stream.
logAnalyticsEvent('createPeerConnection', 'Failure', 'reason|hasRelayCandidates', [
'Subscriber PeerConnection Error: ' + reason,
_peerConnection && _peerConnection.hasRelayCandidates
].join('|'));
_state.set('Failed');
this.trigger('subscribeComplete', new OT.Error(null, reason));
} else if (_state.subscribing) {
// we were disconnected after we were already subscribing
_state.set('Failed');
this.trigger('error', reason);
}
this.disconnect();
logAnalyticsEvent('subscribe', 'Failure', 'reason',
(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);
},
onRemoteStreamAdded = function(webOTStream) {
OT.debug('OT.Subscriber.onRemoteStreamAdded');
_state.set('BindingRemoteStream');
// Disable the audio/video, if needed
this.subscribeToAudio(_properties.subscribeToAudio);
var preserver = _subscribeAudioFalseWorkaround;
this.subscribeToVideo(_properties.subscribeToVideo);
_subscribeAudioFalseWorkaround = preserver;
var streamElement = new OT.VideoElement();
// Initialize the audio volume
streamElement.setAudioVolume(_audioVolume);
streamElement.on({
streamBound: onLoaded,
loadError: onPeerConnectionFailure,
error: onPeerConnectionFailure
}, this);
streamElement.bindToStream(webOTStream);
_container.video = streamElement;
_streamContainer = streamElement;
_streamContainer.orientation = {
width: _stream.videoDimensions.width,
height: _stream.videoDimensions.height,
videoOrientation: _stream.videoDimensions.orientation
};
logAnalyticsEvent('createPeerConnection', 'StreamAdded', '', '');
this.trigger('streamAdded', this);
},
onRemoteStreamRemoved = function(webOTStream) {
OT.debug('OT.Subscriber.onStreamRemoved');
if (_streamContainer.stream === webOTStream) {
_streamContainer.destroy();
_streamContainer = null;
}
this.trigger('streamRemoved', this);
},
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 'hasVideo':
if(_container) {
_container.showPoster = !(_stream.hasVideo && _properties.subscribeToVideo);
}
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);
break;
case 'showArchiveStatus':
_chrome.archive.setShowArchiveStatus(value);
break;
case 'buttonDisplayMode':
// _chrome.muteButton.setDisplayMode(value);
case 'bugDisplayMode':
// _chrome.name.bugMode = value;
}
},
_createChrome = function() {
if(this.getStyle('bugDisplayMode') === 'off') {
logAnalyticsEvent('bugDisplayMode', 'createChrome', 'mode', 'off');
}
_chrome = new OT.Chrome({
parent: _container.domElement
}).set({
backingBar: new OT.Chrome.BackingBar({
nameMode: 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
})
}).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.recordQOS = function() {
recordQOS();
};
this.subscribe = function(stream) {
OT.debug('OT.Subscriber: subscribe to ' + stream.id);
if (_state.subscribing) {
// @todo error
OT.error('OT.Subscriber.Subscribe: Cannot subscribe, already subscribing.');
return false;
}
_state.set('Init');
if (!stream) {
// @todo error
OT.error('OT.Subscriber: No stream parameter.');
return false;
}
if (_stream) {
// @todo error
OT.error('OT.Subscriber: Already subscribed');
return false;
}
_stream = stream;
_stream.on({
updated: streamUpdated,
destroyed: 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);
_domId = _container.domId;
if(!_properties.subscribeToVideo && OT.$.browser() === 'Chrome') {
_subscribeAudioFalseWorkaround = true;
_properties.subscribeToVideo = true;
}
_startConnectingTime = OT.$.now();
if (_stream.connection.id !== _session.connection.id) {
logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
_state.set('ConnectingToPeer');
_peerConnection = new OT.SubscriberPeerConnection(_stream.connection, _session,
_stream, this, _properties);
_peerConnection.on({
disconnected: onDisconnected,
error: onPeerConnectionFailure,
remoteStreamAdded: onRemoteStreamAdded,
remoteStreamRemoved: onRemoteStreamRemoved
}, this);
// initialize the peer connection AFTER we've added the event listeners
_peerConnection.init();
} else {
logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
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, _session.getPublisherForStream(_stream)._.webRtcStream);
}
logAnalyticsEvent('subscribe', 'Attempt', 'streamId', _stream.id);
return this;
};
this.destroy = function(reason, quiet) {
if(reason === 'streamDestroyed') {
if (_state.attemptingToSubscribe) {
// 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'));
}
}
clearInterval(_qosInterval);
_qosInterval = null;
this.disconnect();
if (_chrome) {
_chrome.destroy();
_chrome = null;
}
if (_container) {
_container.destroy();
_container = null;
}
if (_stream && !_stream.destroyed) {
logAnalyticsEvent('unsubscribe', null, 'streamId', _stream.id);
}
_domId = null;
_stream = null;
_session = null;
_properties = null;
if (quiet !== true) {
this.dispatchEvent(
new OT.DestroyedEvent(
OT.Event.names.SUBSCRIBER_DESTROYED,
this,
reason
),
this.off.bind(this)
);
}
return this;
};
this.disconnect = function() {
_state.set('NotSubscribing');
if (_streamContainer) {
_streamContainer.destroy();
_streamContainer = null;
}
if (_peerConnection) {
_peerConnection.destroy();
_peerConnection = null;
logAnalyticsEvent('disconnect', 'PeerConnection', 'streamId', _stream.id);
}
};
this.processMessage = function(type, fromConnection, message) {
OT.debug('OT.Subscriber.processMessage: Received ' + type + ' message from ' +
fromConnection.id);
OT.debug(message);
if (_fromConnectionId !== fromConnection.id) {
_fromConnectionId = fromConnection.id;
}
if (_peerConnection) {
_peerConnection.processMessage(type, message);
}
};
this.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');
} else {
OT.info('Video was not re-enabled because it was manually disabled');
return;
}
}
this.subscribeToVideo(active, 'auto');
this.dispatchEvent(new OT.Event(active ? 'videoEnabled' : 'videoDisabled'));
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.subscribing) {
OT.error('OT.Subscriber.getImgData: Cannot getImgData before the Subscriber ' +
'is subscribing.');
return null;
}
return _streamContainer.imgData;
};
/**
* Sets the audio volume, between 0 and 100, of the Subscriber.
*
* You can set the initial volume when you call the Session.subscribe()
* method. Pass a audioVolume property of the properties parameter
* of the method.
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.
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 (window.navigator.userAgent.toLowerCase().indexOf('chrome') >= 0) { _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; }; /** * 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) { if(_subscribeAudioFalseWorkaround && pValue === true) { // Turn off the workaround if they enable the video _subscribeAudioFalseWorkaround = false; return; } var value = OT.$.castToBoolean(pValue, true); if(_container) { _container.showPoster = !(value && _stream.hasVideo); if(value && _container.video) { _container.loading = value; _container.video.whenTimeIncrements(function(){ _container.loading = false; }, this); } } if (_peerConnection) { _peerConnection.subscribeToVideo(value); if (_session && _stream && (value !== _properties.subscribeToVideo || reason !== _lastSubscribeToVideoReason)) { _stream.setChannelActiveState('video', value, reason); } } _properties.subscribeToVideo = value; _lastSubscribeToVideoReason = reason; return this; }; Object.defineProperties(this, { id: { get: function() { return _domId; }, enumerable: true }, element: { get: function() { return _container.domElement; }, enumerable: true }, widgetId: { get: function() { return _widgetId; } }, stream: { get: function() { return _stream; }, enumerable: true }, streamId: { get: function() { if (!_stream) return null; return _stream.id; }, enumerable: true }, targetElement: { get: function() { return _streamContainer ? _streamContainer.domElement : null; } }, subscribing: { get: function() { return _state.subscribing; }, enumerable: true }, isWebRTC: { get: function() { return true; } }, loading: { get: function(){ return _container && _container.loading; } }, session: { get: function() { return _session; } }, videoWidth: { get: function() { return _streamContainer.videoWidth; }, enumerable: true }, videoHeight: { get: function() { return _streamContainer.videoHeight; }, enumerable: true } }); /** * 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 server, not in * peer-to-peer sessions. In peer-to-peer sessions, calling this method has no effect. *
* Restricting the subscriber frame rate has the following benefits: *
* 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 { _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 when the OpenTok media server stops sending video to the subscriber. * This feature of the OpenTok media server has a subscriber drop the video stream * when connectivity degrades. The subscriber continues to receive the audio stream, * if there is one. *
* If connectivity improves to support video again, the Subscriber object dispatches * a videoEnabled event, and the Subscriber resumes receiving video. *
* This feature is only available in sessions that use the OpenTok media server, * not in peer-to-peer sessions. * * @see Event * @see event:videoEnabled * @name videoDisabled * @event * @memberof Subscriber */ /** * Dispatched when the OpenTok media server resumes sending video to the subscriber * after video was previously disabled. *
* The OpenTok media server has a subscriber drop the video stream when connectivity * degrades (and the Subscriber dispatches a videoDisabled event). When the connectivity * improves to support video the Subscriber dispatches the videoEnabled event and * video resumes. *
* To prevent video from resuming, in the videoEnabled event listener,
* call subscribeToVideo(false) on the Subscriber object.
*
* This feature is only available in sessions that use the OpenTok media server,
* not in peer-to-peer sessions.
*
* @see Event
* @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.apiURLSSL + '/session/' + session.id + '?extended=true',
startTime = OT.$.now(),
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));
}
};
session.logEvent('getSessionInfo', 'Attempt', 'api_url', OT.properties.apiURLSSL);
OT.$.getJSON(sessionInfoURL, {
headers: {
'X-TB-TOKEN-AUTH': session.token,
'X-TB-VERSION': 1
}
}, function(error, sessionInfo) {
if(error) {
var responseText = sessionInfo;
console.log('getJSON said error:', error);
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(Array.isArray(jsonDocument)) {
var errors = jsonDocument.filter(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.apiURLSSL);
onSuccess( new OT.SessionInfo(rawSessionInfo) );
};
onGetErrorCallback = function(session, onFailure, error, responseText) {
session.logEvent('Connect', 'Failure', 'errorMessage',
'GetSessionInfo:' + error.code + ':' + error.message + ':' + responseText);
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.
* @property {Number} supportsWebRTC Whether the client supports WebRTC (1) or not (0).
*/
OT.Capabilities = function(permissions) {
this.publish = permissions.indexOf('publish') !== -1 ? 1 : 0;
this.subscribe = permissions.indexOf('subscribe') !== -1 ? 1 : 0;
this.forceUnpublish = permissions.indexOf('forceunpublish') !== -1 ? 1 : 0;
this.forceDisconnect = permissions.indexOf('forcedisconnect') !== -1 ? 1 : 0;
this.supportsWebRTC = OT.$.supportsWebRTC() ? 1 : 0;
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) {
// 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;
}
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;
OT.$.eventing(this);
var setState = OT.$.statable(this, [
'disconnected', 'connecting', 'connected', 'disconnecting'
], 'disconnected');
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 = 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);
}.bind(this);
this.dispatchEvent(publicEvent, defaultAction);
};
connectionCreatedHandler = function(connection) {
// We don't broadcast events for the symphony connection
if (connection.id.match(/^symphony\./)) return;
this.dispatchEvent(new OT.ConnectionEvent(
OT.Event.names.CONNECTION_CREATED,
connection
));
};
connectionDestroyedHandler = function(connection, reason) {
// We don't broadcast events for the symphony connection
if (connection.id.match(/^symphony\./)) return;
// Don't delete the connection if it's ours. This only happens when
// we're about to receive a session disconnected and session disconnected
// will also clean up our connection.
if (connection.id === _socket.id) return;
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 === '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.publishers.where({ streamId: stream.id }).forEach(function(publisher) {
publisher._.unpublishFromSession(this, reason);
}.bind(this));
return;
}
var event = new OT.StreamEvent('streamDestroyed', stream, reason, true);
var defaultAction = function() {
if (!event.isDefaultPrevented()) {
// If we are subscribed to any of the streams we should unsubscribe
OT.subscribers.where({streamId: stream.id}).forEach(function(subscriber) {
if (subscriber.session.id === this.id) {
if(subscriber.stream) {
subscriber.destroy('streamDestroyed');
}
}
}, this);
} else {
// @TODO Add a one time warning that this no longer cleans up the publisher
}
}.bind(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() {
_token = null;
setState('disconnected');
this.connections.destroy();
this.streams.destroy();
this.archives.destroy();
};
disconnectComponents = function(reason) {
OT.publishers.where({session: this}).forEach(function(publisher) {
publisher.disconnect(reason);
});
OT.subscribers.where({session: this}).forEach(function(subscriber) {
subscriber.disconnect();
});
};
destroyPublishers = function(reason) {
OT.publishers.where({session: this}).forEach(function(publisher) {
publisher._.streamDestroyed(reason);
});
};
destroySubscribers = function(reason) {
OT.subscribers.where({session: this}).forEach(function(subscriber) {
subscriber.destroy(reason);
});
};
connectMessenger = function() {
OT.debug('OT.Session: connecting to Raptor');
var socketUrl = OT.properties.messagingProtocol + '://' + this.sessionInfo.messagingServer +
':' + OT.properties.messagingPort + '/rumorwebsocketsv2',
symphonyUrl = OT.properties.symphonyAddresss || 'symphony.' +
this.sessionInfo.messagingServer;
_socket = new OT.Raptor.Socket(_widgetId, socketUrl, symphonyUrl,
OT.SessionDispatcher(this));
var analyticsPayload = [
socketUrl, navigator.userAgent, OT.properties.version,
window.externalHost ? 'yes' : 'no'
];
_socket.connect(_token, this.sessionInfo, function(error, sessionState) {
if (error) {
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);
setState('connected');
this.logEvent('Connect', 'Success',
'webSocketServerUrl|userAgent|sdkVersion|chromeFrame',
analyticsPayload.map(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), function() {
this.connections._triggerAddEvents(); // { id: this.connection.id }
this.streams._triggerAddEvents(); // { id: this.stream.id }
this.archives._triggerAddEvents();
}.bind(this)
);
}.bind(this));
};
getSessionInfo = function() {
if (this.is('connecting')) {
OT.SessionInfo.get(
this,
onSessionInfoResponse.bind(this),
function(error) {
sessionConnectFailed.call(this, error.message +
(error.code ? ' (' + error.code + ')' : ''), error.code);
}.bind(this)
);
}
};
onSessionInfoResponse = function(sessionInfo) {
if (this.is('connecting')) {
this.sessionInfo = sessionInfo;
if (this.sessionInfo.partnerId && this.sessionInfo.partnerId !== _apiKey) {
_apiKey = this.sessionInfo.partnerId;
var reason = 'Authentication Error: The 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 = function(action) {
return this.capabilities.permittedTo(action);
}.bind(this);
dispatchError = function(code, message, completionHandler) {
OT.dispatchError(code, message, completionHandler, this);
}.bind(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. Pass your API key as the apiKey parameter.
* You get an API key when you sign up
* for an OpenTok account. Pass a token string as the token parameter. You generate
* a token using the
* OpenTok server-side
* libraries or the Dashboard page. For
* more information, see Connection token creation.
*
* Upon a successful connection, the Session object dispatches a sessionConnected
* event. Call the on() method to set up an event handler to process this event before
* calling other methods of the Session object.
*
* The Session object dispatches a connectionCreated event when other clients
* create connections to the session.
*
* The OT object dispatches an exception event if the session ID,
* API key, or token string are invalid. See ExceptionEvent
* and OT.on().
*
* The application throws an error if the system requirements are not met * (see OT.checkSystemRequirements()). * The application also throws an error if the session has peer-to-peer streaming enabled * and two clients are already connected to the session (see our * server-side libraries). *
* ** With the peer-to-peer option enabled for a session, the session supports two connections. * Without the peer-to-peer option enabled, the session supports up to four connections. On * clients that attempt to a session that already has the maximum number of connections, the * OT object dispatches an `exception` event (with the `code` property set to 1007). See * Session Creation documentation. *
* * ** 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}.
*
* 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.
*
connect() 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 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.
*
* 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).
*
* 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.
*
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. *
* *
* 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.
*
* 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. 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' || publisher.nodeType === Node.ELEMENT_NODE) {
// 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).
*
* 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.
*
* <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.
*
* 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:
* 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).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).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).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.
*
* * 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);
}.bind(this),
jsepCandidate: function(streamId, candidate) {
return _socket.jsepCandidate(streamId, candidate);
}.bind(this),
jsepOffer: function(streamId, offerSdp) {
return _socket.jsepOffer(streamId, offerSdp);
}.bind(this),
jsepOfferP2p: function(streamId, subscriberId, offerSdp) {
return _socket.jsepOfferP2p(streamId, subscriberId, offerSdp);
}.bind(this),
jsepAnswer: function(streamId, answerSdp) {
return _socket.jsepAnswer(streamId, answerSdp);
}.bind(this),
jsepAnswerP2p: function(streamId, subscriberId, answerSdp) {
return _socket.jsepAnswerP2p(streamId, subscriberId, answerSdp);
}.bind(this),
// session.on("signal", function(SignalEvent))
// session.on("signal:{type}", function(SignalEvent))
dispatchSignal: function(fromConnection, type, data) {
var event = new OT.SignalEvent(type, data, fromConnection);
event.target = this;
// signal a "signal" event
// NOTE: trigger doesn't support defaultAction, and therefore preventDefault.
this.trigger(OT.Event.names.SIGNAL, event);
// signal an "signal:{type}" event" if there was a custom type
if (type) this.dispatchEvent(event);
}.bind(this),
subscriberCreate: function(stream, subscriber, channelsToSubscribeTo, completion) {
return _socket.subscriberCreate(stream.id, subscriber.widgetId,
channelsToSubscribeTo, completion);
}.bind(this),
subscriberDestroy: function(stream, subscriber) {
return _socket.subscriberDestroy(stream.id, subscriber.widgetId);
}.bind(this),
subscriberUpdate: function(stream, subscriber, attributes) {
return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes);
}.bind(this),
subscriberChannelUpdate: function(stream, subscriber, channel, attributes) {
return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id,
attributes);
}.bind(this),
streamCreate: 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);
}.bind(this),
streamDestroy: function(streamId) {
_socket.streamDestroy(streamId);
}.bind(this),
streamChannelUpdate: function(stream, channel, attributes) {
_socket.streamChannelUpdate(stream.id, channel.id, attributes);
}.bind(this)
};
/**
* Sends a signal to each client or specified clients in the session. Specify a
* connections property of the signal parameter to limit the
* recipients of the signal; 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 to all * clients connected to the session: *
* session.signal({
* type: "foo",
* data: "hello"
* },
* function(error) {
* if (error) {
* console.log("signal error: " + error.reason);
* } else {
* console.log("signal sent");
* }
* }
* );
*
* * Calling this method without limiting the set of recipient clients will result in * multiple signals sent (one to each client in the session). For information on charges * for signaling, see the OpenTok * pricing page. *
* The following example sends a signal of type "foo" with a specified data payload to two * specific clients connected to the session: *
* session.signal({
* type: "foo",
* to: [connection1, connection2]; // connection1 and 2 are Connection objects
* data: "hello"
* },
* function(error) {
* if (error) {
* console.log("signal error: " + error.reason);
* } 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:
*
to — (Array) An array of Connection
* objects, corresponding to clients that the message is to be sent to. If you do not
* specify this property, the signal is sent to all clients connected to the session.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.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. 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). | *
| 500 | You are not connected to the OpenTok session. | *
reason — (String) A description of the error.signal — (Object) An object with properties corresponding to the
* values passed into the signal() method — data,
* to, and type.
* 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 sucessfully
* 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. *
* *
* 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.
*
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().
*
* 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.
*
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');
};
OT.$.defineGetters(this, {
apiKey: function() { return _apiKey; },
token: function() { return _token; },
connected: function() { return this.is('connected'); },
connection: function() {
return _socket && _socket.id ? this.connections.get(_socket.id) : null;
},
sessionId: function() { return _sessionId; },
id: function() { return _sessionId; },
capabilities: function() {
var connection = this.connection;
return connection ? connection.permissions : new OT.Capabilities([]);
}.bind(this)
}, true);
/**
* A new client, other than 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).
* @name streamPropertyChanged
* @event
* @memberof Session
* @see StreamPropertyChangedEvent
* @see Publisher.publishAudio()
* @see Publisher.publishVideo()
* @see Stream.hasAudio
* @see Stream.hasVideo
* @see Stream.videoDimensions
*/
/**
* 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 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);