diff --git a/browser/base/content/aboutTabCrashed.css b/browser/base/content/aboutTabCrashed.css new file mode 100644 index 000000000000..4122506da9d6 --- /dev/null +++ b/browser/base/content/aboutTabCrashed.css @@ -0,0 +1,8 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +html:not(.crashDumpSubmitted) #reportSent, +html:not(.crashDumpAvailable) #report-box { + display: none; +} diff --git a/browser/base/content/aboutTabCrashed.js b/browser/base/content/aboutTabCrashed.js index d12a6c16afba..a0abedc0e58f 100644 --- a/browser/base/content/aboutTabCrashed.js +++ b/browser/base/content/aboutTabCrashed.js @@ -12,21 +12,35 @@ function parseQueryString() { document.title = parseQueryString(); -addEventListener("DOMContentLoaded", () => { - let tryAgain = document.getElementById("tryAgain"); - let sendCrashReport = document.getElementById("checkSendReport"); +function shouldSendReport() { + if (!document.documentElement.classList.contains("crashDumpAvailable")) + return false; + return document.getElementById("sendReport").checked; +} - tryAgain.addEventListener("click", () => { - let event = new CustomEvent("AboutTabCrashedTryAgain", { - bubbles: true, - detail: { - sendCrashReport: sendCrashReport.checked, - }, - }); - - document.dispatchEvent(event); +function sendEvent(message) { + let event = new CustomEvent("AboutTabCrashedMessage", { + bubbles: true, + detail: { + message, + sendCrashReport: shouldSendReport(), + }, }); -}); + + document.dispatchEvent(event); +} + +function closeTab() { + sendEvent("closeTab"); +} + +function restoreTab() { + sendEvent("restoreTab"); +} + +function restoreAll() { + sendEvent("restoreAll"); +} // Error pages are loaded as LOAD_BACKGROUND, so they don't get load events. var event = new CustomEvent("AboutTabCrashedLoad", {bubbles:true}); diff --git a/browser/base/content/aboutTabCrashed.xhtml b/browser/base/content/aboutTabCrashed.xhtml index d19429adff9c..ec1f37161bf1 100644 --- a/browser/base/content/aboutTabCrashed.xhtml +++ b/browser/base/content/aboutTabCrashed.xhtml @@ -12,18 +12,19 @@ %globalDTD; - - %browserDTD; %brandDTD; - + + %tabCrashedDTD; ]>
+ @@ -36,12 +37,19 @@&tabCrashed.message;
&tabCrashed.reportSent;
+ diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 8bf3e15c39cc..a5d21f6ab708 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -271,6 +271,12 @@ XPCOMUtils.defineLazyGetter(this, "PageMenuParent", function() { return new tmp.PageMenuParent(); }); +function* browserWindows() { + let windows = Services.wm.getEnumerator("navigator:browser"); + while (windows.hasMoreElements()) + yield windows.getNext(); +} + /** * We can avoid adding multiple load event listeners and save some time by adding * one listener that calls all real handlers. @@ -1116,7 +1122,7 @@ var gBrowserInit = { #endif }, false, true); - gBrowser.addEventListener("AboutTabCrashedTryAgain", function(event) { + gBrowser.addEventListener("AboutTabCrashedMessage", function(event) { let ownerDoc = event.originalTarget; if (!ownerDoc.documentURI.startsWith("about:tabcrashed")) { @@ -1134,8 +1140,23 @@ var gBrowserInit = { TabCrashReporter.submitCrashReport(browser); } #endif + let tab = gBrowser.getTabForBrowser(browser); - SessionStore.reviveCrashedTab(tab); + switch (event.detail.message) { + case "closeTab": + gBrowser.removeTab(tab, { animate: true }); + break; + case "restoreTab": + SessionStore.reviveCrashedTab(tab); + break; + case "restoreAll": + for (let browserWin of browserWindows()) { + for (let tab of window.gBrowser.tabs) { + SessionStore.reviveCrashedTab(tab); + } + } + break; + } }, false, true); let uriToLoad = this._getUriToLoad(); @@ -6471,11 +6492,9 @@ function warnAboutClosingWindow() { return gBrowser.warnAboutClosingTabs(gBrowser.closingTabsEnum.ALL); // Figure out if there's at least one other browser window around. - let e = Services.wm.getEnumerator("navigator:browser"); let otherPBWindowExists = false; let nonPopupPresent = false; - while (e.hasMoreElements()) { - let win = e.getNext(); + for (let win of browserWindows()) { if (!win.closed && win != window) { if (isPBWindow && PrivateBrowsingUtils.isWindowPrivate(win)) otherPBWindowExists = true; @@ -7574,9 +7593,7 @@ function switchToTabHavingURI(aURI, aOpenNew, aOpenParams={}) { if (isBrowserWindow && switchIfURIInWindow(window)) return true; - let winEnum = Services.wm.getEnumerator("navigator:browser"); - while (winEnum.hasMoreElements()) { - let browserWin = winEnum.getNext(); + for (let browserWin of browserWindows()) { // Skip closed (but not yet destroyed) windows, // and the current window (which was checked earlier). if (browserWin.closed || browserWin == window) diff --git a/browser/base/content/tabbrowser.css b/browser/base/content/tabbrowser.css index a9dc8391f2e1..121a7ad42940 100644 --- a/browser/base/content/tabbrowser.css +++ b/browser/base/content/tabbrowser.css @@ -51,9 +51,10 @@ tabpanels { } } -.tab-icon-image:not([src]):not([pinned]), +.tab-icon-image:not([src]):not([pinned]):not([crashed]), .tab-throbber:not([busy]), -.tab-throbber[busy] + .tab-icon-image { +.tab-icon-image[busy], +.tab-icon-overlay[busy] { display: none; } diff --git a/browser/base/content/tabbrowser.xml b/browser/base/content/tabbrowser.xml index abb3d08e59ff..963467ee37c3 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -664,9 +664,13 @@ // We need to add 2 because loadURIWithFlags may have // cancelled a pending load which would have cleared // its anchor scroll detection temporary increment. - if (aWebProgress.isTopLevel) + if (aWebProgress.isTopLevel) { this.mBrowser.userTypedClear += 2; + // If the browser is loading it must not be crashed anymore + this.mTab.removeAttribute("crashed"); + } + if (this._shouldShowProgress(aRequest)) { if (!(aStateFlags & nsIWebProgressListener.STATE_RESTORING)) { this.mTab.setAttribute("busy", "true"); @@ -1484,6 +1488,10 @@ if (aShouldBeRemote) { tab.setAttribute("remote", "true"); + // Switching the browser to be remote will connect to a new child + // process so the browser can no longer be considered to be + // crashed. + tab.removeAttribute("crashed"); } else { tab.removeAttribute("remote"); aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned }) @@ -3579,6 +3587,7 @@ browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null); browser.removeAttribute("crashedPageTitle"); let tab = this.getTabForBrowser(browser); + tab.setAttribute("crashed", true); this.setIcon(tab, icon); ]]> @@ -4980,11 +4989,14 @@ class="tab-throbber" role="presentation" layer="true" /> -once
, and off
* methods of objects that can dispatch events.
*
* @class EventDispatcher
*/
- OTHelpers.eventing = function(self, syncronous) {
- var _events = {};
+OTHelpers.eventing = function(self, syncronous) {
+ var _events = {};
+ // Call the defaultAction, passing args
+ function executeDefaultAction(defaultAction, args) {
+ if (!defaultAction) return;
- // Call the defaultAction, passing args
- function executeDefaultAction(defaultAction, args) {
- if (!defaultAction) return;
+ defaultAction.apply(null, args.slice());
+ }
- 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;
- // 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;
- var listenerAcks = listeners.length;
+ OTHelpers.forEach(listeners, function(listener) { // , index
+ function filterHandlerAndContext(_listener) {
+ return _listener.context === listener.context && _listener.handler === listener.handler;
+ }
- 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--;
- // 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);
- }
+ if (listenerAcks === 0) {
+ executeDefaultAction(defaultAction, 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;
});
- });
- }
-
-
- // This is identical to executeListenersAsyncronously except that handlers will
- // be executed syncronously.
- //
- // On completion the defaultAction handler will be executed with the args.
- //
- // @param [Array] listeners
- // An array of functions to execute. Each will be passed args.
- //
- // @param [Array] args
- // An array of arguments to execute each function in +listeners+ with.
- //
- // @param [String] name
- // The name of this event.
- //
- // @param [Function, Null, Undefined] defaultAction
- // An optional function to execute after every other handler. This will execute even
- // if +listeners+ is empty. +defaultAction+ will be passed args as a normal
- // handler would.
- //
- // @return Undefined
- //
- function executeListenersSyncronously(name, args) { // defaultAction is not used
- var listeners = _events[name];
- if (!listeners || listeners.length === 0) return;
-
- OTHelpers.forEach(listeners, function(listener) { // index
- (listener.closure || listener.handler).apply(listener.context || null, args);
- });
- }
-
- var executeListeners = syncronous === true ?
- executeListenersSyncronously : executeListenersAsyncronously;
-
-
- var removeAllListenersNamed = function (eventName, context) {
- if (_events[eventName]) {
- if (context) {
- // We are removing by context, get only events that don't
- // match that context
- _events[eventName] = OTHelpers.filter(_events[eventName], function(listener){
- return listener.context !== context;
- });
- }
- else {
- delete _events[eventName];
- }
- }
- };
-
- var addListeners = OTHelpers.bind(function (eventNames, handler, context, closure) {
- var listener = {handler: handler};
- if (context) listener.context = context;
- if (closure) listener.closure = closure;
-
- OTHelpers.forEach(eventNames, function(name) {
- if (!_events[name]) _events[name] = [];
- _events[name].push(listener);
- var addedListener = name + ':added';
- if (_events[addedListener]) {
- executeListeners(addedListener, [_events[name].length]);
- }
- });
- }, self);
-
-
- var removeListeners = function (eventNames, handler, context) {
- function filterHandlerAndContext(listener) {
- return !(listener.handler === handler && listener.context === context);
- }
-
- OTHelpers.forEach(eventNames, OTHelpers.bind(function(name) {
- if (_events[name]) {
- _events[name] = OTHelpers.filter(_events[name], filterHandlerAndContext);
- if (_events[name].length === 0) delete _events[name];
- var removedListener = name + ':removed';
- if (_events[ removedListener]) {
- executeListeners(removedListener, [_events[name] ? _events[name].length : 0]);
- }
- }
- }, self));
-
- };
-
- // Execute any listeners bound to the +event+ Event.
- //
- // Each handler will be executed async. On completion the defaultAction
- // handler will be executed with the args.
- //
- // @param [Event] event
- // An Event object.
- //
- // @param [Function, Null, Undefined] defaultAction
- // An optional function to execute after every other handler. This will execute even
- // if there are listeners bound to this event. +defaultAction+ will be passed
- // args as a normal handler would.
- //
- // @return this
- //
- self.dispatchEvent = function(event, defaultAction) {
- if (!event.type) {
- OTHelpers.error('OTHelpers.Eventing.dispatchEvent: Event has no type');
- OTHelpers.error(event);
-
- throw new Error('OTHelpers.Eventing.dispatchEvent: Event has no type');
- }
-
- if (!event.target) {
- event.target = this;
- }
-
- if (!_events[event.type] || _events[event.type].length === 0) {
- executeDefaultAction(defaultAction, [event]);
- return;
- }
-
- executeListeners(event.type, [event], defaultAction);
-
- return this;
- };
-
- // Execute each handler for the event called +name+.
- //
- // Each handler will be executed async, and any exceptions that they throw will
- // be caught and logged
- //
- // How to pass these?
- // * defaultAction
- //
- // @example
- // foo.on('bar', function(name, message) {
- // alert("Hello " + name + ": " + message);
- // });
- //
- // foo.trigger('OpenTok', 'asdf'); // -> Hello OpenTok: asdf
- //
- //
- // @param [String] eventName
- // The name of this event.
- //
- // @param [Array] arguments
- // Any additional arguments beyond +eventName+ will be passed to the handlers.
- //
- // @return this
- //
- self.trigger = function(eventName) {
- if (!_events[eventName] || _events[eventName].length === 0) {
- return;
- }
-
- var args = Array.prototype.slice.call(arguments);
-
- // Remove the eventName arg
- args.shift();
-
- executeListeners(eventName, args);
-
- return this;
- };
-
- /**
- * Adds an event handler function for one or more events.
- *
- * - * The following code adds an event handler for one event: - *
- * - *- * obj.on("eventName", function (event) { - * // This is the event handler. - * }); - *- * - *
If you pass in multiple event names and a handler method, the handler is - * registered for each of those events:
- * - *- * obj.on("eventName1 eventName2", - * function (event) { - * // This is the event handler. - * }); - *- * - *
You can also pass in a third context
parameter (which is optional) to
- * define the value of this
in the handler method:
obj.on("eventName", - * function (event) { - * // This is the event handler. - * }, - * obj); - *- * - *
- * The method also supports an alternate syntax, in which the first parameter is an object - * that is a hash map of event names and handler functions and the second parameter (optional) - * is the context for this in each handler: - *
- *- * obj.on( - * { - * eventName1: function (event) { - * // This is the handler for eventName1. - * }, - * eventName2: function (event) { - * // This is the handler for eventName2. - * } - * }, - * obj); - *- * - *
- * If you do not add a handler for an event, the event is ignored locally. - *
- * - * @param {String} type The string identifying the type of event. You can specify multiple event - * names in this string, separating them with a space. The event handler will process each of - * the events. - * @param {Function} handler The handler function to process the event. This function takes - * the event object as a parameter. - * @param {Object} context (Optional) Defines the value ofthis
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);
- }
+ delete _events[eventName];
+ }
+ }
+ };
+
+ var addListeners = OTHelpers.bind(function (eventNames, handler, context, closure) {
+ var listener = {handler: handler};
+ if (context) listener.context = context;
+ if (closure) listener.closure = closure;
+
+ OTHelpers.forEach(eventNames, function(name) {
+ if (!_events[name]) _events[name] = [];
+ _events[name].push(listener);
+ var addedListener = name + ':added';
+ if (_events[addedListener]) {
+ executeListeners(addedListener, [_events[name].length]);
+ }
+ });
+ }, self);
+
+
+ var removeListeners = function (eventNames, handler, context) {
+ function filterHandlerAndContext(listener) {
+ return !(listener.handler === handler && listener.context === context);
+ }
+
+ OTHelpers.forEach(eventNames, OTHelpers.bind(function(name) {
+ if (_events[name]) {
+ _events[name] = OTHelpers.filter(_events[name], filterHandlerAndContext);
+ if (_events[name].length === 0) delete _events[name];
+ var removedListener = name + ':removed';
+ if (_events[ removedListener]) {
+ executeListeners(removedListener, [_events[name] ? _events[name].length : 0]);
}
}
+ }, self));
- return this;
- };
+ };
- /**
- * Removes an event handler or handlers.
- *
- * If you pass in one event name and a handler method, the handler is removed for that - * event:
- * - *obj.off("eventName", eventHandler);- * - *
If you pass in multiple event names and a handler method, the handler is removed for - * those events:
- * - *obj.off("eventName1 eventName2", eventHandler);- * - *
If you pass in an event name (or names) and no handler method, all handlers are - * removed for those events:
- * - *obj.off("event1Name event2Name");- * - *
If you pass in no arguments, all event handlers are removed for all events - * dispatched by the object:
- * - *obj.off();- * - *
- * The method also supports an alternate syntax, in which the first parameter is an object that - * is a hash map of event names and handler functions and the second parameter (optional) is - * the context for this in each handler: - *
- *- * obj.off( - * { - * eventName1: event1Handler, - * eventName2: event2Handler - * }); - *- * - * @param {String} type (Optional) The string identifying the type of event. You can - * use a space to specify multiple events, as in "accessAllowed accessDenied - * accessDialogClosed". If you pass in no
type
value (or other arguments),
- * all event handlers are removed for the object.
- * @param {Function} handler (Optional) The event handler function to remove. The handler
- * must be the same function object as was passed into on()
. Be careful with
- * helpers like bind()
that return a new function when called. If you pass in
- * no handler
, all event handlers are removed for the specified event
- * type
.
- * @param {Object} context (Optional) If you specify a context
, the event handler
- * is removed for all specified events and handlers that use the specified context. (The
- * context must match the context passed into on()
.)
- *
- * @returns {Object} The object that dispatched the event.
- *
- * @memberOf EventDispatcher
- * @method #off
- * @see on()
- * @see once()
- * @see Events
- */
- self.off = function(eventNames, handlerOrContext, context) {
- if (typeof eventNames === 'string') {
- if (handlerOrContext && OTHelpers.isFunction(handlerOrContext)) {
- removeListeners(eventNames.split(' '), handlerOrContext, context);
+ // 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 = prototypeSlice.call(arguments);
+
+ // Remove the eventName arg
+ args.shift();
+
+ executeListeners(eventName, args);
+
+ return this;
+ };
+
+ /**
+ * Adds an event handler function for one or more events.
+ *
+ * + * The following code adds an event handler for one event: + *
+ * + *+ * obj.on("eventName", function (event) { + * // This is the event handler. + * }); + *+ * + *
If you pass in multiple event names and a handler method, the handler is + * registered for each of those events:
+ * + *+ * obj.on("eventName1 eventName2", + * function (event) { + * // This is the event handler. + * }); + *+ * + *
You can also pass in a third context
parameter (which is optional) to
+ * define the value of this
in the handler method:
obj.on("eventName", + * function (event) { + * // This is the event handler. + * }, + * obj); + *+ * + *
+ * The method also supports an alternate syntax, in which the first parameter is an object + * that is a hash map of event names and handler functions and the second parameter (optional) + * is the context for this in each handler: + *
+ *+ * obj.on( + * { + * eventName1: function (event) { + * // This is the handler for eventName1. + * }, + * eventName2: function (event) { + * // This is the handler for eventName2. + * } + * }, + * obj); + *+ * + *
+ * If you do not add a handler for an event, the event is ignored locally. + *
+ * + * @param {String} type The string identifying the type of event. You can specify multiple event + * names in this string, separating them with a space. The event handler will process each of + * the events. + * @param {Function} handler The handler function to process the event. This function takes + * the event object as a parameter. + * @param {Object} context (Optional) Defines the value ofthis
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);
}
- else {
- OTHelpers.forEach(eventNames.split(' '), function(name) {
- removeAllListenersNamed(name, handlerOrContext);
- }, this);
+ }
+ }
+
+ return this;
+ };
+
+ /**
+ * Removes an event handler or handlers.
+ *
+ * If you pass in one event name and a handler method, the handler is removed for that + * event:
+ * + *obj.off("eventName", eventHandler);+ * + *
If you pass in multiple event names and a handler method, the handler is removed for + * those events:
+ * + *obj.off("eventName1 eventName2", eventHandler);+ * + *
If you pass in an event name (or names) and no handler method, all handlers are + * removed for those events:
+ * + *obj.off("event1Name event2Name");+ * + *
If you pass in no arguments, all event handlers are removed for all events + * dispatched by the object:
+ * + *obj.off();+ * + *
+ * The method also supports an alternate syntax, in which the first parameter is an object that + * is a hash map of event names and handler functions and the second parameter (optional) is + * the context for this in each handler: + *
+ *+ * obj.off( + * { + * eventName1: event1Handler, + * eventName2: event2Handler + * }); + *+ * + * @param {String} type (Optional) The string identifying the type of event. You can + * use a space to specify multiple events, as in "accessAllowed accessDenied + * accessDialogClosed". If you pass in no
type
value (or other arguments),
+ * all event handlers are removed for the object.
+ * @param {Function} handler (Optional) The event handler function to remove. The handler
+ * must be the same function object as was passed into on()
. Be careful with
+ * helpers like bind()
that return a new function when called. If you pass in
+ * no handler
, all event handlers are removed for the specified event
+ * type
.
+ * @param {Object} context (Optional) If you specify a context
, the event handler
+ * is removed for all specified events and handlers that use the specified context. (The
+ * context must match the context passed into on()
.)
+ *
+ * @returns {Object} The object that dispatched the event.
+ *
+ * @memberOf EventDispatcher
+ * @method #off
+ * @see on()
+ * @see once()
+ * @see Events
+ */
+ self.off = function(eventNames, handlerOrContext, context) {
+ if (typeof eventNames === 'string') {
+ if (handlerOrContext && OTHelpers.isFunction(handlerOrContext)) {
+ removeListeners(eventNames.split(' '), handlerOrContext, context);
+ }
+ else {
+ OTHelpers.forEach(eventNames.split(' '), function(name) {
+ removeAllListenersNamed(name, handlerOrContext);
+ }, this);
+ }
+
+ } else if (!eventNames) {
+ // remove all bound events
+ _events = {};
+
+ } else {
+ for (var name in eventNames) {
+ if (eventNames.hasOwnProperty(name)) {
+ removeListeners([name], eventNames[name], handlerOrContext);
}
+ }
+ }
- } else if (!eventNames) {
- // remove all bound events
- _events = {};
+ return this;
+ };
+
+ /**
+ * Adds an event handler function for one or more events. Once the handler is called,
+ * the specified handler method is removed as a handler for this event. (When you use
+ * the on()
method to add an event handler, the handler is not
+ * removed when it is called.) The once()
method is the equivilent of
+ * calling the on()
+ * method and calling off()
the first time the handler is invoked.
+ *
+ *
+ * The following code adds a one-time event handler for the accessAllowed
event:
+ *
+ * obj.once("eventName", function (event) { + * // This is the event handler. + * }); + *+ * + *
If you pass in multiple event names and a handler method, the handler is registered + * for each of those events:
+ * + *obj.once("eventName1 eventName2" + * function (event) { + * // This is the event handler. + * }); + *+ * + *
You can also pass in a third context
parameter (which is optional) to define
+ * the value of
+ * this
in the handler method:
obj.once("eventName", + * function (event) { + * // This is the event handler. + * }, + * obj); + *+ * + *
+ * The method also supports an alternate syntax, in which the first parameter is an object that + * is a hash map of event names and handler functions and the second parameter (optional) is the + * context for this in each handler: + *
+ *+ * obj.once( + * { + * eventName1: function (event) { + * // This is the event handler for eventName1. + * }, + * eventName2: function (event) { + * // This is the event handler for eventName1. + * } + * }, + * obj); + *+ * + * @param {String} type The string identifying the type of event. You can specify multiple + * event names in this string, separating them with a space. The event handler will process + * the first occurence of the events. After the first event, the handler is removed (for + * all specified events). + * @param {Function} handler The handler function to process the event. This function takes + * the event object as a parameter. + * @param {Object} context (Optional) Defines the value of
this
in the event
+ * handler function.
+ *
+ * @returns {Object} The object that dispatched the event.
+ *
+ * @memberOf EventDispatcher
+ * @method #once
+ * @see on()
+ * @see off()
+ * @see Events
+ */
+ self.once = function(eventNames, handler, context) {
+ var names = eventNames.split(' '),
+ fun = OTHelpers.bind(function() {
+ var result = handler.apply(context || null, arguments);
+ removeListeners(names, handler, context);
+
+ return result;
+ }, this);
+
+ addListeners(names, handler, context, fun);
+ return this;
+ };
+
+
+ /**
+ * Deprecated; use on() or once() instead.
+ * + * This method registers a method as an event listener for a specific event. + *
+ * + *
+ * If a handler is not registered for an event, the event is ignored locally. If the + * event listener function does not exist, the event is ignored locally. + *
+ *
+ * Throws an exception if the listener
name is invalid.
+ *
this
in the event
+ * handler function.
+ *
+ * @memberOf EventDispatcher
+ * @method #addEventListener
+ * @see on()
+ * @see once()
+ * @see Events
+ */
+ // See 'on' for usage.
+ // @depreciated will become a private helper function in the future.
+ self.addEventListener = function(eventName, handler, context) {
+ OTHelpers.warn('The addEventListener() method is deprecated. Use on() or once() instead.');
+ addListeners([eventName], handler, context);
+ };
+
+
+ /**
+ * Deprecated; use on() or once() instead.
+ * + * Removes an event listener for a specific event. + *
+ * + *
+ * Throws an exception if the listener
name is invalid.
+ *
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 {
- for (var name in eventNames) {
- if (eventNames.hasOwnProperty(name)) {
- removeListeners([name], eventNames[name], handlerOrContext);
- }
+ OTHelpers.warn('Event.preventDefault :: Trying to preventDefault ' +
+ 'on an Event that isn\'t cancelable');
+ }
+ };
+
+ this.isDefaultPrevented = function() {
+ return _defaultPrevented;
+ };
+ };
+};
+
+/*jshint browser:true, smarttabs:true */
+
+// tb_require('../helpers.js')
+// tb_require('./callbacks.js')
+// tb_require('./dom_events.js')
+
+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]);
+ }
+ }
+ }
- return this;
- };
-
-
- /**
- * Adds an event handler function for one or more events. Once the handler is called,
- * the specified handler method is removed as a handler for this event. (When you use
- * the on()
method to add an event handler, the handler is not
- * removed when it is called.) The once()
method is the equivilent of
- * calling the on()
- * method and calling off()
the first time the handler is invoked.
- *
- *
- * The following code adds a one-time event handler for the accessAllowed
event:
- *
- * obj.once("eventName", function (event) { - * // This is the event handler. - * }); - *- * - *
If you pass in multiple event names and a handler method, the handler is registered - * for each of those events:
- * - *obj.once("eventName1 eventName2" - * function (event) { - * // This is the event handler. - * }); - *- * - *
You can also pass in a third context
parameter (which is optional) to define
- * the value of
- * this
in the handler method:
obj.once("eventName", - * function (event) { - * // This is the event handler. - * }, - * obj); - *- * - *
- * The method also supports an alternate syntax, in which the first parameter is an object that - * is a hash map of event names and handler functions and the second parameter (optional) is the - * context for this in each handler: - *
- *- * obj.once( - * { - * eventName1: function (event) { - * // This is the event handler for eventName1. - * }, - * eventName2: function (event) { - * // This is the event handler for eventName1. - * } - * }, - * obj); - *- * - * @param {String} type The string identifying the type of event. You can specify multiple - * event names in this string, separating them with a space. The event handler will process - * the first occurence of the events. After the first event, the handler is removed (for - * all specified events). - * @param {Function} handler The handler function to process the event. This function takes - * the event object as a parameter. - * @param {Object} context (Optional) Defines the value of
this
in the event
- * handler function.
- *
- * @returns {Object} The object that dispatched the event.
- *
- * @memberOf EventDispatcher
- * @method #once
- * @see on()
- * @see off()
- * @see Events
- */
- self.once = function(eventNames, handler, context) {
- var names = eventNames.split(' '),
- fun = OTHelpers.bind(function() {
- var result = handler.apply(context || null, arguments);
- removeListeners(names, handler, context);
-
- return result;
- }, this);
-
- addListeners(names, handler, context, fun);
- return this;
- };
-
-
- /**
- * Deprecated; use on() or once() instead.
- * - * This method registers a method as an event listener for a specific event. - *
- * - *
- * If a handler is not registered for an event, the event is ignored locally. If the - * event listener function does not exist, the event is ignored locally. - *
- *
- * Throws an exception if the listener
name is invalid.
- *
this
in the event
- * handler function.
- *
- * @memberOf EventDispatcher
- * @method #addEventListener
- * @see on()
- * @see once()
- * @see Events
- */
- // See 'on' for usage.
- // @depreciated will become a private helper function in the future.
- self.addEventListener = function(eventName, handler, context) {
- OTHelpers.warn('The addEventListener() method is deprecated. Use on() or once() instead.');
- addListeners([eventName], handler, context);
- };
-
-
- /**
- * Deprecated; use on() or once() instead.
- * - * Removes an event listener for a specific event. - *
- * - *
- * Throws an exception if the listener
name is invalid.
- *
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;
+ var setChildren = function(child) {
+ if(typeof child === 'string') {
+ element.innerHTML = element.innerHTML + child;
+ } else {
+ element.appendChild(child);
+ }
};
- OTHelpers.eventing.Event = function() {
+ if($.isArray(children)) {
+ $.forEach(children, setChildren);
+ } else if(children) {
+ setChildren(children);
+ }
- return function (type, cancelable) {
- this.type = type;
- this.cancelable = cancelable !== undefined ? cancelable : true;
+ return element;
+};
- var _defaultPrevented = false;
+OTHelpers.createButton = function(innerHTML, attributes, events) {
+ var button = $.createElement('button', attributes, innerHTML);
- this.preventDefault = function() {
- if (this.cancelable) {
- _defaultPrevented = true;
- } else {
- OTHelpers.warn('Event.preventDefault :: Trying to preventDefault ' +
- 'on an Event that isn\'t cancelable');
- }
- };
+ if (events) {
+ for (var name in events) {
+ if (events.hasOwnProperty(name)) {
+ $.on(button, name, events[name]);
+ }
+ }
- this.isDefaultPrevented = function() {
- return _defaultPrevented;
- };
- };
+ button._boundEvents = events;
+ }
- };
-
-})(window, window.OTHelpers);
-
-/*jshint browser:true, smarttabs:true*/
+ return button;
+};
+/*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;
- };
+ElementCollection.prototype.appendTo = function(parentElement) {
+ if (!parentElement) throw new Error('appendTo requires a DOMElement to append to.');
- // Returns true if the client supports element.classList
- OTHelpers.supportsClassList = function() {
- var hasSupport = (typeof document !== 'undefined') &&
- ('classList' in document.createElement('a'));
+ return this.forEach(parentElement.appendChild.bind(parentElement));
+};
- OTHelpers.supportsClassList = function() { return hasSupport; };
+ElementCollection.prototype.after = function(prevElement) {
+ if (!prevElement) throw new Error('after requires a DOMElement to insert after');
- return hasSupport;
- };
+ return this.forEach(function(element) {
+ if (element.parentElement) {
+ if (prevElement !== element.parentNode.lastChild) {
+ element.parentElement.before(element, prevElement);
+ }
+ else {
+ element.parentElement.appendChild(element);
+ }
+ }
+ });
+};
- OTHelpers.removeElement = function(element) {
- if (element && element.parentNode) {
+ElementCollection.prototype.before = function(nextElement) {
+ if (!nextElement) throw new Error('before requires a DOMElement to insert before');
+
+ return this.forEach(function(element) {
+ if (element.parentElement) {
+ element.parentElement.before(element, nextElement);
+ }
+ });
+};
+
+ElementCollection.prototype.remove = function () {
+ return this.forEach(function(element) {
+ if (element.parentNode) {
element.parentNode.removeChild(element);
}
- };
-
- OTHelpers.removeElementById = function(elementId) {
- /*jshint newcap:false */
- this.removeElement(OTHelpers(elementId));
- };
-
- OTHelpers.removeElementsByType = function(parentElem, type) {
- if (!parentElem) return;
-
- var elements = parentElem.getElementsByTagName(type);
+ });
+};
+ElementCollection.prototype.empty = function () {
+ return this.forEach(function(element) {
// elements is a "live" NodesList collection. Meaning that the collection
// itself will be mutated as we remove elements from the DOM. This means
// that "while there are still elements" is safer than "iterate over each
// element" as the collection length and the elements indices will be modified
// with each iteration.
- while (elements.length) {
- parentElem.removeChild(elements[0]);
- }
- };
-
- OTHelpers.emptyElement = function(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
- return element;
- };
-
- OTHelpers.createElement = function(nodeName, attributes, children, doc) {
- var element = (doc || document).createElement(nodeName);
-
- if (attributes) {
- for (var name in attributes) {
- if (typeof(attributes[name]) === 'object') {
- if (!element[name]) element[name] = {};
-
- var subAttrs = attributes[name];
- for (var n in subAttrs) {
- element[name][n] = subAttrs[n];
- }
- }
- else if (name === 'className') {
- element.className = attributes[name];
- }
- else {
- element.setAttribute(name, attributes[name]);
- }
- }
- }
-
- var setChildren = function(child) {
- if(typeof child === 'string') {
- element.innerHTML = element.innerHTML + child;
- } else {
- element.appendChild(child);
- }
- };
-
- if(OTHelpers.isArray(children)) {
- OTHelpers.forEach(children, setChildren);
- } else if(children) {
- setChildren(children);
- }
-
- return element;
- };
-
- OTHelpers.createButton = function(innerHTML, attributes, events) {
- var button = OTHelpers.createElement('button', attributes, innerHTML);
-
- if (events) {
- for (var name in events) {
- if (events.hasOwnProperty(name)) {
- OTHelpers.on(button, name, events[name]);
- }
- }
-
- button._boundEvents = events;
- }
-
- return button;
- };
+ });
+};
- // Detects when an element is not part of the document flow because
- // it or one of it's ancesters has display:none.
- OTHelpers.isDisplayNone = function(element) {
+// Detects when an element is not part of the document flow because
+// it or one of it's ancesters has display:none.
+ElementCollection.prototype.isDisplayNone = function() {
+ return this.some(function(element) {
if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
- OTHelpers.css(element, 'display') === 'none') return true;
+ $(element).css('display') === 'none') return true;
if (element.parentNode && element.parentNode.style) {
- return OTHelpers.isDisplayNone(element.parentNode);
+ return $(element.parentNode).isDisplayNone();
}
+ });
+};
- return false;
- };
+ElementCollection.prototype.findElementWithDisplayNone = function(element) {
+ return $.findElementWithDisplayNone(element);
+};
- 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;
- };
+OTHelpers.isElementNode = function(node) {
+ return node && typeof node === 'object' && node.nodeType === 1;
+};
- function objectHasProperties(obj) {
- for (var key in obj) {
- if (obj.hasOwnProperty(key)) return true;
- }
- return false;
+
+// @remove
+OTHelpers.removeElement = function(element) {
+ $(element).remove();
+};
+
+// @remove
+OTHelpers.removeElementById = function(elementId) {
+ return $('#'+elementId).remove();
+};
+
+// @remove
+OTHelpers.removeElementsByType = function(parentElem, type) {
+ return $(type, parentElem).remove();
+};
+
+// @remove
+OTHelpers.emptyElement = function(element) {
+ return $(element).empty();
+};
+
+
+
+
+
+// @remove
+OTHelpers.isDisplayNone = function(element) {
+ return $(element).isDisplayNone();
+};
+
+OTHelpers.findElementWithDisplayNone = function(element) {
+ if ( (element.offsetWidth === 0 || element.offsetHeight === 0) &&
+ $.css(element, 'display') === 'none') return element;
+
+ if (element.parentNode && element.parentNode.style) {
+ return $.findElementWithDisplayNone(element.parentNode);
}
-
- // Allows an +onChange+ callback to be triggered when specific style properties
- // of +element+ are notified. The callback accepts a single parameter, which is
- // a hash where the keys are the style property that changed and the values are
- // an array containing the old and new values ([oldValue, newValue]).
- //
- // Width and Height changes while the element is display: none will not be
- // fired until such time as the element becomes visible again.
- //
- // This function returns the MutationObserver itself. Once you no longer wish
- // to observe the element you should call disconnect on the observer.
- //
- // Observing changes:
- // // observe changings to the width and height of object
- // dimensionsObserver = OTHelpers.observeStyleChanges(object,
- // ['width', 'height'], function(changeSet) {
- // OT.debug("The new width and height are " +
- // changeSet.width[1] + ',' + changeSet.height[1]);
- // });
- //
- // Cleaning up
- // // stop observing changes
- // dimensionsObserver.disconnect();
- // dimensionsObserver = null;
- //
- OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) {
- var oldStyles = {};
-
- var getStyle = function getStyle(style) {
- switch (style) {
- case 'width':
- return OTHelpers.width(element);
-
- case 'height':
- return OTHelpers.height(element);
-
- default:
- return OTHelpers.css(element);
- }
- };
-
- // get the inital values
- OTHelpers.forEach(stylesToObserve, function(style) {
- oldStyles[style] = getStyle(style);
- });
-
- var observer = new MutationObserver(function(mutations) {
- var changeSet = {};
-
- OTHelpers.forEach(mutations, function(mutation) {
- if (mutation.attributeName !== 'style') return;
-
- var isHidden = OTHelpers.isDisplayNone(element);
-
- OTHelpers.forEach(stylesToObserve, function(style) {
- if(isHidden && (style === 'width' || style === 'height')) return;
-
- var newValue = getStyle(style);
-
- if (newValue !== oldStyles[style]) {
- changeSet[style] = [oldStyles[style], newValue];
- oldStyles[style] = newValue;
- }
- });
- });
-
- if (objectHasProperties(changeSet)) {
- // Do this after so as to help avoid infinite loops of mutations.
- OTHelpers.callAsync(function() {
- onChange.call(null, changeSet);
- });
- }
- });
-
- observer.observe(element, {
- attributes:true,
- attributeFilter: ['style'],
- childList:false,
- characterData:false,
- subtree:false
- });
-
- return observer;
- };
-
-
- // trigger the +onChange+ callback whenever
- // 1. +element+ is removed
- // 2. or an immediate child of +element+ is removed.
- //
- // This function returns the MutationObserver itself. Once you no longer wish
- // to observe the element you should call disconnect on the observer.
- //
- // Observing changes:
- // // observe changings to the width and height of object
- // nodeObserver = OTHelpers.observeNodeOrChildNodeRemoval(object, function(removedNodes) {
- // OT.debug("Some child nodes were removed");
- // OTHelpers.forEach(removedNodes, function(node) {
- // OT.debug(node);
- // });
- // });
- //
- // Cleaning up
- // // stop observing changes
- // nodeObserver.disconnect();
- // nodeObserver = null;
- //
- OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) {
- var observer = new MutationObserver(function(mutations) {
- var removedNodes = [];
-
- OTHelpers.forEach(mutations, function(mutation) {
- if (mutation.removedNodes.length) {
- removedNodes = removedNodes.concat(Array.prototype.slice.call(mutation.removedNodes));
- }
- });
-
- if (removedNodes.length) {
- // Do this after so as to help avoid infinite loops of mutations.
- OTHelpers.callAsync(function() {
- onChange(removedNodes);
- });
- }
- });
-
- observer.observe(element, {
- attributes:false,
- childList:true,
- characterData:false,
- subtree:true
- });
-
- return observer;
- };
-
-})(window, window.OTHelpers);
+ return null;
+};
/*jshint browser:true, smarttabs:true*/
// tb_require('../helpers.js')
+// tb_require('./environment.js')
// tb_require('./dom.js')
-(function(window, OTHelpers, undefined) {
+OTHelpers.Modal = function(options) {
- OTHelpers.Modal = function(options) {
+ OTHelpers.eventing(this, true);
- OTHelpers.eventing(this, true);
+ var callback = arguments[arguments.length - 1];
- var callback = arguments[arguments.length - 1];
+ if(!OTHelpers.isFunction(callback)) {
+ throw new Error('OTHelpers.Modal2 must be given a callback');
+ }
- if(!OTHelpers.isFunction(callback)) {
- throw new Error('OTHelpers.Modal2 must be given a callback');
- }
+ if(arguments.length < 2) {
+ options = {};
+ }
- if(arguments.length < 2) {
- options = {};
- }
+ var domElement = document.createElement('iframe');
- 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';
- 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');
+ }
- 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');
- domElement.scrolling = 'no';
- domElement.setAttribute('scrolling', 'no');
+ // This is necessary for IE, as it will not inherit it's doctype from
+ // the parent frame.
+ var frameContent = '' +
+ '' +
+ '' +
+ '
- * Calling OT.setLogLevel()
sets the log level for runtime log messages that
- * are the OpenTok library generates. The default value for the log level is OT.ERROR
.
- *
- * The OpenTok JavaScript library displays log messages in the debugger console (such as - * Firebug), if one exists. - *
- *
- * The following example logs the session ID to the console, by calling OT.log()
.
- * The code also logs an error message when it attempts to publish a stream before the Session
- * object dispatches a sessionConnected
event.
- *
- * OT.setLogLevel(OT.LOG); - * session = OT.initSession(sessionId); - * OT.log(sessionId); - * publisher = OT.initPublisher("publishContainer"); - * session.publish(publisher); - *- * - * @param {Number} logLevel The degree of logging desired by the developer: - * - *
- *
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() {
-
- var adjustModal = function(callback) {
- return function setFullHeightDocument(window, document) {
- // required in IE8
- document.querySelector('html').style.height = document.body.style.height = '100%';
- callback(window, document);
- };
- };
-
- var addCss = function(document, url, callback) {
- var head = document.head || document.getElementsByTagName('head')[0];
- var cssTag = OT.$.createElement('link', {
- type: 'text/css',
- media: 'screen',
- rel: 'stylesheet',
- href: url
- });
- head.appendChild(cssTag);
- OT.$.on(cssTag, 'error', function(error) {
- OT.error('Could not load CSS for dialog', url, error && error.message || error);
- });
- OT.$.on(cssTag, 'load', callback);
- };
-
- var addDialogCSS = function(document, urls, callback) {
- var allURLs = [
- '//fonts.googleapis.com/css?family=Didact+Gothic',
- OT.properties.cssURL
- ].concat(urls);
- var remainingStylesheets = allURLs.length;
- OT.$.forEach(allURLs, function(stylesheetUrl) {
- addCss(document, stylesheetUrl, function() {
- if(--remainingStylesheets <= 0) {
- callback();
- }
- });
- });
-
- };
-
- var templateElement = function(classes, children, tagName) {
- var el = OT.$.createElement(tagName || 'div', { 'class': classes }, children, this);
- el.on = OT.$.bind(OT.$.on, OT.$, el);
- el.off = OT.$.bind(OT.$.off, OT.$, el);
- return el;
- };
-
- var checkBoxElement = function (classes, nameAndId, onChange) {
- var checkbox = templateElement.call(this, '', null, 'input').on('change', onChange);
-
- if (OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 8) {
- // Fix for IE8 not triggering the change event
- checkbox.on('click', function() {
- checkbox.blur();
- checkbox.focus();
- });
- }
-
- checkbox.setAttribute('name', nameAndId);
- checkbox.setAttribute('id', nameAndId);
- checkbox.setAttribute('type', 'checkbox');
-
- return checkbox;
- };
-
- var linkElement = function(children, href, classes) {
- var link = templateElement.call(this, classes || '', children, 'a');
- link.setAttribute('href', href);
- return link;
- };
-
- OT.Dialogs = {};
-
- OT.Dialogs.Plugin = {};
-
- OT.Dialogs.Plugin.promptToInstall = function() {
- var modal = new OT.$.Modal(adjustModal(function(window, document) {
-
- var el = OT.$.bind(templateElement, document),
- btn = function(children, size) {
- var classes = 'OT_dialog-button ' +
- (size ? 'OT_dialog-button-' + size : 'OT_dialog-button-large'),
- b = el(classes, children);
-
- b.enable = function() {
- OT.$.removeClass(this, 'OT_dialog-button-disabled');
- return this;
- };
-
- b.disable = function() {
- OT.$.addClass(this, 'OT_dialog-button-disabled');
- return this;
- };
-
- return b;
- },
- downloadButton = btn('Download plugin'),
- cancelButton = btn('cancel', 'small'),
- refreshButton = btn('Refresh browser'),
- acceptEULA,
- checkbox,
- close,
- root;
-
- OT.$.addClass(cancelButton, 'OT_dialog-no-natural-margin OT_dialog-button-block');
- OT.$.addClass(refreshButton, 'OT_dialog-no-natural-margin');
-
- function onDownload() {
- modal.trigger('download');
- setTimeout(function() {
- root.querySelector('.OT_dialog-messages-main').innerHTML =
- 'Plugin installation successful';
- var sections = root.querySelectorAll('.OT_dialog-section');
- OT.$.addClass(sections[0], 'OT_dialog-hidden');
- OT.$.removeClass(sections[1], 'OT_dialog-hidden');
- }, 3000);
- }
-
- function onRefresh() {
- modal.trigger('refresh');
- }
-
- function onToggleEULA() {
- if (checkbox.checked) {
- enableButtons();
- }
- else {
- disableButtons();
- }
- }
-
- function enableButtons() {
- downloadButton.enable();
- downloadButton.on('click', onDownload);
-
- refreshButton.enable();
- refreshButton.on('click', onRefresh);
- }
-
- function disableButtons() {
- downloadButton.disable();
- downloadButton.off('click', onDownload);
-
- refreshButton.disable();
- refreshButton.off('click', onRefresh);
- }
-
- downloadButton.disable();
- refreshButton.disable();
-
- cancelButton.on('click', function() {
- modal.trigger('cancelButtonClicked');
- modal.close();
- });
-
- close = el('OT_closeButton', '×')
- .on('click', function() {
- modal.trigger('closeButtonClicked');
- modal.close();
- });
-
- acceptEULA = linkElement.call(document,
- 'end-user license agreement',
- 'http://tokbox.com/support/ie-eula');
-
- checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA);
-
- root = el('OT_dialog-centering', [
- el('OT_dialog-centering-child', [
- el('OT_root OT_dialog OT_dialog-plugin-prompt', [
- close,
- el('OT_dialog-messages', [
- el('OT_dialog-messages-main', 'This app requires real-time communication')
- ]),
- el('OT_dialog-section', [
- el('OT_dialog-single-button-with-title', [
- el('OT_dialog-button-title', [
- checkbox,
- (function() {
- var x = el('', 'accept', 'label');
- x.setAttribute('for', checkbox.id);
- x.style.margin = '0 5px';
- return x;
- })(),
- acceptEULA
- ]),
- el('OT_dialog-actions-card', [
- downloadButton,
- cancelButton
- ])
- ])
- ]),
- el('OT_dialog-section OT_dialog-hidden', [
- el('OT_dialog-button-title', [
- 'You can now enjoy webRTC enabled video via Internet Explorer.'
- ]),
- refreshButton
- ])
- ])
- ])
- ]);
-
- addDialogCSS(document, [], function() {
- document.body.appendChild(root);
- });
-
- }));
- return modal;
- };
-
- OT.Dialogs.Plugin.promptToReinstall = function() {
- var modal = new OT.$.Modal(adjustModal(function(window, document) {
-
- var el = OT.$.bind(templateElement, document),
- close,
- okayButton,
- root;
-
- close = el('OT_closeButton', '×');
- okayButton =
- el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Okay');
-
- OT.$.on(okayButton, 'click', function() {
- modal.trigger('okay');
- });
-
- OT.$.on(close, 'click', function() {
- modal.trigger('closeButtonClicked');
- modal.close();
- });
-
- root = el('OT_dialog-centering', [
- el('OT_dialog-centering-child', [
- el('OT_ROOT OT_dialog OT_dialog-plugin-reinstall', [
- close,
- el('OT_dialog-messages', [
- el('OT_dialog-messages-main', 'Reinstall Opentok Plugin'),
- el('OT_dialog-messages-minor', 'Uh oh! Try reinstalling the OpenTok plugin ' +
- 'again to enable real-time video communication for Internet Explorer.')
- ]),
- el('OT_dialog-section', [
- el('OT_dialog-single-button', okayButton)
- ])
- ])
- ])
- ]);
-
- addDialogCSS(document, [], function() {
- document.body.appendChild(root);
- });
-
- }));
-
- return modal;
- };
-
- OT.Dialogs.Plugin.updateInProgress = function() {
-
- var progressBar,
- progressText,
- progressValue = 0;
-
- var modal = new OT.$.Modal(adjustModal(function(window, document) {
-
- var el = OT.$.bind(templateElement, document),
- root;
-
- progressText = el('OT_dialog-plugin-upgrade-percentage', '0%', 'strong');
-
- progressBar = el('OT_dialog-progress-bar-fill');
-
- root = el('OT_dialog-centering', [
- el('OT_dialog-centering-child', [
- el('OT_ROOT OT_dialog OT_dialog-plugin-upgrading', [
- el('OT_dialog-messages', [
- el('OT_dialog-messages-main', [
- 'One moment please... ',
- progressText
- ]),
- el('OT_dialog-progress-bar', progressBar),
- el('OT_dialog-messages-minor OT_dialog-no-natural-margin',
- 'Please wait while the OpenTok plugin is updated')
- ])
- ])
- ])
- ]);
-
- addDialogCSS(document, [], function() {
- document.body.appendChild(root);
- if(progressValue != null) {
- modal.setUpdateProgress(progressValue);
- }
- });
- }));
-
- modal.setUpdateProgress = function(newProgress) {
- if(progressBar && progressText) {
- if(newProgress > 99) {
- OT.$.css(progressBar, 'width', '');
- progressText.innerHTML = '100%';
- } else if(newProgress < 1) {
- OT.$.css(progressBar, 'width', '0%');
- progressText.innerHTML = '0%';
- } else {
- OT.$.css(progressBar, 'width', newProgress + '%');
- progressText.innerHTML = newProgress + '%';
- }
- } else {
- progressValue = newProgress;
- }
- };
-
- return modal;
- };
-
- OT.Dialogs.Plugin.updateComplete = function(error) {
- var modal = new OT.$.Modal(adjustModal(function(window, document) {
- var el = OT.$.bind(templateElement, document),
- reloadButton,
- root;
-
- reloadButton =
- el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Reload')
- .on('click', function() {
- modal.trigger('reload');
- });
-
- var msgs;
-
- if(error) {
- msgs = ['Update Failed.', error + '' || 'NO ERROR'];
- } else {
- msgs = ['Update Complete.',
- 'The OpenTok plugin has been succesfully updated. ' +
- 'Please reload your browser.'];
- }
-
- root = el('OT_dialog-centering', [
- el('OT_dialog-centering-child', [
- el('OT_root OT_dialog OT_dialog-plugin-upgraded', [
- el('OT_dialog-messages', [
- el('OT_dialog-messages-main', msgs[0]),
- el('OT_dialog-messages-minor', msgs[1])
- ]),
- el('OT_dialog-single-button', reloadButton)
- ])
- ])
- ]);
-
- addDialogCSS(document, [], function() {
- document.body.appendChild(root);
- });
-
- }));
-
- return modal;
-
- };
-
-
-})();
-!(function(window) {
-
- // IMPORTANT This file should be included straight after helpers.js
- if (!window.OT) window.OT = {};
-
- if (!OT.properties) {
- throw new Error('OT.properties does not exist, please ensure that you include a valid ' +
- 'properties file.');
- }
-
- OT.useSSL = function () {
- return OT.properties.supportSSL && (window.location.protocol.indexOf('https') >= 0 ||
- window.location.protocol.indexOf('chrome-extension') >= 0);
- };
-
- // Consumes and overwrites OT.properties. Makes it better and stronger!
- OT.properties = function(properties) {
- var props = OT.$.clone(properties);
-
- props.debug = properties.debug === 'true' || properties.debug === true;
- props.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true;
-
- if (window.OTProperties) {
- // Allow window.OTProperties to override cdnURL, configURL, assetURL and cssURL
- if (window.OTProperties.cdnURL) props.cdnURL = window.OTProperties.cdnURL;
- if (window.OTProperties.cdnURLSSL) props.cdnURLSSL = window.OTProperties.cdnURLSSL;
- if (window.OTProperties.configURL) props.configURL = window.OTProperties.configURL;
- if (window.OTProperties.assetURL) props.assetURL = window.OTProperties.assetURL;
- if (window.OTProperties.cssURL) props.cssURL = window.OTProperties.cssURL;
- }
-
- if (!props.assetURL) {
- if (OT.useSSL()) {
- props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version;
- } else {
- props.assetURL = props.cdnURL + '/webrtc/' + props.version;
- }
- }
-
- var isIE89 = OT.$.browser() === 'IE' && OT.$.browserVersion().version <= 9;
- if (!(isIE89 && window.location.protocol.indexOf('https') < 0)) {
- props.apiURL = props.apiURLSSL;
- props.loggingURL = props.loggingURLSSL;
- }
-
- if (!props.configURL) props.configURL = props.assetURL + '/js/dynamic_config.min.js';
- if (!props.cssURL) props.cssURL = props.assetURL + '/css/ot.min.css';
-
- return props;
- }(OT.properties);
-})(window);
-!(function() {
-
-//--------------------------------------
-// JS Dynamic Config
-//--------------------------------------
-
-
- OT.Config = (function() {
- var _loaded = false,
- _global = {},
- _partners = {},
- _script,
- _head = document.head || document.getElementsByTagName('head')[0],
- _loadTimer,
-
- _clearTimeout = function() {
- if (_loadTimer) {
- clearTimeout(_loadTimer);
- _loadTimer = null;
- }
- },
-
- _cleanup = function() {
- _clearTimeout();
-
- if (_script) {
- _script.onload = _script.onreadystatechange = null;
-
- if ( _head && _script.parentNode ) {
- _head.removeChild( _script );
- }
-
- _script = undefined;
- }
- },
-
- _onLoad = function() {
- // Only IE and Opera actually support readyState on Script elements.
- if (_script.readyState && !/loaded|complete/.test( _script.readyState )) {
- // Yeah, we're not ready yet...
- return;
- }
-
- _clearTimeout();
-
- if (!_loaded) {
- // Our config script is loaded but there is not config (as
- // replaceWith wasn't called). Something went wrong. Possibly
- // the file we loaded wasn't actually a valid config file.
- _this._onLoadTimeout();
- }
- },
-
- _getModule = function(moduleName, apiKey) {
- if (apiKey && _partners[apiKey] && _partners[apiKey][moduleName]) {
- return _partners[apiKey][moduleName];
- }
-
- return _global[moduleName];
- },
-
- _this;
-
- _this = {
- // In ms
- loadTimeout: 4000,
-
- load: function(configUrl) {
- if (!configUrl) throw new Error('You must pass a valid configUrl to Config.load');
-
- _loaded = false;
-
- setTimeout(function() {
- _script = document.createElement( 'script' );
- _script.async = 'async';
- _script.src = configUrl;
- _script.onload = _script.onreadystatechange = OT.$.bind(_onLoad, this);
- _head.appendChild(_script);
- },1);
-
- _loadTimer = setTimeout(function() {
- _this._onLoadTimeout();
- }, this.loadTimeout);
- },
-
- _onLoadTimeout: function() {
- _cleanup();
-
- OT.warn('TB DynamicConfig failed to load in ' + _this.loadTimeout + ' ms');
- this.trigger('dynamicConfigLoadFailed');
- },
-
- isLoaded: function() {
- return _loaded;
- },
-
- reset: function() {
- _cleanup();
- _loaded = false;
- _global = {};
- _partners = {};
- },
-
- // This is public so that the dynamic config file can load itself.
- // Using it for other purposes is discouraged, but not forbidden.
- replaceWith: function(config) {
- _cleanup();
-
- if (!config) config = {};
-
- _global = config.global || {};
- _partners = config.partners || {};
-
- if (!_loaded) _loaded = true;
- this.trigger('dynamicConfigChanged');
- },
-
- // @example Get the value that indicates whether exceptionLogging is enabled
- // OT.Config.get('exceptionLogging', 'enabled');
- //
- // @example Get a key for a specific partner, fallback to the default if there is
- // no key for that partner
- // OT.Config.get('exceptionLogging', 'enabled', 'apiKey');
- //
- get: function(moduleName, key, apiKey) {
- var module = _getModule(moduleName, apiKey);
- return module ? module[key] : null;
- }
- };
-
- OT.$.eventing(_this);
-
- return _this;
- })();
-
-})(window);
/**
- * @license TB Plugin 0.4.0.8 59e99bc HEAD
+ * @license TB Plugin 0.4.0.9 88af499 2014Q4-2.2
* http://www.tokbox.com/
*
* Copyright (c) 2015 TokBox, Inc.
*
- * Date: January 26 03:18:16 2015
+ * Date: January 08 08:54:38 2015
*
*/
/* jshint globalstrict: true, strict: false, undef: true, unused: false,
trailing: true, browser: true, smarttabs:true */
-/* global scope:true, OT:true */
-/* exported TBPlugin */
+/* global scope:true, OT:true, OTHelpers:true */
+/* exported OTPlugin */
/* jshint ignore:start */
(function(scope) {
/* jshint ignore:end */
// If we've already be setup, bail
-if (scope.TBPlugin !== void 0) return;
+if (scope.OTPlugin !== void 0) return;
+
// TB must exist first, otherwise we can't do anything
-if (scope.OT === void 0) return;
+// if (scope.OT === void 0) return;
// Establish the environment that we're running in
-var env = OT.$.browserVersion(),
- isSupported = env.browser === 'IE' && env.version >= 8,
- pluginReady = false;
+// Note: we don't currently support 64bit IE
+var isSupported = (OTHelpers.env.name === 'IE' && OTHelpers.env.version >= 8 &&
+ OTHelpers.env.userAgent.indexOf('x64') === -1),
+ pluginIsReady = false;
-var TBPlugin = {
+
+var OTPlugin = {
isSupported: function () { return isSupported; },
- isReady: function() { return pluginReady; }
+ isReady: function() { return pluginIsReady; },
+ meta: {
+ mimeType: 'application/x-opentokie,version=0.4.0.9',
+ activeXName: 'TokBox.OpenTokIE.0.4.0.9',
+ version: '0.4.0.9'
+ }
};
-scope.TBPlugin = TBPlugin;
-// We only support IE, version 10 or above right now
-if (!TBPlugin.isSupported()) {
- TBPlugin.isInstalled = function isInstalled () { return false; };
+// Add logging methods
+OTHelpers.useLogHelpers(OTPlugin);
+
+scope.OTPlugin = OTPlugin;
+
+// If this client isn't supported we still make sure that OTPlugin is defined
+// and the basic API (isSupported() and isInstalled()) is created.
+if (!OTPlugin.isSupported()) {
+ OTPlugin.isInstalled = function isInstalled () { return false; };
return;
}
@@ -3793,36 +4310,51 @@ var shim = function shim () {
// tb_require('./header.js')
// tb_require('./shims.js')
-/* global OT:true */
-/* exported PluginRumorSocket */
+/* exported RumorSocket */
-var PluginRumorSocket = function(plugin, server) {
+var RumorSocket = function(plugin, server) {
var connected = false,
rumorID;
+ var _onOpen,
+ _onClose;
+
+
try {
rumorID = plugin._.RumorInit(server, '');
}
catch(e) {
- OT.error('Error creating the Rumor Socket: ', e.message);
+ OTPlugin.error('Error creating the Rumor Socket: ', e.message);
}
if(!rumorID) {
- throw new Error('Could not initialise plugin rumor connection');
+ throw new Error('Could not initialise OTPlugin rumor connection');
}
- var socket = {
+ plugin._.SetOnRumorOpen(rumorID, function() {
+ if (_onOpen && OTHelpers.isFunction(_onOpen)) {
+ _onOpen.call(null);
+ }
+ });
+
+ plugin._.SetOnRumorClose(rumorID, function(code) {
+ _onClose(code);
+
+ // We're done. Clean up ourselves
+ plugin.removeRef(this);
+ });
+
+ var api = {
open: function() {
connected = true;
plugin._.RumorOpen(rumorID);
},
close: function(code, reason) {
- if (!connected) return;
- connected = false;
-
- plugin._.RumorClose(rumorID, code, reason);
- plugin.removeRef(this);
+ if (connected) {
+ connected = false;
+ plugin._.RumorClose(rumorID, code, reason);
+ }
},
destroy: function() {
@@ -3835,11 +4367,11 @@ var PluginRumorSocket = function(plugin, server) {
},
onOpen: function(callback) {
- plugin._.SetOnRumorOpen(rumorID, callback);
+ _onOpen = callback;
},
onClose: function(callback) {
- plugin._.SetOnRumorClose(rumorID, callback);
+ _onClose = callback;
},
onError: function(callback) {
@@ -3851,9 +4383,8 @@ var PluginRumorSocket = function(plugin, server) {
}
};
- plugin.addRef(socket);
- return socket;
-
+ plugin.addRef(api);
+ return api;
};
// tb_require('./header.js')
@@ -3861,8 +4392,7 @@ var PluginRumorSocket = function(plugin, server) {
/* jshint globalstrict: true, strict: false, undef: true, unused: true,
trailing: true, browser: true, smarttabs:true */
-/* global OT:true, TBPlugin:true, pluginInfo:true, debug:true, scope:true,
- _document:true */
+/* global OT:true, scope:true, injectObject:true */
/* exported createMediaCaptureController:true, createPeerController:true,
injectObject:true, plugins:true, mediaCaptureObject:true,
removeAllObjects:true, curryCallAsync:true */
@@ -3875,14 +4405,10 @@ var curryCallAsync = function curryCallAsync (fn) {
return function() {
var args = Array.prototype.slice.call(arguments);
args.unshift(fn);
- OT.$.callAsync.apply(OT.$, args);
+ OTHelpers.callAsync.apply(OTHelpers, args);
};
};
-var generatePluginUuid = function generatePluginUuid () {
- return OT.$.uuid().replace(/\-+/g, '');
-};
-
var clearObjectLoadTimeout = function clearObjectLoadTimeout (callbackId) {
if (!callbackId) return;
@@ -3902,7 +4428,7 @@ var clearObjectLoadTimeout = function clearObjectLoadTimeout (callbackId) {
};
var removeObjectFromDom = function removeObjectFromDom (object) {
- clearObjectLoadTimeout(object.getAttribute('tb_callbackId'));
+ clearObjectLoadTimeout(object.getAttribute('tbCallbackId'));
if (mediaCaptureObject && mediaCaptureObject.id === object.id) {
mediaCaptureObject = null;
@@ -3911,7 +4437,7 @@ var removeObjectFromDom = function removeObjectFromDom (object) {
delete plugins[object.id];
}
- object.parentNode.removeChild(object);
+ OTHelpers.removeElement(object);
};
// @todo bind destroy to unload, may need to coordinate with TB
@@ -3927,7 +4453,7 @@ var removeAllObjects = function removeAllObjects () {
};
// Reference counted wrapper for a plugin object
-var PluginObject = function PluginObject (plugin) {
+var PluginProxy = function PluginProxy (plugin) {
var _plugin = plugin,
_liveObjects = [];
@@ -3961,7 +4487,7 @@ var PluginObject = function PluginObject (plugin) {
var eventHandlers = {};
- var onCustomEvent = OT.$.bind(curryCallAsync(function onCustomEvent() {
+ var onCustomEvent = OTHelpers.bind(curryCallAsync(function onCustomEvent() {
var args = Array.prototype.slice.call(arguments),
name = args.shift();
@@ -3969,7 +4495,7 @@ var PluginObject = function PluginObject (plugin) {
return;
}
- OT.$.forEach(eventHandlers[name], function(handler) {
+ OTHelpers.forEach(eventHandlers[name], function(handler) {
handler[0].apply(handler[1], args);
});
}), this);
@@ -3990,7 +4516,7 @@ var PluginObject = function PluginObject (plugin) {
return;
}
- OT.$.filter(eventHandlers[name], function(listener) {
+ OTHelpers.filter(eventHandlers[name], function(listener) {
return listener[0] === callback &&
listener[1] === context;
});
@@ -4017,7 +4543,7 @@ var PluginObject = function PluginObject (plugin) {
// Only the main plugin has an initialise method
if (_plugin.initialise) {
- this.on('ready', OT.$.bind(curryCallAsync(readyCallback), this));
+ this.on('ready', OTHelpers.bind(curryCallAsync(readyCallback), this));
_plugin.initialise();
}
else {
@@ -4030,7 +4556,7 @@ var PluginObject = function PluginObject (plugin) {
_liveObjects.shift().destroy();
}
- removeObjectFromDom(_plugin);
+ if (_plugin) removeObjectFromDom(_plugin);
_plugin = null;
};
@@ -4040,6 +4566,8 @@ var PluginObject = function PluginObject (plugin) {
// FIX ME renderingStarted currently doesn't first
// this.once('renderingStarted', completion);
var verifyStream = function() {
+ if (!_plugin) return;
+
if (_plugin.videoWidth > 0) {
// This fires a little too soon.
setTimeout(completion, 200);
@@ -4061,136 +4589,12 @@ var PluginObject = function PluginObject (plugin) {
};
};
-// Stops and cleans up after the plugin object load timeout.
-var injectObject = function injectObject (mimeType, isVisible, params, completion) {
- var callbackId = 'TBPlugin_loaded_' + generatePluginUuid();
- params.onload = callbackId;
- params.userAgent = window.navigator.userAgent.toLowerCase();
-
- scope[callbackId] = function() {
- clearObjectLoadTimeout(callbackId);
-
- o.setAttribute('id', 'tb_plugin_' + o.uuid);
- o.removeAttribute('tb_callbackId');
-
- pluginRefCounted.uuid = o.uuid;
- pluginRefCounted.id = o.id;
-
- pluginRefCounted.onReady(function(err) {
- if (err) {
- OT.error('Error while starting up plugin ' + o.uuid + ': ' + err);
- return;
- }
-
- debug('Plugin ' + o.id + ' is loaded');
-
- if (completion && OT.$.isFunction(completion)) {
- completion.call(TBPlugin, null, pluginRefCounted);
- }
- });
- };
-
- var tmpContainer = document.createElement('div'),
- objBits = [],
- extraAttributes = ['width="0" height="0"'],
- pluginRefCounted,
- o;
-
- if (isVisible !== true) {
- extraAttributes.push('visibility="hidden"');
- }
-
- objBits.push('');
- tmpContainer.innerHTML = objBits.join('');
-
- _document.body.appendChild(tmpContainer);
-
- function firstElementChild(element) {
- if(element.firstElementChild) {
- return element.firstElementChild;
- }
- for(var i = 0, len = element.childNodes.length; i < len; ++i) {
- if(element.childNodes[i].nodeType === 1) {
- return element.childNodes[i];
- }
- }
- return null;
- }
-
- o = firstElementChild(tmpContainer);
- o.setAttribute('tb_callbackId', callbackId);
-
- pluginRefCounted = new PluginObject(o);
-
- _document.body.appendChild(o);
- _document.body.removeChild(tmpContainer);
-
- objectTimeouts[callbackId] = setTimeout(function() {
- clearObjectLoadTimeout(callbackId);
-
- completion.call(TBPlugin, 'The object with the mimeType of ' +
- mimeType + ' timed out while loading.');
-
- _document.body.removeChild(o);
- }, 3000);
-
- return pluginRefCounted;
-};
-
-
-// Creates the Media Capture controller. This exposes selectSources and is
-// used in the private API.
-//
-// Only one Media Capture controller can exist at once, calling this method
-// more than once will raise an exception.
-//
-var createMediaCaptureController = function createMediaCaptureController (completion) {
- if (mediaCaptureObject) {
- throw new Error('TBPlugin.createMediaCaptureController called multiple times!');
- }
-
- mediaCaptureObject = injectObject(pluginInfo.mimeType, false, {windowless: false}, completion);
-
- mediaCaptureObject.selectSources = function() {
- return this._.selectSources.apply(this._, arguments);
- };
-
- return mediaCaptureObject;
-};
-
-// Create an instance of the publisher/subscriber/peerconnection object.
-// Many of these can exist at once, but the +id+ of each must be unique
-// within a single instance of scope (window or window-like thing).
-//
-var createPeerController = function createPeerController (completion) {
- var o = injectObject(pluginInfo.mimeType, true, {windowless: true}, function(err, plugin) {
- if (err) {
- completion.call(TBPlugin, err);
- return;
- }
-
- plugins[plugin.id] = plugin;
- completion.call(TBPlugin, null, plugin);
- });
-
- return o;
-};
-
// tb_require('./header.js')
// tb_require('./shims.js')
-// tb_require('./plugin_object.js')
+// tb_require('./proxy.js')
/* jshint globalstrict: true, strict: false, undef: true, unused: true,
trailing: true, browser: true, smarttabs:true */
-/* global OT:true, debug:true */
/* exported VideoContainer */
var VideoContainer = function VideoContainer (plugin, stream) {
@@ -4201,38 +4605,38 @@ var VideoContainer = function VideoContainer (plugin, stream) {
this.appendTo = function (parentDomElement) {
if (parentDomElement && plugin._.parentNode !== parentDomElement) {
- debug('VideoContainer appendTo', parentDomElement);
+ OTPlugin.debug('VideoContainer appendTo', parentDomElement);
parentDomElement.appendChild(plugin._);
this.parentElement = parentDomElement;
}
};
this.show = function (completion) {
- debug('VideoContainer show');
+ OTPlugin.debug('VideoContainer show');
plugin._.removeAttribute('width');
plugin._.removeAttribute('height');
plugin.setStream(stream, completion);
- OT.$.show(plugin._);
+ OTHelpers.show(plugin._);
};
this.setWidth = function (width) {
- debug('VideoContainer setWidth to ' + width);
+ OTPlugin.debug('VideoContainer setWidth to ' + width);
plugin._.setAttribute('width', width);
};
this.setHeight = function (height) {
- debug('VideoContainer setHeight to ' + height);
+ OTPlugin.debug('VideoContainer setHeight to ' + height);
plugin._.setAttribute('height', height);
};
this.setVolume = function (value) {
// TODO
- debug('VideoContainer setVolume not implemented: called with ' + value);
+ OTPlugin.debug('VideoContainer setVolume not implemented: called with ' + value);
};
this.getVolume = function () {
// TODO
- debug('VideoContainer getVolume not implemented');
+ OTPlugin.debug('VideoContainer getVolume not implemented');
return 0.5;
};
@@ -4256,7 +4660,7 @@ var VideoContainer = function VideoContainer (plugin, stream) {
// tb_require('./header.js')
// tb_require('./shims.js')
-// tb_require('./plugin_object.js')
+// tb_require('./proxy.js')
/* jshint globalstrict: true, strict: false, undef: true, unused: true,
trailing: true, browser: true, smarttabs:true */
@@ -4272,238 +4676,23 @@ var RTCStatsReport = function (reports) {
// tb_require('./header.js')
// tb_require('./shims.js')
-// tb_require('./plugin_object.js')
-// tb_require('./stats.js')
-
-/* jshint globalstrict: true, strict: false, undef: true, unused: true,
- trailing: true, browser: true, smarttabs:true */
-/* global OT:true, TBPlugin:true, MediaStream:true, RTCStatsReport:true */
-/* exported PeerConnection */
-
-// Our RTCPeerConnection shim, it should look like a normal PeerConection
-// from the outside, but it actually delegates to our plugin.
-//
-var PeerConnection = function PeerConnection (iceServers, options, plugin) {
- var id = OT.$.uuid(),
- hasLocalDescription = false,
- hasRemoteDescription = false,
- candidates = [];
-
- plugin.addRef(this);
-
- var onAddIceCandidate = function onAddIceCandidate () {/* success */},
-
- onAddIceCandidateFailed = function onAddIceCandidateFailed (err) {
- OT.error('Failed to process candidate');
- OT.error(err);
- },
-
- processPendingCandidates = function processPendingCandidates () {
- for (var i=0; i' + 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) { - - var NativeRTCPeerConnection = (window.webkitRTCPeerConnection || - window.mozRTCPeerConnection); - - if (navigator.webkitGetUserMedia) { - /*global webkitMediaStream, webkitRTCPeerConnection*/ - // Stub for getVideoTracks for Chrome < 26 - if (!webkitMediaStream.prototype.getVideoTracks) { - webkitMediaStream.prototype.getVideoTracks = function() { - return this.videoTracks; - }; - } - - // Stubs for getAudioTracks for Chrome < 26 - if (!webkitMediaStream.prototype.getAudioTracks) { - webkitMediaStream.prototype.getAudioTracks = function() { - return this.audioTracks; - }; - } - - if (!webkitRTCPeerConnection.prototype.getLocalStreams) { - webkitRTCPeerConnection.prototype.getLocalStreams = function() { - return this.localStreams; - }; - } - - if (!webkitRTCPeerConnection.prototype.getRemoteStreams) { - webkitRTCPeerConnection.prototype.getRemoteStreams = function() { - return this.remoteStreams; - }; - } - - } else if (navigator.mozGetUserMedia) { - // Firefox < 23 doesn't support get Video/Audio tracks, we'll just stub them out for now. - /* global MediaStream */ - if (!MediaStream.prototype.getVideoTracks) { - MediaStream.prototype.getVideoTracks = function() { - return []; - }; - } - - if (!MediaStream.prototype.getAudioTracks) { - MediaStream.prototype.getAudioTracks = function() { - return []; - }; - } - - // This won't work as mozRTCPeerConnection is a weird internal Firefox - // object (a wrapped native object I think). - // if (!window.mozRTCPeerConnection.prototype.getLocalStreams) { - // window.mozRTCPeerConnection.prototype.getLocalStreams = function() { - // return this.localStreams; - // }; - // } - - // This won't work as mozRTCPeerConnection is a weird internal Firefox - // object (a wrapped native object I think). - // if (!window.mozRTCPeerConnection.prototype.getRemoteStreams) { - // window.mozRTCPeerConnection.prototype.getRemoteStreams = function() { - // return this.remoteStreams; - // }; - // } - } - - // The setEnabled method on MediaStreamTracks is a TBPlugin - // construct. In this particular instance it's easier to bring - // all the good browsers down to IE's level than bootstrap it up. - if (typeof window.MediaStreamTrack !== 'undefined') { - if (!window.MediaStreamTrack.prototype.setEnabled) { - window.MediaStreamTrack.prototype.setEnabled = function (enabled) { - this.enabled = OT.$.castToBoolean(enabled); - }; - } - } - - - OT.$.createPeerConnection = function (config, options, publishersWebRtcStream, completion) { - if (TBPlugin.isInstalled()) { - TBPlugin.initPeerConnection(config, options, - publishersWebRtcStream, completion); - } - else { - var pc; - - try { - pc = new NativeRTCPeerConnection(config, options); - } catch(e) { - completion(e.message); - return; - } - - completion(null, pc); - } - }; - - // Returns a String representing the supported WebRTC crypto scheme. The possible - // values are SDES_SRTP, DTLS_SRTP, and NONE; - // - // Broadly: - // * Firefox only supports DTLS - // * Older versions of Chrome (<= 24) only support SDES - // * Newer versions of Chrome (>= 25) support DTLS and SDES - // - OT.$.supportedCryptoScheme = function() { - if (!OT.$.hasCapabilities('webrtc')) return 'NONE'; - - var chromeVersion = window.navigator.userAgent.toLowerCase().match(/chrome\/([0-9\.]+)/i); - return chromeVersion && parseFloat(chromeVersion[1], 10) < 25 ? 'SDES_SRTP' : 'DTLS_SRTP'; - }; - -})(window); -// Web OT Helpers -!(function(window) { - - /* jshint globalstrict: true, strict: false, undef: true, unused: true, - trailing: true, browser: true, smarttabs:true */ - /* global TBPlugin, OT */ - - /// - // Capabilities - // - // Support functions to query browser/client Media capabilities. - // - - - // Indicates whether this client supports the getUserMedia - // API. - // - OT.$.registerCapability('getUserMedia', function() { - return !!(navigator.webkitGetUserMedia || navigator.mozGetUserMedia || TBPlugin.isInstalled()); - }); - - - // TODO Remove all PeerConnection stuff, that belongs to the messaging layer not the Media layer. - // Indicates whether this client supports the PeerConnection - // API. - // - // Chrome Issues: - // * The explicit prototype.addStream check is because webkitRTCPeerConnection was - // partially implemented, but not functional, in Chrome 22. - // - // Firefox Issues: - // * No real support before Firefox 19 - // * Firefox 19 has issues with generating Offers. - // * Firefox 20 doesn't interoperate with Chrome. - // - OT.$.registerCapability('PeerConnection', function() { - var browser = OT.$.browserVersion(); - - if (navigator.webkitGetUserMedia) { - return typeof(window.webkitRTCPeerConnection) === 'function' && - !!window.webkitRTCPeerConnection.prototype.addStream; - - } else if (navigator.mozGetUserMedia) { - if (typeof(window.mozRTCPeerConnection) === 'function' && browser.version > 20.0) { - try { - new window.mozRTCPeerConnection(); - return true; - } catch (err) { - return false; - } - } - } else { - return TBPlugin.isInstalled(); - } - }); - - - // Indicates whether this client supports WebRTC - // - // This is defined as: getUserMedia + PeerConnection + exceeds min browser version - // - OT.$.registerCapability('webrtc', function() { - var browser = OT.$.browserVersion(), - minimumVersions = OT.properties.minimumVersion || {}, - minimumVersion = minimumVersions[browser.browser.toLowerCase()]; - - if(minimumVersion && minimumVersion > browser.version) { - OT.debug('Support for', browser.browser, 'is disabled because we require', - minimumVersion, 'but this is', browser.version); - return false; - } - - - return OT.$.hasCapabilities('getUserMedia', 'PeerConnection'); - }); - - - // TODO Remove all transport stuff, that belongs to the messaging layer not the Media layer. - // Indicates if the browser supports bundle - // - // Broadly: - // * Firefox doesn't support bundle - // * Chrome support bundle - // * OT Plugin supports bundle - // - OT.$.registerCapability('bundle', function() { - return OT.$.hasCapabilities('webrtc') && - (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()); - }); - - - // Indicates if the browser supports rtcp mux - // - // Broadly: - // * Older versions of Firefox (<= 25) don't support rtcp mux - // * Older versions of Firefox (>= 26) support rtcp mux (not tested yet) - // * Chrome support rtcp mux - // * OT Plugin supports rtcp mux - // - OT.$.registerCapability('RTCPMux', function() { - return OT.$.hasCapabilities('webrtc') && - (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()); - }); - - - - // Indicates whether this browser supports the getMediaDevices (getSources) API. - // - OT.$.registerCapability('getMediaDevices', function() { - return OT.$.isFunction(window.MediaStreamTrack) && - OT.$.isFunction(window.MediaStreamTrack.getSources); - }); - -})(window); -// Web OT Helpers -!(function() { - - var nativeGetUserMedia, - vendorToW3CErrors, - gumNamesToMessages, - mapVendorErrorName, - parseErrorEvent, - areInvalidConstraints; - - // Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth - nativeGetUserMedia = (function() { - if (navigator.getUserMedia) { - return OT.$.bind(navigator.getUserMedia, navigator); - } else if (navigator.mozGetUserMedia) { - return OT.$.bind(navigator.mozGetUserMedia, navigator); - } else if (navigator.webkitGetUserMedia) { - return OT.$.bind(navigator.webkitGetUserMedia, navigator); - } else if (TBPlugin.isInstalled()) { - return OT.$.bind(TBPlugin.getUserMedia, TBPlugin); - } - })(); - - // Mozilla error strings and the equivalent W3C names. NOT_SUPPORTED_ERROR does not - // exist in the spec right now, so we'll include Mozilla's error description. - // Chrome TrackStartError is triggered when the camera is already used by another app (Windows) - vendorToW3CErrors = { - PERMISSION_DENIED: 'PermissionDeniedError', - NOT_SUPPORTED_ERROR: 'NotSupportedError', - MANDATORY_UNSATISFIED_ERROR: ' ConstraintNotSatisfiedError', - NO_DEVICES_FOUND: 'NoDevicesFoundError', - HARDWARE_UNAVAILABLE: 'HardwareUnavailableError', - TrackStartError: 'HardwareUnavailableError' - }; - - gumNamesToMessages = { - PermissionDeniedError: 'End-user denied permission to hardware devices', - PermissionDismissedError: 'End-user dismissed permission to hardware devices', - NotSupportedError: 'A constraint specified is not supported by the browser.', - ConstraintNotSatisfiedError: 'It\'s not possible to satisfy one or more constraints ' + - 'passed into the getUserMedia function', - OverconstrainedError: 'Due to changes in the environment, one or more mandatory ' + - 'constraints can no longer be satisfied.', - NoDevicesFoundError: 'No voice or video input devices are available on this machine.', - HardwareUnavailableError: 'The selected voice or video devices are unavailable. Verify ' + - 'that the chosen devices are not in use by another application.' - }; - - // Map vendor error strings to names and messages if possible - mapVendorErrorName = function mapVendorErrorName(vendorErrorName, vendorErrors) { - var errorName, errorMessage; - - if(vendorErrors.hasOwnProperty(vendorErrorName)) { - errorName = vendorErrors[vendorErrorName]; - } else { - // This doesn't map to a known error from the Media Capture spec, it's - // probably a custom vendor error message. - errorName = vendorErrorName; - } - - if(gumNamesToMessages.hasOwnProperty(errorName)) { - errorMessage = gumNamesToMessages[errorName]; - } else { - errorMessage = 'Unknown Error while getting user media'; - } - - return { - name: errorName, - message: errorMessage - }; - }; - - // Parse and normalise a getUserMedia error event from Chrome or Mozilla - // @ref http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError - parseErrorEvent = function parseErrorObject(event) { - var error; - - if (OT.$.isObject(event) && event.name) { - error = mapVendorErrorName(event.name, vendorToW3CErrors); - error.constraintName = event.constraintName; - } else if (typeof event === 'string') { - error = mapVendorErrorName(event, vendorToW3CErrors); - } else { - error = { - message: 'Unknown Error type while getting media' - }; - } - - return error; - }; - - // Validates a Hash of getUserMedia constraints. Currently we only - // check to see if there is at least one non-false constraint. - areInvalidConstraints = function(constraints) { - if (!constraints || !OT.$.isObject(constraints)) return true; - - for (var key in constraints) { - if(!constraints.hasOwnProperty(key)) { - continue; - } - if (constraints[key]) return false; - } - - return true; - }; - - - // A wrapper for the builtin navigator.getUserMedia. In addition to the usual - // getUserMedia behaviour, this helper method also accepts a accessDialogOpened - // and accessDialogClosed callback. - // - // @memberof OT.$ - // @private - // - // @param {Object} constraints - // A dictionary of constraints to pass to getUserMedia. See - // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints - // in the Media Capture and Streams spec for more info. - // - // @param {function} success - // Called when getUserMedia completes successfully. The callback will be passed a WebRTC - // Stream object. - // - // @param {function} failure - // Called when getUserMedia fails to access a user stream. It will be passed an object - // with a code property representing the error that occurred. - // - // @param {function} accessDialogOpened - // Called when the access allow/deny dialog is opened. - // - // @param {function} accessDialogClosed - // Called when the access allow/deny dialog is closed. - // - // @param {function} accessDenied - // Called when access is denied to the camera/mic. This will be either because - // the user has clicked deny or because a particular origin is permanently denied. - // - OT.$.getUserMedia = function(constraints, success, failure, accessDialogOpened, - accessDialogClosed, accessDenied, customGetUserMedia) { - - var getUserMedia = nativeGetUserMedia; - - if(OT.$.isFunction(customGetUserMedia)) { - getUserMedia = customGetUserMedia; - } - - // All constraints are false, we don't allow this. This may be valid later - // depending on how/if we integrate data channels. - if (areInvalidConstraints(constraints)) { - OT.error('Couldn\'t get UserMedia: All constraints were false'); - // Using a ugly dummy-code for now. - failure.call(null, { - name: 'NO_VALID_CONSTRAINTS', - message: 'Video and Audio was disabled, you need to enabled at least one' - }); - - return; - } - - var triggerOpenedTimer = null, - displayedPermissionDialog = false, - - finaliseAccessDialog = function() { - if (triggerOpenedTimer) { - clearTimeout(triggerOpenedTimer); - } - - if (displayedPermissionDialog && accessDialogClosed) accessDialogClosed(); - }, - - triggerOpened = function() { - triggerOpenedTimer = null; - displayedPermissionDialog = true; - - if (accessDialogOpened) accessDialogOpened(); - }, - - onStream = function(stream) { - finaliseAccessDialog(); - success.call(null, stream); - }, - - onError = function(event) { - finaliseAccessDialog(); - var error = parseErrorEvent(event); - - // The error name 'PERMISSION_DENIED' is from an earlier version of the spec - if (error.name === 'PermissionDeniedError' || error.name === 'PermissionDismissedError') { - accessDenied.call(null, error); - } else { - failure.call(null, error); - } - }; - - try { - getUserMedia(constraints, onStream, onError); - } catch (e) { - OT.error('Couldn\'t get UserMedia: ' + e.toString()); - onError(); - return; - } - - // The 'remember me' functionality of WebRTC only functions over HTTPS, if - // we aren't on HTTPS then we should definitely be displaying the access - // dialog. - // - // If we are on HTTPS, we'll wait 500ms to see if we get a stream - // immediately. If we do then the user had clicked 'remember me'. Otherwise - // we assume that the accessAllowed dialog is visible. - // - // @todo benchmark and see if 500ms is a reasonable number. It seems like - // we should know a lot quicker. - // - if (location.protocol.indexOf('https') === -1) { - // Execute after, this gives the client a chance to bind to the - // accessDialogOpened event. - triggerOpenedTimer = setTimeout(triggerOpened, 100); - - } else { - // wait a second and then trigger accessDialogOpened - triggerOpenedTimer = setTimeout(triggerOpened, 500); - } - }; - -})(); -// Web OT Helpers -!(function(window) { - - /* jshint globalstrict: true, strict: false, undef: true, unused: true, - trailing: true, browser: true, smarttabs:true */ - /* global OT */ - - /// - // Device Helpers - // - // Support functions to enumerating and guerying device info - // - - var chromeToW3CDeviceKinds = { - audio: 'audioInput', - video: 'videoInput' - }; - - - OT.$.shouldAskForDevices = function(callback) { - var MST = window.MediaStreamTrack; - - if(MST != null && OT.$.isFunction(MST.getSources)) { - window.MediaStreamTrack.getSources(function(sources) { - var hasAudio = sources.some(function(src) { - return src.kind === 'audio'; - }); - - var hasVideo = sources.some(function(src) { - return src.kind === 'video'; - }); - - callback.call(null, { video: hasVideo, audio: hasAudio }); - }); - - } else { - // This environment can't enumerate devices anyway, so we'll memorise this result. - OT.$.shouldAskForDevices = function(callback) { - setTimeout(OT.$.bind(callback, null, { video: true, audio: true })); - }; - - OT.$.shouldAskForDevices(callback); - } - }; - - - OT.$.getMediaDevices = function(callback) { - if(OT.$.hasCapabilities('getMediaDevices')) { - window.MediaStreamTrack.getSources(function(sources) { - var filteredSources = OT.$.filter(sources, function(source) { - return chromeToW3CDeviceKinds[source.kind] != null; - }); - callback(void 0, OT.$.map(filteredSources, function(source) { - return { - deviceId: source.id, - label: source.label, - kind: chromeToW3CDeviceKinds[source.kind] - }; - })); - }); - } else { - callback(new Error('This browser does not support getMediaDevices APIs')); - } - }; - -})(window); -(function(window) { - - var VideoOrientationTransforms = { - 0: 'rotate(0deg)', - 270: 'rotate(90deg)', - 90: 'rotate(-90deg)', - 180: 'rotate(180deg)' - }; - - OT.VideoOrientation = { - ROTATED_NORMAL: 0, - ROTATED_LEFT: 270, - ROTATED_RIGHT: 90, - ROTATED_UPSIDE_DOWN: 180 - }; - - var DefaultAudioVolume = 50; - - var DEGREE_TO_RADIANS = Math.PI * 2 / 360; - - // - // - // var _videoElement = new OT.VideoElement({ - // fallbackText: 'blah' - // }, errorHandler); - // - // _videoElement.bindToStream(webRtcStream, completion); // => VideoElement - // _videoElement.appendTo(DOMElement) // => VideoElement - // - // _videoElement.domElement // => DomNode - // - // _videoElement.imgData // => PNG Data string - // - // _videoElement.orientation = OT.VideoOrientation.ROTATED_LEFT; - // - // _videoElement.unbindStream(); - // _videoElement.destroy() // => Completely cleans up and - // removes the video element - // - // - OT.VideoElement = function(/* optional */ options/*, optional errorHandler*/) { - var _options = OT.$.defaults( options && !OT.$.isFunction(options) ? options : {}, { - fallbackText: 'Sorry, Web RTC is not available in your browser' - }), - - errorHandler = OT.$.isFunction(arguments[arguments.length-1]) ? - arguments[arguments.length-1] : void 0, - - orientationHandler = OT.$.bind(function(orientation) { - this.trigger('orientationChanged', orientation); - }, this), - - _videoElement = TBPlugin.isInstalled() ? - new PluginVideoElement(_options, errorHandler, orientationHandler) : - new NativeDOMVideoElement(_options, errorHandler, orientationHandler), - _streamBound = false, - _stream, - _preInitialisedVolue; - - OT.$.eventing(this); - - // Public Properties - OT.$.defineProperties(this, { - - domElement: { - get: function() { - return _videoElement.domElement(); - } - }, - - videoWidth: { - get: function() { - return _videoElement['video' + (this.isRotated() ? 'Height' : 'Width')](); - } - }, - - videoHeight: { - get: function() { - return _videoElement['video' + (this.isRotated() ? 'Width' : 'Height')](); - } - }, - - aspectRatio: { - get: function() { - return (this.videoWidth() + 0.0) / this.videoHeight(); - } - }, - - isRotated: { - get: function() { - return _videoElement.isRotated(); - } - }, - - orientation: { - get: function() { - return _videoElement.orientation(); - }, - set: function(orientation) { - _videoElement.orientation(orientation); - } - }, - - audioChannelType: { - get: function() { - return _videoElement.audioChannelType(); - }, - set: function(type) { - _videoElement.audioChannelType(type); - } - } - }); - - // Public Methods - - this.imgData = function() { - return _videoElement.imgData(); - }; - - this.appendTo = function(parentDomElement) { - _videoElement.appendTo(parentDomElement); - return this; - }; - - this.bindToStream = function(webRtcStream, completion) { - _streamBound = false; - _stream = webRtcStream; - - _videoElement.bindToStream(webRtcStream, OT.$.bind(function(err) { - if (err) { - completion(err); - return; - } - - _streamBound = true; - - if (_preInitialisedVolue) { - this.setAudioVolume(_preInitialisedVolue); - _preInitialisedVolue = null; - } - - completion(null); - }, this)); - - return this; - }; - - this.unbindStream = function() { - if (!_stream) return this; - - _stream = null; - _videoElement.unbindStream(); - return this; - }; - - this.setAudioVolume = function (value) { - if (_streamBound) _videoElement.setAudioVolume( OT.$.roundFloat(value / 100, 2) ); - else _preInitialisedVolue = value; - - return this; - }; - - this.getAudioVolume = function () { - if (_streamBound) return parseInt(_videoElement.getAudioVolume() * 100, 10); - else return _preInitialisedVolue || 50; - }; - - - this.whenTimeIncrements = function (callback, context) { - _videoElement.whenTimeIncrements(callback, context); - return this; - }; - - this.destroy = function () { - // unbind all events so they don't fire after the object is dead - this.off(); - - _videoElement.destroy(); - return void 0; - }; - }; - - var PluginVideoElement = function PluginVideoElement (options, - errorHandler, - orientationChangedHandler) { - var _videoProxy, - _parentDomElement; - - canBeOrientatedMixin(this, - function() { return _videoProxy.domElement; }, - orientationChangedHandler); - - /// Public methods - - this.domElement = function() { - return _videoProxy ? _videoProxy.domElement : void 0; - }; - - this.videoWidth = function() { - return _videoProxy ? _videoProxy.getVideoWidth() : void 0; - }; - - this.videoHeight = function() { - return _videoProxy ? _videoProxy.getVideoHeight() : void 0; - }; - - this.imgData = function() { - return _videoProxy ? _videoProxy.getImgData() : null; - }; - - // Append the Video DOM element to a parent node - this.appendTo = function(parentDomElement) { - _parentDomElement = parentDomElement; - return this; - }; - - // Bind a stream to the video element. - this.bindToStream = function(webRtcStream, completion) { - if (!_parentDomElement) { - completion('The VideoElement must attached to a DOM node before a stream can be bound'); - return; - } - - _videoProxy = webRtcStream._.render(); - _videoProxy.appendTo(_parentDomElement); - _videoProxy.show(completion); - - return this; - }; - - // Unbind the currently bound stream from the video element. - this.unbindStream = function() { - // TODO: some way to tell TBPlugin to release that stream and controller - - if (_videoProxy) { - _videoProxy.destroy(); - _parentDomElement = null; - _videoProxy = null; - } - - return this; - }; - - this.setAudioVolume = function(value) { - if (_videoProxy) _videoProxy.setVolume(value); - }; - - this.getAudioVolume = function() { - // Return the actual volume of the DOM element - if (_videoProxy) return _videoProxy.getVolume(); - return DefaultAudioVolume; - }; - - // see https://wiki.mozilla.org/WebAPI/AudioChannels - // The audioChannelType is not currently supported in the plugin. - this.audioChannelType = function(/* type */) { - return 'unknown'; - }; - - this.whenTimeIncrements = function(callback, context) { - // exists for compatibility with NativeVideoElement - OT.$.callAsync(OT.$.bind(callback, context)); - }; - - this.destroy = function() { - this.unbindStream(); - - return void 0; - }; - }; - - - var NativeDOMVideoElement = function NativeDOMVideoElement (options, - errorHandler, - orientationChangedHandler) { - var _domElement, - _videoElementMovedWarning = false; - - - /// Private API - var _onVideoError = OT.$.bind(function(event) { - var reason = 'There was an unexpected problem with the Video Stream: ' + - videoElementErrorCodeToStr(event.target.error.code); - errorHandler(reason, this, 'VideoElement'); - }, this), - - // The video element pauses itself when it's reparented, this is - // unfortunate. This function plays the video again and is triggered - // on the pause event. - _playVideoOnPause = function() { - if(!_videoElementMovedWarning) { - OT.warn('Video element paused, auto-resuming. If you intended to do this, ' + - 'use publishVideo(false) or subscribeToVideo(false) instead.'); - - _videoElementMovedWarning = true; - } - - _domElement.play(); - }; - - - _domElement = createNativeVideoElement(options.fallbackText, options.attributes); - - _domElement.addEventListener('pause', _playVideoOnPause); - - canBeOrientatedMixin(this, function() { return _domElement; }, orientationChangedHandler); - - /// Public methods - - this.domElement = function() { - return _domElement; - }; - - this.videoWidth = function() { - return _domElement.videoWidth; - }; - - this.videoHeight = function() { - return _domElement.videoHeight; - }; - - this.imgData = function() { - var canvas = OT.$.createElement('canvas', { - width: _domElement.videoWidth, - height: _domElement.videoHeight, - style: { display: 'none' } - }); - - document.body.appendChild(canvas); - try { - canvas.getContext('2d').drawImage(_domElement, 0, 0, canvas.width, canvas.height); - } catch(err) { - OT.warn('Cannot get image data yet'); - return null; - } - var imgData = canvas.toDataURL('image/png'); - - OT.$.removeElement(canvas); - - return OT.$.trim(imgData.replace('data:image/png;base64,', '')); - }; - - // Append the Video DOM element to a parent node - this.appendTo = function(parentDomElement) { - parentDomElement.appendChild(_domElement); - return this; - }; - - // Bind a stream to the video element. - this.bindToStream = function(webRtcStream, completion) { - bindStreamToNativeVideoElement(_domElement, webRtcStream, function(err) { - if (err) { - completion(err); - return; - } - - _domElement.addEventListener('error', _onVideoError, false); - completion(null); - }); - - return this; - }; - - - // Unbind the currently bound stream from the video element. - this.unbindStream = function() { - if (_domElement) { - unbindNativeStream(_domElement); - } - - return this; - }; - - this.setAudioVolume = function(value) { - if (_domElement) _domElement.volume = value; - }; - - this.getAudioVolume = function() { - // Return the actual volume of the DOM element - if (_domElement) return _domElement.volume; - return DefaultAudioVolume; - }; - - // see https://wiki.mozilla.org/WebAPI/AudioChannels - // The audioChannelType is currently only available in Firefox. This property returns - // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel" - this.audioChannelType = function(type) { - if (type !== void 0) { - _domElement.mozAudioChannelType = type; - } - - if ('mozAudioChannelType' in _domElement) { - return _domElement.mozAudioChannelType; - } else { - return 'unknown'; - } - }; - - this.whenTimeIncrements = function(callback, context) { - if(_domElement) { - var lastTime, handler; - handler = OT.$.bind(function() { - if(!lastTime || lastTime >= _domElement.currentTime) { - lastTime = _domElement.currentTime; - } else { - _domElement.removeEventListener('timeupdate', handler, false); - callback.call(context, this); - } - }, this); - _domElement.addEventListener('timeupdate', handler, false); - } - }; - - this.destroy = function() { - this.unbindStream(); - - if (_domElement) { - // Unbind this first, otherwise it will trigger when the - // video element is removed from the DOM. - _domElement.removeEventListener('pause', _playVideoOnPause); - - OT.$.removeElement(_domElement); - _domElement = null; - } - - return void 0; - }; - }; - -/// Private Helper functions - - // A mixin to create the orientation API implementation on +self+ - // +getDomElementCallback+ is a function that the mixin will call when it wants to - // get the native Dom element for +self+. - // - // +initialOrientation+ sets the initial orientation (shockingly), it's currently unused - // so the initial value is actually undefined. - // - var canBeOrientatedMixin = function canBeOrientatedMixin (self, - getDomElementCallback, - orientationChangedHandler, - initialOrientation) { - var _orientation = initialOrientation; - - OT.$.defineProperties(self, { - isRotated: { - get: function() { - return this.orientation() && - (this.orientation().videoOrientation === 270 || - this.orientation().videoOrientation === 90); - } - }, - - orientation: { - get: function() { return _orientation; }, - set: function(orientation) { - _orientation = orientation; - - var transform = VideoOrientationTransforms[orientation.videoOrientation] || - VideoOrientationTransforms.ROTATED_NORMAL; - - switch(OT.$.browser()) { - case 'Chrome': - case 'Safari': - getDomElementCallback().style.webkitTransform = transform; - break; - - case 'IE': - if (OT.$.browserVersion().version >= 9) { - getDomElementCallback().style.msTransform = transform; - } - else { - // So this basically defines matrix that represents a rotation - // of a single vector in a 2d basis. - // - // R = [cos(Theta) -sin(Theta)] - // [sin(Theta) cos(Theta)] - // - // Where Theta is the number of radians to rotate by - // - // Then to rotate the vector v: - // v' = Rv - // - // We then use IE8 Matrix filter property, which takes - // a 2x2 rotation matrix, to rotate our DOM element. - // - var radians = orientation.videoOrientation * DEGREE_TO_RADIANS, - element = getDomElementCallback(), - costheta = Math.cos(radians), - sintheta = Math.sin(radians); - - // element.filters.item(0).M11 = costheta; - // element.filters.item(0).M12 = -sintheta; - // element.filters.item(0).M21 = sintheta; - // element.filters.item(0).M22 = costheta; - - element.style.filter = 'progid:DXImageTransform.Microsoft.Matrix(' + - 'M11='+costheta+',' + - 'M12='+(-sintheta)+',' + - 'M21='+sintheta+',' + - 'M22='+costheta+',SizingMethod=\'auto expand\')'; - } - - - break; - - default: - // The standard version, just Firefox, Opera, and IE > 9 - getDomElementCallback().style.transform = transform; - } - - orientationChangedHandler(_orientation); - - } - }, - - // see https://wiki.mozilla.org/WebAPI/AudioChannels - // The audioChannelType is currently only available in Firefox. This property returns - // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel" - audioChannelType: { - get: function() { - if ('mozAudioChannelType' in this.domElement) { - return this.domElement.mozAudioChannelType; - } else { - return 'unknown'; - } - }, - set: function(type) { - if ('mozAudioChannelType' in this.domElement) { - this.domElement.mozAudioChannelType = type; - } - } - } - }); - }; - - function createNativeVideoElement(fallbackText, attributes) { - var videoElement = document.createElement('video'); - videoElement.setAttribute('autoplay', ''); - videoElement.innerHTML = fallbackText; - - if (attributes) { - if (attributes.muted === true) { - delete attributes.muted; - videoElement.muted = 'true'; - } - - for (var key in attributes) { - if(!attributes.hasOwnProperty(key)) { - continue; - } - videoElement.setAttribute(key, attributes[key]); - } - } - - return videoElement; - } - - - // See http://www.w3.org/TR/2010/WD-html5-20101019/video.html#error-codes - var _videoErrorCodes = {}; - - // Checking for window.MediaError for IE compatibility, just so we don't throw - // exceptions when the script is included - if (window.MediaError) { - _videoErrorCodes[window.MediaError.MEDIA_ERR_ABORTED] = 'The fetching process for the media ' + - 'resource was aborted by the user agent at the user\'s request.'; - _videoErrorCodes[window.MediaError.MEDIA_ERR_NETWORK] = 'A network error of some description ' + - 'caused the user agent to stop fetching the media resource, after the resource was ' + - 'established to be usable.'; - _videoErrorCodes[window.MediaError.MEDIA_ERR_DECODE] = 'An error of some description ' + - 'occurred while decoding the media resource, after the resource was established to be ' + - ' usable.'; - _videoErrorCodes[window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED] = 'The media resource ' + - 'indicated by the src attribute was not suitable.'; - } - - function videoElementErrorCodeToStr(errorCode) { - return _videoErrorCodes[parseInt(errorCode, 10)] || 'An unknown error occurred.'; - } - - function bindStreamToNativeVideoElement(videoElement, webRtcStream, completion) { - var cleanup, - onLoad, - onError, - onStoppedLoading, - timeout; - - // Note: onloadedmetadata doesn't fire in Chrome for audio only crbug.com/110938 - // After version 36 it will fire if the video track is disabled. - var browser = OT.$.browserVersion(), - needsDisabledAudioProtection = browser.browser === 'Chrome' && browser.version < 36; - - if (navigator.mozGetUserMedia || !(needsDisabledAudioProtection && - (webRtcStream.getVideoTracks().length > 0 && webRtcStream.getVideoTracks()[0].enabled))) { - - cleanup = function cleanup () { - clearTimeout(timeout); - videoElement.removeEventListener('loadedmetadata', onLoad, false); - videoElement.removeEventListener('error', onError, false); - webRtcStream.onended = null; - }; - - onLoad = function onLoad () { - cleanup(); - completion(null); - }; - - onError = function onError (event) { - cleanup(); - unbindNativeStream(videoElement); - completion('There was an unexpected problem with the Video Stream: ' + - videoElementErrorCodeToStr(event.target.error.code)); - }; - - onStoppedLoading = function onStoppedLoading () { - // The stream ended before we fully bound it. Maybe the other end called - // stop on it or something else went wrong. - cleanup(); - unbindNativeStream(videoElement); - completion('Stream ended while trying to bind it to a video element.'); - }; - - // Timeout if it takes too long - timeout = setTimeout(OT.$.bind(function() { - if (videoElement.currentTime === 0) { - cleanup(); - completion('The video stream failed to connect. Please notify the site ' + - 'owner if this continues to happen.'); - } else if (webRtcStream.ended === true) { - // The ended event should have fired by here, but support for it isn't - // always so awesome. - onStoppedLoading(); - } else { - - OT.warn('Never got the loadedmetadata event but currentTime > 0'); - onLoad(null); - } - }, this), 30000); - - videoElement.addEventListener('loadedmetadata', onLoad, false); - videoElement.addEventListener('error', onError, false); - webRtcStream.onended = onStoppedLoading; - } else { - OT.$.callAsync(completion, null); - } - - // The official spec way is 'srcObject', we are slowly converging there. - if (videoElement.srcObject !== void 0) { - videoElement.srcObject = webRtcStream; - } else if (videoElement.mozSrcObject !== void 0) { - videoElement.mozSrcObject = webRtcStream; - } else { - videoElement.src = window.URL.createObjectURL(webRtcStream); - } - - videoElement.play(); - } - - - function unbindNativeStream(videoElement) { - if (videoElement.srcObject !== void 0) { - videoElement.srcObject = null; - } else if (videoElement.mozSrcObject !== void 0) { - videoElement.mozSrcObject = null; - } else { - window.URL.revokeObjectURL(videoElement.src); - } - } - - -})(window); -// tb_require('../helpers/helpers.js') - -!(function() { - /* jshint globalstrict: true, strict: false, undef: true, unused: true, - trailing: true, browser: true, smarttabs:true */ - /* global OT */ - - var currentGuidStorage, - currentGuid; - - var isInvalidStorage = function isInvalidStorage (storageInterface) { - return !(OT.$.isFunction(storageInterface.get) && OT.$.isFunction(storageInterface.set)); - }; - - var getClientGuid = function getClientGuid (completion) { - if (currentGuid) { - completion(null, currentGuid); - return; - } - - // It's the first time that getClientGuid has been called - // in this page lifetime. Attempt to load any existing Guid - // from the storage - currentGuidStorage.get(completion); - }; - - OT.overrideGuidStorage = function (storageInterface) { - if (isInvalidStorage(storageInterface)) { - throw new Error('The storageInterface argument does not seem to be valid, ' + - 'it must implement get and set methods'); - } - - if (currentGuidStorage === storageInterface) { - return; - } - - currentGuidStorage = storageInterface; - - // If a client Guid has already been assigned to this client then - // let the new storage know about it so that it's in sync. - if (currentGuid) { - currentGuidStorage.set(currentGuid, function(error) { - if (error) { - OT.error('Failed to send initial Guid value (' + currentGuid + - ') to the newly assigned Guid Storage. The error was: ' + error); - // @todo error - } - }); - } - }; - - if (!OT._) OT._ = {}; - OT._.getClientGuid = function (completion) { - getClientGuid(function(error, guid) { - if (error) { - completion(error); - return; - } - - if (!guid) { - // Nothing came back, this client is entirely new. - // generate a new Guid and persist it - guid = OT.$.uuid(); - currentGuidStorage.set(guid, function(error) { - if (error) { - completion(error); - return; - } - - currentGuid = guid; - }); - } - else if (!currentGuid) { - currentGuid = guid; - } - - completion(null, currentGuid); - }); - }; - - - // Implement our default storage mechanism, which sets/gets a cookie - // called 'opentok_client_id' - OT.overrideGuidStorage({ - get: function(completion) { - completion(null, OT.$.getCookie('opentok_client_id')); - }, - - set: function(guid, completion) { - OT.$.setCookie('opentok_client_id', guid); - completion(null); - } - }); - -})(window); -!(function(window) { - - // Singleton interval - var logQueue = [], - queueRunning = false; - - - OT.Analytics = function() { - - var endPoint = OT.properties.loggingURL + '/logging/ClientEvent', - endPointQos = OT.properties.loggingURL + '/logging/ClientQos', - - reportedErrors = {}, - - // Map of camel-cased keys to underscored - camelCasedKeys, - - browser = OT.$.browserVersion(), - - send = function(data, isQos, callback) { - OT.$.post((isQos ? endPointQos : endPoint) + '?_=' + OT.$.uuid.v4(), { - body: data, - xdomainrequest: (browser.browser === 'IE' && browser.version < 10), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, callback); - }, - - throttledPost = function() { - // Throttle logs so that they only happen 1 at a time - if (!queueRunning && logQueue.length > 0) { - queueRunning = true; - var curr = logQueue[0]; - - // Remove the current item and send the next log - var processNextItem = function() { - logQueue.shift(); - queueRunning = false; - throttledPost(); - }; - - if (curr) { - send(curr.data, curr.isQos, function(err) { - if(err) { - OT.debug('Failed to send ClientEvent, moving on to the next item.'); - // There was an error, move onto the next item - } else { - curr.onComplete(); - } - setTimeout(processNextItem, 50); - }); - } - } - }, - - post = function(data, onComplete, isQos) { - logQueue.push({ - data: data, - onComplete: onComplete, - isQos: isQos - }); - - throttledPost(); - }, - - shouldThrottleError = function(code, type, partnerId) { - if (!partnerId) return false; - - var errKey = [partnerId, type, code].join('_'), - //msgLimit = DynamicConfig.get('exceptionLogging', 'messageLimitPerPartner', partnerId); - msgLimit = 100; - if (msgLimit === null || msgLimit === undefined) return false; - return (reportedErrors[errKey] || 0) <= msgLimit; - }; - - camelCasedKeys = { - payloadType: 'payload_type', - partnerId: 'partner_id', - streamId: 'stream_id', - sessionId: 'session_id', - connectionId: 'connection_id', - widgetType: 'widget_type', - widgetId: 'widget_id', - avgAudioBitrate: 'avg_audio_bitrate', - avgVideoBitrate: 'avg_video_bitrate', - localCandidateType: 'local_candidate_type', - remoteCandidateType: 'remote_candidate_type', - transportType: 'transport_type' - }; - - // Log an error via ClientEvents. - // - // @param [String] code - // @param [String] type - // @param [String] message - // @param [Hash] details additional error details - // - // @param [Hash] options the options to log the client event with. - // @option options [String] action The name of the Event that we are logging. E.g. - // 'TokShowLoaded'. Required. - // @option options [String] variation Usually used for Split A/B testing, when you - // have multiple variations of the +_action+. - // @option options [String] payloadType A text description of the payload. Required. - // @option options [String] payload The payload. Required. - // @option options [String] sessionId The active OpenTok session, if there is one - // @option options [String] connectionId The active OpenTok connectionId, if there is one - // @option options [String] partnerId - // @option options [String] guid ... - // @option options [String] widgetId ... - // @option options [String] streamId ... - // @option options [String] section ... - // @option options [String] build ... - // - // Reports will be throttled to X reports (see exceptionLogging.messageLimitPerPartner - // from the dynamic config for X) of each error type for each partner. Reports can be - // disabled/enabled globally or on a per partner basis (per partner settings - // take precedence) using exceptionLogging.enabled. - // - this.logError = function(code, type, message, details, options) { - if (!options) options = {}; - var partnerId = options.partnerId; - - if (OT.Config.get('exceptionLogging', 'enabled', partnerId) !== true) { - return; - } - - if (shouldThrottleError(code, type, partnerId)) { - //OT.log('ClientEvents.error has throttled an error of type ' + type + '.' + - // code + ' for partner ' + (partnerId || 'No Partner Id')); - return; - } - - var errKey = [partnerId, type, code].join('_'), - - payload = this.escapePayload(OT.$.extend(details || {}, { - message: payload, - userAgent: OT.$.userAgent() - })); - - - reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ? - reportedErrors[errKey] + 1 : 1; - - return this.logEvent(OT.$.extend(options, { - action: type + '.' + code, - payloadType: payload[0], - payload: payload[1] - })); - }; - - // Log a client event to the analytics backend. - // - // @example Logs a client event called 'foo' - // OT.ClientEvents.log({ - // action: 'foo', - // payload_type: 'foo's payload', - // payload: 'bar', - // session_id: sessionId, - // connection_id: connectionId - // }) - // - // @param [Hash] options the options to log the client event with. - // @option options [String] action The name of the Event that we are logging. - // E.g. 'TokShowLoaded'. Required. - // @option options [String] variation Usually used for Split A/B testing, when - // you have multiple variations of the +_action+. - // @option options [String] payloadType A text description of the payload. Required. - // @option options [String] payload The payload. Required. - // @option options [String] session_id The active OpenTok session, if there is one - // @option options [String] connection_id The active OpenTok connectionId, if there is one - // @option options [String] partner_id - // @option options [String] guid ... - // @option options [String] widget_id ... - // @option options [String] stream_id ... - // @option options [String] section ... - // @option options [String] build ... - // - this.logEvent = function(options) { - var partnerId = options.partnerId; - - if (!options) options = {}; - - OT._.getClientGuid(function(error, guid) { - if (error) { - // @todo - return; - } - - // Set a bunch of defaults - var data = OT.$.extend({ - 'variation' : '', - 'guid' : guid, - 'widget_id' : '', - 'session_id': '', - 'connection_id': '', - 'stream_id' : '', - 'partner_id' : partnerId, - 'source' : window.location.href, - 'section' : '', - 'build' : '' - }, options), - - onComplete = function(){ - // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] - // + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}'); - }; - - // We camel-case our names, but the ClientEvents backend wants them - // underscored... - for (var key in camelCasedKeys) { - if (camelCasedKeys.hasOwnProperty(key) && data[key]) { - data[camelCasedKeys[key]] = data[key]; - delete data[key]; - } - } - - post(data, onComplete, false); - }); - }; - - // Log a client QOS to the analytics backend. - // - this.logQOS = function(options) { - var partnerId = options.partnerId; - - if (!options) options = {}; - - OT._.getClientGuid(function(error, guid) { - if (error) { - // @todo - return; - } - - // Set a bunch of defaults - var data = OT.$.extend({ - 'guid' : guid, - 'widget_id' : '', - 'session_id': '', - 'connection_id': '', - 'stream_id' : '', - 'partner_id' : partnerId, - 'source' : window.location.href, - 'build' : '', - 'duration' : 0 //in milliseconds - }, options), - - onComplete = function(){ - // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] - // + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}'); - }; - - // We camel-case our names, but the ClientEvents backend wants them - // underscored... - for (var key in camelCasedKeys) { - if (camelCasedKeys.hasOwnProperty(key)) { - if(data[key]) { - data[camelCasedKeys[key]] = data[key]; - } - delete data[key]; - } - } - - post(data, onComplete, true); - }); - }; - - // Converts +payload+ to two pipe seperated strings. Doesn't currently handle - // edgecases, e.g. escaping '\\|' will break stuff. - // - // *Note:* It strip any keys that have null values. - this.escapePayload = function(payload) { - var escapedPayload = [], - escapedPayloadDesc = []; - - for (var key in payload) { - if (payload.hasOwnProperty(key) && payload[key] !== null && payload[key] !== undefined) { - escapedPayload.push( payload[key] ? payload[key].toString().replace('|', '\\|') : '' ); - escapedPayloadDesc.push( key.toString().replace('|', '\\|') ); - } - } - - return [ - escapedPayloadDesc.join('|'), - escapedPayload.join('|') - ]; - }; - }; - -})(window); -!(function() { - - OT.$.registerCapability('audioOutputLevelStat', function() { - return OT.$.browserVersion().browser === 'Chrome'; - }); - - OT.$.registerCapability('webAudioCapableRemoteStream', function() { - return OT.$.browserVersion().browser === 'Firefox'; - }); - - OT.$.registerCapability('getStatsWithSingleParameter', function() { - return OT.$.browserVersion().browser === 'Chrome'; - }); - - OT.$.registerCapability('webAudio', function() { - return 'AudioContext' in window; - }); - -})(); -!(function(window) { - - // This is not obvious, so to prevent end-user frustration we'll let them know - // explicitly rather than failing with a bunch of permission errors. We don't - // handle this using an OT Exception as it's really only a development thing. - if (location.protocol === 'file:') { - /*global alert*/ - alert('You cannot test a page using WebRTC through the file system due to browser ' + - 'permissions. You must run it over a web server.'); - } - - if (!window.OT) window.OT = {}; - - if (!window.URL && window.webkitURL) { - window.URL = window.webkitURL; - } - - var _analytics = new OT.Analytics(); - - var // Global parameters used by upgradeSystemRequirements - _intervalId, - _lastHash = document.location.hash; - - -/** -* The first step in using the OpenTok API is to call the ' : '') + - 'OT.initSession()
-* method. Other methods of the OT object check for system requirements and set up error logging.
-*
-* @class OT
-*/
-
-/**
-* -* Initializes and returns the local session object for a specified session ID. -*
-*
-* You connect to an OpenTok session using the connect()
method
-* of the Session object returned by the OT.initSession()
method.
-* Note that calling OT.initSession()
does not initiate communications
-* with the cloud. It simply initializes the Session object that you can use to
-* connect (and to perform other operations once connected).
-*
-* For an example, see Session.connect(). -*
-* -* @method OT.initSession -* @memberof OT -* @param {String} apiKey Your OpenTok API key (see the -* OpenTok dashboard). -* @param {String} sessionId The session ID identifying the OpenTok session. For more -* information, see Session creation. -* @returns {Session} The session object through which all further interactions with -* the session will occur. -*/ - OT.initSession = function(apiKey, sessionId) { - - if(sessionId == null) { - sessionId = apiKey; - apiKey = null; - } - - var session = OT.sessions.get(sessionId); - - if (!session) { - session = new OT.Session(apiKey, sessionId); - OT.sessions.add(session); - } - - return session; - }; - -/** -*
-* Initializes and returns a Publisher object. You can then pass this Publisher
-* object to Session.publish()
to publish a stream to a session.
-*
-* Note: If you intend to reuse a Publisher object created using
-* OT.initPublisher()
to publish to different sessions sequentially,
-* call either Session.disconnect()
or Session.unpublish()
.
-* Do not call both. Then call the preventDefault()
method of the
-* streamDestroyed
or sessionDisconnected
event object to prevent the
-* Publisher object from being removed from the page.
-*
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.
-*
OT.initPublisher()
fails with an
-* error (error code 1500, "Unable to Publish") passed to the completion handler function.
-* If the publisher specifies a frame rate, the actual frame rate of the video stream
-* is set as the frameRate
property of the Stream object, though the actual frame rate
-* will vary based on changing network and system conditions. If the developer does not specify a
-* frame rate, this property is undefined.
-*
-* For sessions that use the OpenTok Media Router (sessions with -* the media mode -* set to routed, lowering the frame rate or lowering the resolution reduces -* the maximum bandwidth the stream can use. However, in sessions with the media mode set to -* relayed, lowering the frame rate or resolution may not reduce the stream's bandwidth. -*
-*
-* You can also restrict the frame rate of a Subscriber's video stream. To restrict the frame rate
-* a Subscriber, call the restrictFrameRate()
method of the subscriber, passing in
-* true
.
-* (See Subscriber.restrictFrameRate().)
-*
height
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 sessions that use the OpenTok Media Router (sessions with the -* media mode -* set to routed, lowering the frame rate or lowering the resolution reduces the maximum bandwidth -* the stream can use. However, in sessions that have the media mode set to relayed, lowering the -* frame rate or resolution may not reduce the stream's bandwidth. -*
-*style
object includes
-* the following properties:
-* audioLevelDisplayMode
(String) — How to display the audio level
-* indicator. Possible values are: "auto"
(the indicator is displayed when the
-* video is disabled), "off"
(the indicator is not displayed), and
-* "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
-* the background image when a video is not displayed. (A video may not be displayed if
-* you call publishVideo(false)
on the Publisher object). You can pass an http
-* or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
-* data
URI scheme (instead of http or https) and pass in base-64-encrypted
-* PNG data, such as that obtained from the
-* Publisher.getImgData() method. For example,
-* you could set the property to "data:VBORw0KGgoAA..."
, where the portion of the
-* string after "data:"
is the result of a call to
-* Publisher.getImgData()
. If the URL or the image data is invalid, the property
-* is ignored (the attempt to set the image fails silently).
-*
-* Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
-* you cannot set the backgroundImageURI
style to a string larger than 32 kB.
-* This is due to an IE 8 limitation on the size of URI strings. Due to this limitation,
-* you cannot set the backgroundImageURI
style to a string obtained with the
-* getImgData()
method.
-*
buttonDisplayMode
(String) — How to display the microphone controls
-* Possible values are: "auto"
(controls are displayed when the stream is first
-* displayed and when the user mouses over the display), "off"
(controls are not
-* displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
-* Possible values are: "auto"
(the name is displayed when the stream is first
-* displayed and when the user mouses over the display), "off"
(the name is not
-* displayed), and "on"
(the name is always displayed).OT.initPublisher()
fails with an
-* error (error code 1500, "Unable to Publish") passed to the completion handler function.
-* 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 for audio input - * devices or
"videoInput"
for video input devices.
- *
- * The deviceId
property is a unique ID for the device. You can pass
- * the deviceId
in as the audioSource
or videoSource
- * property of the the options
parameter of the
- * OT.initPublisher() method.
- *
- * The label
property identifies the device. The label
- * property is set to an empty string if the user has not previously granted access to
- * a camera and microphone. In HTTP, the user must have granted access to a camera and
- * microphone in the current page (for example, in response to a call to
- * OT.initPublisher()
). In HTTPS, the user must have previously granted access
- * to the camera and microphone in the current page or in a page previously loaded from the
- * domain.
- *
- *
- * @see OT.initPublisher()
- * @method OT.getDevices
- * @memberof OT
- */
- OT.getDevices = function(callback) {
- OT.$.getMediaDevices(callback);
- };
-
-
-/**
-* Checks if the system supports OpenTok for WebRTC.
-* @return {Number} Whether the system supports OpenTok for WebRTC (1) or not (0).
-* @see OT.upgradeSystemRequirements()
-* @method OT.checkSystemRequirements
-* @memberof OT
-*/
- OT.checkSystemRequirements = function() {
- OT.debug('OT.checkSystemRequirements()');
-
- // Try native support first, then TBPlugin...
- var systemRequirementsMet = OT.$.hasCapabilities('websockets', 'webrtc') ||
- TBPlugin.isInstalled();
-
- systemRequirementsMet = systemRequirementsMet ?
- this.HAS_REQUIREMENTS : this.NOT_HAS_REQUIREMENTS;
-
- OT.checkSystemRequirements = function() {
- OT.debug('OT.checkSystemRequirements()');
- return systemRequirementsMet;
- };
-
- if(systemRequirementsMet === this.NOT_HAS_REQUIREMENTS) {
- _analytics.logEvent({
- action: 'checkSystemRequirements',
- variation: 'notHasRequirements',
- 'payload_type': 'userAgent',
- 'partner_id': OT.APIKEY,
- payload: OT.$.userAgent()
- });
- }
-
- return systemRequirementsMet;
- };
-
-
-/**
-* Displays information about system requirments for OpenTok for WebRTC. This
-* information is displayed in an iframe element that fills the browser window.
-*
-* Note: this information is displayed automatically when you call the
-* OT.initSession()
or the OT.initPublisher()
method
-* if the client does not support OpenTok for WebRTC.
-*
-* 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
-*/
+ * Sets the API log level.
+ *
+ * Calling OT.setLogLevel()
sets the log level for runtime log messages that
+ * are the OpenTok library generates. The default value for the log level is OT.ERROR
.
+ *
+ * The OpenTok JavaScript library displays log messages in the debugger console (such as + * Firebug), if one exists. + *
+ *
+ * The following example logs the session ID to the console, by calling OT.log()
.
+ * The code also logs an error message when it attempts to publish a stream before the Session
+ * object dispatches a sessionConnected
event.
+ *
+ * OT.setLogLevel(OT.LOG); + * session = OT.initSession(sessionId); + * OT.log(sessionId); + * publisher = OT.initPublisher("publishContainer"); + * session.publish(publisher); + *+ * + * @param {Number} logLevel The degree of logging desired by the developer: + * + *
+ *
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.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
-*/
+ * Sends a string to the the debugger console (such as Firebug), if one exists.
+ * However, the function only logs to the console if you have set the log level
+ * to OT.LOG
or OT.DEBUG
,
+ * by calling OT.setLogLevel(OT.LOG)
or OT.setLogLevel(OT.DEBUG)
.
+ *
+ * @param {String} message The string to log.
+ *
+ * @name OT.log
+ * @memberof OT
+ * @function
+ * @see OT.setLogLevel()
+ */
+// tb_require('../../../helpers/helpers.js')
-/**
-* 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
-*/
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
-/**
- * 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
- */
+// Rumor Messaging for JS
+//
+// https://tbwiki.tokbox.com/index.php/Rumor_:_Messaging_FrameWork
+//
+// @todo Rumor {
+// Add error codes for all the error cases
+// Add Dependability commands
+// }
- if (!window.OT) window.OT = OT;
- if (!window.TB) window.TB = OT;
+OT.Rumor = {
+ MessageType: {
+ // This is used to subscribe to address/addresses. The address/addresses the
+ // client specifies here is registered on the server. Once any message is sent to
+ // that address/addresses, the client receives that message.
+ SUBSCRIBE: 0,
+
+ // This is used to unsubscribe to address / addresses. Once the client unsubscribe
+ // to an address, it will stop getting messages sent to that address.
+ UNSUBSCRIBE: 1,
+
+ // This is used to send messages to arbitrary address/ addresses. Messages can be
+ // anything and Rumor will not care about what is included.
+ MESSAGE: 2,
+
+ // This will be the first message that the client sends to the server. It includes
+ // the uniqueId for that client connection and a disconnect_notify address that will
+ // be notified once the client disconnects.
+ CONNECT: 3,
+
+ // This will be the message used by the server to notify an address that a
+ // client disconnected.
+ DISCONNECT: 4,
+
+ //Enhancements to support Keepalives
+ PING: 7,
+ PONG: 8,
+ STATUS: 9
+ }
+};
+
+// tb_require('../../../helpers/helpers.js')
+// tb_require('./rumor.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT, OTPlugin */
-})(window);
!(function() {
- OT.Collection = function(idField) {
- var _models = [],
- _byId = {},
- _idField = idField || 'id';
+ OT.Rumor.PluginSocket = function(messagingURL, events) {
- OT.$.eventing(this, true);
+ var webSocket,
+ state = 'initializing';
- var modelProperty = function(model, property) {
- if(OT.$.isFunction(model[property])) {
- return model[property]();
+ OTPlugin.initRumorSocket(messagingURL, OT.$.bind(function(err, rumorSocket) {
+ if(err) {
+ state = 'closed';
+ events.onClose({ code: 4999 });
+ } else if(state === 'initializing') {
+ webSocket = rumorSocket;
+
+ webSocket.onOpen(function() {
+ state = 'open';
+ events.onOpen();
+ });
+ webSocket.onClose(function(error) {
+ state = 'closed'; /* CLOSED */
+ events.onClose({ code: error });
+ });
+ webSocket.onError(function(error) {
+ state = 'closed'; /* CLOSED */
+ events.onError(error);
+ /* native websockets seem to do this, so should we */
+ events.onClose({ code: error });
+ });
+
+ webSocket.onMessage(function(type, addresses, headers, payload) {
+ var msg = new OT.Rumor.Message(type, addresses, headers, payload);
+ events.onMessage(msg);
+ });
+
+ webSocket.open();
} else {
- return model[property];
+ this.close();
+ }
+ }, this));
+
+ this.close = function() {
+ if(state === 'initializing' || state === 'closed') {
+ state = 'closed';
+ return;
+ }
+
+ webSocket.close(1000, '');
+ };
+
+ this.send = function(msg) {
+ if(state === 'open') {
+ webSocket.send(msg);
}
};
- var onModelUpdate = OT.$.bind(function onModelUpdate (event) {
- this.trigger('update', event);
- this.trigger('update:'+event.target.id, event);
- }, this),
-
- onModelDestroy = OT.$.bind(function onModelDestroyed (event) {
- this.remove(event.target, event.reason);
- }, this);
-
-
- this.reset = function() {
- // Stop listening on the models, they are no longer our problem
- OT.$.forEach(_models, function(model) {
- model.off('updated', onModelUpdate, this);
- model.off('destroyed', onModelDestroy, this);
- }, this);
-
- _models = [];
- _byId = {};
+ this.isClosed = function() {
+ return state === 'closed';
};
- this.destroy = function(reason) {
- OT.$.forEach(_models, function(model) {
- if(model && typeof model.destroy === 'function') {
- model.destroy(reason, true);
- }
- });
-
- this.reset();
- this.off();
- };
-
- this.get = function(id) { return id && _byId[id] !== void 0 ? _models[_byId[id]] : void 0; };
- this.has = function(id) { return id && _byId[id] !== void 0; };
-
- this.toString = function() { return _models.toString(); };
-
- // Return only models filtered by either a dict of properties
- // or a filter function.
- //
- // @example Return all publishers with a streamId of 1
- // OT.publishers.where({streamId: 1})
- //
- // @example The same thing but filtering using a filter function
- // OT.publishers.where(function(publisher) {
- // return publisher.stream.id === 4;
- // });
- //
- // @example The same thing but filtering using a filter function
- // executed with a specific this
- // OT.publishers.where(function(publisher) {
- // return publisher.stream.id === 4;
- // }, self);
- //
- this.where = function(attrsOrFilterFn, context) {
- if (OT.$.isFunction(attrsOrFilterFn)) return OT.$.filter(_models, attrsOrFilterFn, context);
-
- return OT.$.filter(_models, function(model) {
- for (var key in attrsOrFilterFn) {
- if(!attrsOrFilterFn.hasOwnProperty(key)) {
- continue;
- }
- if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
- }
-
- return true;
- });
- };
-
- // Similar to where in behaviour, except that it only returns
- // the first match.
- this.find = function(attrsOrFilterFn, context) {
- var filterFn;
-
- if (OT.$.isFunction(attrsOrFilterFn)) {
- filterFn = attrsOrFilterFn;
- }
- else {
- filterFn = function(model) {
- for (var key in attrsOrFilterFn) {
- if(!attrsOrFilterFn.hasOwnProperty(key)) {
- continue;
- }
- if (modelProperty(model, key) !== attrsOrFilterFn[key]) return false;
- }
-
- return true;
- };
- }
-
- filterFn = OT.$.bind(filterFn, context);
-
- for (var i=0; i<_models.length; ++i) {
- if (filterFn(_models[i]) === true) return _models[i];
- }
-
- return null;
- };
-
- this.add = function(model) {
- var id = modelProperty(model, _idField);
-
- if (this.has(id)) {
- OT.warn('Model ' + id + ' is already in the collection', _models);
- return this;
- }
-
- _byId[id] = _models.push(model) - 1;
-
- model.on('updated', onModelUpdate, this);
- model.on('destroyed', onModelDestroy, this);
-
- this.trigger('add', model);
- this.trigger('add:'+id, model);
-
- return this;
- };
-
- this.remove = function(model, reason) {
- var id = modelProperty(model, _idField);
-
- _models.splice(_byId[id], 1);
-
- // Shuffle everyone down one
- for (var i=_byId[id]; i<_models.length; ++i) {
- _byId[_models[i][_idField]] = i;
- }
-
- delete _byId[id];
-
- model.off('updated', onModelUpdate, this);
- model.off('destroyed', onModelDestroy, this);
-
- this.trigger('remove', model, reason);
- this.trigger('remove:'+id, model, reason);
-
- return this;
- };
-
- // Used by session connecto fire add events after adding listeners
- this._triggerAddEvents = function() {
- var models = this.where.apply(this, arguments);
- OT.$.forEach(models, function(model) {
- this.trigger('add', model);
- this.trigger('add:' + modelProperty(model, _idField), model);
- }, this);
- };
-
- this.length = function() {
- return _models.length;
- };
};
}(this));
-!(function() {
- /**
- * The Event object defines the basic OpenTok event object that is passed to
- * event listeners. Other OpenTok event classes implement the properties and methods of
- * the Event object.
- *
- * For example, the Stream object dispatches a streamPropertyChanged
event when
- * the stream's properties are updated. You add a callback for an event using the
- * on()
method of the Stream object:
- * stream.on("streamPropertyChanged", function (event) { - * alert("Properties changed for stream " + event.target.streamId); - * });- * - * @class Event - * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable - * (
true
) or not (false
). You can cancel the default behavior by
- * calling the preventDefault()
method of the Event object in the callback
- * function. (See preventDefault().)
- *
- * @property {Object} target The object that dispatched the event.
- *
- * @property {String} type The type of event.
- */
- OT.Event = OT.$.eventing.Event();
- /**
- * Prevents the default behavior associated with the event from taking place.
- *
- * To see whether an event has a default behavior, check the cancelable
property
- * of the event object.
Call the preventDefault()
method in the callback function for the event.
The following events have default behaviors:
- * - *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
- */
+// tb_require('../../../helpers/helpers.js')
- // Event names lookup
- OT.Event.names = {
- // Activity Status for cams/mics
- ACTIVE: 'active',
- INACTIVE: 'inactive',
- UNKNOWN: 'unknown',
-
- // Archive types
- PER_SESSION: 'perSession',
- PER_STREAM: 'perStream',
-
- // OT Events
- EXCEPTION: 'exception',
- ISSUE_REPORTED: 'issueReported',
-
- // Session Events
- SESSION_CONNECTED: 'sessionConnected',
- SESSION_DISCONNECTED: 'sessionDisconnected',
- STREAM_CREATED: 'streamCreated',
- STREAM_DESTROYED: 'streamDestroyed',
- CONNECTION_CREATED: 'connectionCreated',
- CONNECTION_DESTROYED: 'connectionDestroyed',
- SIGNAL: 'signal',
- STREAM_PROPERTY_CHANGED: 'streamPropertyChanged',
- MICROPHONE_LEVEL_CHANGED: 'microphoneLevelChanged',
-
-
- // Publisher Events
- RESIZE: 'resize',
- SETTINGS_BUTTON_CLICK: 'settingsButtonClick',
- DEVICE_INACTIVE: 'deviceInactive',
- INVALID_DEVICE_NAME: 'invalidDeviceName',
- ACCESS_ALLOWED: 'accessAllowed',
- ACCESS_DENIED: 'accessDenied',
- ACCESS_DIALOG_OPENED: 'accessDialogOpened',
- ACCESS_DIALOG_CLOSED: 'accessDialogClosed',
- ECHO_CANCELLATION_MODE_CHANGED: 'echoCancellationModeChanged',
- PUBLISHER_DESTROYED: 'destroyed',
-
- // Subscriber Events
- SUBSCRIBER_DESTROYED: 'destroyed',
-
- // DeviceManager Events
- DEVICES_DETECTED: 'devicesDetected',
-
- // DevicePanel Events
- DEVICES_SELECTED: 'devicesSelected',
- CLOSE_BUTTON_CLICK: 'closeButtonClick',
-
- MICLEVEL : 'microphoneActivityLevel',
- MICGAINCHANGED : 'microphoneGainChanged',
-
- // Environment Loader
- ENV_LOADED: 'envLoaded',
- ENV_UNLOADED: 'envUnloaded',
-
- // Audio activity Events
- AUDIO_LEVEL_UPDATED: 'audioLevelUpdated'
- };
-
- OT.ExceptionCodes = {
- JS_EXCEPTION: 2000,
- AUTHENTICATION_ERROR: 1004,
- INVALID_SESSION_ID: 1005,
- CONNECT_FAILED: 1006,
- CONNECT_REJECTED: 1007,
- CONNECTION_TIMEOUT: 1008,
- NOT_CONNECTED: 1010,
- P2P_CONNECTION_FAILED: 1013,
- API_RESPONSE_FAILURE: 1014,
- UNABLE_TO_PUBLISH: 1500,
- UNABLE_TO_SUBSCRIBE: 1501,
- UNABLE_TO_FORCE_DISCONNECT: 1520,
- UNABLE_TO_FORCE_UNPUBLISH: 1530
- };
-
- /**
- * The {@link OT} class dispatches exception
events when the OpenTok API encounters
- * an exception (error). The ExceptionEvent object defines the properties of the event
- * object that is dispatched.
- *
- * Note that you set up a callback for the exception
event by calling the
- * OT.on()
method.
- * 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);
- };
-
-
-/**
- * Dispatched by the Session object when a client connects to or disconnects from a {@link Session}.
- * For the local client, the Session object dispatches a "sessionConnected" or "sessionDisconnected"
- * event, defined by the {@link SessionConnectEvent} and {@link SessionDisconnectEvent} classes.
- *
- * The following code keeps a running total of the number of connections to a session
- * by monitoring the connections
property of the sessionConnect
,
- * connectionCreated
and connectionDestroyed
events:
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects - * var sessionID = ""; // Replace with your own session ID. - * // See https://dashboard.tokbox.com/projects - * var token = ""; // Replace with a generated token that has been assigned the moderator role. - * // See https://dashboard.tokbox.com/projects - * var connectionCount = 0; - * - * var session = OT.initSession(apiKey, sessionID); - * session.on("connectionCreated", function(event) { - * connectionCount++; - * displayConnectionCount(); - * }); - * session.on("connectionDestroyed", function(event) { - * connectionCount--; - * displayConnectionCount(); - * }); - * session.connect(token); - * - * function displayConnectionCount() { - * document.getElementById("connectionCountField").value = connectionCount.toString(); - * }- * - *
This example assumes that there is an input text field in the HTML DOM
- * with the id
set to "connectionCountField"
:
<input type="text" id="connectionCountField" value="0"></input>- * - * - * @property {Connection} connection A Connection objects for the connections that was - * created or deleted. - * - * @property {Array} connections Deprecated. Use the
connection
property. A
- * connectionCreated
or connectionDestroyed
event is dispatched
- * for each connection created and destroyed in the session.
- *
- * @property {String} reason For a connectionDestroyed
event,
- * a description of why the connection ended. This property can have two values:
- *
- * "clientDisconnected"
A client disconnected from the session by calling
- * the disconnect()
method of the Session object or by closing the browser.
- * (See Session.disconnect().)"forceDisconnected"
A moderator has disconnected the publisher
- * from the session, by calling the forceDisconnect()
method of the Session
- * object. (See Session.forceDisconnect().)"networkDisconnected"
The network connection terminated abruptly
- * (for example, the client lost their internet connection).Depending on the context, this description may allow the developer to refine - * the course of action they take in response to an event.
- * - *For a connectionCreated
event, this string is undefined.
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 {Boolean} cancelable Whether the event has a default behavior that is cancelable - * (
true
) or not (false
). You can cancel the default behavior by calling
- * the preventDefault()
method of the StreamEvent object in the event listener
- * function. The streamDestroyed
- * event is cancelable. (See preventDefault().)
- *
- * @property {String} reason For a streamDestroyed
event,
- * a description of why the session disconnected. This property can have one of the following
- * values:
- *
- * "clientDisconnected"
A client disconnected from the session by calling
- * the disconnect()
method of the Session object or by closing the browser.
- * (See Session.disconnect().)"forceDisconnected"
A moderator has disconnected the publisher of the
- * stream from the session, by calling the forceDisconnect()
method of the Session
-* object. (See Session.forceDisconnect().)"forceUnpublished"
A moderator has forced the publisher of the stream
- * to stop publishing the stream, by calling the forceUnpublish()
method of the
- * Session object. (See Session.forceUnpublish().)"networkDisconnected"
The network connection terminated abruptly (for
- * example, the client lost their internet connection).Depending on the context, this description may allow the developer to refine - * the course of action they take in response to an event.
- * - *For a streamCreated
event, this string is undefined.
streamCreated
event) or deleted (in the case of a
- * streamDestroyed
event).
- *
- * @property {Array} streams Deprecated. Use the stream
property. A
- * streamCreated
or streamDestroyed
event is dispatched for
- * each stream added or destroyed.
- *
- * @augments Event
- */
-
- var streamEventPluralDeprecationWarningShown = false;
- OT.StreamEvent = function (type, stream, reason, cancelable) {
- OT.Event.call(this, type, cancelable);
-
- if (OT.$.canDefineProperty) {
- Object.defineProperty(this, 'streams', {
- get: function() {
- if(!streamEventPluralDeprecationWarningShown) {
- OT.warn('OT.StreamEvent streams property is deprecated, use stream instead.');
- streamEventPluralDeprecationWarningShown = true;
- }
- return [stream];
- }
- });
- } else {
- this.streams = [stream];
- }
-
- this.stream = stream;
- this.reason = reason;
- };
-
-/**
-* Prevents the default behavior associated with the event from taking place.
-*
-* For the streamDestroyed
event dispatched by the Session object,
-* the default behavior is that all Subscriber objects that are subscribed to the stream are
-* unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a
-* destroyed
event when the element is removed from the HTML DOM. If you call the
-* preventDefault()
method in the event listener for the streamDestroyed
-* event, the default behavior is prevented and you can clean up Subscriber objects using your
-* own code. See
-* Session.getSubscribersForStream().
-* For the streamDestroyed
event dispatched by a Publisher object, the default
-* behavior is that the Publisher object is removed from the HTML DOM. The Publisher object
-* dispatches a destroyed
event when the element is removed from the HTML DOM.
-* If you call the preventDefault()
method in the event listener for the
-* streamDestroyed
event, the default behavior is prevented, and you can
-* retain the Publisher for reuse or clean it up using your own code.
-*
To see whether an event has a default behavior, check the cancelable
property of
-* the event object.
Call the preventDefault()
method in the event listener function for the event.
connect()
method of the Session object.
- *
- * In version 2.2, the completionHandler of the Session.connect()
method
- * indicates success or failure in connecting to the session.
- *
- * @class SessionConnectEvent
- * @property {Array} connections Deprecated in version 2.2 (and set to an empty array). In
- * version 2.2, listen for the connectionCreated
event dispatched by the Session
- * object. In version 2.2, the Session object dispatches a connectionCreated
event
- * for each connection (including your own). This includes connections present when you first
- * connect to the session.
- *
- * @property {Array} streams Deprecated in version 2.2 (and set to an empty array). In version
- * 2.2, listen for the streamCreated
event dispatched by the Session object. In
- * version 2.2, the Session object dispatches a streamCreated
event for each stream
- * other than those published by your client. This includes streams
- * present when you first connect to the session.
- *
- * @see Session.connect()
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 {Object} newValue The new value of the property (after the change).
- * @property {Object} oldValue The old value of the property (before the change).
- * @property {Stream} stream The Stream object for which a property has changed.
- *
- * @see Publisher.publishAudio()
- * @see Publisher.publishVideo()
- * @see Stream.videoDimensions
- * @augments Event
- */
- OT.StreamPropertyChangedEvent = function (type, stream, changedProperty, oldValue, newValue) {
- OT.Event.call(this, type, false);
- this.type = type;
- this.stream = stream;
- this.changedProperty = changedProperty;
- this.oldValue = oldValue;
- this.newValue = newValue;
- };
-
-/**
- * Defines event objects for the archiveStarted
and archiveStopped
events.
- * The Session object dispatches these events when an archive recording of the session starts and
- * stops.
- *
- * @property {String} id The archive ID.
- * @property {String} name The name of the archive. You can assign an archive a name when you create
- * it, using the OpenTok REST API or one of the
- * OpenTok server SDKs.
- *
- * @class ArchiveEvent
- * @augments Event
- */
- OT.ArchiveEvent = function (type, archive) {
- OT.Event.call(this, type, false);
- this.type = type;
- this.id = archive.id;
- this.name = archive.name;
- this.status = archive.status;
- this.archive = archive;
- };
-
- OT.ArchiveUpdatedEvent = function (stream, key, oldValue, newValue) {
- OT.Event.call(this, 'updated', false);
- this.target = stream;
- this.changedProperty = key;
- this.oldValue = oldValue;
- this.newValue = newValue;
- };
-
-/**
- * The Session object dispatches a signal event when the client receives a signal from the session.
- *
- * @class SignalEvent
- * @property {String} type The type assigned to the signal (if there is one). Use the type to
- * filter signals received (by adding an event handler for signal:type1 or signal:type2, etc.)
- * @property {String} data The data string sent with the signal (if there is one).
- * @property {Connection} from The Connection corresponding to the client that sent with the signal.
- *
- * @see Session.signal()
- * @see Session events (signal and signal:type)
- * @augments Event
- */
- OT.SignalEvent = function(type, data, from) {
- OT.Event.call(this, type ? 'signal:' + type : OT.Event.names.SIGNAL, false);
- this.data = data;
- this.from = from;
- };
-
- OT.StreamUpdatedEvent = function (stream, key, oldValue, newValue) {
- OT.Event.call(this, 'updated', false);
- this.target = stream;
- this.changedProperty = key;
- this.oldValue = oldValue;
- this.newValue = newValue;
- };
-
- OT.DestroyedEvent = function(type, target, reason) {
- OT.Event.call(this, type, false);
- this.target = target;
- this.reason = reason;
- };
-
-/**
- * Defines the event object for the videoDisabled
and videoEnabled
events
- * dispatched by the Subscriber.
- *
- * @class VideoEnabledChangedEvent
- *
- * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable
- * (true
) or not (false
). You can cancel the default behavior by
- * calling the preventDefault()
method of the event object in the callback
- * function. (See preventDefault().)
- *
- * @property {String} reason The reason the video was disabled or enabled. This can be set to one of
- * the following values:
- *
- * "publishVideo"
— The publisher started or stopped publishing video,
- * by calling publishVideo(true)
or publishVideo(false)
."quality"
— The OpenTok Media Router starts or stops sending video
- * to the subscriber based on stream quality changes. This feature of the OpenTok Media
- * Router has a subscriber drop the video stream when connectivity degrades. (The subscriber
- * continues to receive the audio stream, if there is one.)
- *
- * If connectivity improves to support video again, the Subscriber object dispatches
- * a videoEnabled
event, and the Subscriber resumes receiving video.
- *
- * By default, the Subscriber displays a video disabled indicator when a
- * videoDisabled
event with this reason is dispatched and removes the indicator
- * when the videoDisabled
event with this reason is dispatched. You can control
- * the display of this icon by calling the setStyle()
method of the Subscriber,
- * setting the videoDisabledDisplayMode
property(or you can set the style when
- * calling the Session.subscribe()
method, setting the style
property
- * of the properties
parameter).
- *
- * This feature is only available in sessions that use the OpenTok Media Router (sessions with - * the media mode - * set to routed), not in sessions with the media mode set to relayed. - *
"subscribeToVideo"
— The subscriber started or stopped subscribing to
- * video, by calling subscribeToVideo(true)
or subscribeToVideo(false)
.
- * "videoDisabled"
or
- * "videoEnabled"
.
- *
- * @see Subscriber videoDisabled event
- * @see Subscriber videoEnabled event
- * @augments Event
- */
- OT.VideoEnabledChangedEvent = function(type, properties) {
- OT.Event.call(this, type, false);
- this.reason = properties.reason;
- };
-
- OT.VideoDisableWarningEvent = function(type/*, properties*/) {
- OT.Event.call(this, type, false);
- };
-
-/**
- * Dispatched periodically by a Subscriber or Publisher object to indicate the audio
- * level. This event is dispatched up to 60 times per second, depending on the browser.
- *
- * @property {String} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically
- * for use in adjusting a user interface element, such as a volume meter. Use a moving average
- * to smooth the data.
- *
- * @class AudioLevelUpdatedEvent
- * @augments Event
- */
- OT.AudioLevelUpdatedEvent = function(audioLevel) {
- OT.Event.call(this, OT.Event.names.AUDIO_LEVEL_UPDATED, false);
- this.audioLevel = audioLevel;
- };
-
-})(window);
-/* jshint ignore:start */
// https://code.google.com/p/stringencoding/
// An implementation of http://encoding.spec.whatwg.org/#api
+// Modified by TokBox to remove all encoding support except for utf-8
/**
* @license Copyright 2014 Joshua Bell
@@ -9006,21 +6199,23 @@ waitForDomReady();
*
* Original source: https://github.com/inexorabletash/text-encoding
***/
+/*jshint unused:false*/
(function(global) {
'use strict';
- var browser = OT.$.browserVersion();
- if(browser && browser.browser === 'IE' && browser.version < 10) {
+
+ if(OT.$.env && OT.$.env.name === 'IE' && OT.$.env.version < 10) {
return; // IE 8 doesn't do websockets. No websockets, no encoding.
}
if ( (global.TextEncoder !== void 0) && (global.TextDecoder !== void 0)) {
// defer to the native ones
- // @todo is this a good idea?
return;
}
+ /* jshint ignore:start */
+
//
// Utilities
//
@@ -9058,8 +6253,10 @@ waitForDomReady();
// 4. Encodings
//
- /** @const */ var EOF_byte = -1;
- /** @const */ var EOF_code_point = -1;
+ /** @const */
+ var EOF_byte = -1;
+ /** @const */
+ var EOF_code_point = -1;
/**
* @constructor
@@ -9690,52 +6887,6 @@ waitForDomReady();
/** @type {Object.' + errorMsg + + (helpMsg ? '
'; + OT.$.addClass(container, classNames || 'OT_subscriber_error'); + if(container.querySelector('p').offsetHeight > container.offsetHeight) { + container.querySelector('span').style.display = 'none'; + } + }; + + return widgetView; + }; + +})(window); + +// tb_require('../helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +if (!OT.properties) { + throw new Error('OT.properties does not exist, please ensure that you include a valid ' + + 'properties file.'); +} + +OT.useSSL = function () { + return OT.properties.supportSSL && (window.location.protocol.indexOf('https') >= 0 || + window.location.protocol.indexOf('chrome-extension') >= 0); +}; + +// Consumes and overwrites OT.properties. Makes it better and stronger! +OT.properties = function(properties) { + var props = OT.$.clone(properties); + + props.debug = properties.debug === 'true' || properties.debug === true; + props.supportSSL = properties.supportSSL === 'true' || properties.supportSSL === true; + + if (window.OTProperties) { + // Allow window.OTProperties to override cdnURL, configURL, assetURL and cssURL + if (window.OTProperties.cdnURL) props.cdnURL = window.OTProperties.cdnURL; + if (window.OTProperties.cdnURLSSL) props.cdnURLSSL = window.OTProperties.cdnURLSSL; + if (window.OTProperties.configURL) props.configURL = window.OTProperties.configURL; + if (window.OTProperties.assetURL) props.assetURL = window.OTProperties.assetURL; + if (window.OTProperties.cssURL) props.cssURL = window.OTProperties.cssURL; + } + + if (!props.assetURL) { + if (OT.useSSL()) { + props.assetURL = props.cdnURLSSL + '/webrtc/' + props.version; + } else { + props.assetURL = props.cdnURL + '/webrtc/' + props.version; + } + } + + var isIE89 = OT.$.env.name === 'IE' && OT.$.env.version <= 9; + if (!(isIE89 && window.location.protocol.indexOf('https') < 0)) { + props.apiURL = props.apiURLSSL; + props.loggingURL = props.loggingURLSSL; + } + + if (!props.configURL) props.configURL = props.assetURL + '/js/dynamic_config.min.js'; + if (!props.cssURL) props.cssURL = props.assetURL + '/css/TB.min.css'; + + return props; +}(OT.properties); + +// tb_require('../helpers.js') + +!(function() { + /* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ + /* global OT */ + + var currentGuidStorage, + currentGuid; + + var isInvalidStorage = function isInvalidStorage (storageInterface) { + return !(OT.$.isFunction(storageInterface.get) && OT.$.isFunction(storageInterface.set)); + }; + + var getClientGuid = function getClientGuid (completion) { + if (currentGuid) { + completion(null, currentGuid); + return; + } + + // It's the first time that getClientGuid has been called + // in this page lifetime. Attempt to load any existing Guid + // from the storage + currentGuidStorage.get(completion); + }; + + /* + * Sets the methods for storing and retrieving client GUIDs persistently + * across sessions. By default, OpenTok.js attempts to use browser cookies to + * store GUIDs. + * ' : '') + + '
+ * Pass in an object that has a get()
method and
+ * a set()
method.
+ *
+ * The get()
method must take one parameter: the callback
+ * method to invoke. The callback method is passed two parameters —
+ * the first parameter is an error object or null if the call is successful;
+ * and the second parameter is the GUID (a string) if successful.
+ *
+ * The set()
method must include two parameters: the GUID to set
+ * (a string) and the callback method to invoke. The callback method is
+ * passed an error object on error, or it is passed no parameter if the call is
+ * successful.
+ *
+ * Here is an example: + *
+ *
+ * var ComplexStorage = function() { + * this.set = function(guid, completion) { + * AwesomeBackendService.set(guid, function(response) { + * completion(response.error || null); + * }); + * }; + * this.get = function(completion) { + * AwesomeBackendService.get(function(response, guid) { + * completion(response.error || null, guid); + * }); + * }; + * }; + * + * OT.overrideGuidStorage(new ComplexStorage()); + *+ */ + OT.overrideGuidStorage = function (storageInterface) { + if (isInvalidStorage(storageInterface)) { + throw new Error('The storageInterface argument does not seem to be valid, ' + + 'it must implement get and set methods'); + } + + if (currentGuidStorage === storageInterface) { + return; + } + + currentGuidStorage = storageInterface; + + // If a client Guid has already been assigned to this client then + // let the new storage know about it so that it's in sync. + if (currentGuid) { + currentGuidStorage.set(currentGuid, function(error) { + if (error) { + OT.error('Failed to send initial Guid value (' + currentGuid + + ') to the newly assigned Guid Storage. The error was: ' + error); + // @todo error + } + }); + } + }; + + if (!OT._) OT._ = {}; + OT._.getClientGuid = function (completion) { + getClientGuid(function(error, guid) { + if (error) { + completion(error); + return; + } + + if (!guid) { + // Nothing came back, this client is entirely new. + // generate a new Guid and persist it + guid = OT.$.uuid(); + currentGuidStorage.set(guid, function(error) { + if (error) { + completion(error); + return; + } + + currentGuid = guid; + }); + } + else if (!currentGuid) { + currentGuid = guid; + } + + completion(null, currentGuid); + }); + }; + + + // Implement our default storage mechanism, which sets/gets a cookie + // called 'opentok_client_id' + OT.overrideGuidStorage({ + get: function(completion) { + completion(null, OT.$.getCookie('opentok_client_id')); + }, + + set: function(guid, completion) { + OT.$.setCookie('opentok_client_id', guid); + completion(null); + } + }); + +})(window); + +// tb_require('../helpers.js') +// tb_require('./web_rtc.js') + +// Web OT Helpers +!(function() { + /* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ + /* global OT */ + + var nativeGetUserMedia, + vendorToW3CErrors, + gumNamesToMessages, + mapVendorErrorName, + parseErrorEvent, + areInvalidConstraints; + + // Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth + nativeGetUserMedia = (function() { + if (navigator.getUserMedia) { + return OT.$.bind(navigator.getUserMedia, navigator); + } else if (navigator.mozGetUserMedia) { + return OT.$.bind(navigator.mozGetUserMedia, navigator); + } else if (navigator.webkitGetUserMedia) { + return OT.$.bind(navigator.webkitGetUserMedia, navigator); + } else if (OTPlugin.isInstalled()) { + return OT.$.bind(OTPlugin.getUserMedia, OTPlugin); + } + })(); + + // Mozilla error strings and the equivalent W3C names. NOT_SUPPORTED_ERROR does not + // exist in the spec right now, so we'll include Mozilla's error description. + // Chrome TrackStartError is triggered when the camera is already used by another app (Windows) + vendorToW3CErrors = { + PERMISSION_DENIED: 'PermissionDeniedError', + NOT_SUPPORTED_ERROR: 'NotSupportedError', + MANDATORY_UNSATISFIED_ERROR: ' ConstraintNotSatisfiedError', + NO_DEVICES_FOUND: 'NoDevicesFoundError', + HARDWARE_UNAVAILABLE: 'HardwareUnavailableError', + TrackStartError: 'HardwareUnavailableError' + }; + + gumNamesToMessages = { + PermissionDeniedError: 'End-user denied permission to hardware devices', + PermissionDismissedError: 'End-user dismissed permission to hardware devices', + NotSupportedError: 'A constraint specified is not supported by the browser.', + ConstraintNotSatisfiedError: 'It\'s not possible to satisfy one or more constraints ' + + 'passed into the getUserMedia function', + OverconstrainedError: 'Due to changes in the environment, one or more mandatory ' + + 'constraints can no longer be satisfied.', + NoDevicesFoundError: 'No voice or video input devices are available on this machine.', + HardwareUnavailableError: 'The selected voice or video devices are unavailable. Verify ' + + 'that the chosen devices are not in use by another application.' + }; + + // Map vendor error strings to names and messages if possible + mapVendorErrorName = function mapVendorErrorName(vendorErrorName, vendorErrors) { + var errorName, errorMessage; + + if(vendorErrors.hasOwnProperty(vendorErrorName)) { + errorName = vendorErrors[vendorErrorName]; + } else { + // This doesn't map to a known error from the Media Capture spec, it's + // probably a custom vendor error message. + errorName = vendorErrorName; + } + + if(gumNamesToMessages.hasOwnProperty(errorName)) { + errorMessage = gumNamesToMessages[errorName]; + } else { + errorMessage = 'Unknown Error while getting user media'; + } + + return { + name: errorName, + message: errorMessage + }; + }; + + // Parse and normalise a getUserMedia error event from Chrome or Mozilla + // @ref http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError + parseErrorEvent = function parseErrorObject(event) { + var error; + + if (OT.$.isObject(event) && event.name) { + error = mapVendorErrorName(event.name, vendorToW3CErrors); + error.constraintName = event.constraintName; + } else if (typeof event === 'string') { + error = mapVendorErrorName(event, vendorToW3CErrors); + } else { + error = { + message: 'Unknown Error type while getting media' + }; + } + + return error; + }; + + // Validates a Hash of getUserMedia constraints. Currently we only + // check to see if there is at least one non-false constraint. + areInvalidConstraints = function(constraints) { + if (!constraints || !OT.$.isObject(constraints)) return true; + + for (var key in constraints) { + if(!constraints.hasOwnProperty(key)) { + continue; + } + if (constraints[key]) return false; + } + + return true; + }; + + + // A wrapper for the builtin navigator.getUserMedia. In addition to the usual + // getUserMedia behaviour, this helper method also accepts a accessDialogOpened + // and accessDialogClosed callback. + // + // @memberof OT.$ + // @private + // + // @param {Object} constraints + // A dictionary of constraints to pass to getUserMedia. See + // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints + // in the Media Capture and Streams spec for more info. + // + // @param {function} success + // Called when getUserMedia completes successfully. The callback will be passed a WebRTC + // Stream object. + // + // @param {function} failure + // Called when getUserMedia fails to access a user stream. It will be passed an object + // with a code property representing the error that occurred. + // + // @param {function} accessDialogOpened + // Called when the access allow/deny dialog is opened. + // + // @param {function} accessDialogClosed + // Called when the access allow/deny dialog is closed. + // + // @param {function} accessDenied + // Called when access is denied to the camera/mic. This will be either because + // the user has clicked deny or because a particular origin is permanently denied. + // + OT.$.getUserMedia = function(constraints, success, failure, accessDialogOpened, + accessDialogClosed, accessDenied, customGetUserMedia) { + + var getUserMedia = nativeGetUserMedia; + + if(OT.$.isFunction(customGetUserMedia)) { + getUserMedia = customGetUserMedia; + } + + // All constraints are false, we don't allow this. This may be valid later + // depending on how/if we integrate data channels. + if (areInvalidConstraints(constraints)) { + OT.error('Couldn\'t get UserMedia: All constraints were false'); + // Using a ugly dummy-code for now. + failure.call(null, { + name: 'NO_VALID_CONSTRAINTS', + message: 'Video and Audio was disabled, you need to enabled at least one' + }); + + return; + } + + var triggerOpenedTimer = null, + displayedPermissionDialog = false, + + finaliseAccessDialog = function() { + if (triggerOpenedTimer) { + clearTimeout(triggerOpenedTimer); + } + + if (displayedPermissionDialog && accessDialogClosed) accessDialogClosed(); + }, + + triggerOpened = function() { + triggerOpenedTimer = null; + displayedPermissionDialog = true; + + if (accessDialogOpened) accessDialogOpened(); + }, + + onStream = function(stream) { + finaliseAccessDialog(); + success.call(null, stream); + }, + + onError = function(event) { + finaliseAccessDialog(); + var error = parseErrorEvent(event); + + // The error name 'PERMISSION_DENIED' is from an earlier version of the spec + if (error.name === 'PermissionDeniedError' || error.name === 'PermissionDismissedError') { + accessDenied.call(null, error); + } else { + failure.call(null, error); + } + }; + + try { + getUserMedia(constraints, onStream, onError); + } catch (e) { + OT.error('Couldn\'t get UserMedia: ' + e.toString()); + onError(); + return; + } + + // The 'remember me' functionality of WebRTC only functions over HTTPS, if + // we aren't on HTTPS then we should definitely be displaying the access + // dialog. + // + // If we are on HTTPS, we'll wait 500ms to see if we get a stream + // immediately. If we do then the user had clicked 'remember me'. Otherwise + // we assume that the accessAllowed dialog is visible. + // + // @todo benchmark and see if 500ms is a reasonable number. It seems like + // we should know a lot quicker. + // + if (location.protocol.indexOf('https') === -1) { + // Execute after, this gives the client a chance to bind to the + // accessDialogOpened event. + triggerOpenedTimer = setTimeout(triggerOpened, 100); + + } else { + // wait a second and then trigger accessDialogOpened + triggerOpenedTimer = setTimeout(triggerOpened, 500); + } }; })(); + +// tb_require('../helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + !(function() { + var adjustModal = function(callback) { + return function setFullHeightDocument(window, document) { + // required in IE8 + document.querySelector('html').style.height = document.body.style.height = '100%'; + callback(window, document); + }; + }; + + var addCss = function(document, url, callback) { + var head = document.head || document.getElementsByTagName('head')[0]; + var cssTag = OT.$.createElement('link', { + type: 'text/css', + media: 'screen', + rel: 'stylesheet', + href: url + }); + head.appendChild(cssTag); + OT.$.on(cssTag, 'error', function(error) { + OT.error('Could not load CSS for dialog', url, error && error.message || error); + }); + OT.$.on(cssTag, 'load', callback); + }; + + var addDialogCSS = function(document, urls, callback) { + var allURLs = [ + '//fonts.googleapis.com/css?family=Didact+Gothic', + OT.properties.cssURL + ].concat(urls); + var remainingStylesheets = allURLs.length; + OT.$.forEach(allURLs, function(stylesheetUrl) { + addCss(document, stylesheetUrl, function() { + if(--remainingStylesheets <= 0) { + callback(); + } + }); + }); + + }; + + var templateElement = function(classes, children, tagName) { + var el = OT.$.createElement(tagName || 'div', { 'class': classes }, children, this); + el.on = OT.$.bind(OT.$.on, OT.$, el); + el.off = OT.$.bind(OT.$.off, OT.$, el); + return el; + }; + + var checkBoxElement = function (classes, nameAndId, onChange) { + var checkbox = templateElement.call(this, '', null, 'input'); + checkbox = OT.$(checkbox).on('change', onChange); + + if (OT.$.env.name === 'IE' && OT.$.env.version <= 8) { + // Fix for IE8 not triggering the change event + checkbox.on('click', function() { + checkbox.first.blur(); + checkbox.first.focus(); + }); + } + + checkbox.attr({ + name: nameAndId, + id: nameAndId, + type: 'checkbox' + }); + + return checkbox.first; + }; + + var linkElement = function(children, href, classes) { + var link = templateElement.call(this, classes || '', children, 'a'); + link.setAttribute('href', href); + return link; + }; + + OT.Dialogs = {}; + + OT.Dialogs.Plugin = {}; + + OT.Dialogs.Plugin.promptToInstall = function() { + var modal = new OT.$.Modal(adjustModal(function(window, document) { + + var el = OT.$.bind(templateElement, document), + btn = function(children, size) { + var classes = 'OT_dialog-button ' + + (size ? 'OT_dialog-button-' + size : 'OT_dialog-button-large'), + b = el(classes, children); + + b.enable = function() { + OT.$.removeClass(this, 'OT_dialog-button-disabled'); + return this; + }; + + b.disable = function() { + OT.$.addClass(this, 'OT_dialog-button-disabled'); + return this; + }; + + return b; + }, + downloadButton = btn('Download plugin'), + cancelButton = btn('cancel', 'small'), + refreshButton = btn('Refresh browser'), + acceptEULA, + checkbox, + close, + root; + + OT.$.addClass(cancelButton, 'OT_dialog-no-natural-margin OT_dialog-button-block'); + OT.$.addClass(refreshButton, 'OT_dialog-no-natural-margin'); + + function onDownload() { + modal.trigger('download'); + setTimeout(function() { + root.querySelector('.OT_dialog-messages-main').innerHTML = + 'Plugin installation successful'; + var sections = root.querySelectorAll('.OT_dialog-section'); + OT.$.addClass(sections[0], 'OT_dialog-hidden'); + OT.$.removeClass(sections[1], 'OT_dialog-hidden'); + }, 3000); + } + + function onRefresh() { + modal.trigger('refresh'); + } + + function onToggleEULA() { + if (checkbox.checked) { + enableButtons(); + } + else { + disableButtons(); + } + } + + function enableButtons() { + downloadButton.enable(); + downloadButton.on('click', onDownload); + + refreshButton.enable(); + refreshButton.on('click', onRefresh); + } + + function disableButtons() { + downloadButton.disable(); + downloadButton.off('click', onDownload); + + refreshButton.disable(); + refreshButton.off('click', onRefresh); + } + + downloadButton.disable(); + refreshButton.disable(); + + cancelButton.on('click', function() { + modal.trigger('cancelButtonClicked'); + modal.close(); + }); + + close = el('OT_closeButton', '×') + .on('click', function() { + modal.trigger('closeButtonClicked'); + modal.close(); + }).first; + + var protocol = (window.location.protocol.indexOf('https') >= 0 ? 'https' : 'http'); + acceptEULA = linkElement.call(document, + 'end-user license agreement', + protocol + '://tokbox.com/support/ie-eula'); + + checkbox = checkBoxElement.call(document, null, 'acceptEULA', onToggleEULA); + + root = el('OT_dialog-centering', [ + el('OT_dialog-centering-child', [ + el('OT_root OT_dialog OT_dialog-plugin-prompt', [ + close, + el('OT_dialog-messages', [ + el('OT_dialog-messages-main', 'This app requires real-time communication') + ]), + el('OT_dialog-section', [ + el('OT_dialog-single-button-with-title', [ + el('OT_dialog-button-title', [ + checkbox, + (function() { + var x = el('', 'accept', 'label'); + x.setAttribute('for', checkbox.id); + x.style.margin = '0 5px'; + return x; + })(), + acceptEULA + ]), + el('OT_dialog-actions-card', [ + downloadButton, + cancelButton + ]) + ]) + ]), + el('OT_dialog-section OT_dialog-hidden', [ + el('OT_dialog-button-title', [ + 'You can now enjoy webRTC enabled video via Internet Explorer.' + ]), + refreshButton + ]) + ]) + ]) + ]); + + addDialogCSS(document, [], function() { + document.body.appendChild(root); + }); + + })); + return modal; + }; + + OT.Dialogs.Plugin.promptToReinstall = function() { + var modal = new OT.$.Modal(adjustModal(function(window, document) { + + var el = OT.$.bind(templateElement, document), + close, + okayButton, + root; + + close = el('OT_closeButton', '×') + .on('click', function() { + modal.trigger('closeButtonClicked'); + modal.close(); + }).first; + + okayButton = + el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Okay') + .on('click', function() { + modal.trigger('okay'); + }).first; + + root = el('OT_dialog-centering', [ + el('OT_dialog-centering-child', [ + el('OT_ROOT OT_dialog OT_dialog-plugin-reinstall', [ + close, + el('OT_dialog-messages', [ + el('OT_dialog-messages-main', 'Reinstall Opentok Plugin'), + el('OT_dialog-messages-minor', 'Uh oh! Try reinstalling the OpenTok plugin again ' + + 'to enable real-time video communication for Internet Explorer.') + ]), + el('OT_dialog-section', [ + el('OT_dialog-single-button', okayButton) + ]) + ]) + ]) + ]); + + addDialogCSS(document, [], function() { + document.body.appendChild(root); + }); + + })); + + return modal; + }; + + OT.Dialogs.Plugin.updateInProgress = function() { + + var progressBar, + progressText, + progressValue = 0; + + var modal = new OT.$.Modal(adjustModal(function(window, document) { + + var el = OT.$.bind(templateElement, document), + root; + + progressText = el('OT_dialog-plugin-upgrade-percentage', '0%', 'strong'); + + progressBar = el('OT_dialog-progress-bar-fill'); + + root = el('OT_dialog-centering', [ + el('OT_dialog-centering-child', [ + el('OT_ROOT OT_dialog OT_dialog-plugin-upgrading', [ + el('OT_dialog-messages', [ + el('OT_dialog-messages-main', [ + 'One moment please... ', + progressText + ]), + el('OT_dialog-progress-bar', progressBar), + el('OT_dialog-messages-minor OT_dialog-no-natural-margin', + 'Please wait while the OpenTok plugin is updated') + ]) + ]) + ]) + ]); + + addDialogCSS(document, [], function() { + document.body.appendChild(root); + if(progressValue != null) { + modal.setUpdateProgress(progressValue); + } + }); + })); + + modal.setUpdateProgress = function(newProgress) { + if(progressBar && progressText) { + if(newProgress > 99) { + OT.$.css(progressBar, 'width', ''); + progressText.innerHTML = '100%'; + } else if(newProgress < 1) { + OT.$.css(progressBar, 'width', '0%'); + progressText.innerHTML = '0%'; + } else { + OT.$.css(progressBar, 'width', newProgress + '%'); + progressText.innerHTML = newProgress + '%'; + } + } else { + progressValue = newProgress; + } + }; + + return modal; + }; + + OT.Dialogs.Plugin.updateComplete = function(error) { + var modal = new OT.$.Modal(adjustModal(function(window, document) { + var el = OT.$.bind(templateElement, document), + reloadButton, + root; + + reloadButton = + el('OT_dialog-button OT_dialog-button-large OT_dialog-no-natural-margin', 'Reload') + .on('click', function() { + modal.trigger('reload'); + }).first; + + var msgs; + + if(error) { + msgs = ['Update Failed.', error + '' || 'NO ERROR']; + } else { + msgs = ['Update Complete.', + 'The OpenTok plugin has been succesfully updated. ' + + 'Please reload your browser.']; + } + + root = el('OT_dialog-centering', [ + el('OT_dialog-centering-child', [ + el('OT_root OT_dialog OT_dialog-plugin-upgraded', [ + el('OT_dialog-messages', [ + el('OT_dialog-messages-main', msgs[0]), + el('OT_dialog-messages-minor', msgs[1]) + ]), + el('OT_dialog-single-button', reloadButton) + ]) + ]) + ]); + + addDialogCSS(document, [], function() { + document.body.appendChild(root); + }); + + })); + + return modal; + + }; + + +})(); + +// tb_require('../helpers.js') +// tb_require('./web_rtc.js') + +// Web OT Helpers +!(function(window) { + + /* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ + /* global OT */ + + /// + // Device Helpers + // + // Support functions to enumerating and guerying device info + // + + var chromeToW3CDeviceKinds = { + audio: 'audioInput', + video: 'videoInput' + }; + + + OT.$.shouldAskForDevices = function(callback) { + var MST = window.MediaStreamTrack; + + if(MST != null && OT.$.isFunction(MST.getSources)) { + window.MediaStreamTrack.getSources(function(sources) { + var hasAudio = sources.some(function(src) { + return src.kind === 'audio'; + }); + + var hasVideo = sources.some(function(src) { + return src.kind === 'video'; + }); + + callback.call(null, { video: hasVideo, audio: hasAudio }); + }); + + } else { + // This environment can't enumerate devices anyway, so we'll memorise this result. + OT.$.shouldAskForDevices = function(callback) { + setTimeout(OT.$.bind(callback, null, { video: true, audio: true })); + }; + + OT.$.shouldAskForDevices(callback); + } + }; + + + OT.$.getMediaDevices = function(callback) { + if(OT.$.hasCapabilities('getMediaDevices')) { + window.MediaStreamTrack.getSources(function(sources) { + var filteredSources = OT.$.filter(sources, function(source) { + return chromeToW3CDeviceKinds[source.kind] != null; + }); + callback(void 0, OT.$.map(filteredSources, function(source) { + return { + deviceId: source.id, + label: source.label, + kind: chromeToW3CDeviceKinds[source.kind] + }; + })); + }); + } else { + callback(new Error('This browser does not support getMediaDevices APIs')); + } + }; + +})(window); +// tb_require('../helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* exported loadCSS */ + +var loadCSS = function loadCSS(cssURL) { + var style = document.createElement('link'); + style.type = 'text/css'; + style.media = 'screen'; + style.rel = 'stylesheet'; + style.href = cssURL; + var head = document.head || document.getElementsByTagName('head')[0]; + head.appendChild(style); +}; + +// tb_require('../helpers.js') +// tb_require('./properties.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +//-------------------------------------- +// 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(); + } + }, + + _onLoadError = function(/* event */) { + _cleanup(); + + OT.warn('TB DynamicConfig failed to load due to an error'); + this.trigger('dynamicConfigLoadFailed'); + }, + + _getModule = function(moduleName, apiKey) { + if (apiKey && _partners[apiKey] && _partners[apiKey][moduleName]) { + return _partners[apiKey][moduleName]; + } + + return _global[moduleName]; + }, + + _this; + + _this = { + // In ms + loadTimeout: 4000, + + _onLoadTimeout: function() { + _cleanup(); + + OT.warn('TB DynamicConfig failed to load in ' + _this.loadTimeout + ' ms'); + this.trigger('dynamicConfigLoadFailed'); + }, + + load: function(configUrl) { + if (!configUrl) throw new Error('You must pass a valid configUrl to Config.load'); + + _loaded = false; + + setTimeout(function() { + _script = document.createElement( 'script' ); + _script.async = 'async'; + _script.src = configUrl; + _script.onload = _script.onreadystatechange = OT.$.bind(_onLoad, _this); + _script.onerror = OT.$.bind(_onLoadError, _this); + _head.appendChild(_script); + },1); + + _loadTimer = setTimeout(function() { + _this._onLoadTimeout(); + }, this.loadTimeout); + }, + + + isLoaded: function() { + return _loaded; + }, + + reset: function() { + this.off(); + _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; +})(); + +// tb_require('../helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OTPlugin, OT */ + +/// +// Capabilities +// +// Support functions to query browser/client Media capabilities. +// + + +// Indicates whether this client supports the getUserMedia +// API. +// +OT.$.registerCapability('getUserMedia', function() { + if (OT.$.env === 'Node') return false; + return !!(navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || + OTPlugin.isInstalled()); +}); + + + +// TODO Remove all PeerConnection stuff, that belongs to the messaging layer not the Media layer. +// Indicates whether this client supports the PeerConnection +// API. +// +// Chrome Issues: +// * The explicit prototype.addStream check is because webkitRTCPeerConnection was +// partially implemented, but not functional, in Chrome 22. +// +// Firefox Issues: +// * No real support before Firefox 19 +// * Firefox 19 has issues with generating Offers. +// * Firefox 20 doesn't interoperate with Chrome. +// +OT.$.registerCapability('PeerConnection', function() { + if (OT.$.env === 'Node') { + return false; + } + else if (typeof(window.webkitRTCPeerConnection) === 'function' && + !!window.webkitRTCPeerConnection.prototype.addStream) { + return true; + } else if (typeof(window.mozRTCPeerConnection) === 'function' && OT.$.env.version > 20.0) { + return true; + } else { + return OTPlugin.isInstalled(); + } +}); + + + +// Indicates whether this client supports WebRTC +// +// This is defined as: getUserMedia + PeerConnection + exceeds min browser version +// +OT.$.registerCapability('webrtc', function() { + if (OT.properties) { + var minimumVersions = OT.properties.minimumVersion || {}, + minimumVersion = minimumVersions[OT.$.env.name.toLowerCase()]; + + if(minimumVersion && OT.$.env.versionGreaterThan(minimumVersion)) { + OT.debug('Support for', OT.$.env.name, 'is disabled because we require', + minimumVersion, 'but this is', OT.$.env.version); + return false; + } + } + + if (OT.$.env === 'Node') { + // Node works, even though it doesn't have getUserMedia + return true; + } + + return OT.$.hasCapabilities('getUserMedia', 'PeerConnection'); +}); + + +// TODO Remove all transport stuff, that belongs to the messaging layer not the Media layer. +// Indicates if the browser supports bundle +// +// Broadly: +// * Firefox doesn't support bundle +// * Chrome support bundle +// * OT Plugin supports bundle +// * We assume NodeJs supports bundle (e.g. 'you're on your own' mode) +// +OT.$.registerCapability('bundle', function() { + return OT.$.hasCapabilities('webrtc') && + (OT.$.env.name === 'Chrome' || + OT.$.env.name === 'Node' || + OTPlugin.isInstalled()); +}); + +// Indicates if the browser supports RTCP Mux +// +// Broadly: +// * Older versions of Firefox (<= 25) don't support RTCP Mux +// * Older versions of Firefox (>= 26) support RTCP Mux (not tested yet) +// * Chrome support RTCP Mux +// * OT Plugin supports RTCP Mux +// * We assume NodeJs supports RTCP Mux (e.g. 'you're on your own' mode) +// +OT.$.registerCapability('RTCPMux', function() { + return OT.$.hasCapabilities('webrtc') && + (OT.$.env.name === 'Chrome' || + OT.$.env.name === 'Node' || + OTPlugin.isInstalled()); +}); + + + +// Indicates whether this browser supports the getMediaDevices (getSources) API. +// +OT.$.registerCapability('getMediaDevices', function() { + return OT.$.isFunction(window.MediaStreamTrack) && + OT.$.isFunction(window.MediaStreamTrack.getSources); +}); + + +OT.$.registerCapability('audioOutputLevelStat', function() { + return OT.$.env.name === 'Chrome' || OT.$.env.name === 'IE'; +}); + +OT.$.registerCapability('webAudioCapableRemoteStream', function() { + return OT.$.env.name === 'Firefox'; +}); + +OT.$.registerCapability('webAudio', function() { + return 'AudioContext' in window; +}); + + +// tb_require('../helpers.js') +// tb_require('./config.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + + +OT.Analytics = function(loggingUrl) { + + var LOG_VERSION = '1'; + var _analytics = new OT.$.Analytics(loggingUrl, OT.debug, OT._.getClientGuid); + + this.logError = function(code, type, message, details, options) { + if (!options) options = {}; + var partnerId = options.partnerId; + + if (OT.Config.get('exceptionLogging', 'enabled', partnerId) !== true) { + return; + } + + OT._.getClientGuid(function(error, guid) { + if (error) { + // @todo + return; + } + var data = OT.$.extend({ + // TODO: OT.properties.version only gives '2.2', not '2.2.9.3'. + 'clientVersion' : 'js-' + OT.properties.version.replace('v', ''), + 'guid' : guid, + 'partnerId' : partnerId, + 'source' : window.location.href, + 'logVersion' : LOG_VERSION, + 'clientSystemTime' : new Date().getTime() + }, options); + _analytics.logError(code, type, message, details, data); + }); + + }; + + this.logEvent = function(options, throttle) { + var partnerId = options.partnerId; + + if (!options) options = {}; + + OT._.getClientGuid(function(error, guid) { + if (error) { + // @todo + return; + } + + // Set a bunch of defaults + var data = OT.$.extend({ + // TODO: OT.properties.version only gives '2.2', not '2.2.9.3'. + 'clientVersion' : 'js-' + OT.properties.version.replace('v', ''), + 'guid' : guid, + 'partnerId' : partnerId, + 'source' : window.location.href, + 'logVersion' : LOG_VERSION, + 'clientSystemTime' : new Date().getTime() + }, options); + _analytics.logEvent(data, false, throttle); + }); + }; + + this.logQOS = function(options) { + var partnerId = options.partnerId; + + if (!options) options = {}; + + OT._.getClientGuid(function(error, guid) { + if (error) { + // @todo + return; + } + + // Set a bunch of defaults + var data = OT.$.extend({ + // TODO: OT.properties.version only gives '2.2', not '2.2.9.3'. + 'clientVersion' : 'js-' + OT.properties.version.replace('v', ''), + 'guid' : guid, + 'partnerId' : partnerId, + 'source' : window.location.href, + 'logVersion' : LOG_VERSION, + 'clientSystemTime' : new Date().getTime(), + 'duration' : 0 //in milliseconds + }, options); + + _analytics.logQOS(data); + }); + }; +}; + +// tb_require('../helpers.js') +// tb_require('./config.js') +// tb_require('./analytics.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +OT.ConnectivityAttemptPinger = function (options) { + var _state = 'Initial', + _previousState, + states = ['Initial', 'Attempt', 'Success', 'Failure'], + pingTimer, // Timer for the Attempting pings + PING_INTERVAL = 5000, + PING_COUNT_TOTAL = 6, + pingCount; + + //// Private API + var stateChanged = function(newState) { + _state = newState; + var invalidSequence = false; + switch (_state) { + case 'Attempt': + if (_previousState !== 'Initial') { + invalidSequence = true; + } + startAttemptPings(); + break; + case 'Success': + if (_previousState !== 'Attempt') { + invalidSequence = true; + } + stopAttemptPings(); + break; + case 'Failure': + if (_previousState !== 'Attempt') { + invalidSequence = true; + } + stopAttemptPings(); + break; + } + if (invalidSequence) { + var data = options ? OT.$.clone(options) : {}; + data.action = 'Internal Error'; + data.variation = 'Non-fatal'; + data.payload = { + debug: 'Invalid sequence: ' + options.action + ' ' + + _previousState + ' -> ' + _state + }; + OT.analytics.logEvent(data); + } + }, + + setState = OT.$.statable(this, states, 'Failure', stateChanged), + + startAttemptPings = function() { + pingCount = 0; + pingTimer = setInterval(function() { + if (pingCount < PING_COUNT_TOTAL) { + var data = OT.$.extend(options, {variation: 'Attempting'}); + OT.analytics.logEvent(data); + } else { + stopAttemptPings(); + } + pingCount++; + }, PING_INTERVAL); + }, + + stopAttemptPings = function() { + clearInterval(pingTimer); + }; + + this.setVariation = function(variation) { + _previousState = _state; + setState(variation); + + // We could change the ConnectivityAttemptPinger to a ConnectivityAttemptLogger + // that also logs events in addition to logging the ping ('Attempting') events. + // + // var payload = OT.$.extend(options, {variation:variation}); + // OT.analytics.logEvent(payload); + }; + + this.stop = function() { + stopAttemptPings(); + }; +}; + +// tb_require('../helpers/helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +/* Stylable Notes + * Some bits are controlled by multiple flags, i.e. buttonDisplayMode and nameDisplayMode. + * When there are multiple flags how is the final setting chosen? + * When some style bits are set updates will need to be pushed through to the Chrome + */ + +// Mixes the StylableComponent behaviour into the +self+ object. It will +// also set the default styles to +initialStyles+. +// +// @note This Mixin is dependent on OT.Eventing. +// +// +// @example +// +// function SomeObject { +// OT.StylableComponent(this, { +// name: 'SomeObject', +// foo: 'bar' +// }); +// } +// +// var obj = new SomeObject(); +// obj.getStyle('foo'); // => 'bar' +// obj.setStyle('foo', 'baz') +// obj.getStyle('foo'); // => 'baz' +// obj.getStyle(); // => {name: 'SomeObject', foo: 'baz'} +// +OT.StylableComponent = function(self, initalStyles, showControls, logSetStyleWithPayload) { + if (!self.trigger) { + throw new Error('OT.StylableComponent is dependent on the mixin OT.$.eventing. ' + + 'Ensure that this is included in the object before StylableComponent.'); + } + + var _readOnly = false; + + // Broadcast style changes as the styleValueChanged event + var onStyleChange = function(key, value, oldValue) { + if (oldValue) { + self.trigger('styleValueChanged', key, value, oldValue); + } else { + self.trigger('styleValueChanged', key, value); + } + }; + + if(showControls === false) { + initalStyles = { + buttonDisplayMode: 'off', + nameDisplayMode: 'off', + audioLevelDisplayMode: 'off' + }; + + _readOnly = true; + logSetStyleWithPayload({ + showControls: false + }); + } + + var _style = new Style(initalStyles, onStyleChange); + +/** + * Returns an object that has the properties that define the current user interface controls of + * the Publisher. You can modify the properties of this object and pass the object to the + *
setStyle()
method of thePublisher object. (See the documentation for
+ * setStyle() to see the styles that define this object.)
+ * @return {Object} The object that defines the styles of the Publisher.
+ * @see setStyle()
+ * @method #getStyle
+ * @memberOf Publisher
+ */
+
+/**
+ * Returns an object that has the properties that define the current user interface controls of
+ * the Subscriber. You can modify the properties of this object and pass the object to the
+ * setStyle()
method of the Subscriber object. (See the documentation for
+ * setStyle() to see the styles that define this object.)
+ * @return {Object} The object that defines the styles of the Subscriber.
+ * @see setStyle()
+ * @method #getStyle
+ * @memberOf Subscriber
+ */
+ // If +key+ is falsly then all styles will be returned.
+ self.getStyle = function(key) {
+ return _style.get(key);
+ };
+
+/**
+ * Sets properties that define the appearance of some user interface controls of the Publisher.
+ *
+ * You can either pass one parameter or two parameters to this method.
+ * + *If you pass one parameter, style
, it is an object that has the following
+ * properties:
+ *
+ *
audioLevelDisplayMode
(String) — How to display the audio level
+ * indicator. Possible values are: "auto"
(the indicator is displayed when the
+ * video is disabled), "off"
(the indicator is not displayed), and
+ * "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
+ * the background image when a video is not displayed. (A video may not be displayed if
+ * you call publishVideo(false)
on the Publisher object). You can pass an http
+ * or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
+ * data
URI scheme (instead of http or https) and pass in base-64-encrypted
+ * PNG data, such as that obtained from the
+ * Publisher.getImgData() method. For example,
+ * you could set the property to "data:VBORw0KGgoAA..."
, where the portion of
+ * the string after "data:"
is the result of a call to
+ * Publisher.getImgData()
. If the URL or the image data is invalid, the
+ * property is ignored (the attempt to set the image fails silently).
+ *
+ * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+ * you cannot set the backgroundImageURI
style to a string larger than
+ * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
+ * limitation, you cannot set the backgroundImageURI
style to a string obtained
+ * with the getImgData()
method.
+ *
buttonDisplayMode
(String) — How to display the microphone
+ * controls. Possible values are: "auto"
(controls are displayed when the
+ * stream is first displayed and when the user mouses over the display), "off"
+ * (controls are not displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
+ * Possible values are: "auto"
(the name is displayed when the stream is first
+ * displayed and when the user mouses over the display), "off"
(the name is not
+ * displayed), and "on"
(the name is always displayed).For example, the following code passes one parameter to the method:
+ * + *myPublisher.setStyle({nameDisplayMode: "off"});+ * + *
If you pass two parameters, style
and value
, they are
+ * key-value pair that define one property of the display style. For example, the following
+ * code passes two parameter values to the method:
myPublisher.setStyle("nameDisplayMode", "off");+ * + *
You can set the initial settings when you call the Session.publish()
+ * or OT.initPublisher()
method. Pass a style
property as part of the
+ * properties
parameter of the method.
The OT object dispatches an exception
event if you pass in an invalid style
+ * to the method. The code
property of the ExceptionEvent object is set to 1011.
style
passed in. Pass a value
+ * for this parameter only if the value of the style
parameter is a String.
+ *
+ * @see getStyle()
+ * @return {Publisher} The Publisher object
+ * @see setStyle()
+ *
+ * @see Session.publish()
+ * @see OT.initPublisher()
+ * @method #setStyle
+ * @memberOf Publisher
+ */
+
+/**
+ * Sets properties that define the appearance of some user interface controls of the Subscriber.
+ *
+ * You can either pass one parameter or two parameters to this method.
+ * + *If you pass one parameter, style
, it is an object that has the following
+ * properties:
+ *
+ *
audioLevelDisplayMode
(String) — How to display the audio level
+ * indicator. Possible values are: "auto"
(the indicator is displayed when the
+ * video is disabled), "off"
(the indicator is not displayed), and
+ * "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
+ * the background image when a video is not displayed. (A video may not be displayed if
+ * you call subscribeToVideo(false)
on the Publisher object). You can pass an
+ * http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
+ * data
URI scheme (instead of http or https) and pass in base-64-encrypted
+ * PNG data, such as that obtained from the
+ * Subscriber.getImgData() method. For example,
+ * you could set the property to "data:VBORw0KGgoAA..."
, where the portion of
+ * the string after "data:"
is the result of a call to
+ * Publisher.getImgData()
. If the URL or the image data is invalid, the
+ * property is ignored (the attempt to set the image fails silently).
+ *
+ * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+ * you cannot set the backgroundImageURI
style to a string larger than
+ * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
+ * limitation, you cannot set the backgroundImageURI
style to a string obtained
+ * with the getImgData()
method.
+ *
buttonDisplayMode
(String) — How to display the speaker
+ * controls. Possible values are: "auto"
(controls are displayed when the
+ * stream is first displayed and when the user mouses over the display), "off"
+ * (controls are not displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
+ * Possible values are: "auto"
(the name is displayed when the stream is first
+ * displayed and when the user mouses over the display), "off"
(the name is not
+ * displayed), and "on"
(the name is always displayed).videoDisabledDisplayMode
(String) Whether to display the video
+ * disabled indicator and video disabled warning icons for a Subscriber. These icons
+ * indicate that the video has been disabled (or is in risk of being disabled for
+ * the warning icon) due to poor stream quality. Possible values are: "auto"
+ * (the icons are automatically when the displayed video is disabled or in risk of being
+ * disabled due to poor stream quality), "off"
(do not display the icons), and
+ * "on"
(display the icons).For example, the following code passes one parameter to the method:
+ * + *mySubscriber.setStyle({nameDisplayMode: "off"});+ * + *
If you pass two parameters, style
and value
, they are key-value
+ * pair that define one property of the display style. For example, the following code passes
+ * two parameter values to the method:
mySubscriber.setStyle("nameDisplayMode", "off");+ * + *
You can set the initial settings when you call the Session.subscribe()
method.
+ * Pass a style
property as part of the properties
parameter of the
+ * method.
The OT object dispatches an exception
event if you pass in an invalid style
+ * to the method. The code
property of the ExceptionEvent object is set to 1011.
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
+ */
+
+ if(_readOnly) {
+ self.setStyle = function() {
+ OT.warn('Calling setStyle() has no effect because the' +
+ 'showControls option was set to false');
+ return this;
+ };
+ } else {
+ self.setStyle = function(keyOrStyleHash, value, silent) {
+ var logPayload = {};
+ if (typeof(keyOrStyleHash) !== 'string') {
+ _style.setAll(keyOrStyleHash, silent);
+ logPayload = keyOrStyleHash;
+ } else {
+ _style.set(keyOrStyleHash, value);
+ logPayload[keyOrStyleHash] = value;
+ }
+ if (logSetStyleWithPayload) logSetStyleWithPayload(logPayload);
+ return this;
+ };
+ }
+};
+
+
+/*jshint latedef:false */
+var Style = function(initalStyles, onStyleChange) {
+/*jshint latedef:true */
+ var _style = {},
+ _COMPONENT_STYLES,
+ _validStyleValues,
+ isValidStyle,
+ castValue;
+
+
+ _COMPONENT_STYLES = [
+ 'showMicButton',
+ 'showSpeakerButton',
+ 'nameDisplayMode',
+ 'buttonDisplayMode',
+ 'backgroundImageURI',
+ 'audioLevelDisplayMode'
+ ];
+
+ _validStyleValues = {
+ buttonDisplayMode: ['auto', 'mini', 'mini-auto', 'off', 'on'],
+ nameDisplayMode: ['auto', 'off', 'on'],
+ audioLevelDisplayMode: ['auto', 'off', 'on'],
+ showSettingsButton: [true, false],
+ showMicButton: [true, false],
+ backgroundImageURI: null,
+ showControlBar: [true, false],
+ showArchiveStatus: [true, false],
+ videoDisabledDisplayMode: ['auto', 'off', 'on']
+ };
+
+ // Validates the style +key+ and also whether +value+ is valid for +key+
+ isValidStyle = function(key, value) {
+ return key === 'backgroundImageURI' ||
+ (_validStyleValues.hasOwnProperty(key) &&
+ OT.$.arrayIndexOf(_validStyleValues[key], value) !== -1 );
+ };
+
+ castValue = function(value) {
+ switch(value) {
+ case 'true':
+ return true;
+ case 'false':
+ return false;
+ default:
+ return value;
+ }
+ };
+
+ // Returns a shallow copy of the styles.
+ this.getAll = function() {
+ var style = OT.$.clone(_style);
+
+ for (var key in style) {
+ if(!style.hasOwnProperty(key)) {
+ continue;
+ }
+ if (OT.$.arrayIndexOf(_COMPONENT_STYLES, key) < 0) {
+
+ // Strip unnecessary properties out, should this happen on Set?
+ delete style[key];
+ }
+ }
+
+ return style;
+ };
+
+ this.get = function(key) {
+ if (key) {
+ return _style[key];
+ }
+
+ // We haven't been asked for any specific key, just return the lot
+ return this.getAll();
+ };
+
+ // *note:* this will not trigger onStyleChange if +silent+ is truthy
+ this.setAll = function(newStyles, silent) {
+ var oldValue, newValue;
+
+ for (var key in newStyles) {
+ if(!newStyles.hasOwnProperty(key)) {
+ continue;
+ }
+ newValue = castValue(newStyles[key]);
+
+ if (isValidStyle(key, newValue)) {
+ oldValue = _style[key];
+
+ if (newValue !== oldValue) {
+ _style[key] = newValue;
+ if (!silent) onStyleChange(key, newValue, oldValue);
+ }
+
+ } else {
+ OT.warn('Style.setAll::Invalid style property passed ' + key + ' : ' + newValue);
+ }
+ }
+
+ return this;
+ };
+
+ this.set = function(key, value) {
+ OT.debug('setStyle: ' + key.toString());
+
+ var newValue = castValue(value),
+ oldValue;
+
+ if (!isValidStyle(key, newValue)) {
+ OT.warn('Style.set::Invalid style property passed ' + key + ' : ' + newValue);
+ return this;
+ }
+
+ oldValue = _style[key];
+ if (newValue !== oldValue) {
+ _style[key] = newValue;
+
+ onStyleChange(key, value, oldValue);
+ }
+
+ return this;
+ };
+
+ if (initalStyles) this.setAll(initalStyles, true);
+};
+
+// tb_require('../helpers/helpers.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+
+// A Factory method for generating simple state machine classes.
+//
+// @usage
+// var StateMachine = OT.generateSimpleStateMachine('start', ['start', 'middle', 'end', {
+// start: ['middle'],
+// middle: ['end'],
+// end: ['start']
+// }]);
+//
+// var states = new StateMachine();
+// state.current; // <-- start
+// state.set('middle');
+//
+OT.generateSimpleStateMachine = function(initialState, states, transitions) {
+ var validStates = states.slice(),
+ validTransitions = OT.$.clone(transitions);
+
+ var isValidState = function (state) {
+ return OT.$.arrayIndexOf(validStates, state) !== -1;
+ };
+
+ var isValidTransition = function(fromState, toState) {
+ return validTransitions[fromState] &&
+ OT.$.arrayIndexOf(validTransitions[fromState], toState) !== -1;
+ };
+
+ return function(stateChangeFailed) {
+ var currentState = initialState,
+ previousState = null;
+
+ this.current = currentState;
+
+ function signalChangeFailed(message, newState) {
+ stateChangeFailed({
+ message: message,
+ newState: newState,
+ currentState: currentState,
+ previousState: previousState
+ });
+ }
+
+ // Validates +newState+. If it's invalid it triggers stateChangeFailed and returns false.
+ function handleInvalidStateChanges(newState) {
+ if (!isValidState(newState)) {
+ signalChangeFailed('\'' + newState + '\' is not a valid state', newState);
+
+ return false;
+ }
+
+ if (!isValidTransition(currentState, newState)) {
+ signalChangeFailed('\'' + currentState + '\' cannot transition to \'' +
+ newState + '\'', newState);
+
+ return false;
+ }
+
+ return true;
+ }
+
+
+ this.set = function(newState) {
+ if (!handleInvalidStateChanges(newState)) return;
+ previousState = currentState;
+ this.current = currentState = newState;
+ };
+
+ };
+};
+
+// tb_require('../helpers/helpers.js')
+// tb_require('./state_machine.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+!(function() {
+
+// Models a Subscriber's subscribing State
+//
+// Valid States:
+// NotSubscribing (the initial state
+// Init (basic setup of DOM
+// ConnectingToPeer (Failure Cases -> No Route, Bad Offer, Bad Answer
+// BindingRemoteStream (Failure Cases -> Anything to do with the media being
+// (invalid, the media never plays
+// Subscribing (this is 'onLoad'
+// Failed (terminal state, with a reason that maps to one of the
+// (failure cases above
+// Destroyed (The subscriber has been cleaned up, terminal state
+//
+//
+// Valid Transitions:
+// NotSubscribing ->
+// Init
+//
+// Init ->
+// ConnectingToPeer
+// | BindingRemoteStream (if we are subscribing to ourselves and we alreay
+// (have a stream
+// | NotSubscribing (destroy()
+//
+// ConnectingToPeer ->
+// BindingRemoteStream
+// | NotSubscribing
+// | Failed
+// | NotSubscribing (destroy()
+//
+// BindingRemoteStream ->
+// Subscribing
+// | Failed
+// | NotSubscribing (destroy()
+//
+// Subscribing ->
+// NotSubscribing (unsubscribe
+// | Failed (probably a peer connection failure after we began
+// (subscribing
+//
+// Failed ->
+// Destroyed
+//
+// Destroyed -> (terminal state)
+//
+//
+// @example
+// var state = new SubscribingState(function(change) {
+// console.log(change.message);
+// });
+//
+// state.set('Init');
+// state.current; -> 'Init'
+//
+// state.set('Subscribing'); -> triggers stateChangeFailed and logs out the error message
+//
+//
+ var validStates,
+ validTransitions,
+ initialState = 'NotSubscribing';
+
+ validStates = [
+ 'NotSubscribing', 'Init', 'ConnectingToPeer',
+ 'BindingRemoteStream', 'Subscribing', 'Failed',
+ 'Destroyed'
+ ];
+
+ validTransitions = {
+ NotSubscribing: ['NotSubscribing', 'Init', 'Destroyed'],
+ Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream', 'Destroyed'],
+ ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed', 'Destroyed'],
+ BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed', 'Destroyed'],
+ Subscribing: ['NotSubscribing', 'Failed', 'Destroyed'],
+ Failed: ['Destroyed'],
+ Destroyed: []
+ };
+
+ OT.SubscribingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
+
+ OT.SubscribingState.prototype.isDestroyed = function() {
+ return this.current === 'Destroyed';
+ };
+
+ OT.SubscribingState.prototype.isFailed = function() {
+ return this.current === 'Failed';
+ };
+
+ OT.SubscribingState.prototype.isSubscribing = function() {
+ return this.current === 'Subscribing';
+ };
+
+ OT.SubscribingState.prototype.isAttemptingToSubscribe = function() {
+ return OT.$.arrayIndexOf(
+ [ 'Init', 'ConnectingToPeer', 'BindingRemoteStream' ],
+ this.current
+ ) !== -1;
+ };
+
+})(window);
+
+// tb_require('../helpers/helpers.js')
+// tb_require('./state_machine.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+!(function() {
+
+// Models a Publisher's publishing State
+//
+// Valid States:
+// NotPublishing
+// GetUserMedia
+// BindingMedia
+// MediaBound
+// PublishingToSession
+// Publishing
+// Failed
+// Destroyed
+//
+//
+// Valid Transitions:
+// NotPublishing ->
+// GetUserMedia
+//
+// GetUserMedia ->
+// BindingMedia
+// | Failed (Failure Reasons -> stream error, constraints,
+// (permission denied
+// | NotPublishing (destroy()
+//
+//
+// BindingMedia ->
+// MediaBound
+// | Failed (Failure Reasons -> Anything to do with the media
+// (being invalid, the media never plays
+// | NotPublishing (destroy()
+//
+// MediaBound ->
+// PublishingToSession (MediaBound could transition to PublishingToSession
+// (if a stand-alone publish is bound to a session
+// | Failed (Failure Reasons -> media issues with a stand-alone publisher
+// | NotPublishing (destroy()
+//
+// PublishingToSession
+// Publishing
+// | Failed (Failure Reasons -> timeout while waiting for ack of
+// (stream registered. We do not do this right now
+// | NotPublishing (destroy()
+//
+//
+// Publishing ->
+// NotPublishing (Unpublish
+// | Failed (Failure Reasons -> loss of network, media error, anything
+// (that causes *all* Peer Connections to fail (less than all
+// (failing is just an error, all is failure)
+// | NotPublishing (destroy()
+//
+// Failed ->
+// Destroyed
+//
+// Destroyed -> (Terminal state
+//
+//
+
+ var validStates = [
+ 'NotPublishing', 'GetUserMedia', 'BindingMedia', 'MediaBound',
+ 'PublishingToSession', 'Publishing', 'Failed',
+ 'Destroyed'
+ ],
+
+ validTransitions = {
+ NotPublishing: ['NotPublishing', 'GetUserMedia', 'Destroyed'],
+ GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing', 'Destroyed'],
+ BindingMedia: ['MediaBound', 'Failed', 'NotPublishing', 'Destroyed'],
+ MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed', 'Destroyed'],
+ PublishingToSession: ['NotPublishing', 'Publishing', 'Failed', 'Destroyed'],
+ Publishing: ['NotPublishing', 'MediaBound', 'Failed', 'Destroyed'],
+ Failed: ['Destroyed'],
+ Destroyed: []
+ },
+
+ initialState = 'NotPublishing';
+
+ OT.PublishingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
+
+ OT.PublishingState.prototype.isDestroyed = function() {
+ return this.current === 'Destroyed';
+ };
+
+ OT.PublishingState.prototype.isAttemptingToPublish = function() {
+ return OT.$.arrayIndexOf(
+ [ 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession' ],
+ this.current) !== -1;
+ };
+
+ OT.PublishingState.prototype.isPublishing = function() {
+ return this.current === 'Publishing';
+ };
+
+})(window);
+
+// tb_require('../helpers/helpers.js')
+// tb_require('../helpers/lib/web_rtc.js')
+
+!(function() {
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+/*
+ * A Publishers Microphone.
+ *
+ * TODO
+ * * bind to changes in mute/unmute/volume/etc and respond to them
+ */
+ OT.Microphone = function(webRTCStream, muted) {
+ var _muted;
+
+ OT.$.defineProperties(this, {
+ muted: {
+ get: function() {
+ return _muted;
+ },
+ set: function(muted) {
+ if (_muted === muted) return;
+
+ _muted = muted;
+
+ var audioTracks = webRTCStream.getAudioTracks();
+
+ for (var i=0, num=audioTracks.length; iFor 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',
+ MEDIA_STOPPED: 'mediaStopped',
+ PUBLISHER_DESTROYED: 'destroyed',
+
+ // Subscriber Events
+ SUBSCRIBER_DESTROYED: 'destroyed',
+
+ // DeviceManager Events
+ DEVICES_DETECTED: 'devicesDetected',
+
+ // DevicePanel Events
+ DEVICES_SELECTED: 'devicesSelected',
+ CLOSE_BUTTON_CLICK: 'closeButtonClick',
+
+ MICLEVEL : 'microphoneActivityLevel',
+ MICGAINCHANGED : 'microphoneGainChanged',
+
+ // Environment Loader
+ ENV_LOADED: 'envLoaded',
+ ENV_UNLOADED: 'envUnloaded',
+
+ // Audio activity Events
+ AUDIO_LEVEL_UPDATED: 'audioLevelUpdated'
+ };
+
+ OT.ExceptionCodes = {
+ JS_EXCEPTION: 2000,
+ AUTHENTICATION_ERROR: 1004,
+ INVALID_SESSION_ID: 1005,
+ CONNECT_FAILED: 1006,
+ CONNECT_REJECTED: 1007,
+ CONNECTION_TIMEOUT: 1008,
+ NOT_CONNECTED: 1010,
+ P2P_CONNECTION_FAILED: 1013,
+ API_RESPONSE_FAILURE: 1014,
+ TERMS_OF_SERVICE_FAILURE: 1026,
+ 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 + * | + *
+ * 1026 + * | + *+ * Terms of Service Violation: Export Compliance + * | + *
+ * 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);
+ };
+
+
+/**
+ * Defines connectionCreated
and connectionDestroyed
events dispatched by
+ * the {@link Session} object.
+ *
+ * The Session object dispatches a connectionCreated
event when a client (including
+ * your own) connects to a Session. It also dispatches a connectionCreated
event for
+ * every client in the session when you first connect. (when your local client connects, the Session
+ * object also dispatches a sessionConnected
event, defined by the
+ * {@link SessionConnectEvent} class.)
+ *
+ * While you are connected to the session, the Session object dispatches a
+ * connectionDestroyed
event when another client disconnects from the Session.
+ * (When you disconnect, the Session object also dispatches a sessionDisconnected
+ * event, defined by the {@link SessionDisconnectEvent} class.)
+ *
+ *
The following code keeps a running total of the number of connections to a session
+ * by monitoring the connections
property of the sessionConnect
,
+ * connectionCreated
and connectionDestroyed
events:
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects + * var sessionID = ""; // Replace with your own session ID. + * // See https://dashboard.tokbox.com/projects + * var token = ""; // Replace with a generated token that has been assigned the moderator role. + * // See https://dashboard.tokbox.com/projects + * var connectionCount = 0; + * + * var session = OT.initSession(apiKey, sessionID); + * session.on("connectionCreated", function(event) { + * connectionCount++; + * displayConnectionCount(); + * }); + * session.on("connectionDestroyed", function(event) { + * connectionCount--; + * displayConnectionCount(); + * }); + * session.connect(token); + * + * function displayConnectionCount() { + * document.getElementById("connectionCountField").value = connectionCount.toString(); + * }+ * + *
This example assumes that there is an input text field in the HTML DOM
+ * with the id
set to "connectionCountField"
:
<input type="text" id="connectionCountField" value="0"></input>+ * + * + * @property {Connection} connection A Connection objects for the connections that was + * created or deleted. + * + * @property {Array} connections Deprecated. Use the
connection
property. A
+ * connectionCreated
or connectionDestroyed
event is dispatched
+ * for each connection created and destroyed in the session.
+ *
+ * @property {String} reason For a connectionDestroyed
event,
+ * a description of why the connection ended. This property can have two values:
+ *
+ * "clientDisconnected"
A client disconnected from the session by calling
+ * the disconnect()
method of the Session object or by closing the browser.
+ * (See Session.disconnect().)"forceDisconnected"
A moderator has disconnected the publisher
+ * from the session, by calling the forceDisconnect()
method of the Session
+ * object. (See Session.forceDisconnect().)"networkDisconnected"
The network connection terminated abruptly
+ * (for example, the client lost their internet connection).Depending on the context, this description may allow the developer to refine + * the course of action they take in response to an event.
+ * + *For a connectionCreated
event, this string is undefined.
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 {Boolean} cancelable Whether the event has a default behavior that is cancelable + * (
true
) or not (false
). You can cancel the default behavior by calling
+ * the preventDefault()
method of the StreamEvent object in the event listener
+ * function. The streamDestroyed
+ * event is cancelable. (See preventDefault().)
+ *
+ * @property {String} reason For a streamDestroyed
event,
+ * a description of why the session disconnected. This property can have one of the following
+ * values:
+ *
+ * "clientDisconnected"
A client disconnected from the session by calling
+ * the disconnect()
method of the Session object or by closing the browser.
+ * (See Session.disconnect().)"forceDisconnected"
A moderator has disconnected the publisher of the
+ * stream from the session, by calling the forceDisconnect()
method of the Session
+* object. (See Session.forceDisconnect().)"forceUnpublished"
A moderator has forced the publisher of the stream
+ * to stop publishing the stream, by calling the forceUnpublish()
method of the
+ * Session object. (See Session.forceUnpublish().)"mediaStopped"
The user publishing the stream has stopped sharing the
+ * screen. This value is only used in screen-sharing video streams."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.
streamCreated
event) or deleted (in the case of a
+ * streamDestroyed
event).
+ *
+ * @property {Array} streams Deprecated. Use the stream
property. A
+ * streamCreated
or streamDestroyed
event is dispatched for
+ * each stream added or destroyed.
+ *
+ * @augments Event
+ */
+
+ var streamEventPluralDeprecationWarningShown = false;
+ OT.StreamEvent = function (type, stream, reason, cancelable) {
+ OT.Event.call(this, type, cancelable);
+
+ if (OT.$.canDefineProperty) {
+ Object.defineProperty(this, 'streams', {
+ get: function() {
+ if(!streamEventPluralDeprecationWarningShown) {
+ OT.warn('OT.StreamEvent streams property is deprecated, use stream instead.');
+ streamEventPluralDeprecationWarningShown = true;
+ }
+ return [stream];
+ }
+ });
+ } else {
+ this.streams = [stream];
+ }
+
+ this.stream = stream;
+ this.reason = reason;
+ };
+
+/**
+* Prevents the default behavior associated with the event from taking place.
+*
+* For the streamDestroyed
event dispatched by the Session object,
+* the default behavior is that all Subscriber objects that are subscribed to the stream are
+* unsubscribed and removed from the HTML DOM. Each Subscriber object dispatches a
+* destroyed
event when the element is removed from the HTML DOM. If you call the
+* preventDefault()
method in the event listener for the streamDestroyed
+* event, the default behavior is prevented and you can clean up Subscriber objects using your
+* own code. See
+* Session.getSubscribersForStream().
+* For the streamDestroyed
event dispatched by a Publisher object, the default
+* behavior is that the Publisher object is removed from the HTML DOM. The Publisher object
+* dispatches a destroyed
event when the element is removed from the HTML DOM.
+* If you call the preventDefault()
method in the event listener for the
+* streamDestroyed
event, the default behavior is prevented, and you can
+* retain the Publisher for reuse or clean it up using your own code.
+*
To see whether an event has a default behavior, check the cancelable
property of
+* the event object.
Call the preventDefault()
method in the event listener function for the event.
connect()
method of the Session object.
+ *
+ * In version 2.2, the completionHandler of the Session.connect()
method
+ * indicates success or failure in connecting to the session.
+ *
+ * @class SessionConnectEvent
+ * @property {Array} connections Deprecated in version 2.2 (and set to an empty array). In
+ * version 2.2, listen for the connectionCreated
event dispatched by the Session
+ * object. In version 2.2, the Session object dispatches a connectionCreated
event
+ * for each connection (including your own). This includes connections present when you first
+ * connect to the session.
+ *
+ * @property {Array} streams Deprecated in version 2.2 (and set to an empty array). In version
+ * 2.2, listen for the streamCreated
event dispatched by the Session object. In
+ * version 2.2, the Session object dispatches a streamCreated
event for each stream
+ * other than those published by your client. This includes streams
+ * present when you first connect to the session.
+ *
+ * @see Session.connect()
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:
+ *
+ * publishAudio()
or
+ * publishVideo()
methods of the Publish object. Note that a
+ * subscriber's video can be disabled or enabled for reasons other than the
+ * publisher disabling or enabling it. A Subscriber object dispatches
+ * videoDisabled
and videoEnabled
events in all
+ * conditions that cause the subscriber's stream to be disabled or enabled.
+ * videoDimensions
property of the Stream object has
+ * changed (see Stream.videoDimensions).
+ * videoType
property of the Stream object has changed.
+ * This can happen in a stream published by a mobile device. (See
+ * Stream.videoType.)
+ * "hasAudio"
, "hasVideo"
, or "videoDimensions"
.
+ * @property {Object} newValue The new value of the property (after the change).
+ * @property {Object} oldValue The old value of the property (before the change).
+ * @property {Stream} stream The Stream object for which a property has changed.
+ *
+ * @see Publisher.publishAudio()
+ * @see Publisher.publishVideo()
+ * @see Stream.videoDimensions
+ * @augments Event
+ */
+ OT.StreamPropertyChangedEvent = function (type, stream, changedProperty, oldValue, newValue) {
+ OT.Event.call(this, type, false);
+ this.type = type;
+ this.stream = stream;
+ this.changedProperty = changedProperty;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ };
+
+ OT.VideoDimensionsChangedEvent = function (target, oldValue, newValue) {
+ OT.Event.call(this, 'videoDimensionsChanged', false);
+ this.type = 'videoDimensionsChanged';
+ this.target = target;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ };
+
+/**
+ * Defines event objects for the archiveStarted
and archiveStopped
events.
+ * The Session object dispatches these events when an archive recording of the session starts and
+ * stops.
+ *
+ * @property {String} id The archive ID.
+ * @property {String} name The name of the archive. You can assign an archive a name when you create
+ * it, using the OpenTok REST API or one of the
+ * OpenTok server SDKs.
+ *
+ * @class ArchiveEvent
+ * @augments Event
+ */
+ OT.ArchiveEvent = function (type, archive) {
+ OT.Event.call(this, type, false);
+ this.type = type;
+ this.id = archive.id;
+ this.name = archive.name;
+ this.status = archive.status;
+ this.archive = archive;
+ };
+
+ OT.ArchiveUpdatedEvent = function (stream, key, oldValue, newValue) {
+ OT.Event.call(this, 'updated', false);
+ this.target = stream;
+ this.changedProperty = key;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ };
+
+/**
+ * The Session object dispatches a signal event when the client receives a signal from the session.
+ *
+ * @class SignalEvent
+ * @property {String} type The type assigned to the signal (if there is one). Use the type to
+ * filter signals received (by adding an event handler for signal:type1 or signal:type2, etc.)
+ * @property {String} data The data string sent with the signal (if there is one).
+ * @property {Connection} from The Connection corresponding to the client that sent with the signal.
+ *
+ * @see Session.signal()
+ * @see Session events (signal and signal:type)
+ * @augments Event
+ */
+ OT.SignalEvent = function(type, data, from) {
+ OT.Event.call(this, type ? 'signal:' + type : OT.Event.names.SIGNAL, false);
+ this.data = data;
+ this.from = from;
+ };
+
+ OT.StreamUpdatedEvent = function (stream, key, oldValue, newValue) {
+ OT.Event.call(this, 'updated', false);
+ this.target = stream;
+ this.changedProperty = key;
+ this.oldValue = oldValue;
+ this.newValue = newValue;
+ };
+
+ OT.DestroyedEvent = function(type, target, reason) {
+ OT.Event.call(this, type, false);
+ this.target = target;
+ this.reason = reason;
+ };
+
+/**
+ * Defines the event object for the videoDisabled
and videoEnabled
events
+ * dispatched by the Subscriber.
+ *
+ * @class VideoEnabledChangedEvent
+ *
+ * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable
+ * (true
) or not (false
). You can cancel the default behavior by
+ * calling the preventDefault()
method of the event object in the callback
+ * function. (See preventDefault().)
+ *
+ * @property {String} reason The reason the video was disabled or enabled. This can be set to one of
+ * the following values:
+ *
+ * "publishVideo"
— The publisher started or stopped publishing video,
+ * by calling publishVideo(true)
or publishVideo(false)
."quality"
— The OpenTok Media Router starts or stops sending video
+ * to the subscriber based on stream quality changes. This feature of the OpenTok Media
+ * Router has a subscriber drop the video stream when connectivity degrades. (The subscriber
+ * continues to receive the audio stream, if there is one.)
+ *
+ * If connectivity improves to support video again, the Subscriber object dispatches
+ * a videoEnabled
event, and the Subscriber resumes receiving video.
+ *
+ * By default, the Subscriber displays a video disabled indicator when a
+ * videoDisabled
event with this reason is dispatched and removes the indicator
+ * when the videoDisabled
event with this reason is dispatched. You can control
+ * the display of this icon by calling the setStyle()
method of the Subscriber,
+ * setting the videoDisabledDisplayMode
property(or you can set the style when
+ * calling the Session.subscribe()
method, setting the style
property
+ * of the properties
parameter).
+ *
+ * This feature is only available in sessions that use the OpenTok Media Router (sessions with + * the media mode + * set to routed), not in sessions with the media mode set to relayed. + *
"subscribeToVideo"
— The subscriber started or stopped subscribing to
+ * video, by calling subscribeToVideo(true)
or subscribeToVideo(false)
.
+ * "videoDisabled"
or
+ * "videoEnabled"
.
+ *
+ * @see Subscriber videoDisabled event
+ * @see Subscriber videoEnabled event
+ * @augments Event
+ */
+ OT.VideoEnabledChangedEvent = function(type, properties) {
+ OT.Event.call(this, type, false);
+ this.reason = properties.reason;
+ };
+
+ OT.VideoDisableWarningEvent = function(type/*, properties*/) {
+ OT.Event.call(this, type, false);
+ };
+
+/**
+ * Dispatched periodically by a Subscriber or Publisher object to indicate the audio
+ * level. This event is dispatched up to 60 times per second, depending on the browser.
+ *
+ * @property {String} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically
+ * for use in adjusting a user interface element, such as a volume meter. Use a moving average
+ * to smooth the data.
+ *
+ * @class AudioLevelUpdatedEvent
+ * @augments Event
+ */
+ OT.AudioLevelUpdatedEvent = function(audioLevel) {
+ OT.Event.call(this, OT.Event.names.AUDIO_LEVEL_UPDATED, false);
+ this.audioLevel = audioLevel;
+ };
+
+ OT.MediaStoppedEvent = function(target) {
+ OT.Event.call(this, OT.Event.names.MEDIA_STOPPED, true);
+ this.target = target;
+ };
+
+})(window);
+
+// tb_require('../../helpers/helpers.js')
+// tb_require('../events.js')
+
+var screenSharingExtensionByKind = {},
+ screenSharingExtensionClasses = {};
+
+OT.registerScreenSharingExtensionHelper = function(kind, helper) {
+ screenSharingExtensionClasses[kind] = helper;
+ if (helper.autoRegisters && helper.isSupportedInThisBrowser) {
+ OT.registerScreenSharingExtension(kind);
+ }
+};
+
+/**
+ * Register a Chrome extension for screen-sharing support.
+ *
+ * Use the OT.checkScreenSharingCapability()
method to check if an extension is
+ * required, registered, and installed.
+ *
+ * The OpenTok
+ * screensharing-extensions
+ * sample includes code for creating a Chrome extension for screen-sharing support.
+ *
+ * @param {String} kind Set this parameter to "chrome"
. Currently, you can only
+ * register a screen-sharing extension for Chrome.
+ *
+ * @see OT.initPublisher()
+ * @see OT.checkScreenSharingCapability()
+ * @method OT.registerScreenSharingExtension
+ * @memberof OT
+ */
+
+OT.registerScreenSharingExtension = function(kind) {
+ var initArgs = Array.prototype.slice.call(arguments, 1);
+
+ if (screenSharingExtensionClasses[kind] == null) {
+ throw new Error('Unsupported kind passed to OT.registerScreenSharingExtension');
+ }
+
+ var x = screenSharingExtensionClasses[kind]
+ .register.apply(screenSharingExtensionClasses[kind], initArgs);
+ screenSharingExtensionByKind[kind] = x;
+};
+
+var screenSharingPickHelper = function() {
+
+ var foundClass = OT.$.find(OT.$.keys(screenSharingExtensionClasses), function(cls) {
+ return screenSharingExtensionClasses[cls].isSupportedInThisBrowser;
+ });
+
+ if (foundClass === void 0) {
+ return {};
+ }
+
+ return {
+ name: foundClass,
+ proto: screenSharingExtensionClasses[foundClass],
+ instance: screenSharingExtensionByKind[foundClass]
+ };
+
+};
+
+OT.pickScreenSharingHelper = function() {
+ return screenSharingPickHelper();
+};
+
+/**
+ * Checks for screen sharing support on the client browser. The object passed to the callback
+ * function defines whether screen sharing is supported as well as whether an extension is
+ * required, installed, and registered (if needed).
+ *
+ *
+ * OT.checkScreenSharingCapability(function(response) { + * if (!response.supported || response.extensionRegistered === false) { + * // This browser does not support screen sharing + * } else if(response.extensionInstalled === false) { + * // Prompt to install the extension + * } else { + * // Screen sharing is available. + * } + * }); + *+ * + * @param {function} callback The callback invoked with the support options object passed as + * the parameter. This object has the following properties: + *
+ *
supported
(Boolean) — Set to true if screen sharing is supported in the
+ * browser. Check the extensionRequired
property to see if the browser requires
+ * an extension for screen sharing.
+ * extensionRequired
(String) — Set to "chrome"
on Chrome,
+ * which requires a screen sharing extension to be installed. Otherwise, this property is
+ * undefined.
+ * extensionRegistered
(Boolean) — On Chrome, this property is set to
+ * true
if a screen-sharing extension is registered; otherwise it is set to
+ * false
. If the extension type does not require registration (as in the
+ * case of of the OpenTok plugin for Internet Explorer), this property is set to
+ * true
. In other browsers (which do not require an extension), this property
+ * is undefined. Use the OT.registerScreenSharingExtension()
method to register
+ * an extension in Chrome.
+ * extensionInstalled
(Boolean) — If an extension is required, this is set
+ * to true
if the extension installed (and registered, if needed); otherwise it
+ * is set to false
. If an extension is not required (for example on FireFox),
+ * this property is undefined.
+ * 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} frameRate The frame rate of the video stream. This property is only set if the
+ * publisher of the stream specifies a frame rate when calling the OT.initPublisher()
+ * method; otherwise, this property is undefined.
+ *
+ * @property {Boolean} hasAudio Whether the stream has audio. This property can change if the
+ * publisher turns on or off audio (by calling
+ * Publisher.publishAudio()). When this occurs, the
+ * {@link Session} object dispatches a streamPropertyChanged
event (see
+ * {@link StreamPropertyChangedEvent}).
+ *
+ * @property {Boolean} hasVideo Whether the stream has video. This property can change if the
+ * publisher turns on or off video (by calling
+ * Publisher.publishVideo()). When this occurs, the
+ * {@link Session} object dispatches a streamPropertyChanged
event (see
+ * {@link StreamPropertyChangedEvent}).
+ *
+ * @property {String} name The name of the stream. Publishers can specify a name when publishing
+ * a stream (using the publish()
method of the publisher's Session object).
+ *
+ * @property {String} streamId The unique ID of the stream.
+ *
+ * @property {Object} videoDimensions This object has two properties: width
and
+ * height
. Both are numbers. The width
property is the width of the
+ * encoded stream; the height
property is the height of the encoded stream. (These
+ * are independent of the actual width of Publisher and Subscriber objects corresponding to the
+ * stream.) This property can change if a stream published from a mobile device resizes, based on
+ * a change in the device orientation. It can also occur if the video source is a screen-sharing
+ * window and the user publishing the stream resizes the window. When the video dimensions change,
+ * the {@link Session} object dispatches a streamPropertyChanged
event
+ * (see {@link StreamPropertyChangedEvent}).
+ *
+ * @property {String} videoType The type of video — either "camera"
or
+ * "screen"
. A "screen"
video uses screen sharing on the publisher
+ * as the video source; for other videos, this property is set to "camera"
.
+ * This property can change if a stream published from a mobile device changes from a
+ * camera to a screen-sharing video type. When the video type changes, the {@link Session} object
+ * dispatches a streamPropertyChanged
event (see {@link StreamPropertyChangedEvent}).
+ */
+
+
+ OT.Stream = function(id, name, creationTime, connection, session, channel) {
+ var destroyedReason;
+
+ this.id = this.streamId = id;
+ this.name = name;
+ this.creationTime = Number(creationTime);
+
+ this.connection = connection;
+ this.channel = channel;
+ this.publisher = OT.publishers.find({streamId: this.id});
+
+ OT.$.eventing(this);
+
+ var onChannelUpdate = OT.$.bind(function(channel, key, oldValue, newValue) {
+ var _key = key;
+
+ switch(_key) {
+ case 'active':
+ _key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo';
+ this[_key] = newValue;
+ break;
+
+ case 'disableWarning':
+ _key = channel.type === 'audio' ? 'audioDisableWarning': 'videoDisableWarning';
+ this[_key] = newValue;
+ if (!this[channel.type === 'audio' ? 'hasAudio' : 'hasVideo']) {
+ return; // Do NOT event in this case.
+ }
+ break;
+
+ case 'fitMode':
+ _key = 'defaultFitMode';
+ this[_key] = newValue;
+ break;
+
+ case 'source':
+ _key = channel.type === 'audio' ? 'audioType' : 'videoType';
+ 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) );
+ }, this);
+
+ var associatedWidget = OT.$.bind(function() {
+ if(this.publisher) {
+ return this.publisher;
+ } else {
+ return OT.subscribers.find(function(subscriber) {
+ return subscriber.stream.id === this.id &&
+ subscriber.session.id === session.id;
+ });
+ }
+ }, this);
+
+ // Returns all channels that have a type of +type+.
+ this.getChannelsOfType = function (type) {
+ return OT.$.filter(this.channel, function(channel) {
+ return channel.type === type;
+ });
+ };
+
+ this.getChannel = function (id) {
+ for (var i=0; iSession.subscribe()
:
- * code
* Errors when calling TB.initPublisher()
:
Errors when calling OT.initPublisher()
:
1550 | + *Screen sharing is not supported (and you set the videoSource property
+ * of the options parameter of OT.initPublisher() to
+ * "screen" ). Before calling OT.initPublisher() , you can call
+ * OT.checkScreenSharingCapability()
+ * to check if screen sharing is supported. |
+ *
1551 | + *A screen sharing extension needs to be registered but it is not. This error can occur
+ * when you set the videoSource property of the options parameter
+ * of OT.initPublisher() to "screen" . Before calling
+ * OT.initPublisher() , you can call
+ * OT.checkScreenSharingCapability()
+ * to check if screen sharing requires an extension to be registered. |
+ *
1552 | + *A screen sharing extension is required, but it is not installed. This error can occur
+ * when you set the videoSource property of the options parameter
+ * of OT.initPublisher() to "screen" . Before calling
+ * OT.initPublisher() , you can call
+ * OT.checkScreenSharingCapability()
+ * to check if screen sharing requires an extension to be installed. |
+ *
General errors that can occur when calling any method:
@@ -14062,17 +17407,21 @@ waitForDomReady(); 1012: 'Peer-to-peer Stream Play Failed', 1013: 'Connection Failed', 1014: 'API Response Failure', + 1015: 'Session connected, cannot test network', + 1021: 'Request Timeout', + 1026: 'Terms of Service Violation: Export Compliance', 1500: 'Unable to Publish', + 1503: 'No TURN server found', 1520: 'Unable to Force Disconnect', 1530: 'Unable to Force Unpublish', + 1553: 'ICEWorkflow failed', + 1600: 'createOffer, createAnswer, setLocalDescription, setRemoteDescription', 2000: 'Internal Error', - 2001: 'Embed Failed', + 2001: 'Unexpected HTTP error codes (f.e. 500)', 4000: 'WebSocket Connection Failed', 4001: 'WebSocket Network Disconnected' }; - var analytics; - function _exceptionHandler(component, msg, errorCode, context) { var title = errorsCodesToTitle[errorCode], contextCopy = context ? OT.$.clone(context) : {}; @@ -14082,8 +17431,7 @@ waitForDomReady(); if (!contextCopy.partnerId) contextCopy.partnerId = OT.APIKEY; try { - if (!analytics) analytics = new OT.Analytics(); - analytics.logError(errorCode, 'tb.exception', title, {details:msg}, contextCopy); + OT.analytics.logError(errorCode, 'tb.exception', title, {details:msg}, contextCopy); OT.dispatchEvent( new OT.ExceptionEvent(OT.Event.names.EXCEPTION, msg, title, errorCode, component, component) @@ -14143,3399 +17491,4515 @@ waitForDomReady(); }; })(window); + +// tb_require('../helpers/helpers.js') +// tb_require('../helpers/lib/config.js') +// tb_require('./events.js') + !(function() { - OT.ConnectionCapabilities = function(capabilitiesHash) { - // Private helper methods - var castCapabilities = function(capabilitiesHash) { - capabilitiesHash.supportsWebRTC = OT.$.castToBoolean(capabilitiesHash.supportsWebRTC); - return capabilitiesHash; - }; + /* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ + /* global OT */ - // Private data - var _caps = castCapabilities(capabilitiesHash); - this.supportsWebRTC = _caps.supportsWebRTC; - }; + // Helper to synchronise several startup tasks and then dispatch a unified + // 'envLoaded' event. + // + // This depends on: + // * OT + // * OT.Config + // + function EnvironmentLoader() { + var _configReady = false, -})(window); -!(function() { + // If the plugin is installed, then we should wait for it to + // be ready as well. + _pluginSupported = OTPlugin.isSupported(), + _pluginLoadAttemptComplete = _pluginSupported ? OTPlugin.isReady() : true, - /** - * The Connection object represents a connection to an OpenTok session. Each client that connects - * to a session has a unique connection, with a unique connection ID (represented by the - *id
property of the Connection object for the client).
- *
- * The Session object has a connection
property that is a Connection object.
- * It represents the local client's connection. (A client only has a connection once the
- * client has successfully called the connect()
method of the {@link Session}
- * object.)
- *
- * The Session object dispatches a connectionCreated
event when each client
- * (including your own) connects to a session (and for clients that are present in the
- * session when you connect). The connectionCreated
event object has a
- * connection
property, which is a Connection object corresponding to the client
- * the event pertains to.
- *
- * The Stream object has a connection
property that is a Connection object.
- * It represents the connection of the client that is publishing the stream.
- *
- * @class Connection
- * @property {String} connectionId The ID of this connection.
- * @property {Number} creationTime The timestamp for the creation of the connection. This
- * value is calculated in milliseconds.
- * You can convert this value to a Date object by calling new Date(creationTime)
,
- * where creationTime
- * is the creationTime
property of the Connection object.
- * @property {String} data A string containing metadata describing the
- * connection. When you generate a user token string pass the connection data string to the
- * generate_token()
method of our
- * server-side libraries. You can also generate a token
- * and define connection data on the
- * Dashboard page.
- */
- OT.Connection = function(id, creationTime, data, capabilitiesHash, permissionsHash) {
- var destroyedReason;
+ isReady = function() {
+ return !OT.$.isDOMUnloaded() && OT.$.isReady() &&
+ _configReady && _pluginLoadAttemptComplete;
+ },
- this.id = this.connectionId = id;
- this.creationTime = creationTime ? Number(creationTime) : null;
- this.data = data;
- this.capabilities = new OT.ConnectionCapabilities(capabilitiesHash);
- this.permissions = new OT.Capabilities(permissionsHash);
- this.quality = null;
-
- OT.$.eventing(this);
-
- this.destroy = OT.$.bind(function(reason, quiet) {
- destroyedReason = reason || 'clientDisconnected';
-
- if (quiet !== true) {
- this.dispatchEvent(
- new OT.DestroyedEvent(
- 'destroyed', // This should be OT.Event.names.CONNECTION_DESTROYED, but
- // the value of that is currently shared with Session
- this,
- destroyedReason
- )
- );
- }
- }, this);
-
- this.destroyed = function() {
- return destroyedReason !== void 0;
- };
-
- this.destroyedReason = function() {
- return destroyedReason;
- };
-
- };
-
- OT.Connection.fromHash = function(hash) {
- return new OT.Connection(hash.id,
- hash.creationTime,
- hash.data,
- OT.$.extend(hash.capablities || {}, { supportsWebRTC: true }),
- hash.permissions || [] );
- };
-
-})(window);
-!(function() {
-
- // id: String | mandatory | immutable
- // type: String {video/audio/data/...} | mandatory | immutable
- // active: Boolean | mandatory | mutable
- // orientation: Integer? | optional | mutable
- // frameRate: Float | optional | mutable
- // height: Integer | optional | mutable
- // width: Integer | optional | mutable
- OT.StreamChannel = function(options) {
- this.id = options.id;
- this.type = options.type;
- this.active = OT.$.castToBoolean(options.active);
- this.orientation = options.orientation || OT.VideoOrientation.ROTATED_NORMAL;
- if (options.frameRate) this.frameRate = parseFloat(options.frameRate, 10);
- this.width = parseInt(options.width, 10);
- this.height = parseInt(options.height, 10);
-
- OT.$.eventing(this, true);
-
- // Returns true if a property was updated.
- this.update = function(attributes) {
- var videoDimensions = {},
- oldVideoDimensions = {};
-
- for (var key in attributes) {
- if(!attributes.hasOwnProperty(key)) {
- continue;
- }
- // we shouldn't really read this before we know the key is valid
- var oldValue = this[key];
-
- switch(key) {
- case 'active':
- this.active = OT.$.castToBoolean(attributes[key]);
- break;
-
- case 'disableWarning':
- this.disableWarning = OT.$.castToBoolean(attributes[key]);
- break;
-
- case 'frameRate':
- this.frameRate = parseFloat(attributes[key], 10);
- break;
-
- case 'width':
- case 'height':
- this[key] = parseInt(attributes[key], 10);
-
- videoDimensions[key] = this[key];
- oldVideoDimensions[key] = oldValue;
- break;
-
- case 'orientation':
- this[key] = attributes[key];
-
- videoDimensions[key] = this[key];
- oldVideoDimensions[key] = oldValue;
- break;
-
- default:
- OT.warn('Tried to update unknown key ' + key + ' on ' + this.type +
- ' channel ' + this.id);
- return;
- }
-
- this.trigger('update', this, key, oldValue, this[key]);
- }
-
- if (OT.$.keys(videoDimensions).length) {
- // To make things easier for the public API, we broadcast videoDimensions changes,
- // which is an aggregate of width, height, and orientation changes.
- this.trigger('update', this, 'videoDimensions', oldVideoDimensions, videoDimensions);
- }
-
- return true;
- };
- };
-
-})(window);
-!(function() {
-
- var validPropertyNames = ['name', 'archiving'];
-
-/**
- * Specifies a stream. A stream is a representation of a published stream in a session. When a
- * client calls the Session.publish() method, a new stream is
- * created. Properties of the Stream object provide information about the stream.
- *
- *
When a stream is added to a session, the Session object dispatches a
- * streamCreatedEvent
. When a stream is destroyed, the Session object dispatches a
- * streamDestroyed
event. The StreamEvent object, which defines these event objects,
- * has a stream
property, which is an array of Stream object. For details and a code
- * example, see {@link StreamEvent}.
When a connection to a session is made, the Session object dispatches a
- * sessionConnected
event, defined by the SessionConnectEvent object. The
- * SessionConnectEvent object has a streams
property, which is an array of Stream
- * objects pertaining to the streams in the session at that time. For details and a code example,
- * see {@link SessionConnectEvent}.
connection
property of the Session object to see if the stream is being published
- * by the local web page.
- *
- * @property {Number} creationTime The timestamp for the creation
- * of the stream. This value is calculated in milliseconds. You can convert this value to a
- * Date object by calling new Date(creationTime)
, where creationTime
is
- * the creationTime
property of the Stream object.
- *
- * @property {Number} frameRate The frame rate of the video stream. This property is only set if the
- * publisher of the stream specifies a frame rate when calling the OT.initPublisher()
- * method; otherwise, this property is undefined.
- *
- * @property {Boolean} hasAudio Whether the stream has audio. This property can change if the
- * publisher turns on or off audio (by calling
- * Publisher.publishAudio()). When this occurs, the
- * {@link Session} object dispatches a streamPropertyChanged
event (see
- * {@link StreamPropertyChangedEvent}.)
- *
- * @property {Boolean} hasVideo Whether the stream has video. This property can change if the
- * publisher turns on or off video (by calling
- * Publisher.publishVideo()). When this occurs, the
- * {@link Session} object dispatches a streamPropertyChanged
event (see
- * {@link StreamPropertyChangedEvent}.)
- *
- * @property {String} name The name of the stream. Publishers can specify a name when publishing
- * a stream (using the publish()
method of the publisher's Session object).
- *
- * @property {String} streamId The unique ID of the stream.
- *
- * @property {Object} videoDimensions This object has two properties: width
and
- * height
. Both are numbers. The width
property is the width of the
- * encoded stream; the height
property is the height of the encoded stream. (These
- * are independent of the actual width of Publisher and Subscriber objects corresponding to the
- * stream.) This property can change if a stream
- * published from an iOS device resizes, based on a change in the device orientation. When this
- * occurs, the {@link Session} object dispatches a streamPropertyChanged
event (see
- * {@link StreamPropertyChangedEvent}.)
- */
-
-
- OT.Stream = function(id, name, creationTime, connection, session, channel) {
- var destroyedReason;
-
- this.id = this.streamId = id;
- this.name = name;
- this.creationTime = Number(creationTime);
-
- this.connection = connection;
- this.channel = channel;
- this.publisher = OT.publishers.find({streamId: this.id});
-
- OT.$.eventing(this);
-
- var onChannelUpdate = OT.$.bind(function(channel, key, oldValue, newValue) {
- var _key = key;
-
- switch(_key) {
- case 'active':
- _key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo';
- this[_key] = newValue;
- break;
-
- case 'disableWarning':
- _key = channel.type === 'audio' ? 'audioDisableWarning': 'videoDisableWarning';
- this[_key] = newValue;
- if (!this[channel.type === 'audio' ? 'hasAudio' : 'hasVideo']) {
- return; // Do NOT event in this case.
+ onLoaded = function() {
+ if (isReady()) {
+ OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_LOADED));
}
- break;
-
- case 'orientation':
- case 'width':
- case 'height':
- this.videoDimensions = {
- width: channel.width,
- height: channel.height,
- orientation: channel.orientation
- };
-
- // We dispatch this via the videoDimensions key instead
- return;
- }
-
- this.dispatchEvent( new OT.StreamUpdatedEvent(this, _key, oldValue, newValue) );
- }, this);
-
- var associatedWidget = OT.$.bind(function() {
- if(this.publisher) {
- return this.publisher;
- } else {
- return OT.subscribers.find(function(subscriber) {
- return subscriber.stream.id === this.id &&
- subscriber.session.id === session.id;
- });
- }
- }, this);
-
- // Returns all channels that have a type of +type+.
- this.getChannelsOfType = function (type) {
- return OT.$.filter(this.channel, function(channel) {
- return channel.type === type;
- });
- };
-
- this.getChannel = function (id) {
- for (var i=0; iRTCPeerConnection.getStats
based audio level sampler.
- *
- * It uses the the getStats
method to get the audioOutputLevel
.
- * This implementation expects the single parameter version of the getStats
method.
- *
- * Currently the audioOutputLevel
stats is only supported in Chrome.
- *
- * @param {OT.SubscriberPeerConnection} peerConnection the peer connection to use to get the stats
- * @constructor
- */
- OT.GetStatsAudioLevelSampler = function(peerConnection) {
+/**
+ * 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.
+ *
null
if no value could be acquired
- */
- this.sample = function(done) {
- _peerConnection.getStatsWithSingleParameter(function(statsReport) {
- var results = statsReport.result();
+ // Load the iframe over the whole page.
+ document.body.appendChild((function() {
+ var d = document.createElement('iframe');
+ d.id = id;
+ d.style.position = 'absolute';
+ d.style.position = 'fixed';
+ d.style.height = '100%';
+ d.style.width = '100%';
+ d.style.top = '0px';
+ d.style.left = '0px';
+ d.style.right = '0px';
+ d.style.bottom = '0px';
+ d.style.zIndex = 1000;
+ try {
+ d.style.backgroundColor = 'rgba(0,0,0,0.2)';
+ } catch (err) {
+ // Old IE browsers don't support rgba and we still want to show the upgrade message
+ // but we just make the background of the iframe completely transparent.
+ d.style.backgroundColor = 'transparent';
+ d.setAttribute('allowTransparency', 'true');
+ }
+ d.setAttribute('frameBorder', '0');
+ d.frameBorder = '0';
+ d.scrolling = 'no';
+ d.setAttribute('scrolling', 'no');
- for (var i = 0; i < results.length; i++) {
- var result = results[i];
- if (result.local) {
- var audioOutputLevel = parseFloat(result.local.stat(_statsProperty));
- if (!isNaN(audioOutputLevel)) {
- // the mex value delivered by getStats for audio levels is 2^15
- done(audioOutputLevel / 32768);
- return;
- }
- }
+ var minimumBrowserVersion = OT.properties.minimumVersion[OT.$.env.name.toLowerCase()],
+ isSupportedButOld = minimumBrowserVersion > OT.$.env.version;
+ d.src = OT.properties.assetURL + '/html/upgrade.html#' +
+ encodeURIComponent(isSupportedButOld ? 'true' : 'false') + ',' +
+ encodeURIComponent(JSON.stringify(OT.properties.minimumVersion)) + '|' +
+ encodeURIComponent(document.location.href);
+
+ return d;
+ })());
+
+ // Now we need to listen to the event handler if the user closes this dialog.
+ // Since this is from an IFRAME within another domain we are going to listen to hash
+ // changes. The best cross browser solution is to poll for a change in the hashtag.
+ if (_intervalId) clearInterval(_intervalId);
+ _intervalId = setInterval(function(){
+ var hash = document.location.hash,
+ re = /^#?\d+&/;
+ if (hash !== _lastHash && re.test(hash)) {
+ _lastHash = hash;
+ if (hash.replace(re, '') === 'close_window'){
+ document.body.removeChild(document.getElementById(id));
+ document.location.hash = '';
}
-
- done(null);
- });
- };
- };
-
-
- /*
- * An AudioContext
based audio level sampler. It returns the maximum value in the
- * last 1024 samples.
- *
- * It is worth noting that the remote MediaStream
audio analysis is currently only
- * available in FF.
- *
- * This implementation gracefully handles the case where the MediaStream
has not
- * been set yet by returning a null
value until the stream is set. It is up to the
- * call site to decide what to do with this value (most likely ignore it and retry later).
- *
- * @constructor
- * @param {AudioContext} audioContext an audio context instance to get an analyser node
- */
- OT.AnalyserAudioLevelSampler = function(audioContext) {
-
- var _sampler = this,
- _analyser = null,
- _timeDomainData = null;
-
- var _getAnalyser = function(stream) {
- var sourceNode = audioContext.createMediaStreamSource(stream);
- var analyser = audioContext.createAnalyser();
- sourceNode.connect(analyser);
- return analyser;
- };
-
- this.webOTStream = null;
-
- this.sample = function(done) {
-
- if (!_analyser && _sampler.webOTStream) {
- _analyser = _getAnalyser(_sampler.webOTStream);
- _timeDomainData = new Uint8Array(_analyser.frequencyBinCount);
}
-
- if (_analyser) {
- _analyser.getByteTimeDomainData(_timeDomainData);
-
- // varies from 0 to 255
- var max = 0;
- for (var idx = 0; idx < _timeDomainData.length; idx++) {
- max = Math.max(max, Math.abs(_timeDomainData[idx] - 128));
- }
-
- // normalize the collected level to match the range delivered by
- // the getStats' audioOutputLevel
- done(max / 128);
- } else {
- done(null);
- }
- };
- };
-
- /*
- * Transforms a raw audio level to produce a "smoother" animation when using displaying the
- * audio level. This transformer is state-full because it needs to keep the previous average
- * value of the signal for filtering.
- *
- * It applies a low pass filter to get rid of level jumps and apply a log scale.
- *
- * @constructor
- */
- OT.AudioLevelTransformer = function() {
-
- var _averageAudioLevel = null;
-
- /*
- *
- * @param {number} audioLevel a level in the [0,1] range
- * @returns {number} a level in the [0,1] range transformed
- */
- this.transform = function(audioLevel) {
- if (_averageAudioLevel === null || audioLevel >= _averageAudioLevel) {
- _averageAudioLevel = audioLevel;
- } else {
- // a simple low pass filter with a smoothing of 70
- _averageAudioLevel = audioLevel * 0.3 + _averageAudioLevel * 0.7;
- }
-
- // 1.5 scaling to map -30-0 dBm range to [0,1]
- var logScaled = (Math.log(_averageAudioLevel) / Math.LN10) / 1.5 + 1;
-
- return Math.min(Math.max(logScaled, 0), 1);
- };
- };
-
-})(window);
-!(function() {
-
- /*
- * Executes the provided callback thanks to window.setInterval
.
- *
- * @param {function()} callback
- * @param {number} frequency how many times per second we want to execute the callback
- * @constructor
- */
- OT.IntervalRunner = function(callback, frequency) {
- var _callback = callback,
- _frequency = frequency,
- _intervalId = null;
-
- this.start = function() {
- _intervalId = window.setInterval(_callback, 1000 / _frequency);
- };
-
- this.stop = function() {
- window.clearInterval(_intervalId);
- _intervalId = null;
- };
- };
-
-})(window);
-// tb_require('../../helpers/helpers.js')
+ }, 100);
+ });
+};
+// tb_require('../helpers/helpers.js')
/* jshint globalstrict: true, strict: false, undef: true, unused: true,
trailing: true, browser: true, smarttabs:true */
/* global OT */
-/* exported SDPHelpers */
-var findIndex = function(array, iter, ctx) {
- if (!OT.$.isFunction(iter)) {
- throw new TypeError('iter must be a function');
- }
+OT.ConnectionCapabilities = function(capabilitiesHash) {
+ // Private helper methods
+ var castCapabilities = function(capabilitiesHash) {
+ capabilitiesHash.supportsWebRTC = OT.$.castToBoolean(capabilitiesHash.supportsWebRTC);
+ return capabilitiesHash;
+ };
- for (var i = 0, count = array.length || 0; i < count; ++i) {
- if (i in array && iter.call(ctx, array[i], i, array)) {
- return i;
- }
- }
-
- return -1;
+ // Private data
+ var _caps = castCapabilities(capabilitiesHash);
+ this.supportsWebRTC = _caps.supportsWebRTC;
};
-// Here are the structure of the rtpmap attribute and the media line, most of the
-// complex Regular Expressions in this code are matching against one of these two
-// formats:
-// * a=rtpmap:capabilities
property of a
+ * Session object. See Session.capabilities.
+ *
+ * All Capabilities properties are undefined until you have connected to a session
+ * and the Session object has dispatched the sessionConnected
event.
+ *
+ * For more information on token roles, see the
+ * generate_token()
+ * method of the OpenTok server-side libraries.
+ *
+ * @class Capabilities
+ *
+ * @property {Number} forceDisconnect Specifies whether you can call
+ * the Session.forceDisconnect()
method (1) or not (0). To call the
+ * Session.forceDisconnect()
method,
+ * the user must have a token that is assigned the role of moderator.
+ * @property {Number} forceUnpublish Specifies whether you can call
+ * the Session.forceUnpublish()
method (1) or not (0). To call the
+ * Session.forceUnpublish()
method, the user must have a token that
+ * is assigned the role of moderator.
+ * @property {Number} publish Specifies whether you can publish to the session (1) or not (0).
+ * The ability to publish is based on a few factors. To publish, the user must have a token that
+ * is assigned a role that supports publishing. There must be a connected camera and microphone.
+ * @property {Number} subscribe Specifies whether you can subscribe to streams
+ * in the session (1) or not (0). Currently, this capability is available for all users on all
+ * platforms.
+ */
+OT.Capabilities = function(permissions) {
+ this.publish = OT.$.arrayIndexOf(permissions, 'publish') !== -1 ? 1 : 0;
+ this.subscribe = OT.$.arrayIndexOf(permissions, 'subscribe') !== -1 ? 1 : 0;
+ this.forceUnpublish = OT.$.arrayIndexOf(permissions, 'forceunpublish') !== -1 ? 1 : 0;
+ this.forceDisconnect = OT.$.arrayIndexOf(permissions, 'forcedisconnect') !== -1 ? 1 : 0;
+ this.supportsWebRTC = OT.$.hasCapabilities('webrtc') ? 1 : 0;
- // Get all payload types that the line supports
- var payloadTypes = mediaLine.match(mLineSelector);
- if (!payloadTypes || payloadTypes.length < 2) {
- // Error, invalid M line?
- return [];
+ this.permittedTo = function(action) {
+ return this.hasOwnProperty(action) && this[action] === 1;
+ };
+};
+
+// tb_require('../helpers/helpers.js')
+// tb_require('../helpers/lib/properties.js')
+// tb_require('./events.js')
+// tb_require('./capabilities.js')
+// tb_require('./connection_capabilities.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+/**
+ * The Connection object represents a connection to an OpenTok session. Each client that connects
+ * to a session has a unique connection, with a unique connection ID (represented by the
+ * id
property of the Connection object for the client).
+ *
+ * The Session object has a connection
property that is a Connection object.
+ * It represents the local client's connection. (A client only has a connection once the
+ * client has successfully called the connect()
method of the {@link Session}
+ * object.)
+ *
+ * The Session object dispatches a connectionCreated
event when each client (including
+ * your own) connects to a session (and for clients that are present in the session when you
+ * connect). The connectionCreated
event object has a connection
+ * property, which is a Connection object corresponding to the client the event pertains to.
+ *
+ * The Stream object has a You can either pass one parameter or two parameters to this method. If you pass one parameter,
- * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
- * you cannot set the connection
property that is a Connection object.
+ * It represents the connection of the client that is publishing the stream.
+ *
+ * @class Connection
+ * @property {String} connectionId The ID of this connection.
+ * @property {Number} creationTime The timestamp for the creation of the connection. This
+ * value is calculated in milliseconds.
+ * You can convert this value to a Date object by calling new Date(creationTime)
,
+ * where creationTime
+ * is the creationTime
property of the Connection object.
+ * @property {String} data A string containing metadata describing the
+ * connection. When you generate a user token string pass the connection data string to the
+ * generate_token()
method of our
+ * server-side libraries. You can also generate a token
+ * and define connection data on the
+ * Dashboard page.
+ */
+OT.Connection = function(id, creationTime, data, capabilitiesHash, permissionsHash) {
+ var destroyedReason;
+
+ this.id = this.connectionId = id;
+ this.creationTime = creationTime ? Number(creationTime) : null;
+ this.data = data;
+ this.capabilities = new OT.ConnectionCapabilities(capabilitiesHash);
+ this.permissions = new OT.Capabilities(permissionsHash);
+ this.quality = null;
+
+ OT.$.eventing(this);
+
+ this.destroy = OT.$.bind(function(reason, quiet) {
+ destroyedReason = reason || 'clientDisconnected';
+
+ if (quiet !== true) {
+ this.dispatchEvent(
+ new OT.DestroyedEvent(
+ 'destroyed', // This should be OT.Event.names.CONNECTION_DESTROYED, but
+ // the value of that is currently shared with Session
+ this,
+ destroyedReason
+ )
+ );
}
+ }, this);
- return OT.$.trim(payloadTypes[2]).split(' ');
- },
+ this.destroyed = function() {
+ return destroyedReason !== void 0;
+ };
- removeTypesFromMLine: function removeTypesFromMLine (mediaLine, payloadTypes) {
- return mediaLine.replace(new RegExp(' ' + payloadTypes.join(' |'), 'ig') , '')
- .replace(/\s+/g, ' ');
- },
+ this.destroyedReason = function() {
+ return destroyedReason;
+ };
+};
- // Remove all references to a particular encodingName from a particular media type
- //
- removeMediaEncoding: function removeMediaEncoding (sdp, mediaType, encodingName) {
- var sdpLines = sdp.split('\r\n'),
- mLineIndex = SDPHelpers.getMLineIndex(sdpLines, mediaType),
- mLine = mLineIndex > -1 ? sdpLines[mLineIndex] : void 0,
- typesToRemove = [],
- payloadTypes,
- match;
-
- if (mLineIndex === -1) {
- // Error, missing M line
- return sdpLines.join('\r\n');
- }
-
- // Get all payload types that the line supports
- payloadTypes = SDPHelpers.getMLinePayloadTypes(mLine, mediaType);
- if (payloadTypes.length === 0) {
- // Error, invalid M line?
- return sdpLines.join('\r\n');
- }
-
- // Find the location of all the rtpmap lines that relate to +encodingName+
- // and any of the supported payload types
- var matcher = new RegExp('a=rtpmap:(' + payloadTypes.join('|') + ') ' +
- encodingName + '\\/\\d+', 'i');
-
- sdpLines = OT.$.filter(sdpLines, function(line, index) {
- match = line.match(matcher);
- if (match === null) return true;
-
- typesToRemove.push(match[1]);
-
- if (index < mLineIndex) {
- // This removal changed the index of the mline, track it
- mLineIndex--;
- }
-
- // remove this one
- return false;
- });
-
- if (typesToRemove.length > 0 && mLineIndex > -1) {
- // Remove all the payload types and we've removed from the media line
- sdpLines[mLineIndex] = SDPHelpers.removeTypesFromMLine(mLine, typesToRemove);
- }
-
- return sdpLines.join('\r\n');
- },
-
- // Removes all Confort Noise from +sdp+.
- //
- // See https://jira.tokbox.com/browse/OPENTOK-7176
- //
- removeComfortNoise: function removeComfortNoise (sdp) {
- return SDPHelpers.removeMediaEncoding(sdp, 'audio', 'CN');
- },
-
- removeVideoCodec: function removeVideoCodec (sdp, codec) {
- return SDPHelpers.removeMediaEncoding(sdp, 'video', codec);
- }
+OT.Connection.fromHash = function(hash) {
+ return new OT.Connection(hash.id,
+ hash.creationTime,
+ hash.data,
+ OT.$.extend(hash.capablities || {}, { supportsWebRTC: true }),
+ hash.permissions || [] );
};
-!(function(window) {
- /* global SDPHelpers */
+// tb_require('../../../helpers/helpers.js')
+// tb_require('./message.js')
+// tb_require('../../connection.js')
- // Normalise these
- var NativeRTCSessionDescription,
- NativeRTCIceCandidate;
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
- if (!TBPlugin.isInstalled()) {
- // order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless
- NativeRTCSessionDescription = (window.mozRTCSessionDescription ||
- window.RTCSessionDescription);
- NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate);
- }
- else {
- NativeRTCSessionDescription = TBPlugin.RTCSessionDescription;
- NativeRTCIceCandidate = TBPlugin.RTCIceCandidate;
- }
-
- // Helper function to forward Ice Candidates via +messageDelegate+
- var iceCandidateForwarder = function(messageDelegate) {
- return function(event) {
- if (event.candidate) {
- messageDelegate(OT.Raptor.Actions.CANDIDATE, event.candidate);
- } else {
- OT.debug('IceCandidateForwarder: No more ICE candidates.');
- }
- };
- };
-
-
- // Process incoming Ice Candidates from a remote connection (which have been
- // forwarded via iceCandidateForwarder). The Ice Candidates cannot be processed
- // until a PeerConnection is available. Once a PeerConnection becomes available
- // the pending PeerConnections can be processed by calling processPending.
- //
- // @example
- //
- // var iceProcessor = new IceCandidateProcessor();
- // iceProcessor.process(iceMessage1);
- // iceProcessor.process(iceMessage2);
- // iceProcessor.process(iceMessage3);
- //
- // iceProcessor.setPeerConnection(peerConnection);
- // iceProcessor.processPending();
- //
- var IceCandidateProcessor = function() {
- var _pendingIceCandidates = [],
- _peerConnection = null;
-
- this.setPeerConnection = function(peerConnection) {
- _peerConnection = peerConnection;
- };
-
- this.process = function(message) {
- var iceCandidate = new NativeRTCIceCandidate(message.content);
-
- if (_peerConnection) {
- _peerConnection.addIceCandidate(iceCandidate);
- } else {
- _pendingIceCandidates.push(iceCandidate);
- }
- };
-
- this.processPending = function() {
- while(_pendingIceCandidates.length) {
- _peerConnection.addIceCandidate(_pendingIceCandidates.shift());
- }
- };
- };
-
-
- // Attempt to completely process +offer+. This will:
- // * set the offer as the remote description
- // * create an answer and
- // * set the new answer as the location description
- //
- // If there are no issues, the +success+ callback will be executed on completion.
- // Errors during any step will result in the +failure+ callback being executed.
- //
- var offerProcessor = function(peerConnection, offer, success, failure) {
- var generateErrorCallback,
- setLocalDescription,
- createAnswer;
-
- generateErrorCallback = function(message, prefix) {
- return function(errorReason) {
- OT.error(message);
- OT.error(errorReason);
-
- if (failure) failure(message, errorReason, prefix);
- };
- };
-
- setLocalDescription = function(answer) {
- answer.sdp = SDPHelpers.removeComfortNoise(answer.sdp);
- answer.sdp = SDPHelpers.removeVideoCodec(answer.sdp, 'ulpfec');
- answer.sdp = SDPHelpers.removeVideoCodec(answer.sdp, 'red');
-
- peerConnection.setLocalDescription(
- answer,
-
- // Success
- function() {
- success(answer);
- },
-
- // Failure
- generateErrorCallback('Error while setting LocalDescription', 'SetLocalDescription')
- );
- };
-
- createAnswer = function() {
- peerConnection.createAnswer(
- // Success
- setLocalDescription,
-
- // Failure
- generateErrorCallback('Error while setting createAnswer', 'CreateAnswer'),
-
- null, // MediaConstraints
- false // createProvisionalAnswer
- );
- };
-
- // Workaround for a Chrome issue. Add in the SDES crypto line into offers
- // from Firefox
- if (offer.sdp.indexOf('a=crypto') === -1) {
- var cryptoLine = 'a=crypto:1 AES_CM_128_HMAC_SHA1_80 ' +
- 'inline:FakeFakeFakeFakeFakeFakeFakeFakeFakeFake\\r\\n';
-
- // insert the fake crypto line for every M line
- offer.sdp = offer.sdp.replace(/^c=IN(.*)$/gmi, 'c=IN$1\r\n'+cryptoLine);
- }
-
- if (offer.sdp.indexOf('a=rtcp-fb') === -1) {
- var rtcpFbLine = 'a=rtcp-fb:* ccm fir\r\na=rtcp-fb:* nack ';
-
- // insert the fake crypto line for every M line
- offer.sdp = offer.sdp.replace(/^m=video(.*)$/gmi, 'm=video$1\r\n'+rtcpFbLine);
- }
-
- peerConnection.setRemoteDescription(
- offer,
-
- // Success
- createAnswer,
-
- // Failure
- generateErrorCallback('Error while setting RemoteDescription', 'SetRemoteDescription')
- );
-
- };
-
- // Attempt to completely process a subscribe message. This will:
- // * create an Offer
- // * set the new offer as the location description
- //
- // If there are no issues, the +success+ callback will be executed on completion.
- // Errors during any step will result in the +failure+ callback being executed.
- //
- var suscribeProcessor = function(peerConnection, success, failure) {
- var constraints,
- generateErrorCallback,
- setLocalDescription;
-
- constraints = {
- mandatory: {},
- optional: []
- },
-
- generateErrorCallback = function(message, prefix) {
- return function(errorReason) {
- OT.error(message);
- OT.error(errorReason);
-
- if (failure) failure(message, errorReason, prefix);
- };
- };
-
- setLocalDescription = function(offer) {
- offer.sdp = SDPHelpers.removeComfortNoise(offer.sdp);
- offer.sdp = SDPHelpers.removeVideoCodec(offer.sdp, 'ulpfec');
- offer.sdp = SDPHelpers.removeVideoCodec(offer.sdp, 'red');
-
-
- peerConnection.setLocalDescription(
- offer,
-
- // Success
- function() {
- success(offer);
- },
-
- // Failure
- generateErrorCallback('Error while setting LocalDescription', 'SetLocalDescription')
- );
- };
-
- // For interop with FireFox. Disable Data Channel in createOffer.
- if (navigator.mozGetUserMedia) {
- constraints.mandatory.MozDontOfferDataChannel = true;
- }
-
- peerConnection.createOffer(
- // Success
- setLocalDescription,
-
- // Failure
- generateErrorCallback('Error while creating Offer', 'CreateOffer'),
-
- constraints
- );
- };
-
- /*
- * Negotiates a WebRTC PeerConnection.
- *
- * Responsible for:
- * * offer-answer exchange
- * * iceCandidates
- * * notification of remote streams being added/removed
- *
- */
- OT.PeerConnection = function(config) {
- var _peerConnection,
- _peerConnectionCompletionHandlers = [],
- _iceProcessor = new IceCandidateProcessor(),
- _offer,
- _answer,
- _state = 'new',
- _messageDelegates = [];
-
-
- OT.$.eventing(this);
-
- // if ice servers doesn't exist Firefox will throw an exception. Chrome
- // interprets this as 'Use my default STUN servers' whereas FF reads it
- // as 'Don't use STUN at all'. *Grumble*
- if (!config.iceServers) config.iceServers = [];
-
- // Private methods
- var delegateMessage = OT.$.bind(function(type, messagePayload) {
- if (_messageDelegates.length) {
- // We actually only ever send to the first delegate. This is because
- // each delegate actually represents a Publisher/Subscriber that
- // shares a single PeerConnection. If we sent to all delegates it
- // would result in each message being processed multiple times by
- // each PeerConnection.
- _messageDelegates[0](type, messagePayload);
- }
- }, this),
-
- // Create and initialise the PeerConnection object. This deals with
- // any differences between the various browser implementations and
- // our own TBPlugin version.
- //
- // +completion+ is the function is call once we've either successfully
- // created the PeerConnection or on failure.
- //
- // +localWebRtcStream+ will be null unless the callee is representing
- // a publisher. This is an unfortunate implementation limitation
- // of TBPlugin, it's not used for vanilla WebRTC. Hopefully this can
- // be tidied up later.
- //
- createPeerConnection = OT.$.bind(function (completion, localWebRtcStream) {
- if (_peerConnection) {
- completion.call(null, null, _peerConnection);
- return;
- }
-
- _peerConnectionCompletionHandlers.push(completion);
-
- if (_peerConnectionCompletionHandlers.length > 1) {
- // The PeerConnection is already being setup, just wait for
- // it to be ready.
- return;
- }
-
- var pcConstraints = {
- optional: [
- {DtlsSrtpKeyAgreement: true}
- ]
- };
-
- OT.debug('Creating peer connection config "' + JSON.stringify(config) + '".');
-
- if (!config.iceServers || config.iceServers.length === 0) {
- // This should never happen unless something is misconfigured
- OT.error('No ice servers present');
- }
-
- OT.$.createPeerConnection(config, pcConstraints, localWebRtcStream,
- OT.$.bind(attachEventsToPeerConnection, this));
- }, this),
-
- // An auxiliary function to createPeerConnection. This binds the various event callbacks
- // once the peer connection is created.
- //
- // +err+ will be non-null if an err occured while creating the PeerConnection
- // +pc+ will be the PeerConnection object itself.
- //
- attachEventsToPeerConnection = OT.$.bind(function(err, pc) {
- if (err) {
- triggerError('Failed to create PeerConnection, exception: ' +
- err.toString(), 'NewPeerConnection');
-
- _peerConnectionCompletionHandlers = [];
- return;
- }
-
- OT.debug('OT attachEventsToPeerConnection');
- _peerConnection = pc;
-
- _peerConnection.onicecandidate = iceCandidateForwarder(delegateMessage);
- _peerConnection.onaddstream = OT.$.bind(onRemoteStreamAdded, this);
- _peerConnection.onremovestream = OT.$.bind(onRemoteStreamRemoved, this);
-
- if (_peerConnection.onsignalingstatechange !== undefined) {
- _peerConnection.onsignalingstatechange = OT.$.bind(routeStateChanged, this);
- } else if (_peerConnection.onstatechange !== undefined) {
- _peerConnection.onstatechange = OT.$.bind(routeStateChanged, this);
- }
-
- if (_peerConnection.oniceconnectionstatechange !== undefined) {
- var failedStateTimer;
- _peerConnection.oniceconnectionstatechange = function (event) {
- if (event.target.iceConnectionState === 'failed') {
- if (failedStateTimer) {
- clearTimeout(failedStateTimer);
- }
- // We wait 5 seconds and make sure that it's still in the failed state
- // before we trigger the error. This is because we sometimes see
- // 'failed' and then 'connected' afterwards.
- setTimeout(function () {
- if (event.target.iceConnectionState === 'failed') {
- triggerError('The stream was unable to connect due to a network error.' +
- ' Make sure your connection isn\'t blocked by a firewall.', 'ICEWorkflow');
- }
- }, 5000);
- }
- };
- }
-
- triggerPeerConnectionCompletion(null);
- }, this),
-
- triggerPeerConnectionCompletion = function () {
- while (_peerConnectionCompletionHandlers.length) {
- _peerConnectionCompletionHandlers.shift().call(null);
- }
- },
-
- // Clean up the Peer Connection and trigger the close event.
- // This function can be called safely multiple times, it will
- // only trigger the close event once (per PeerConnection object)
- tearDownPeerConnection = function() {
- // Our connection is dead, stop processing ICE candidates
- if (_iceProcessor) _iceProcessor.setPeerConnection(null);
-
- qos.stopCollecting();
-
- if (_peerConnection !== null) {
- if (_peerConnection.destroy) {
- // OTPlugin defines a destroy method on PCs. This allows
- // the plugin to release any resources that it's holding.
- _peerConnection.destroy();
- }
-
- _peerConnection = null;
- this.trigger('close');
- }
- },
-
- routeStateChanged = function(event) {
- var newState;
-
- if (typeof(event) === 'string') {
- // The newest version of the API
- newState = event;
-
- } else if (event.target && event.target.signalingState) {
- // The slightly older version
- newState = event.target.signalingState;
-
- } else {
- // At least six months old version. Positively ancient, yeah?
- newState = event.target.readyState;
- }
-
- if (newState && newState.toLowerCase() !== _state) {
- _state = newState.toLowerCase();
- OT.debug('PeerConnection.stateChange: ' + _state);
-
- switch(_state) {
- case 'closed':
- tearDownPeerConnection.call(this);
- break;
- }
- }
- },
-
- qosCallback = OT.$.bind(function(parsedStats) {
- this.trigger('qos', parsedStats);
- }, this),
-
- getRemoteStreams = function() {
- var streams;
-
- if (_peerConnection.getRemoteStreams) {
- streams = _peerConnection.getRemoteStreams();
- } else if (_peerConnection.remoteStreams) {
- streams = _peerConnection.remoteStreams;
- } else {
- throw new Error('Invalid Peer Connection object implements no ' +
- 'method for retrieving remote streams');
- }
-
- // Force streams to be an Array, rather than a 'Sequence' object,
- // which is browser dependent and does not behaviour like an Array
- // in every case.
- return Array.prototype.slice.call(streams);
- },
-
- /// PeerConnection signaling
- onRemoteStreamAdded = function(event) {
- this.trigger('streamAdded', event.stream);
- },
-
- onRemoteStreamRemoved = function(event) {
- this.trigger('streamRemoved', event.stream);
- },
-
- // ICE Negotiation messages
-
-
- // Relays a SDP payload (+sdp+), that is part of a message of type +messageType+
- // via the registered message delegators
- relaySDP = function(messageType, sdp) {
- delegateMessage(messageType, sdp);
- },
-
-
- // Process an offer that
- processOffer = function(message) {
- var offer = new NativeRTCSessionDescription({type: 'offer', sdp: message.content.sdp}),
-
- // Relays +answer+ Answer
- relayAnswer = function(answer) {
- _iceProcessor.setPeerConnection(_peerConnection);
- _iceProcessor.processPending();
- relaySDP(OT.Raptor.Actions.ANSWER, answer);
-
- qos.startCollecting(_peerConnection);
- },
-
- reportError = function(message, errorReason, prefix) {
- triggerError('PeerConnection.offerProcessor ' + message + ': ' +
- errorReason, prefix);
- };
-
- createPeerConnection(function() {
- offerProcessor(
- _peerConnection,
- offer,
- relayAnswer,
- reportError
- );
- });
- },
-
- processAnswer = function(message) {
- if (!message.content.sdp) {
- OT.error('PeerConnection.processMessage: Weird answer message, no SDP.');
- return;
- }
-
- _answer = new NativeRTCSessionDescription({type: 'answer', sdp: message.content.sdp});
-
- _peerConnection.setRemoteDescription(_answer,
- function () {
- OT.debug('setRemoteDescription Success');
- }, function (errorReason) {
- triggerError('Error while setting RemoteDescription ' + errorReason,
- 'SetRemoteDescription');
- });
-
- _iceProcessor.setPeerConnection(_peerConnection);
- _iceProcessor.processPending();
-
- qos.startCollecting(_peerConnection);
- },
-
- processSubscribe = function() {
- OT.debug('PeerConnection.processSubscribe: Sending offer to subscriber.');
-
- if (!_peerConnection) {
- // TODO(rolly) I need to examine whether this can
- // actually happen. If it does happen in the short
- // term, I want it to be noisy.
- throw new Error('PeerConnection broke!');
- }
-
- createPeerConnection(function() {
- suscribeProcessor(
- _peerConnection,
-
- // Success: Relay Offer
- function(offer) {
- _offer = offer;
- relaySDP(OT.Raptor.Actions.OFFER, _offer);
- },
-
- // Failure
- function(message, errorReason, prefix) {
- triggerError('PeerConnection.suscribeProcessor ' + message + ': ' +
- errorReason, prefix);
- }
- );
- });
- },
-
- triggerError = OT.$.bind(function(errorReason, prefix) {
- OT.error(errorReason);
- this.trigger('error', errorReason, prefix);
- }, this);
-
- this.addLocalStream = function(webRTCStream) {
- createPeerConnection(function() {
- _peerConnection.addStream(webRTCStream);
- }, webRTCStream);
- };
-
- this.disconnect = function() {
- _iceProcessor = null;
-
- if (_peerConnection) {
- var currentState = (_peerConnection.signalingState || _peerConnection.readyState);
- if (currentState && currentState.toLowerCase() !== 'closed') _peerConnection.close();
-
- // In theory, calling close on the PeerConnection should trigger a statechange
- // event with 'close'. For some reason I'm not seeing this in FF, hence we're
- // calling it manually below
- tearDownPeerConnection.call(this);
- }
-
- this.off();
- };
-
- this.processMessage = function(type, message) {
- OT.debug('PeerConnection.processMessage: Received ' +
- type + ' from ' + message.fromAddress);
-
- OT.debug(message);
-
- switch(type) {
- case 'generateoffer':
- processSubscribe.call(this, message);
- break;
-
- case 'offer':
- processOffer.call(this, message);
- break;
-
- case 'answer':
- case 'pranswer':
- processAnswer.call(this, message);
- break;
-
- case 'candidate':
- _iceProcessor.process(message);
- break;
-
- default:
- OT.debug('PeerConnection.processMessage: Received an unexpected message of type ' +
- type + ' from ' + message.fromAddress + ': ' + JSON.stringify(message));
- }
-
- return this;
- };
-
- this.setIceServers = function (iceServers) {
- if (iceServers) {
- config.iceServers = iceServers;
- }
- };
-
- this.registerMessageDelegate = function(delegateFn) {
- return _messageDelegates.push(delegateFn);
- };
-
- this.unregisterMessageDelegate = function(delegateFn) {
- var index = OT.$.arrayIndexOf(_messageDelegates, delegateFn);
-
- if ( index !== -1 ) {
- _messageDelegates.splice(index, 1);
- }
- return _messageDelegates.length;
- };
-
- this.remoteStreams = function() {
- return _peerConnection ? getRemoteStreams() : [];
- };
-
- this.getStatsWithSingleParameter = function(callback) {
- if (OT.$.hasCapabilities('getStatsWithSingleParameter')) {
- createPeerConnection(function() {
- _peerConnection.getStats(callback);
- });
- }
- };
-
- var qos = new OT.PeerConnection.QOS(qosCallback);
- };
-
-})(window);
-//
-// There are three implementations of stats parsing in this file.
-// 1. For Chrome: Chrome is currently using an older version of the API
-// 2. For OTPlugin: The plugin is using a newer version of the API that
-// exists in the latest WebRTC codebase
-// 3. For Firefox: FF is using a version that looks a lot closer to the
-// current spec.
-//
-// I've attempted to keep the three implementations from sharing any code,
-// accordingly you'll notice a bunch of duplication between the three.
-//
-// This is acceptable as the goal is to be able to remove each implementation
-// as it's no longer needed without any risk of affecting the others. If there
-// was shared code between them then each removal would require an audit of
-// all the others.
-//
-//
!(function() {
- ///
- // Get Stats using the older API. Used by all current versions
- // of Chrome.
+ var MAX_SIGNAL_DATA_LENGTH = 8192,
+ MAX_SIGNAL_TYPE_LENGTH = 128;
+
//
- var parseStatsOldAPI = function parseStatsOldAPI (peerConnection,
- prevStats,
- currentStats,
- completion) {
-
- /* this parses a result if there it contains the video bitrate */
- var parseAvgVideoBitrate = function (result) {
- if (result.stat('googFrameHeightSent')) {
- currentStats.videoBytesTransferred = result.stat('bytesSent');
- } else if (result.stat('googFrameHeightReceived')) {
- currentStats.videoBytesTransferred = result.stat('bytesReceived');
- } else {
- return NaN;
- }
-
- var transferDelta = currentStats.videoBytesTransferred -
- (prevStats.videoBytesTransferred || 0);
-
- return Math.round(transferDelta * 8 / currentStats.deltaSecs);
- },
-
- /* this parses a result if there it contains the audio bitrate */
- parseAvgAudioBitrate = function (result) {
- if (result.stat('audioInputLevel')) {
- currentStats.audioBytesTransferred = result.stat('bytesSent');
- } else if (result.stat('audioOutputLevel')) {
- currentStats.audioBytesTransferred = result.stat('bytesReceived');
- } else {
- return NaN;
- }
-
- var transferDelta = currentStats.audioBytesTransferred -
- (prevStats.audioBytesTransferred || 0);
- return Math.round(transferDelta * 8 / currentStats.deltaSecs);
- },
-
- parseFrameRate = function (result) {
- if (result.stat('googFrameRateSent')) {
- return result.stat('googFrameRateSent');
- } else if (result.stat('googFrameRateReceived')) {
- return result.stat('googFrameRateReceived');
- }
- return null;
- },
-
- parseStatsReports = function (stats) {
- if (stats.result) {
- var resultList = stats.result();
- for (var resultIndex = 0; resultIndex < resultList.length; resultIndex++) {
- var result = resultList[resultIndex];
-
- if (result.stat) {
-
- if(result.stat('googActiveConnection') === 'true') {
- currentStats.localCandidateType = result.stat('googLocalCandidateType');
- currentStats.remoteCandidateType = result.stat('googRemoteCandidateType');
- currentStats.transportType = result.stat('googTransportType');
- }
-
- var avgVideoBitrate = parseAvgVideoBitrate(result);
- if (!isNaN(avgVideoBitrate)) {
- currentStats.avgVideoBitrate = avgVideoBitrate;
- }
-
- var avgAudioBitrate = parseAvgAudioBitrate(result);
- if (!isNaN(avgAudioBitrate)) {
- currentStats.avgAudioBitrate = avgAudioBitrate;
- }
-
- var frameRate = parseFrameRate(result);
- if (frameRate != null) {
- currentStats.frameRate = frameRate;
- }
- }
- }
- }
-
- completion(null, currentStats);
- };
-
- peerConnection.getStats(parseStatsReports);
- };
-
- ///
- // Get Stats for the OT Plugin, newer than Chromes version, but
- // still not in sync with the spec.
+ // Error Codes:
+ // 413 - Type too long
+ // 400 - Type is invalid
+ // 413 - Data too long
+ // 400 - Data is invalid (can't be parsed as JSON)
+ // 429 - Rate limit exceeded
+ // 500 - Websocket connection is down
+ // 404 - To connection does not exist
+ // 400 - To is invalid
//
- var parseStatsOTPlugin = function parseStatsOTPlugin (peerConnection,
- prevStats,
- currentStats,
- completion) {
+ OT.Signal = function(sessionId, fromConnectionId, options) {
+ var isInvalidType = function(type) {
+ // Our format matches the unreserved characters from the URI RFC:
+ // http://www.ietf.org/rfc/rfc3986
+ return !/^[a-zA-Z0-9\-\._~]+$/.exec(type);
+ },
- var onStatsError = function onStatsError (error) {
- completion(error);
- },
-
- ///
- // From the Audio Tracks
- // * avgAudioBitrate
- // * audioBytesTransferred
- //
- parseAudioStats = function (statsReport) {
- var lastBytesSent = prevStats.audioBytesTransferred || 0,
- transferDelta;
-
- if (statsReport.audioInputLevel) {
- currentStats.audioBytesTransferred = statsReport.bytesSent;
- }
- else if (statsReport.audioOutputLevel) {
- currentStats.audioBytesTransferred = statsReport.bytesReceived;
- }
-
- if (currentStats.audioBytesTransferred) {
- transferDelta = currentStats.audioBytesTransferred - lastBytesSent;
- currentStats.avgAudioBitrate = Math.round(transferDelta * 8 / currentStats.deltaSecs);
- }
- },
-
- ///
- // From the Video Tracks
- // * frameRate
- // * avgVideoBitrate
- // * videoBytesTransferred
- //
- parseVideoStats = function (statsReport) {
-
- var lastBytesSent = prevStats.videoBytesTransferred || 0,
- transferDelta;
-
- if (statsReport.googFrameHeightSent) {
- currentStats.videoBytesTransferred = statsReport.bytesSent;
- }
- else if (statsReport.googFrameHeightReceived) {
- currentStats.videoBytesTransferred = statsReport.bytesReceived;
- }
-
- if (currentStats.videoBytesTransferred) {
- transferDelta = currentStats.videoBytesTransferred - lastBytesSent;
- currentStats.avgVideoBitrate = Math.round(transferDelta * 8 / currentStats.deltaSecs);
- }
-
- if (statsReport.googFrameRateSent) {
- currentStats.frameRate = statsReport.googFrameRateSent;
- } else if (statsReport.googFrameRateReceived) {
- currentStats.frameRate = statsReport.googFrameRateReceived;
- }
- },
-
- isStatsForVideoTrack = function(statsReport) {
- return statsReport.googFrameHeightSent !== void 0 ||
- statsReport.googFrameHeightReceived !== void 0 ||
- currentStats.videoBytesTransferred !== void 0 ||
- statsReport.googFrameRateSent !== void 0;
- },
-
- isStatsForIceCandidate = function(statsReport) {
- return statsReport.googActiveConnection === 'true';
- };
-
- peerConnection.getStats(null, function(statsReports) {
- statsReports.forEach(function(statsReport) {
- if (isStatsForIceCandidate(statsReport)) {
- currentStats.localCandidateType = statsReport.googLocalCandidateType;
- currentStats.remoteCandidateType = statsReport.googRemoteCandidateType;
- currentStats.transportType = statsReport.googTransportType;
+ validateTo = function(toAddress) {
+ if (!toAddress) {
+ return {
+ code: 400,
+ reason: 'The signal to field was invalid. Either set it to a OT.Connection, ' +
+ 'OT.Session, or omit it entirely'
+ };
}
- else if (isStatsForVideoTrack(statsReport)) {
- parseVideoStats(statsReport);
+
+ if ( !(toAddress instanceof OT.Connection || toAddress instanceof OT.Session) ) {
+ return {
+ code: 400,
+ reason: 'The To field was invalid'
+ };
+ }
+
+ return null;
+ },
+
+ validateType = function(type) {
+ var error = null;
+
+ if (type === null || type === void 0) {
+ error = {
+ code: 400,
+ reason: 'The signal type was null or undefined. Either set it to a String value or ' +
+ 'omit it'
+ };
+ }
+ else if (type.length > MAX_SIGNAL_TYPE_LENGTH) {
+ error = {
+ code: 413,
+ reason: 'The signal type was too long, the maximum length of it is ' +
+ MAX_SIGNAL_TYPE_LENGTH + ' characters'
+ };
+ }
+ else if ( isInvalidType(type) ) {
+ error = {
+ code: 400,
+ reason: 'The signal type was invalid, it can only contain letters, ' +
+ 'numbers, \'-\', \'_\', and \'~\'.'
+ };
+ }
+
+ return error;
+ },
+
+ validateData = function(data) {
+ var error = null;
+ if (data === null || data === void 0) {
+ error = {
+ code: 400,
+ reason: 'The signal data was null or undefined. Either set it to a String value or ' +
+ 'omit it'
+ };
}
else {
- parseAudioStats(statsReport);
- }
- });
-
- completion(null, currentStats);
- }, onStatsError);
- };
-
-
- ///
- // Get Stats using the newer API.
- //
- var parseStatsNewAPI = function parseStatsNewAPI (peerConnection,
- prevStats,
- currentStats,
- completion) {
-
- var onStatsError = function onStatsError (error) {
- completion(error);
- },
-
- parseAvgVideoBitrate = function parseAvgVideoBitrate (result) {
- if (result.bytesSent || result.bytesReceived) {
- currentStats.videoBytesTransferred = result.bytesSent || result.bytesReceived;
- }
- else {
- return NaN;
- }
-
- var transferDelta = currentStats.videoBytesTransferred -
- (prevStats.videoBytesTransferred || 0);
-
- return Math.round(transferDelta * 8 / currentStats.deltaSecs);
- },
-
- parseAvgAudioBitrate = function parseAvgAudioBitrate (result) {
- if (result.bytesSent || result.bytesReceived) {
- currentStats.audioBytesTransferred = result.bytesSent || result.bytesReceived;
- } else {
- return NaN;
- }
-
- var transferDelta = currentStats.audioBytesTransferred -
- (prevStats.audioBytesTransferred || 0);
- return Math.round(transferDelta * 8 / currentStats.deltaSecs);
- };
-
-
- peerConnection.getStats(null, function(stats) {
-
- for (var key in stats) {
- if (stats.hasOwnProperty(key) &&
- (stats[key].type === 'outboundrtp' || stats[key].type === 'inboundrtp')) {
-
- var res = stats[key];
-
- // Find the bandwidth info for video
- if (res.id.indexOf('video') !== -1) {
- var avgVideoBitrate = parseAvgVideoBitrate(res);
- if(!isNaN(avgVideoBitrate)) {
- currentStats.avgVideoBitrate = avgVideoBitrate;
+ try {
+ if (JSON.stringify(data).length > MAX_SIGNAL_DATA_LENGTH) {
+ error = {
+ code: 413,
+ reason: 'The data field was too long, the maximum size of it is ' +
+ MAX_SIGNAL_DATA_LENGTH + ' characters'
+ };
}
-
- } else if (res.id.indexOf('audio') !== -1) {
- var avgAudioBitrate = parseAvgAudioBitrate(res);
- if(!isNaN(avgAudioBitrate)) {
- currentStats.avgAudioBitrate = avgAudioBitrate;
- }
-
+ }
+ catch(e) {
+ error = {code: 400, reason: 'The data field was not valid JSON'};
}
}
- }
- completion(null, currentStats);
- }, onStatsError);
- };
-
-
- var parseQOS = function (peerConnection, prevStats, currentStats, completion) {
- var firefoxVersion = window.navigator.userAgent
- .toLowerCase().match(/Firefox\/([0-9\.]+)/i);
-
- if (TBPlugin.isInstalled()) {
- parseQOS = parseStatsOTPlugin;
- return parseStatsOTPlugin(peerConnection, prevStats, currentStats, completion);
- }
- else if (firefoxVersion !== null && parseFloat(firefoxVersion[1], 10) >= 27.0) {
- parseQOS = parseStatsNewAPI;
- return parseStatsNewAPI(peerConnection, prevStats, currentStats, completion);
- }
- else {
- parseQOS = parseStatsOldAPI;
- return parseStatsOldAPI(peerConnection, prevStats, currentStats, completion);
- }
- };
-
- OT.PeerConnection.QOS = function (qosCallback) {
- var _creationTime = OT.$.now(),
- _peerConnection;
-
- var calculateQOS = OT.$.bind(function calculateQOS (prevStats) {
- if (!_peerConnection) {
- // We don't have a PeerConnection yet, or we did and
- // it's been closed. Either way we're done.
- return;
- }
-
- var now = OT.$.now();
-
- var currentStats = {
- timeStamp: now,
- duration: Math.round(now - _creationTime),
- deltaSecs: (now - prevStats.timeStamp) / 1000
+ return error;
};
- var onParsedStats = function (err, parsedStats) {
- if (err) {
- OT.error('Failed to Parse QOS Stats: ' + JSON.stringify(err));
- return;
- }
- qosCallback(parsedStats, prevStats);
+ this.toRaptorMessage = function() {
+ var to = this.to;
- // Recalculate the stats
- setTimeout(OT.$.bind(calculateQOS, null, parsedStats), OT.PeerConnection.QOS.INTERVAL);
- };
-
- parseQOS(_peerConnection, prevStats, currentStats, onParsedStats);
- }, this);
-
-
- this.startCollecting = function (peerConnection) {
- if (!peerConnection || !peerConnection.getStats) {
- // It looks like this browser doesn't support getStats
- // Bail.
- return;
+ if (to && typeof(to) !== 'string') {
+ to = to.id;
}
- _peerConnection = peerConnection;
-
- calculateQOS({
- timeStamp: OT.$.now()
- });
+ return OT.Raptor.Message.signals.create(OT.APIKEY, sessionId, to, this.type, this.data);
};
- this.stopCollecting = function () {
- _peerConnection = null;
+ this.toHash = function() {
+ return options;
};
- };
- // Recalculate the stats in 30 sec
- OT.PeerConnection.QOS.INTERVAL = 30000;
-})();
-!(function() {
- var _peerConnections = {};
+ this.error = null;
- OT.PeerConnections = {
- add: function(remoteConnection, streamId, config) {
- var key = remoteConnection.id + '_' + streamId,
- ref = _peerConnections[key];
-
- if (!ref) {
- ref = _peerConnections[key] = {
- count: 0,
- pc: new OT.PeerConnection(config)
- };
+ if (options) {
+ if (options.hasOwnProperty('data')) {
+ this.data = OT.$.clone(options.data);
+ this.error = validateData(this.data);
}
- // increase the PCs ref count by 1
- ref.count += 1;
+ if (options.hasOwnProperty('to')) {
+ this.to = options.to;
- return ref.pc;
- },
-
- remove: function(remoteConnection, streamId) {
- var key = remoteConnection.id + '_' + streamId,
- ref = _peerConnections[key];
-
- if (ref) {
- ref.count -= 1;
-
- if (ref.count === 0) {
- ref.pc.disconnect();
- delete _peerConnections[key];
+ if (!this.error) {
+ this.error = validateTo(this.to);
}
}
+
+ if (options.hasOwnProperty('type')) {
+ if (!this.error) {
+ this.error = validateType(options.type);
+ }
+ this.type = options.type;
+ }
}
+
+ this.valid = this.error === null;
};
-})(window);
-!(function() {
+}(this));
- /*
- * Abstracts PeerConnection related stuff away from OT.Publisher.
- *
- * Responsible for:
- * * setting up the underlying PeerConnection (delegates to OT.PeerConnections)
- * * triggering a connected event when the Peer connection is opened
- * * triggering a disconnected event when the Peer connection is closed
- * * providing a destroy method
- * * providing a processMessage method
- *
- * Once the PeerConnection is connected and the video element playing it triggers
- * the connected event
- *
- * Triggers the following events
- * * connected
- * * disconnected
- */
- OT.PublisherPeerConnection = function(remoteConnection, session, streamId, webRTCStream) {
- var _peerConnection,
- _hasRelayCandidates = false,
- _subscriberId = session._.subscriberMap[remoteConnection.id + '_' + streamId],
- _onPeerClosed,
- _onPeerError,
- _relayMessageToPeer,
- _onQOS;
+// tb_require('../../../helpers/helpers.js')
+// tb_require('../rumor/rumor.js')
+// tb_require('./message.js')
+// tb_require('./dispatch.js')
+// tb_require('./signal.js')
- // Private
- _onPeerClosed = function() {
- this.destroy();
- this.trigger('disconnected', this);
- };
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
- // Note: All Peer errors are fatal right now.
- _onPeerError = function(errorReason, prefix) {
- this.trigger('error', null, errorReason, this, prefix);
- this.destroy();
- };
+function SignalError(code, message) {
+ this.code = code;
+ this.message = message;
- _relayMessageToPeer = OT.$.bind(function(type, payload) {
- if (!_hasRelayCandidates){
- var extractCandidates = type === OT.Raptor.Actions.CANDIDATE ||
- type === OT.Raptor.Actions.OFFER ||
- type === OT.Raptor.Actions.ANSWER ||
- type === OT.Raptor.Actions.PRANSWER ;
+ // Undocumented. Left in for backwards compatibility:
+ this.reason = message;
+}
- if (extractCandidates) {
- var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp;
- _hasRelayCandidates = message.indexOf('typ relay') !== -1;
+// The Dispatcher bit is purely to make testing simpler, it defaults to a new OT.Raptor.Dispatcher
+// so in normal operation you would omit it.
+OT.Raptor.Socket = function(connectionId, widgetId, messagingSocketUrl, symphonyUrl, dispatcher) {
+ var _states = ['disconnected', 'connecting', 'connected', 'error', 'disconnecting'],
+ _sessionId,
+ _token,
+ _rumor,
+ _dispatcher,
+ _completion;
+
+
+ //// Private API
+ var setState = OT.$.statable(this, _states, 'disconnected'),
+
+ onConnectComplete = function onConnectComplete(error) {
+ if (error) {
+ setState('error');
}
- }
-
- switch(type) {
- case OT.Raptor.Actions.ANSWER:
- case OT.Raptor.Actions.PRANSWER:
- if (session.sessionInfo.p2pEnabled) {
- session._.jsepAnswerP2p(streamId, _subscriberId, payload.sdp);
- } else {
- session._.jsepAnswer(streamId, payload.sdp);
- }
-
- break;
-
- case OT.Raptor.Actions.OFFER:
- this.trigger('connected');
-
- if (session.sessionInfo.p2pEnabled) {
- session._.jsepOfferP2p(streamId, _subscriberId, payload.sdp);
-
- } else {
- session._.jsepOffer(streamId, payload.sdp);
- }
-
- break;
-
- case OT.Raptor.Actions.CANDIDATE:
- if (session.sessionInfo.p2pEnabled) {
- session._.jsepCandidateP2p(streamId, _subscriberId, payload);
-
- } else {
- session._.jsepCandidate(streamId, payload);
- }
- }
- }, this);
-
- _onQOS = OT.$.bind(function _onQOS (parsedStats, prevStats) {
- this.trigger('qos', remoteConnection, parsedStats, prevStats);
- }, this);
-
- OT.$.eventing(this);
-
- // Public
- this.destroy = function() {
- // Clean up our PeerConnection
- if (_peerConnection) {
- _peerConnection.off();
- OT.PeerConnections.remove(remoteConnection, streamId);
- }
-
- _peerConnection = null;
- };
-
- this.processMessage = function(type, message) {
- _peerConnection.processMessage(type, message);
- };
-
- // Init
- this.init = function(iceServers) {
- _peerConnection = OT.PeerConnections.add(remoteConnection, streamId, {
- iceServers: iceServers
- });
-
- _peerConnection.on({
- close: _onPeerClosed,
- error: _onPeerError,
- qos: _onQOS
- }, this);
-
- _peerConnection.registerMessageDelegate(_relayMessageToPeer);
- _peerConnection.addLocalStream(webRTCStream);
-
- this.remoteConnection = function() {
- return remoteConnection;
- };
-
- this.hasRelayCandidates = function() {
- return _hasRelayCandidates;
- };
-
- };
- };
-
-})(window);
-!(function() {
-
- /*
- * Abstracts PeerConnection related stuff away from OT.Subscriber.
- *
- * Responsible for:
- * * setting up the underlying PeerConnection (delegates to OT.PeerConnections)
- * * triggering a connected event when the Peer connection is opened
- * * triggering a disconnected event when the Peer connection is closed
- * * creating a video element when a stream is added
- * * responding to stream removed intelligently
- * * providing a destroy method
- * * providing a processMessage method
- *
- * Once the PeerConnection is connected and the video element playing it
- * triggers the connected event
- *
- * Triggers the following events
- * * connected
- * * disconnected
- * * remoteStreamAdded
- * * remoteStreamRemoved
- * * error
- *
- */
-
- OT.SubscriberPeerConnection = function(remoteConnection, session, stream,
- subscriber, properties) {
- var _peerConnection,
- _destroyed = false,
- _hasRelayCandidates = false,
- _onPeerClosed,
- _onRemoteStreamAdded,
- _onRemoteStreamRemoved,
- _onPeerError,
- _relayMessageToPeer,
- _setEnabledOnStreamTracksCurry,
- _onQOS;
-
- // Private
- _onPeerClosed = function() {
- this.destroy();
- this.trigger('disconnected', this);
- };
-
- _onRemoteStreamAdded = function(remoteRTCStream) {
- this.trigger('remoteStreamAdded', remoteRTCStream, this);
- };
-
- _onRemoteStreamRemoved = function(remoteRTCStream) {
- this.trigger('remoteStreamRemoved', remoteRTCStream, this);
- };
-
- // Note: All Peer errors are fatal right now.
- _onPeerError = function(errorReason, prefix) {
- this.trigger('error', errorReason, this, prefix);
- };
-
- _relayMessageToPeer = OT.$.bind(function(type, payload) {
- if (!_hasRelayCandidates){
- var extractCandidates = type === OT.Raptor.Actions.CANDIDATE ||
- type === OT.Raptor.Actions.OFFER ||
- type === OT.Raptor.Actions.ANSWER ||
- type === OT.Raptor.Actions.PRANSWER ;
-
- if (extractCandidates) {
- var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp;
- _hasRelayCandidates = message.indexOf('typ relay') !== -1;
- }
- }
-
- switch(type) {
- case OT.Raptor.Actions.ANSWER:
- case OT.Raptor.Actions.PRANSWER:
- this.trigger('connected');
-
- session._.jsepAnswerP2p(stream.id, subscriber.widgetId, payload.sdp);
- break;
-
- case OT.Raptor.Actions.OFFER:
- session._.jsepOfferP2p(stream.id, subscriber.widgetId, payload.sdp);
- break;
-
- case OT.Raptor.Actions.CANDIDATE:
- session._.jsepCandidateP2p(stream.id, subscriber.widgetId, payload);
- break;
- }
- }, this);
-
- // Helper method used by subscribeToAudio/subscribeToVideo
- _setEnabledOnStreamTracksCurry = function(isVideo) {
- var method = 'get' + (isVideo ? 'Video' : 'Audio') + 'Tracks';
-
- return function(enabled) {
- var remoteStreams = _peerConnection.remoteStreams(),
- tracks,
- stream;
-
- if (remoteStreams.length === 0 || !remoteStreams[0][method]) {
- // either there is no remote stream or we are in a browser that doesn't
- // expose the media tracks (Firefox)
- return;
+ else {
+ setState('connected');
}
- for (var i=0, num=remoteStreams.length; 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.
- *
- * style
, it is an object that has the following
- * properties:
- *
- *
- *
- * audioLevelDisplayMode
(String) — How to display the audio level
- * indicator. Possible values are: "auto"
(the indicator is displayed when the
- * video is disabled), "off"
(the indicator is not displayed), and
- * "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
- * the background image when a video is not displayed. (A video may not be displayed if
- * you call publishVideo(false)
on the Publisher object). You can pass an http
- * or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
- * data
URI scheme (instead of http or https) and pass in base-64-encrypted
- * PNG data, such as that obtained from the
- * Publisher.getImgData() method. For example,
- * you could set the property to "data:VBORw0KGgoAA..."
, where the portion of
- * the string after "data:"
is the result of a call to
- * Publisher.getImgData()
. If the URL or the image data is invalid, the
- * property is ignored (the attempt to set the image fails silently).
- * backgroundImageURI
style to a string larger than
- * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
- * limitation, you cannot set the backgroundImageURI
style to a string obtained
- * with the getImgData()
method.
- * buttonDisplayMode
(String) — How to display the microphone
- * controls. Possible values are: "auto"
(controls are displayed when the
- * stream is first displayed and when the user mouses over the display), "off"
- * (controls are not displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
- * Possible values are: "auto"
(the name is displayed when the stream is first
- * displayed and when the user mouses over the display), "off"
(the name is not
- * displayed), and "on"
(the name is always displayed).
For example, the following code passes one parameter to the method:
- * - *myPublisher.setStyle({nameDisplayMode: "off"});- * - *
If you pass two parameters, style
and value
, they are
- * key-value pair that define one property of the display style. For example, the following
- * code passes two parameter values to the method:
myPublisher.setStyle("nameDisplayMode", "off");- * - *
You can set the initial settings when you call the Session.publish()
- * or OT.initPublisher()
method. Pass a style
property as part of the
- * properties
parameter of the method.
The OT object dispatches an exception
event if you pass in an invalid style
- * to the method. The code
property of the ExceptionEvent object is set to 1011.
style
passed in. Pass a value
- * for this parameter only if the value of the style
parameter is a String.
- *
- * @see getStyle()
- * @return {Publisher} The Publisher object
- * @see setStyle()
- *
- * @see Session.publish()
- * @see OT.initPublisher()
- * @method #setStyle
- * @memberOf Publisher
- */
-
- /**
- * Sets properties that define the appearance of some user interface controls of the Subscriber.
- *
- * You can either pass one parameter or two parameters to this method.
- * - *If you pass one parameter, style
, it is an object that has the following
- * properties:
- *
- *
audioLevelDisplayMode
(String) — How to display the audio level
- * indicator. Possible values are: "auto"
(the indicator is displayed when the
- * video is disabled), "off"
(the indicator is not displayed), and
- * "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
- * the background image when a video is not displayed. (A video may not be displayed if
- * you call subscribeToVideo(false)
on the Publisher object). You can pass an
- * http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
- * data
URI scheme (instead of http or https) and pass in base-64-encrypted
- * PNG data, such as that obtained from the
- * Subscriber.getImgData() method. For example,
- * you could set the property to "data:VBORw0KGgoAA..."
, where the portion of
- * the string after "data:"
is the result of a call to
- * Publisher.getImgData()
. If the URL or the image data is invalid, the
- * property is ignored (the attempt to set the image fails silently).
- *
- * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
- * you cannot set the backgroundImageURI
style to a string larger than
- * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
- * limitation, you cannot set the backgroundImageURI
style to a string obtained
- * with the getImgData()
method.
- *
buttonDisplayMode
(String) — How to display the speaker
- * controls. Possible values are: "auto"
(controls are displayed when the
- * stream is first displayed and when the user mouses over the display), "off"
- * (controls are not displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
- * Possible values are: "auto"
(the name is displayed when the stream is first
- * displayed and when the user mouses over the display), "off"
(the name is not
- * displayed), and "on"
(the name is always displayed).videoDisabledDisplayMode
(String) Whether to display the video
- * disabled indicator and video disabled warning icons for a Subscriber. These icons
- * indicate that the video has been disabled (or is in risk of being disabled for
- * the warning icon) due to poor stream quality. Possible values are: "auto"
- * (the icons are automatically when the displayed video is disabled or in risk of being
- * disabled due to poor stream quality), "off"
(do not display the icons), and
- * "on"
(display the icons).For example, the following code passes one parameter to the method:
- * - *mySubscriber.setStyle({nameDisplayMode: "off"});- * - *
If you pass two parameters, style
and value
, they are key-value
- * pair that define one property of the display style. For example, the following code passes
- * two parameter values to the method:
mySubscriber.setStyle("nameDisplayMode", "off");- * - *
You can set the initial settings when you call the Session.subscribe()
method.
- * Pass a style
property as part of the properties
parameter of the
- * method.
The OT object dispatches an exception
event if you pass in an invalid style
- * to the method. The code
property of the ExceptionEvent object is set to 1011.
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'
- ];
-
- _validStyleValues = {
- buttonDisplayMode: ['auto', 'mini', 'mini-auto', 'off', 'on'],
- nameDisplayMode: ['auto', 'off', 'on'],
- audioLevelDisplayMode: ['auto', 'off', 'on'],
- showSettingsButton: [true, false],
- showMicButton: [true, false],
- backgroundImageURI: null,
- showControlBar: [true, false],
- showArchiveStatus: [true, false],
- videoDisabledDisplayMode: ['auto', 'off', 'on']
- };
-
-
- // Validates the style +key+ and also whether +value+ is valid for +key+
- isValidStyle = function(key, value) {
- return key === 'backgroundImageURI' ||
- (_validStyleValues.hasOwnProperty(key) &&
- OT.$.arrayIndexOf(_validStyleValues[key], value) !== -1 );
- };
-
- castValue = function(value) {
- switch(value) {
- case 'true':
- return true;
- case 'false':
- return false;
- default:
- return value;
- }
- };
-
- // Returns a shallow copy of the styles.
- this.getAll = function() {
- var style = OT.$.clone(_style);
-
- for (var key in style) {
- if(!style.hasOwnProperty(key)) {
- continue;
- }
- if (OT.$.arrayIndexOf(_COMPONENT_STYLES, key) < 0) {
-
- // Strip unnecessary properties out, should this happen on Set?
- delete style[key];
- }
- }
-
- return style;
- };
-
- this.get = function(key) {
- if (key) {
- return _style[key];
- }
-
- // We haven't been asked for any specific key, just return the lot
- return this.getAll();
- };
-
- // *note:* this will not trigger onStyleChange if +silent+ is truthy
- this.setAll = function(newStyles, silent) {
- var oldValue, newValue;
-
- for (var key in newStyles) {
- if(!newStyles.hasOwnProperty(key)) {
- continue;
- }
- newValue = castValue(newStyles[key]);
-
- if (isValidStyle(key, newValue)) {
- oldValue = _style[key];
-
- if (newValue !== oldValue) {
- _style[key] = newValue;
- if (!silent) onStyleChange(key, newValue, oldValue);
- }
-
- } else {
- OT.warn('Style.setAll::Invalid style property passed ' + key + ' : ' + newValue);
- }
- }
-
- return this;
- };
-
- this.set = function(key, value) {
- OT.debug('setStyle: ' + key.toString());
-
- var newValue = castValue(value),
- oldValue;
-
- if (!isValidStyle(key, newValue)) {
- OT.warn('Style.set::Invalid style property passed ' + key + ' : ' + newValue);
- return this;
- }
-
- oldValue = _style[key];
- if (newValue !== oldValue) {
- _style[key] = newValue;
-
- onStyleChange(key, value, oldValue);
- }
-
- return this;
- };
-
-
- if (initalStyles) this.setAll(initalStyles, true);
- };
-
-})(window);
-!(function() {
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
/*
- * A Publishers Microphone.
+ * A RTCPeerConnection.getStats
based audio level sampler.
*
- * TODO
- * * bind to changes in mute/unmute/volume/etc and respond to them
+ * It uses the the getStats
method to get the audioOutputLevel
.
+ * This implementation expects the single parameter version of the getStats
method.
+ *
+ * Currently the audioOutputLevel
stats is only supported in Chrome.
+ *
+ * @param {OT.SubscriberPeerConnection} peerConnection the peer connection to use to get the stats
+ * @constructor
*/
- OT.Microphone = function(webRTCStream, muted) {
- var _muted;
+OT.GetStatsAudioLevelSampler = function(peerConnection) {
- OT.$.defineProperties(this, {
- muted: {
- get: function() {
- return _muted;
- },
- set: function(muted) {
- if (_muted === muted) return;
+ if (!OT.$.hasCapabilities('audioOutputLevelStat')) {
+ throw new Error('The current platform does not provide the required capabilities');
+ }
- _muted = muted;
+ var _peerConnection = peerConnection,
+ _statsProperty = 'audioOutputLevel';
- var audioTracks = webRTCStream.getAudioTracks();
-
- for (var i=0, num=audioTracks.length; iAudioContext
based audio level sampler. It returns the maximum value in the
+ * last 1024 samples.
+ *
+ * It is worth noting that the remote MediaStream
audio analysis is currently only
+ * available in FF.
+ *
+ * This implementation gracefully handles the case where the MediaStream
has not
+ * been set yet by returning a null
value until the stream is set. It is up to the
+ * call site to decide what to do with this value (most likely ignore it and retry later).
+ *
+ * @constructor
+ * @param {AudioContext} audioContext an audio context instance to get an analyser node
+ */
+OT.AnalyserAudioLevelSampler = function(audioContext) {
- } else {
- this.muted(false);
+ var _sampler = this,
+ _analyser = null,
+ _timeDomainData = null;
+
+ var _getAnalyser = function(stream) {
+ var sourceNode = audioContext.createMediaStreamSource(stream);
+ var analyser = audioContext.createAnalyser();
+ sourceNode.connect(analyser);
+ return analyser;
+ };
+
+ this.webRTCStream = null;
+
+ this.sample = function(done) {
+
+ if (!_analyser && _sampler.webRTCStream) {
+ _analyser = _getAnalyser(_sampler.webRTCStream);
+ _timeDomainData = new Uint8Array(_analyser.frequencyBinCount);
}
+ if (_analyser) {
+ _analyser.getByteTimeDomainData(_timeDomainData);
+
+ // varies from 0 to 255
+ var max = 0;
+ for (var idx = 0; idx < _timeDomainData.length; idx++) {
+ max = Math.max(max, Math.abs(_timeDomainData[idx] - 128));
+ }
+
+ // normalize the collected level to match the range delivered by
+ // the getStats' audioOutputLevel
+ done(max / 128);
+ } else {
+ done(null);
+ }
+ };
+};
+
+/*
+ * Transforms a raw audio level to produce a "smoother" animation when using displaying the
+ * audio level. This transformer is state-full because it needs to keep the previous average
+ * value of the signal for filtering.
+ *
+ * It applies a low pass filter to get rid of level jumps and apply a log scale.
+ *
+ * @constructor
+ */
+OT.AudioLevelTransformer = function() {
+
+ var _averageAudioLevel = null;
+
+ /*
+ *
+ * @param {number} audioLevel a level in the [0,1] range
+ * @returns {number} a level in the [0,1] range transformed
+ */
+ this.transform = function(audioLevel) {
+ if (_averageAudioLevel === null || audioLevel >= _averageAudioLevel) {
+ _averageAudioLevel = audioLevel;
+ } else {
+ // a simple low pass filter with a smoothing of 70
+ _averageAudioLevel = audioLevel * 0.3 + _averageAudioLevel * 0.7;
+ }
+
+ // 1.5 scaling to map -30-0 dBm range to [0,1]
+ var logScaled = (Math.log(_averageAudioLevel) / Math.LN10) / 1.5 + 1;
+
+ return Math.min(Math.max(logScaled, 0), 1);
+ };
+};
+
+// tb_require('../helpers/helpers.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+/*
+ * Lazy instantiates an audio context and always return the same instance on following calls
+ *
+ * @returns {AudioContext}
+ */
+OT.audioContext = function() {
+ var context = new window.AudioContext();
+ OT.audioContext = function() {
+ return context;
+ };
+ return context;
+};
+
+// tb_require('../helpers/helpers.js')
+// tb_require('./events.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+OT.Archive = function(id, name, status) {
+ this.id = id;
+ this.name = name;
+ this.status = status;
+
+ this._ = {};
+
+ OT.$.eventing(this);
+
+ // Mass update, called by Raptor.Dispatcher
+ this._.update = OT.$.bind(function (attributes) {
+ for (var key in attributes) {
+ if(!attributes.hasOwnProperty(key)) {
+ continue;
+ }
+ var oldValue = this[key];
+ this[key] = attributes[key];
+
+ var event = new OT.ArchiveUpdatedEvent(this, key, oldValue, this[key]);
+ this.dispatchEvent(event);
+ }
+ }, this);
+
+ this.destroy = function() {};
+
+};
+
+// tb_require('../helpers/helpers.js')
+// tb_require('../helpers/lib/properties.js')
+// tb_require('../helpers/lib/analytics.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+OT.analytics = new OT.Analytics(OT.properties.loggingURL);
+// tb_require('../helpers/helpers.js')
+// tb_require('../helpers/lib/widget_view.js')
+// tb_require('./analytics.js')
+// tb_require('./events.js')
+// tb_require('./system_requirements.js')
+// tb_require('./stylable_component.js')
+// tb_require('./stream.js')
+// tb_require('./connection.js')
+// tb_require('./subscribing_state.js')
+// tb_require('./environment_loader.js')
+// tb_require('./audio_level_samplers.js')
+// tb_require('./audio_context.js')
+// tb_require('./chrome/chrome.js')
+// tb_require('./chrome/backing_bar.js')
+// tb_require('./chrome/name_panel.js')
+// tb_require('./chrome/mute_button.js')
+// tb_require('./chrome/archiving.js')
+// tb_require('./chrome/audio_level_meter.js')
+// tb_require('./peer_connection/subscriber_peer_connection.js')
+// tb_require('./peer_connection/get_stats_adapter.js')
+// tb_require('./peer_connection/get_stats_helpers.js')
+
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+/**
+ * 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, completionHandler) {
+ var _widgetId = OT.$.uuid(),
+ _domId = targetElement || _widgetId,
+ _container,
+ _streamContainer,
+ _chrome,
+ _audioLevelMeter,
+ _fromConnectionId,
+ _peerConnection,
+ _session = options.session,
+ _stream = options.stream,
+ _subscribeStartTime,
+ _startConnectingTime,
+ _properties,
+ _audioVolume = 100,
+ _state,
+ _prevStats,
+ _lastSubscribeToVideoReason,
+ _audioLevelCapable = OT.$.hasCapabilities('audioOutputLevelStat') ||
+ OT.$.hasCapabilities('webAudioCapableRemoteStream'),
+ _audioLevelSampler,
+ _audioLevelRunner,
+ _frameRateRestricted = false,
+ _connectivityAttemptPinger,
+ _subscriber = this;
+
+ _properties = OT.$.defaults({}, options, {
+ showControls: true,
+ fitMode: _stream.defaultFitMode || 'cover'
+ });
+
+ this.id = _domId;
+ this.widgetId = _widgetId;
+ this.session = _session;
+ this.stream = _stream = _properties.stream;
+ this.streamId = _stream.id;
+
+ _prevStats = {
+ timeStamp: OT.$.now()
};
-})(window);
-!(function(window, OT) {
+ if (!_session) {
+ OT.handleJsException('Subscriber must be passed a session option', 2000, {
+ session: _session,
+ target: this
+ });
- // A Factory method for generating simple state machine classes.
- //
- // @usage
- // var StateMachine = OT.generateSimpleStateMachine('start', ['start', 'middle', 'end', {
- // start: ['middle'],
- // middle: ['end'],
- // end: ['start']
- // }]);
- //
- // var states = new StateMachine();
- // state.current; // <-- start
- // state.set('middle');
- //
- OT.generateSimpleStateMachine = function(initialState, states, transitions) {
- var validStates = states.slice(),
- validTransitions = OT.$.clone(transitions);
+ return;
+ }
- var isValidState = function (state) {
- return OT.$.arrayIndexOf(validStates, state) !== -1;
- };
+ OT.$.eventing(this, false);
- var isValidTransition = function(fromState, toState) {
- return validTransitions[fromState] &&
- OT.$.arrayIndexOf(validTransitions[fromState], toState) !== -1;
- };
+ if (typeof completionHandler === 'function') {
+ this.once('subscribeComplete', completionHandler);
+ }
- return function(stateChangeFailed) {
- var currentState = initialState,
- previousState = null;
+ if(_audioLevelCapable) {
+ this.on({
+ 'audioLevelUpdated:added': function(count) {
+ if (count === 1 && _audioLevelRunner) {
+ _audioLevelRunner.start();
+ }
+ },
+ 'audioLevelUpdated:removed': function(count) {
+ if (count === 0 && _audioLevelRunner) {
+ _audioLevelRunner.stop();
+ }
+ }
+ });
+ }
- this.current = currentState;
+ var logAnalyticsEvent = function(action, variation, payload, throttle) {
+ var args = [{
+ action: action,
+ variation: variation,
+ payload: payload,
+ streamId: _stream ? _stream.id : null,
+ sessionId: _session ? _session.sessionId : null,
+ connectionId: _session && _session.isConnected() ?
+ _session.connection.connectionId : null,
+ partnerId: _session && _session.isConnected() ? _session.sessionInfo.partnerId : null,
+ subscriberId: _widgetId,
+ }];
+ if (throttle) args.push(throttle);
+ OT.analytics.logEvent.apply(OT.analytics, args);
+ },
- function signalChangeFailed(message, newState) {
- stateChangeFailed({
- message: message,
- newState: newState,
- currentState: currentState,
- previousState: previousState
+ logConnectivityEvent = function(variation, payload) {
+ if (variation === 'Attempt' || !_connectivityAttemptPinger) {
+ _connectivityAttemptPinger = new OT.ConnectivityAttemptPinger({
+ action: 'Subscribe',
+ sessionId: _session ? _session.sessionId : null,
+ connectionId: _session && _session.isConnected() ?
+ _session.connection.connectionId : null,
+ partnerId: _session.isConnected() ? _session.sessionInfo.partnerId : null,
+ streamId: _stream ? _stream.id : null
+ });
+ }
+ _connectivityAttemptPinger.setVariation(variation);
+ logAnalyticsEvent('Subscribe', variation, payload);
+ },
+
+ recordQOS = OT.$.bind(function(parsedStats) {
+ var QoSBlob = {
+ streamType : 'WebRTC',
+ width: _container ? Number(OT.$.width(_container.domElement).replace('px', '')) : null,
+ height: _container ? Number(OT.$.height(_container.domElement).replace('px', '')) : null,
+ sessionId: _session ? _session.sessionId : null,
+ connectionId: _session ? _session.connection.connectionId : null,
+ mediaServerName: _session ? _session.sessionInfo.messagingServer : null,
+ p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
+ partnerId: _session ? _session.apiKey : null,
+ streamId: _stream.id,
+ subscriberId: _widgetId,
+ version: OT.properties.version,
+ duration: parseInt(OT.$.now() - _subscribeStartTime, 10),
+ remoteConnectionId: _stream.connection.connectionId
+ };
+
+ OT.analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) );
+ this.trigger('qos', parsedStats);
+ }, this),
+
+
+ stateChangeFailed = function(changeFailed) {
+ OT.error('Subscriber State Change Failed: ', changeFailed.message);
+ OT.debug(changeFailed);
+ },
+
+ onLoaded = function() {
+ if (_state.isSubscribing() || !_streamContainer) return;
+
+ OT.debug('OT.Subscriber.onLoaded');
+
+ _state.set('Subscribing');
+ _subscribeStartTime = OT.$.now();
+
+ var payload = {
+ pcc: parseInt(_subscribeStartTime - _startConnectingTime, 10),
+ hasRelayCandidates: _peerConnection && _peerConnection.hasRelayCandidates()
+ };
+ logAnalyticsEvent('createPeerConnection', 'Success', payload);
+
+ _container.loading(false);
+ _chrome.showAfterLoading();
+
+ if(_frameRateRestricted) {
+ _stream.setRestrictFrameRate(true);
+ }
+
+ if(_audioLevelMeter && _subscriber.getStyle('audioLevelDisplayMode') === 'auto') {
+ _audioLevelMeter[_container.audioOnly() ? 'show' : 'hide']();
+ }
+
+ this.trigger('subscribeComplete', null, this);
+ this.trigger('loaded', this);
+
+ logConnectivityEvent('Success', {streamId: _stream.id});
+ },
+
+ onDisconnected = function() {
+ OT.debug('OT.Subscriber has been disconnected from the Publisher\'s PeerConnection');
+
+ if (_state.isAttemptingToSubscribe()) {
+ // subscribing error
+ _state.set('Failed');
+ this.trigger('subscribeComplete', new OT.Error(null, 'ClientDisconnected'));
+
+ } else if (_state.isSubscribing()) {
+ _state.set('Failed');
+
+ // we were disconnected after we were already subscribing
+ // probably do nothing?
+ }
+
+ this.disconnect();
+ },
+
+ onPeerConnectionFailure = OT.$.bind(function(code, reason, peerConnection, prefix) {
+ var payload;
+ if (_state.isAttemptingToSubscribe()) {
+ // We weren't subscribing yet so this was a failure in setting
+ // up the PeerConnection or receiving the initial stream.
+ payload = {
+ reason: prefix ? prefix : 'PeerConnectionError',
+ message: 'Subscriber PeerConnection Error: ' + reason,
+ hasRelayCandidates: _peerConnection && _peerConnection.hasRelayCandidates()
+ };
+ logAnalyticsEvent('createPeerConnection', 'Failure', payload);
+
+ _state.set('Failed');
+ this.trigger('subscribeComplete', new OT.Error(null, reason));
+
+ } else if (_state.isSubscribing()) {
+ // we were disconnected after we were already subscribing
+ _state.set('Failed');
+ this.trigger('error', reason);
+ }
+
+ this.disconnect();
+
+ payload = {
+ reason: prefix ? prefix : 'PeerConnectionError',
+ message: 'Subscriber PeerConnection Error: ' + reason,
+ code: OT.ExceptionCodes.P2P_CONNECTION_FAILED
+ };
+ logConnectivityEvent('Failure', payload);
+
+ OT.handleJsException('Subscriber PeerConnection Error: ' + reason,
+ OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
+ session: _session,
+ target: this
+ }
+ );
+ _showError.call(this, reason);
+ }, this),
+
+ onRemoteStreamAdded = function(webRTCStream) {
+ OT.debug('OT.Subscriber.onRemoteStreamAdded');
+
+ _state.set('BindingRemoteStream');
+
+ // Disable the audio/video, if needed
+ this.subscribeToAudio(_properties.subscribeToAudio);
+
+ _lastSubscribeToVideoReason = 'loading';
+ this.subscribeToVideo(_properties.subscribeToVideo, 'loading');
+
+ var videoContainerOptions = {
+ error: onPeerConnectionFailure,
+ audioVolume: _audioVolume
+ };
+
+ // This is a workaround for a bug in Chrome where a track disabled on
+ // the remote end doesn't fire loadedmetadata causing the subscriber to timeout
+ // https://jira.tokbox.com/browse/OPENTOK-15605
+ // Also https://jira.tokbox.com/browse/OPENTOK-16425
+ var tracks,
+ reenableVideoTrack = false;
+ if (!_stream.hasVideo && OT.$.env.name === 'Chrome' && OT.$.env.version >= 35) {
+ tracks = webRTCStream.getVideoTracks();
+ if(tracks.length > 0) {
+ tracks[0].enabled = false;
+ reenableVideoTrack = tracks[0];
+ }
+ }
+
+ _streamContainer = _container.bindVideo(webRTCStream,
+ videoContainerOptions,
+ OT.$.bind(function(err) {
+ if (err) {
+ onPeerConnectionFailure(null, err.message || err, _peerConnection, 'VideoElement');
+ return;
+ }
+
+ // Continues workaround for https://jira.tokbox.com/browse/OPENTOK-15605
+ // Also https://jira.tokbox.com/browse/OPENTOK-16425]
+ if (reenableVideoTrack != null && _properties.subscribeToVideo) {
+ reenableVideoTrack.enabled = true;
+ }
+
+ _streamContainer.orientation({
+ width: _stream.videoDimensions.width,
+ height: _stream.videoDimensions.height,
+ videoOrientation: _stream.videoDimensions.orientation
+ });
+
+ onLoaded.call(this, null);
+ }, this));
+
+ if (OT.$.hasCapabilities('webAudioCapableRemoteStream') && _audioLevelSampler &&
+ webRTCStream.getAudioTracks().length > 0) {
+ _audioLevelSampler.webRTCStream = webRTCStream;
+ }
+
+ logAnalyticsEvent('createPeerConnection', 'StreamAdded');
+ this.trigger('streamAdded', this);
+ },
+
+ onRemoteStreamRemoved = function(webRTCStream) {
+ OT.debug('OT.Subscriber.onStreamRemoved');
+
+ if (_streamContainer.stream === webRTCStream) {
+ _streamContainer.destroy();
+ _streamContainer = null;
+ }
+
+
+ this.trigger('streamRemoved', this);
+ },
+
+ streamDestroyed = function () {
+ this.disconnect();
+ },
+
+ streamUpdated = function(event) {
+
+ switch(event.changedProperty) {
+ case 'videoDimensions':
+ if (!_streamContainer) {
+ // Ignore videoEmension updates before streamContainer is created OPENTOK-17253
+ break;
+ }
+ _streamContainer.orientation({
+ width: event.newValue.width,
+ height: event.newValue.height,
+ videoOrientation: event.newValue.orientation
+ });
+
+ this.dispatchEvent(new OT.VideoDimensionsChangedEvent(
+ this, event.oldValue, event.newValue
+ ));
+
+ break;
+
+ case 'videoDisableWarning':
+ _chrome.videoDisabledIndicator.setWarning(event.newValue);
+ this.dispatchEvent(new OT.VideoDisableWarningEvent(
+ event.newValue ? 'videoDisableWarning' : 'videoDisableWarningLifted'
+ ));
+ break;
+
+ case 'hasVideo':
+
+ setAudioOnly(!(_stream.hasVideo && _properties.subscribeToVideo));
+
+ this.dispatchEvent(new OT.VideoEnabledChangedEvent(
+ _stream.hasVideo ? 'videoEnabled' : 'videoDisabled', {
+ reason: 'publishVideo'
+ }));
+ break;
+
+ case 'hasAudio':
+ // noop
+ }
+ },
+
+ /// Chrome
+
+ // If mode is false, then that is the mode. If mode is true then we'll
+ // definitely display the button, but we'll defer the model to the
+ // Publishers buttonDisplayMode style property.
+ chromeButtonMode = function(mode) {
+ if (mode === false) return 'off';
+
+ var defaultMode = this.getStyle('buttonDisplayMode');
+
+ // The default model is false, but it's overridden by +mode+ being true
+ if (defaultMode === false) return 'on';
+
+ // defaultMode is either true or auto.
+ return defaultMode;
+ },
+
+ updateChromeForStyleChange = function(key, value/*, oldValue*/) {
+ if (!_chrome) return;
+
+ switch(key) {
+ case 'nameDisplayMode':
+ _chrome.name.setDisplayMode(value);
+ _chrome.backingBar.setNameMode(value);
+ break;
+
+ case 'videoDisabledDisplayMode':
+ _chrome.videoDisabledIndicator.setDisplayMode(value);
+ break;
+
+ case 'showArchiveStatus':
+ _chrome.archive.setShowArchiveStatus(value);
+ break;
+
+ case 'buttonDisplayMode':
+ _chrome.muteButton.setDisplayMode(value);
+ _chrome.backingBar.setMuteMode(value);
+ break;
+
+ case 'audioLevelDisplayMode':
+ _chrome.audioLevel.setDisplayMode(value);
+ break;
+
+ case 'bugDisplayMode':
+ // bugDisplayMode can't be updated but is used by some partners
+
+ case 'backgroundImageURI':
+ _container.setBackgroundImageURI(value);
+ }
+ },
+
+ _createChrome = function() {
+
+ var widgets = {
+ backingBar: new OT.Chrome.BackingBar({
+ nameMode: !_properties.name ? 'off' : this.getStyle('nameDisplayMode'),
+ muteMode: chromeButtonMode.call(this, this.getStyle('showMuteButton'))
+ }),
+
+ name: new OT.Chrome.NamePanel({
+ name: _properties.name,
+ mode: this.getStyle('nameDisplayMode')
+ }),
+
+ muteButton: new OT.Chrome.MuteButton({
+ muted: _properties.muted,
+ mode: chromeButtonMode.call(this, this.getStyle('showMuteButton'))
+ }),
+
+ archive: new OT.Chrome.Archiving({
+ show: this.getStyle('showArchiveStatus'),
+ archiving: false
+ })
+ };
+
+ if (_audioLevelCapable) {
+ var audioLevelTransformer = new OT.AudioLevelTransformer();
+
+ var audioLevelUpdatedHandler = function(evt) {
+ _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
+ };
+
+ _audioLevelMeter = new OT.Chrome.AudioLevelMeter({
+ mode: this.getStyle('audioLevelDisplayMode'),
+ onActivate: function() {
+ _subscriber.on('audioLevelUpdated', audioLevelUpdatedHandler);
+ },
+ onPassivate: function() {
+ _subscriber.off('audioLevelUpdated', audioLevelUpdatedHandler);
+ }
+ });
+
+ widgets.audioLevel = _audioLevelMeter;
+ }
+
+ widgets.videoDisabledIndicator = new OT.Chrome.VideoDisabledIndicator({
+ mode: this.getStyle('videoDisabledDisplayMode')
});
- }
- // Validates +newState+. If it's invalid it triggers stateChangeFailed and returns false.
- function handleInvalidStateChanges(newState) {
- if (!isValidState(newState)) {
- signalChangeFailed('\'' + newState + '\' is not a valid state', newState);
+ _chrome = new OT.Chrome({
+ parent: _container.domElement
+ }).set(widgets).on({
+ muted: function() {
+ muteAudio.call(this, true);
+ },
- return false;
+ unmuted: function() {
+ muteAudio.call(this, false);
+ }
+ }, this);
+
+ // Hide the chrome until we explicitly show it
+ _chrome.hideWhileLoading();
+ },
+
+ _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.'
+ );
}
-
- if (!isValidTransition(currentState, newState)) {
- signalChangeFailed('\'' + currentState + '\' cannot transition to \'' +
- newState + '\'', newState);
-
- return false;
- }
-
- return true;
- }
-
-
- this.set = function(newState) {
- if (!handleInvalidStateChanges(newState)) return;
- previousState = currentState;
- this.current = currentState = newState;
};
+ OT.StylableComponent(this, {
+ nameDisplayMode: 'auto',
+ buttonDisplayMode: 'auto',
+ audioLevelDisplayMode: 'auto',
+ videoDisabledDisplayMode: 'auto',
+ backgroundImageURI: null,
+ showArchiveStatus: true,
+ showMicButton: true
+ }, _properties.showControls, function (payload) {
+ logAnalyticsEvent('SetStyle', 'Subscriber', payload, 0.1);
+ });
+
+ var setAudioOnly = function(audioOnly) {
+ if (_container) {
+ _container.audioOnly(audioOnly);
+ _container.showPoster(audioOnly);
+ }
+
+ if (_audioLevelMeter && _subscriber.getStyle('audioLevelDisplayMode') === 'auto') {
+ _audioLevelMeter[audioOnly ? 'show' : 'hide']();
+ }
+ };
+
+ // logs an analytics event for getStats every 100 calls
+ var notifyGetStatsCalled = (function() {
+ var callCount = 0;
+ return function throttlingNotifyGetStatsCalled() {
+ if (callCount % 100 === 0) {
+ logAnalyticsEvent('getStats', 'Called');
+ }
+ callCount++;
};
+ })();
+
+ this.destroy = function(reason, quiet) {
+ if (_state.isDestroyed()) return;
+
+ if(reason === 'streamDestroyed') {
+ if (_state.isAttemptingToSubscribe()) {
+ // We weren't subscribing yet so the stream was destroyed before we setup
+ // the PeerConnection or receiving the initial stream.
+ this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID'));
+ }
+ }
+
+ _state.set('Destroyed');
+
+ if(_audioLevelRunner) {
+ _audioLevelRunner.stop();
+ }
+
+ this.disconnect();
+
+ if (_chrome) {
+ _chrome.destroy();
+ _chrome = null;
+ }
+
+ if (_container) {
+ _container.destroy();
+ _container = null;
+ this.element = null;
+ }
+
+ if (_stream && !_stream.destroyed) {
+ logAnalyticsEvent('unsubscribe', null, {streamId: _stream.id});
+ }
+
+ this.id = _domId = null;
+ this.stream = _stream = null;
+ this.streamId = null;
+
+ this.session =_session = null;
+ _properties = null;
+
+ if (quiet !== true) {
+ this.dispatchEvent(
+ new OT.DestroyedEvent(
+ OT.Event.names.SUBSCRIBER_DESTROYED,
+ this,
+ reason
+ ),
+ OT.$.bind(this.off, this)
+ );
+ }
+
+ return this;
};
-})(window, window.OT);
-!(function() {
+ this.disconnect = function() {
+ if (!_state.isDestroyed() && !_state.isFailed()) {
+ // If we are already in the destroyed state then disconnect
+ // has been called after (or from within) destroy.
+ _state.set('NotSubscribing');
+ }
-// Models a Subscriber's subscribing State
-//
-// Valid States:
-// NotSubscribing (the initial state
-// Init (basic setup of DOM
-// ConnectingToPeer (Failure Cases -> No Route, Bad Offer, Bad Answer
-// BindingRemoteStream (Failure Cases -> Anything to do with the media being
-// (invalid, the media never plays
-// Subscribing (this is 'onLoad'
-// Failed (terminal state, with a reason that maps to one of the
-// (failure cases above
-// Destroyed (The subscriber has been cleaned up, terminal state
-//
-//
-// Valid Transitions:
-// NotSubscribing ->
-// Init
-//
-// Init ->
-// ConnectingToPeer
-// | BindingRemoteStream (if we are subscribing to ourselves and we alreay
-// (have a stream
-// | NotSubscribing (destroy()
-//
-// ConnectingToPeer ->
-// BindingRemoteStream
-// | NotSubscribing
-// | Failed
-// | NotSubscribing (destroy()
-//
-// BindingRemoteStream ->
-// Subscribing
-// | Failed
-// | NotSubscribing (destroy()
-//
-// Subscribing ->
-// NotSubscribing (unsubscribe
-// | Failed (probably a peer connection failure after we began
-// (subscribing
-//
-// Failed ->
-// Destroyed
-//
-// Destroyed -> (terminal state)
-//
-//
-// @example
-// var state = new SubscribingState(function(change) {
-// console.log(change.message);
-// });
-//
-// state.set('Init');
-// state.current; -> 'Init'
-//
-// state.set('Subscribing'); -> triggers stateChangeFailed and logs out the error message
-//
-//
- var validStates,
- validTransitions,
- initialState = 'NotSubscribing';
+ if (_streamContainer) {
+ _streamContainer.destroy();
+ _streamContainer = null;
+ }
- validStates = [
- 'NotSubscribing', 'Init', 'ConnectingToPeer',
- 'BindingRemoteStream', 'Subscribing', 'Failed',
- 'Destroyed'
- ];
+ if (_peerConnection) {
+ _peerConnection.destroy();
+ _peerConnection = null;
- validTransitions = {
- NotSubscribing: ['NotSubscribing', 'Init', 'Destroyed'],
- Init: ['NotSubscribing', 'ConnectingToPeer', 'BindingRemoteStream', 'Destroyed'],
- ConnectingToPeer: ['NotSubscribing', 'BindingRemoteStream', 'Failed', 'Destroyed'],
- BindingRemoteStream: ['NotSubscribing', 'Subscribing', 'Failed', 'Destroyed'],
- Subscribing: ['NotSubscribing', 'Failed', 'Destroyed'],
- Failed: ['Destroyed'],
- Destroyed: []
+ logAnalyticsEvent('disconnect', 'PeerConnection', {streamId: _stream.id});
+ }
};
- OT.SubscribingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
+ this.processMessage = function(type, fromConnection, message) {
+ OT.debug('OT.Subscriber.processMessage: Received ' + type + ' message from ' +
+ fromConnection.id);
+ OT.debug(message);
- OT.SubscribingState.prototype.isDestroyed = function() {
- return this.current === 'Destroyed';
+ if (_fromConnectionId !== fromConnection.id) {
+ _fromConnectionId = fromConnection.id;
+ }
+
+ if (_peerConnection) {
+ _peerConnection.processMessage(type, message);
+ }
};
- OT.SubscribingState.prototype.isFailed = function() {
- return this.current === 'Failed';
+ this.disableVideo = function(active) {
+ if (!active) {
+ OT.warn('Due to high packet loss and low bandwidth, video has been disabled');
+ } else {
+ if (_lastSubscribeToVideoReason === 'auto') {
+ OT.info('Video has been re-enabled');
+ _chrome.videoDisabledIndicator.disableVideo(false);
+ } else {
+ OT.info('Video was not re-enabled because it was manually disabled');
+ return;
+ }
+ }
+ this.subscribeToVideo(active, 'auto');
+ if(!active) {
+ _chrome.videoDisabledIndicator.disableVideo(true);
+ }
+ var payload = active ? {videoEnabled: true} : {videoDisabled: true};
+ logAnalyticsEvent('updateQuality', 'video', payload);
};
- OT.SubscribingState.prototype.isSubscribing = function() {
- return this.current === 'Subscribing';
+ /**
+ * Return the base-64-encoded string of PNG data representing the Subscriber video.
+ *
+ * You can use the string as the value for a data URL scheme passed to the src parameter of + * an image file, as in the following:
+ * + *+ * var imgData = subscriber.getImgData(); + * + * var img = document.createElement("img"); + * img.setAttribute("src", "data:image/png;base64," + imgData); + * var imgWin = window.open("about:blank", "Screenshot"); + * imgWin.document.write("<body></body>"); + * imgWin.document.body.appendChild(img); + *+ * @method #getImgData + * @memberOf Subscriber + * @return {String} The base-64 encoded string. Returns an empty string if there is no video. + */ + this.getImgData = function() { + if (!this.isSubscribing()) { + OT.error('OT.Subscriber.getImgData: Cannot getImgData before the Subscriber ' + + 'is subscribing.'); + return null; + } + + return _streamContainer.imgData(); }; - OT.SubscribingState.prototype.isAttemptingToSubscribe = function() { - return OT.$.arrayIndexOf( - [ 'Init', 'ConnectingToPeer', 'BindingRemoteStream' ], - this.current - ) !== -1; + this.getStats = function(callback) { + if (!_peerConnection) { + callback(new OT.$.Error('Subscriber is not connected cannot getStats', 1015)); + return; + } + + notifyGetStatsCalled(); + + _peerConnection.getStats(function(error, stats) { + if (error) { + callback(error); + return; + } + + var otStats = { + timestamp: 0 + }; + + OT.$.forEach(stats, function(stat) { + if (OT.getStatsHelpers.isInboundStat(stat)) { + var video = OT.getStatsHelpers.isVideoStat(stat); + var audio = OT.getStatsHelpers.isAudioStat(stat); + + // it is safe to override the timestamp of one by another + // if they are from the same getStats "batch" video and audio ts have the same value + if (audio || video) { + otStats.timestamp = OT.getStatsHelpers.normalizeTimestamp(stat.timestamp); + } + if (video) { + otStats.video = OT.getStatsHelpers.parseStatCategory(stat); + } else if (audio) { + otStats.audio = OT.getStatsHelpers.parseStatCategory(stat); + } + } + }); + + callback(null, otStats); + }); }; -})(window); -!(function() { + /** + * 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; + }; -// Models a Publisher's publishing State -// -// Valid States: -// NotPublishing -// GetUserMedia -// BindingMedia -// MediaBound -// PublishingToSession -// Publishing -// Failed -// Destroyed -// -// -// Valid Transitions: -// NotPublishing -> -// GetUserMedia -// -// GetUserMedia -> -// BindingMedia -// | Failed (Failure Reasons -> stream error, constraints, -// (permission denied -// | NotPublishing (destroy() -// -// -// BindingMedia -> -// MediaBound -// | Failed (Failure Reasons -> Anything to do with the media -// (being invalid, the media never plays -// | NotPublishing (destroy() -// -// MediaBound -> -// PublishingToSession (MediaBound could transition to PublishingToSession -// (if a stand-alone publish is bound to a session -// | Failed (Failure Reasons -> media issues with a stand-alone publisher -// | NotPublishing (destroy() -// -// PublishingToSession -// Publishing -// | Failed (Failure Reasons -> timeout while waiting for ack of -// (stream registered. We do not do this right now -// | NotPublishing (destroy() -// -// -// Publishing -> -// NotPublishing (Unpublish -// | Failed (Failure Reasons -> loss of network, media error, anything -// (that causes *all* Peer Connections to fail (less than all -// (failing is just an error, all is failure) -// | NotPublishing (destroy() -// -// Failed -> -// Destroyed -// -// Destroyed -> (Terminal state -// -// + /** + * 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); - validTransitions = { - NotPublishing: ['NotPublishing', 'GetUserMedia', 'Destroyed'], - GetUserMedia: ['BindingMedia', 'Failed', 'NotPublishing', 'Destroyed'], - BindingMedia: ['MediaBound', 'Failed', 'NotPublishing', 'Destroyed'], - MediaBound: ['NotPublishing', 'PublishingToSession', 'Failed', 'Destroyed'], - PublishingToSession: ['NotPublishing', 'Publishing', 'Failed', 'Destroyed'], - Publishing: ['NotPublishing', 'MediaBound', 'Failed', 'Destroyed'], - Failed: ['Destroyed'], - Destroyed: [] + if (_peerConnection) { + _peerConnection.subscribeToAudio(value && !_properties.subscribeMute); + + if (_session && _stream && value !== _properties.subscribeToAudio) { + _stream.setChannelActiveState('audio', value && !_properties.subscribeMute); + } + } + + _properties.subscribeToAudio = value; + + return this; + }; + + var muteAudio = function(_mute) { + _chrome.muteButton.muted(_mute); + + if(_mute === _properties.mute) { + return; + } + if(OT.$.env.name === 'Chrome' || OTPlugin.isInstalled()) { + _properties.subscribeMute = _properties.muted = _mute; + this.subscribeToAudio(_properties.subscribeToAudio); + } else { + if(_mute) { + _properties.premuteVolume = this.getAudioVolume(); + _properties.muted = true; + this.setAudioVolume(0); + } else if(_properties.premuteVolume || _properties.audioVolume) { + _properties.muted = false; + this.setAudioVolume(_properties.premuteVolume || _properties.audioVolume); + } + } + _properties.mute = _properties.mute; + }; + + var reasonMap = { + auto: 'quality', + publishVideo: 'publishVideo', + subscribeToVideo: 'subscribeToVideo' + }; + + + /** + * Toggles video on and off. Starts subscribing to video (if it is available and + * currently not being subscribed to) when the
value
is true
;
+ * stops subscribing to video (if it is currently being subscribed to) when the
+ * value
is false
.
+ * + * Note: This method only affects the local playback of video. It has no impact on + * the video for other connections subscribing to the same stream. If the Publsher is not + * publishing video, enabling the Subscriber video will have no practical video. + *
+ * + * @param {Boolean} value Whether to start subscribing to video (true
) or not
+ * (false
).
+ *
+ * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
+ * following:
+ *
+ * mySubscriber.subscribeToVideo(true).subscribeToAudio(false);+ * + * @see subscribeToAudio() + * @see Session.subscribe() + * @see StreamPropertyChangedEvent + * + * @method #subscribeToVideo + * @memberOf Subscriber + */ + this.subscribeToVideo = function(pValue, reason) { + var value = OT.$.castToBoolean(pValue, true); + + setAudioOnly(!(value && _stream.hasVideo)); + + if ( value && _container && _container.video()) { + _container.loading(value); + _container.video().whenTimeIncrements(function() { + _container.loading(false); + }, this); + } + + if (_chrome && _chrome.videoDisabledIndicator) { + _chrome.videoDisabledIndicator.disableVideo(false); + } + + if (_peerConnection) { + _peerConnection.subscribeToVideo(value); + + if (_session && _stream && (value !== _properties.subscribeToVideo || + reason !== _lastSubscribeToVideoReason)) { + _stream.setChannelActiveState('video', value, reason); + } + } + + _properties.subscribeToVideo = value; + _lastSubscribeToVideoReason = reason; + + if (reason !== 'loading') { + this.dispatchEvent(new OT.VideoEnabledChangedEvent( + value ? 'videoEnabled' : 'videoDisabled', + { + reason: reasonMap[reason] || 'subscribeToVideo' + } + )); + } + + return this; + }; + + this.isSubscribing = function() { + return _state.isSubscribing(); + }; + + this.isWebRTC = true; + + this.isLoading = function() { + return _container && _container.loading(); + }; + + this.videoElement = function() { + return _streamContainer.domElement(); + }; + + this.videoWidth = function() { + return _streamContainer.videoWidth(); + }; + + this.videoHeight = function() { + return _streamContainer.videoHeight(); + }; + + /** + * Restricts the frame rate of the Subscriber's video stream, when you pass in + *
true
. When you pass in false
, the frame rate of the video stream
+ * is not restricted.
+ * + * When the frame rate is restricted, the Subscriber video frame will update once or less per + * second. + *
+ * This feature is only available in sessions that use the OpenTok Media Router (sessions with + * the media mode + * set to routed), not in sessions with the media mode set to relayed. In relayed sessions, + * calling this method has no effect. + *
+ * Restricting the subscriber frame rate has the following benefits: + *
+ * Reducing a subscriber's frame rate has no effect on the frame rate of the video in
+ * other clients.
+ *
+ * @param {Boolean} value Whether to restrict the Subscriber's video frame rate
+ * (true
) or not (false
).
+ *
+ * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
+ * following:
+ *
+ *
mySubscriber.restrictFrameRate(false).subscribeToAudio(true);+ * + * @method #restrictFrameRate + * @memberOf Subscriber + */ + this.restrictFrameRate = function(val) { + OT.debug('OT.Subscriber.restrictFrameRate(' + val + ')'); + + logAnalyticsEvent('restrictFrameRate', val.toString(), {streamId: _stream.id}); + + if (_session.sessionInfo.p2pEnabled) { + OT.warn('OT.Subscriber.restrictFrameRate: Cannot restrictFrameRate on a P2P session'); + } + + if (typeof val !== 'boolean') { + OT.error('OT.Subscriber.restrictFrameRate: expected a boolean value got a ' + typeof val); + } else { + _frameRateRestricted = val; + _stream.setRestrictFrameRate(val); + } + return this; + }; + + this.on('styleValueChanged', updateChromeForStyleChange, this); + + this._ = { + archivingStatus: function(status) { + if(_chrome) { + _chrome.archive.setArchiving(status); + } + } + }; + + _state = new OT.SubscribingState(stateChangeFailed); + + OT.debug('OT.Subscriber: subscribe to ' + _stream.id); + + _state.set('Init'); + + if (!_stream) { + // @todo error + OT.error('OT.Subscriber: No stream parameter.'); + return false; + } + + _stream.on({ + updated: streamUpdated, + destroyed: streamDestroyed + }, this); + + _fromConnectionId = _stream.connection.id; + _properties.name = _properties.name || _stream.name; + _properties.classNames = 'OT_root OT_subscriber'; + + if (_properties.style) { + this.setStyle(_properties.style, null, true); + } + if (_properties.audioVolume) { + this.setAudioVolume(_properties.audioVolume); + } + + _properties.subscribeToAudio = OT.$.castToBoolean(_properties.subscribeToAudio, true); + _properties.subscribeToVideo = OT.$.castToBoolean(_properties.subscribeToVideo, true); + + _container = new OT.WidgetView(targetElement, _properties); + this.id = _domId = _container.domId(); + this.element = _container.domElement; + + _createChrome.call(this); + + _startConnectingTime = OT.$.now(); + + if (_stream.connection.id !== _session.connection.id) { + logAnalyticsEvent('createPeerConnection', 'Attempt', ''); + + _state.set('ConnectingToPeer'); + + _peerConnection = new OT.SubscriberPeerConnection(_stream.connection, _session, + _stream, this, _properties); + + _peerConnection.on({ + disconnected: onDisconnected, + error: onPeerConnectionFailure, + remoteStreamAdded: onRemoteStreamAdded, + remoteStreamRemoved: onRemoteStreamRemoved, + qos: recordQOS + }, this); + + // initialize the peer connection AFTER we've added the event listeners + _peerConnection.init(); + + if (OT.$.hasCapabilities('audioOutputLevelStat')) { + _audioLevelSampler = new OT.GetStatsAudioLevelSampler(_peerConnection, 'out'); + } else if (OT.$.hasCapabilities('webAudioCapableRemoteStream')) { + _audioLevelSampler = new OT.AnalyserAudioLevelSampler(OT.audioContext()); + } + + if(_audioLevelSampler) { + var subscriber = this; + // sample with interval to minimise disturbance on animation loop but dispatch the + // event with RAF since the main purpose is animation of a meter + _audioLevelRunner = new OT.IntervalRunner(function() { + _audioLevelSampler.sample(function(audioOutputLevel) { + if (audioOutputLevel !== null) { + OT.$.requestAnimationFrame(function() { + subscriber.dispatchEvent( + new OT.AudioLevelUpdatedEvent(audioOutputLevel)); + }); + } + }); + }, 60); + } + } else { + logAnalyticsEvent('createPeerConnection', 'Attempt', ''); + + var publisher = _session.getPublisherForStream(_stream); + if(!(publisher && publisher._.webRtcStream())) { + this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID')); + return this; + } + + // Subscribe to yourself edge-case + onRemoteStreamAdded.call(this, publisher._.webRtcStream()); + } + + logConnectivityEvent('Attempt', {streamId: _stream.id}); + + + /** + * Dispatched periodically to indicate the subscriber's audio level. The event is dispatched + * up to 60 times per second, depending on the browser. The
audioLevel
property
+ * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
+ * information.
+ * + * The following example adjusts the value of a meter element that shows volume of the + * subscriber. Note that the audio level is adjusted logarithmically and a moving average + * is applied: + *
+ * var movingAvg = null; + * subscriber.on('audioLevelUpdated', function(event) { + * if (movingAvg === null || movingAvg <= event.audioLevel) { + * movingAvg = event.audioLevel; + * } else { + * movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel; + * } + * + * // 1.5 scaling to map the -30 - 0 dBm range to [0,1] + * var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1; + * logLevel = Math.min(Math.max(logLevel, 0), 1); + * document.getElementById('subscriberMeter').value = logLevel; + * }); + *+ *
This example shows the algorithm used by the default audio level indicator displayed + * in an audio-only Subscriber. + * + * @name audioLevelUpdated + * @event + * @memberof Subscriber + * @see AudioLevelUpdatedEvent + */ + +/** +* Dispatched when the video for the subscriber is disabled. +*
+* The reason
property defines the reason the video was disabled. This can be set to
+* one of the following values:
+*
+* +*
"publishVideo"
— The publisher stopped publishing video by calling
+* publishVideo(false)
."quality"
— The OpenTok Media Router stopped sending video
+* to the subscriber based on stream quality changes. This feature of the OpenTok Media
+* Router has a subscriber drop the video stream when connectivity degrades. (The subscriber
+* continues to receive the audio stream, if there is one.)
+*
+* Before sending this event, when the Subscriber's stream quality deteriorates to a level
+* that is low enough that the video stream is at risk of being disabled, the Subscriber
+* dispatches a videoDisableWarning
event.
+*
+* If connectivity improves to support video again, the Subscriber object dispatches
+* a videoEnabled
event, and the Subscriber resumes receiving video.
+*
+* By default, the Subscriber displays a video disabled indicator when a
+* videoDisabled
event with this reason is dispatched and removes the indicator
+* when the videoDisabled
event with this reason is dispatched. You can control
+* the display of this icon by calling the setStyle()
method of the Subscriber,
+* setting the videoDisabledDisplayMode
property(or you can set the style when
+* calling the Session.subscribe()
method, setting the style
property
+* of the properties
parameter).
+*
+* This feature is only available in sessions that use the OpenTok Media Router (sessions with +* the media mode +* set to routed), not in sessions with the media mode set to relayed. +*
+* You can disable this audio-only fallback feature, by setting the
+* audioFallbackEnabled
property to false
in the options you pass
+* into the OT.initPublisher()
method on the publishing client. (See
+* OT.initPublisher().)
+*
"subscribeToVideo"
— The subscriber started or stopped subscribing to
+* video, by calling subscribeToVideo(false)
.
+* videoDisabled
event.
+*
+* By default, the Subscriber displays a video disabled warning indicator when this event
+* is dispatched (and the video is disabled). You can control the display of this icon by
+* calling the setStyle()
method and setting the
+* videoDisabledDisplayMode
property (or you can set the style when calling
+* the Session.subscribe()
method and setting the style
property
+* of the properties
parameter).
+*
+* This feature is only available in sessions that use the OpenTok Media Router (sessions with
+* the media mode
+* set to routed), not in sessions with the media mode set to relayed.
+*
+* @see Event
+* @see event:videoDisabled
+* @see event:videoDisableWarningLifted
+* @name videoDisableWarning
+* @event
+* @memberof Subscriber
+*/
+
+/**
+* Dispatched when the OpenTok Media Router determines that the stream quality has improved
+* to the point at which the video being disabled is not an immediate risk. This event is
+* dispatched after the Subscriber object dispatches a videoDisableWarning
event.
+*
+* This feature is only available in sessions that use the OpenTok Media Router (sessions with +* the media mode +* set to routed), not in sessions with the media mode set to relayed. +* +* @see Event +* @see event:videoDisabled +* @see event:videoDisableWarning +* @name videoDisableWarningLifted +* @event +* @memberof Subscriber +*/ + +/** +* Dispatched when the OpenTok Media Router resumes sending video to the subscriber +* after video was previously disabled. +*
+* The reason
property defines the reason the video was enabled. This can be set to
+* one of the following values:
+*
+* +*
"publishVideo"
— The publisher started publishing video by calling
+* publishVideo(true)
."quality"
— The OpenTok Media Router resumed sending video
+* to the subscriber based on stream quality changes. This feature of the OpenTok Media
+* Router has a subscriber drop the video stream when connectivity degrades and then resume
+* the video stream if the stream quality improves.
+* +* This feature is only available in sessions that use the OpenTok Media Router (sessions with +* the media mode +* set to routed), not in sessions with the media mode set to relayed. +*
"subscribeToVideo"
— The subscriber started or stopped subscribing to
+* video, by calling subscribeToVideo(false)
.
+*
+* To prevent video from resuming, in the videoEnabled
event listener,
+* call subscribeToVideo(false)
on the Subscriber object.
+*
+* @see VideoEnabledChangedEvent
+* @see event:videoDisabled
+* @name videoEnabled
+* @event
+* @memberof Subscriber
+*/
+
+/**
+* Dispatched when the Subscriber element is removed from the HTML DOM. When this event is
+* dispatched, you may choose to adjust or remove HTML DOM elements related to the subscriber.
+* @see Event
+* @name destroyed
+* @event
+* @memberof Subscriber
+*/
+
+/**
+* Dispatched when the video dimensions of the video change. This can occur when the
+* stream.videoType
property is set to "screen"
(for a screen-sharing
+* video stream), and the user resizes the window being captured. It can also occur if the video
+* is being published by a mobile device and the user rotates the device (causing the camera
+* orientation to change).
+* @name videoDimensionsChanged
+* @event
+* @memberof Subscriber
+*/
+
+};
+
+// tb_require('../helpers/helpers.js')
+// tb_require('../helpers/lib/config.js')
+// tb_require('./analytics.js')
+// tb_require('./events.js')
+// tb_require('./error_handling.js')
+// tb_require('./system_requirements.js')
+// tb_require('./stream.js')
+// tb_require('./connection.js')
+// tb_require('./environment_loader.js')
+// tb_require('./session_info.js')
+// tb_require('./messaging/raptor/raptor.js')
+// tb_require('./messaging/raptor/session-dispatcher.js')
+// tb_require('./qos_testing/webrtc_test.js')
+// tb_require('./qos_testing/http_test.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT, Promise */
+
+
+/**
+ * The Session object returned by the OT.initSession()
method provides access to
+ * much of the OpenTok functionality.
+ *
+ * @class Session
+ * @augments EventDispatcher
+ *
+ * @property {Capabilities} capabilities A {@link Capabilities} object that includes information
+ * about the capabilities of the client. All properties of the capabilities
object
+ * are undefined until you have connected to a session and the Session object has dispatched the
+ * sessionConnected
event.
+ * @property {Connection} connection The {@link Connection} object for this session. The
+ * connection property is only available once the Session object dispatches the sessionConnected
+ * event. The Session object asynchronously dispatches a sessionConnected event in response
+ * to a successful call to the connect() method. See: connect and
+ * {@link Connection}.
+ * @property {String} sessionId The session ID for this session. You pass this value into the
+ * OT.initSession()
method when you create the Session object. (Note: a Session
+ * object is not connected to the OpenTok server until you call the connect() method of the
+ * object and the object dispatches a connected event. See {@link OT.initSession} and
+ * {@link connect}).
+ * For more information on sessions and session IDs, see
+ * Session creation.
+ */
+OT.Session = function(apiKey, sessionId) {
+ OT.$.eventing(this);
+
+ // Check that the client meets the minimum requirements, if they don't the upgrade
+ // flow will be triggered.
+ if (!OT.checkSystemRequirements()) {
+ OT.upgradeSystemRequirements();
+ return;
+ }
+
+ if(sessionId == null) {
+ sessionId = apiKey;
+ apiKey = null;
+ }
+
+ this.id = this.sessionId = sessionId;
+
+ var _initialConnection = true,
+ _apiKey = apiKey,
+ _token,
+ _session = this,
+ _sessionId = sessionId,
+ _socket,
+ _widgetId = OT.$.uuid(),
+ _connectionId = OT.$.uuid(),
+ sessionConnectFailed,
+ sessionDisconnectedHandler,
+ connectionCreatedHandler,
+ connectionDestroyedHandler,
+ streamCreatedHandler,
+ streamPropertyModifiedHandler,
+ streamDestroyedHandler,
+ archiveCreatedHandler,
+ archiveDestroyedHandler,
+ archiveUpdatedHandler,
+ init,
+ reset,
+ disconnectComponents,
+ destroyPublishers,
+ destroySubscribers,
+ connectMessenger,
+ getSessionInfo,
+ onSessionInfoResponse,
+ permittedTo,
+ _connectivityAttemptPinger,
+ dispatchError;
+
+
+
+ var setState = OT.$.statable(this, [
+ 'disconnected', 'connecting', 'connected', 'disconnecting'
+ ], 'disconnected');
+
+ this.connection = null;
+ this.connections = new OT.$.Collection();
+ this.streams = new OT.$.Collection();
+ this.archives = new OT.$.Collection();
+
+
+//--------------------------------------
+// MESSAGE HANDLERS
+//--------------------------------------
+
+// The duplication of this and sessionConnectionFailed will go away when
+// session and messenger are refactored
+ sessionConnectFailed = function(reason, code) {
+ setState('disconnected');
+
+ OT.error(reason);
+
+ this.trigger('sessionConnectFailed',
+ new OT.Error(code || OT.ExceptionCodes.CONNECT_FAILED, reason));
+
+ OT.handleJsException(reason, code || OT.ExceptionCodes.CONNECT_FAILED, {
+ session: this
+ });
+ };
+
+ sessionDisconnectedHandler = function(event) {
+ var reason = event.reason;
+ if(reason === 'networkTimedout') {
+ reason = 'networkDisconnected';
+ this.logEvent('Connect', 'TimeOutDisconnect', {reason: event.reason});
+ } else {
+ this.logEvent('Connect', 'Disconnected', {reason: event.reason});
+ }
+
+ var publicEvent = new OT.SessionDisconnectEvent('sessionDisconnected', reason);
+
+ reset();
+ disconnectComponents.call(this, reason);
+
+ var defaultAction = OT.$.bind(function() {
+ // Publishers handle preventDefault'ing themselves
+ destroyPublishers.call(this, publicEvent.reason);
+ // Subscriers don't, destroy 'em if needed
+ if (!publicEvent.isDefaultPrevented()) destroySubscribers.call(this, publicEvent.reason);
+ }, this);
+
+ this.dispatchEvent(publicEvent, defaultAction);
+ };
+
+ connectionCreatedHandler = function(connection) {
+ // We don't broadcast events for the symphony connection
+ if (connection.id.match(/^symphony\./)) return;
+
+ this.dispatchEvent(new OT.ConnectionEvent(
+ OT.Event.names.CONNECTION_CREATED,
+ connection
+ ));
+ };
+
+ connectionDestroyedHandler = function(connection, reason) {
+ // We don't broadcast events for the symphony connection
+ if (connection.id.match(/^symphony\./)) return;
+
+ // Don't delete the connection if it's ours. This only happens when
+ // we're about to receive a session disconnected and session disconnected
+ // will also clean up our connection.
+ if (connection.id === _socket.id()) return;
+
+ this.dispatchEvent(
+ new OT.ConnectionEvent(
+ OT.Event.names.CONNECTION_DESTROYED,
+ connection,
+ reason
+ )
+ );
+ };
+
+ streamCreatedHandler = function(stream) {
+ if(stream.connection.id !== this.connection.id) {
+ this.dispatchEvent(new OT.StreamEvent(
+ OT.Event.names.STREAM_CREATED,
+ stream,
+ null,
+ false
+ ));
+ }
+ };
+
+ streamPropertyModifiedHandler = function(event) {
+ var stream = event.target,
+ propertyName = event.changedProperty,
+ newValue = event.newValue;
+
+ if (propertyName === 'videoDisableWarning' || propertyName === 'audioDisableWarning') {
+ return; // These are not public properties, skip top level event for them.
+ }
+
+ if (propertyName === 'orientation') {
+ propertyName = 'videoDimensions';
+ newValue = {width: newValue.width, height: newValue.height};
+ }
+
+ this.dispatchEvent(new OT.StreamPropertyChangedEvent(
+ OT.Event.names.STREAM_PROPERTY_CHANGED,
+ stream,
+ propertyName,
+ event.oldValue,
+ newValue
+ ));
+ };
+
+ streamDestroyedHandler = function(stream, reason) {
+
+ // if the stream is one of ours we delegate handling
+ // to the publisher itself.
+ if(stream.connection.id === this.connection.id) {
+ OT.$.forEach(OT.publishers.where({ streamId: stream.id }), OT.$.bind(function(publisher) {
+ publisher._.unpublishFromSession(this, reason);
+ }, this));
+ return;
+ }
+
+ var event = new OT.StreamEvent('streamDestroyed', stream, reason, true);
+
+ var defaultAction = OT.$.bind(function() {
+ if (!event.isDefaultPrevented()) {
+ // If we are subscribed to any of the streams we should unsubscribe
+ OT.$.forEach(OT.subscribers.where({streamId: stream.id}), function(subscriber) {
+ if (subscriber.session.id === this.id) {
+ if(subscriber.stream) {
+ subscriber.destroy('streamDestroyed');
+ }
+ }
+ }, this);
+ } else {
+ // @TODO Add a one time warning that this no longer cleans up the publisher
+ }
+ }, this);
+
+ this.dispatchEvent(event, defaultAction);
+ };
+
+ archiveCreatedHandler = function(archive) {
+ this.dispatchEvent(new OT.ArchiveEvent('archiveStarted', archive));
+ };
+
+ archiveDestroyedHandler = function(archive) {
+ this.dispatchEvent(new OT.ArchiveEvent('archiveDestroyed', archive));
+ };
+
+ archiveUpdatedHandler = function(event) {
+ var archive = event.target,
+ propertyName = event.changedProperty,
+ newValue = event.newValue;
+
+ if(propertyName === 'status' && newValue === 'stopped') {
+ this.dispatchEvent(new OT.ArchiveEvent('archiveStopped', archive));
+ } else {
+ this.dispatchEvent(new OT.ArchiveEvent('archiveUpdated', archive));
+ }
+ };
+
+ init = function() {
+ _session.token = _token = null;
+ setState('disconnected');
+ _session.connection = null;
+ _session.capabilities = new OT.Capabilities([]);
+ _session.connections.destroy();
+ _session.streams.destroy();
+ _session.archives.destroy();
+ };
+
+ // Put ourselves into a pristine state
+ reset = function() {
+ // reset connection id now so that following calls to testNetwork and connect will share
+ // the same new session id. We need to reset here because testNetwork might be call after
+ // and it is always called before the session is connected
+ // on initial connection we don't reset
+ _connectionId = OT.$.uuid();
+ init();
+ };
+
+ disconnectComponents = function(reason) {
+ OT.$.forEach(OT.publishers.where({session: this}), function(publisher) {
+ publisher.disconnect(reason);
+ });
+
+ OT.$.forEach(OT.subscribers.where({session: this}), function(subscriber) {
+ subscriber.disconnect();
+ });
+ };
+
+ destroyPublishers = function(reason) {
+ OT.$.forEach(OT.publishers.where({session: this}), function(publisher) {
+ publisher._.streamDestroyed(reason);
+ });
+ };
+
+ destroySubscribers = function(reason) {
+ OT.$.forEach(OT.subscribers.where({session: this}), function(subscriber) {
+ subscriber.destroy(reason);
+ });
+ };
+
+ connectMessenger = function() {
+ OT.debug('OT.Session: connecting to Raptor');
+
+ var socketUrl = this.sessionInfo.messagingURL,
+ symphonyUrl = OT.properties.symphonyAddresss || this.sessionInfo.symphonyAddress;
+
+ _socket = new OT.Raptor.Socket(_connectionId, _widgetId, socketUrl, symphonyUrl,
+ OT.SessionDispatcher(this));
+
+
+ _socket.connect(_token, this.sessionInfo, OT.$.bind(function(error, sessionState) {
+ if (error) {
+ _socket = void 0;
+ this.logConnectivityEvent('Failure', error);
+
+ sessionConnectFailed.call(this, error.message, error.code);
+ return;
+ }
+
+ OT.debug('OT.Session: Received session state from Raptor', sessionState);
+
+ this.connection = this.connections.get(_socket.id());
+ if(this.connection) {
+ this.capabilities = this.connection.permissions;
+ }
+
+ setState('connected');
+
+ this.logConnectivityEvent('Success', null, {connectionId: this.connection.id});
+
+ // Listen for our own connection's destroyed event so we know when we've been disconnected.
+ this.connection.on('destroyed', sessionDisconnectedHandler, this);
+
+ // Listen for connection updates
+ this.connections.on({
+ add: connectionCreatedHandler,
+ remove: connectionDestroyedHandler
+ }, this);
+
+ // Listen for stream updates
+ this.streams.on({
+ add: streamCreatedHandler,
+ remove: streamDestroyedHandler,
+ update: streamPropertyModifiedHandler
+ }, this);
+
+ this.archives.on({
+ add: archiveCreatedHandler,
+ remove: archiveDestroyedHandler,
+ update: archiveUpdatedHandler
+ }, this);
+
+ this.dispatchEvent(
+ new OT.SessionConnectEvent(OT.Event.names.SESSION_CONNECTED), OT.$.bind(function() {
+ this.connections._triggerAddEvents(); // { id: this.connection.id }
+ this.streams._triggerAddEvents(); // { id: this.stream.id }
+ this.archives._triggerAddEvents();
+ }, this)
+ );
+
+ }, this));
+ };
+
+ getSessionInfo = function() {
+ if (this.is('connecting')) {
+ OT.SessionInfo.get(
+ this,
+ OT.$.bind(onSessionInfoResponse, this),
+ OT.$.bind(function(error) {
+ sessionConnectFailed.call(this, error.message +
+ (error.code ? ' (' + error.code + ')' : ''), error.code);
+ }, this)
+ );
+ }
+ };
+
+ onSessionInfoResponse = function(sessionInfo) {
+ if (this.is('connecting')) {
+ var overrides = OT.properties.sessionInfoOverrides;
+ this.sessionInfo = sessionInfo;
+ if (overrides != null && typeof overrides === 'object') {
+ this.sessionInfo = OT.$.defaults(overrides, this.sessionInfo);
+ }
+ if (this.sessionInfo.partnerId && this.sessionInfo.partnerId !== _apiKey) {
+ this.apiKey = _apiKey = this.sessionInfo.partnerId;
+
+ var reason = 'Authentication Error: The API key does not match the token or session.';
+
+ var payload = {
+ code: OT.ExceptionCodes.AUTHENTICATION_ERROR,
+ message: reason
+ };
+ this.logEvent('Failure', 'SessionInfo', payload);
+
+ sessionConnectFailed.call(this, reason, OT.ExceptionCodes.AUTHENTICATION_ERROR);
+ } else {
+ connectMessenger.call(this);
+ }
+ }
+ };
+
+ // Check whether we have permissions to perform the action.
+ permittedTo = OT.$.bind(function(action) {
+ return this.capabilities.permittedTo(action);
+ }, this);
+
+ dispatchError = OT.$.bind(function(code, message, completionHandler) {
+ OT.dispatchError(code, message, completionHandler, this);
+ }, this);
+
+ this.logEvent = function(action, variation, payload, options) {
+ var event = {
+ action: action,
+ variation: variation,
+ payload: payload,
+ sessionId: _sessionId,
+ partnerId: _apiKey
+ };
+
+ event.connectionId = _connectionId;
+
+ if (options) event = OT.$.extend(options, event);
+ OT.analytics.logEvent(event);
+ };
+
+ /**
+ * @typedef {Object} Stats
+ * @property {number} bytesSentPerSecond
+ * @property {number} bytesReceivedPerSecond
+ * @property {number} packetLossRatio
+ * @property {number} rtt
+ */
+
+ function getTestNetworkConfig(token) {
+ return new Promise(function(resolve, reject) {
+ OT.$.getJSON(
+ [OT.properties.apiURL, '/v2/partner/', _apiKey, '/session/', _sessionId, '/connection/',
+ _connectionId, '/testNetworkConfig'].join(''),
+ {
+ headers: {'X-TB-TOKEN-AUTH': token}
+ },
+ function(errorEvent, response) {
+ if (errorEvent) {
+ var error = JSON.parse(errorEvent.target.responseText);
+ if (error.code === -1) {
+ reject(new OT.$.Error('Unexpected HTTP error codes ' +
+ errorEvent.target.status, '2001'));
+ } else if (error.code === 10001 || error.code === 10002) {
+ reject(new OT.$.Error(error.message, '1004'));
+ } else {
+ reject(new OT.$.Error(error.message, error.code));
+ }
+ } else {
+ resolve(response);
+ }
+ });
+ });
+ }
+
+ /**
+ * @param {string} token
+ * @param {OT.Publisher} publisher
+ * @param {function(?OT.$.Error, Stats=)} callback
+ */
+ this.testNetwork = function(token, publisher, callback) {
+
+ // intercept call to callback to log the result
+ var origCallback = callback;
+ callback = function loggingCallback(error, stats) {
+ if (error) {
+ _session.logEvent('testNetwork', 'Failure', {
+ failureCode: error.name || error.message || 'unknown'
+ });
+ } else {
+ _session.logEvent('testNetwork', 'Success', stats);
+ }
+ origCallback(error, stats);
+ };
+
+ _session.logEvent('testNetwork', 'Attempt', {});
+
+ if(this.isConnected()) {
+ callback(new OT.$.Error('Session connected, cannot test network', 1015));
+ return;
+ }
+
+ var webRtcStreamPromise = new Promise(
+ function(resolve, reject) {
+ var webRtcStream = publisher._.webRtcStream();
+ if (webRtcStream) {
+ resolve(webRtcStream);
+ } else {
+
+ var onAccessAllowed = function() {
+ unbind();
+ resolve(publisher._.webRtcStream());
+ };
+
+ var onPublishComplete = function(error) {
+ if (error) {
+ unbind();
+ reject(error);
+ }
+ };
+
+ var unbind = function() {
+ publisher.off('publishComplete', onPublishComplete);
+ publisher.off('accessAllowed', onAccessAllowed);
+ };
+
+ publisher.on('publishComplete', onPublishComplete);
+ publisher.on('accessAllowed', onAccessAllowed);
+
+ }
+ });
+
+ var testConfig;
+ var webrtcStats;
+ Promise.all([getTestNetworkConfig(token), webRtcStreamPromise])
+ .then(function(values) {
+ var webRtcStream = values[1];
+ testConfig = values[0];
+ return OT.webrtcTest({mediaConfig: testConfig.media, localStream: webRtcStream});
+ })
+ .then(function(stats) {
+ OT.debug('Received stats from webrtcTest: ', stats);
+ if (stats.bandwidth < testConfig.media.thresholdBitsPerSecond) {
+ return Promise.reject(new OT.$.Error('The detect bandwidth form the WebRTC stage of ' +
+ 'the test was not sufficient to run the HTTP stage of the test', 1553));
+ }
+
+ webrtcStats = stats;
+ })
+ .then(function() {
+ return OT.httpTest({httpConfig: testConfig.http});
+ })
+ .then(function(httpStats) {
+ var stats = {
+ uploadBitsPerSecond: httpStats.uploadBandwidth,
+ downloadBitsPerSecond: httpStats.downloadBandwidth,
+ packetLossRatio: webrtcStats.packetLostRatio,
+ roundTripTimeMilliseconds: webrtcStats.roundTripTime
+ };
+ callback(null, stats);
+ // IE8 (ES3 JS engine) requires bracket notation for "catch" keyword
+ })['catch'](function(error) {
+ callback(error);
+ });
+ };
+
+ this.logConnectivityEvent = function(variation, payload, options) {
+ if (variation === 'Attempt' || !_connectivityAttemptPinger) {
+ var pingerOptions = {
+ action: 'Connect',
+ sessionId: _sessionId,
+ partnerId: _apiKey
+ };
+ if (this.connection && this.connection.id) {
+ pingerOptions = event.connectionId = this.connection.id;
+ } else if (_connectionId) {
+ pingerOptions.connectionId = _connectionId;
+ }
+ _connectivityAttemptPinger = new OT.ConnectivityAttemptPinger(pingerOptions);
+ }
+ _connectivityAttemptPinger.setVariation(variation);
+ this.logEvent('Connect', variation, payload, options);
+ };
+
+/**
+* Connects to an OpenTok session.
+*
+* Upon a successful connection, the completion handler (the second parameter of the method) is +* invoked without an error object passed in. (If there is an error connecting, the completion +* handler is invoked with an error object.) Make sure that you have successfully connected to the +* session before calling other methods of the Session object. +*
+*
+* The Session object dispatches a connectionCreated
event when any client
+* (including your own) connects to to the session.
+*
+* 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
(see the Error object).
+* On success, the completionHandler
function is not passed any
+* arguments. On error, the function is passed an error
object parameter
+* (see the Error object). The
+* error
object has two properties: code
(an integer) and
+* message
(a string), which identify the cause of the failure. The following
+* code adds a completionHandler
when calling the connect()
method:
+* +* session.connect(token, function (error) { +* if (error) { +* console.log(error.message); +* } else { +* console.log("Connected to session."); +* } +* }); +*+*
+* Note that upon connecting to the session, the Session object dispatches a
+* sessionConnected
event in addition to calling the completionHandler
.
+* The SessionConnectEvent object, which defines the sessionConnected
event,
+* includes connections
and streams
properties, which
+* list the connections and streams in the session when you connect.
+*
+* 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
+* (see the Error object). The
+* error
object has two properties: code
(an integer) and
+* message
(a string), which identify the cause of the failure. Calling
+* publish()
fails if the role assigned to your token is not "publisher" or
+* "moderator"; in this case error.code
is set to 1500. Calling
+* publish()
also fails the client fails to connect; in this case
+* error.code
is set to 1013. The following code adds a
+* completionHandler
when calling the publish()
method:
+* +* session.publish(publisher, null, function (error) { +* if (error) { +* console.log(error.message); +* } else { +* console.log("Publishing a stream."); +* } +* }); +*+* +* @returns The Publisher object for this stream. +* +* @method #publish +* @memberOf Session +*/ + this.publish = function(publisher, properties, completionHandler) { + if(typeof publisher === 'function') { + completionHandler = publisher; + publisher = undefined; + } + if(typeof properties === 'function') { + completionHandler = properties; + properties = undefined; + } + if (this.isNot('connected')) { + OT.analytics.logError(1010, 'OT.exception', + 'We need to be connected before you can publish', null, { + action: 'Publish', + variation: 'Failure', + payload: { + reason:'unconnected', + code: OT.ExceptionCodes.NOT_CONNECTED, + message: 'We need to be connected before you can publish' + }, + sessionId: _sessionId, + partnerId: _apiKey, + }); + + 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')) { + var errorMessage = 'This token does not allow publishing. The role must be at least ' + + '`publisher` to enable this functionality'; + var payload = { + reason: 'permission', + code: OT.ExceptionCodes.UNABLE_TO_PUBLISH, + message: errorMessage + }; + this.logEvent('publish', 'Failure', payload); + dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, errorMessage, completionHandler); + return null; + } + + // If the user has passed in an ID of a element then we create a new publisher. + if (!publisher || typeof(publisher)==='string' || OT.$.isElementNode(publisher)) { + // Initiate a new Publisher with the new session credentials + publisher = OT.initPublisher(publisher, properties); + + } else if (publisher instanceof OT.Publisher){ + + // If the publisher already has a session attached to it we can + if ('session' in publisher && publisher.session && 'sessionId' in publisher.session) { + // send a warning message that we can't publish again. + if( publisher.session.sessionId === this.sessionId){ + OT.warn('Cannot publish ' + publisher.guid() + ' again to ' + + this.sessionId + '. Please call session.unpublish(publisher) first.'); + } else { + OT.warn('Cannot publish ' + publisher.guid() + ' publisher already attached to ' + + publisher.session.sessionId+ '. Please call session.unpublish(publisher) first.'); + } + } + + } else { + dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, + 'Session.publish :: First parameter passed in is neither a ' + + 'string nor an instance of the Publisher', + completionHandler); + return; + } + + publisher.once('publishComplete', function(err) { + if (err) { + dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, + 'Session.publish :: ' + err.message, + completionHandler); + return; + } + + if (completionHandler && OT.$.isFunction(completionHandler)) { + completionHandler.apply(null, arguments); + } + }); + + // Add publisher reference to the session + publisher._.publishToSession(this); + + // return the embed publisher + return publisher; + }; + +/** +* Ceases publishing the specified publisher's audio-video stream +* to the session. By default, the local representation of the audio-video stream is +* removed from the web page. Upon successful termination, the Session object on every +* connected web page dispatches +* a
streamDestroyed
event.
+*
+*
+*
+* To prevent the Publisher from being removed from the DOM, add an event listener for the
+* streamDestroyed
event dispatched by the Publisher object and call the
+* preventDefault()
method of the event object.
+*
+* Note: If you intend to reuse a Publisher object created using
+* OT.initPublisher()
to publish to different sessions sequentially, call
+* either Session.disconnect()
or Session.unpublish()
. Do not call
+* both. Then call the preventDefault()
method of the streamDestroyed
+* or sessionDisconnected
event object to prevent the Publisher object from being
+* removed from the page. Be sure to call preventDefault()
only if the
+* connection.connectionId
property of the Stream object in the event matches the
+* connection.connectionId
property of your Session object (to ensure that you are
+* preventing the default behavior for your published streams, not for other streams that you
+* subscribe to).
+*
+* 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.fitMode
(String) Determines how the video is displayed if the its
+* dimensions do not match those of the DOM element. You can set this property to one of
+* the following values:
+* +*
"cover"
— The video is cropped if its dimensions do not match
+* those of the DOM element. This is the default setting for screen-sharing videos
+* (for Stream objects with the videoType
property set to
+* "screen"
).
+* "contain"
— The video is letter-boxed if its dimensions do not
+* match those of the DOM element. This is the default setting for videos that have a
+* camera as the source (for Stream objects with the videoType
property
+* set to "camera"
).
+* height
(Number) The desired height, in pixels, of the
+* displayed Subscriber video stream (default: 198). Note: Use the
+* height
and width
properties to set the dimensions
+* of the Subscriber video; do not set the height and width of the DOM element
+* (using CSS).insertMode
(String) Specifies how the Subscriber object will
+* be inserted in the HTML DOM. See the targetElement
parameter. This
+* string can have the following values:
+* "replace"
The Subscriber object replaces contents of the
+* targetElement. This is the default."after"
The Subscriber object is a new element inserted
+* after the targetElement in the HTML DOM. (Both the Subscriber and targetElement
+* have the same parent element.)"before"
The Subscriber object is a new element inserted
+* before the targetElement in the HTML DOM. (Both the Subsciber and targetElement
+* have the same parent element.)"append"
The Subscriber object is a new element added as a
+* child of the targetElement. If there are other child elements, the Subscriber is
+* appended as the last child element of the targetElement.style
(Object) An object containing properties that define the initial
+* appearance of user interface controls of the Subscriber. The style
object
+* includes the following properties:
+* audioLevelDisplayMode
(String) — How to display the audio level
+* indicator. Possible values are: "auto"
(the indicator is displayed when the
+* video is disabled), "off"
(the indicator is not displayed), and
+* "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
+* the background image when a video is not displayed. (A video may not be displayed if
+* you call subscribeToVideo(false)
on the Subscriber object). You can pass an
+* http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
+* data
URI scheme (instead of http or https) and pass in base-64-encrypted
+* PNG data, such as that obtained from the
+* Subscriber.getImgData() method. For example,
+* you could set the property to "data:VBORw0KGgoAA..."
, where the portion of
+* the string after "data:"
is the result of a call to
+* Subscriber.getImgData()
. If the URL or the image data is invalid, the
+* property is ignored (the attempt to set the image fails silently).
+*
+* Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+* you cannot set the backgroundImageURI
style to a string larger than
+* 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
+* limitation, you cannot set the backgroundImageURI
style to a string obtained
+* with the getImgData()
method.
+*
buttonDisplayMode
(String) — How to display the speaker controls
+* Possible values are: "auto"
(controls are displayed when the stream is first
+* displayed and when the user mouses over the display), "off"
(controls are not
+* displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
+* Possible values are: "auto"
(the name is displayed when the stream is first
+* displayed and when the user mouses over the display), "off"
(the name is not
+* displayed), and "on"
(the name is always displayed).videoDisabledDisplayMode
(String) Whether to display the video
+* disabled indicator and video disabled warning icons for a Subscriber. These icons
+* indicate that the video has been disabled (or is in risk of being disabled for
+* the warning icon) due to poor stream quality. This style only applies to the Subscriber
+* object. Possible values are: "auto"
(the icons are automatically when the
+* displayed video is disabled or in risk of being disabled due to poor stream quality),
+* "off"
(do not display the icons), and "on"
(display the
+* icons). The default setting is "auto"
subscribeToAudio
(Boolean) Whether to initially subscribe to audio
+* (if available) for the stream (default: true
).subscribeToVideo
(Boolean) Whether to initially subscribe to video
+* (if available) for the stream (default: true
).width
(Number) The desired width, in pixels, of the
+* displayed Subscriber video stream (default: 264). Note: Use the
+* height
and width
properties to set the dimensions
+* of the Subscriber video; do not set the height and width of the DOM element
+* (using CSS).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; + properties = undefined; + } + + if(typeof properties === 'function') { + completionHandler = properties; + properties = undefined; + } + + var subscriber = new OT.Subscriber(targetElement, OT.$.extend(properties || {}, { + stream: stream, + session: this + }), function(err) { + + if (err) { + dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, + 'Session.subscribe :: ' + err.message, + completionHandler); + + } else if (completionHandler && OT.$.isFunction(completionHandler)) { + completionHandler.apply(null, arguments); + } + + }); + + OT.subscribers.add(subscriber); + + 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);
},
- initialState = 'NotPublishing';
+ jsepCandidate: function(streamId, candidate) {
+ return _socket.jsepCandidate(streamId, candidate);
+ },
- OT.PublishingState = OT.generateSimpleStateMachine(initialState, validStates, validTransitions);
+ jsepOffer: function(streamId, offerSdp) {
+ return _socket.jsepOffer(streamId, offerSdp);
+ },
- OT.PublishingState.prototype.isDestroyed = function() {
- return this.current === 'Destroyed';
+ jsepOfferP2p: function(streamId, subscriberId, offerSdp) {
+ return _socket.jsepOfferP2p(streamId, subscriberId, offerSdp);
+ },
+
+ jsepAnswer: function(streamId, answerSdp) {
+ return _socket.jsepAnswer(streamId, answerSdp);
+ },
+
+ jsepAnswerP2p: function(streamId, subscriberId, answerSdp) {
+ return _socket.jsepAnswerP2p(streamId, subscriberId, answerSdp);
+ },
+
+ // session.on("signal", function(SignalEvent))
+ // session.on("signal:{type}", function(SignalEvent))
+ dispatchSignal: OT.$.bind(function(fromConnection, type, data) {
+ var event = new OT.SignalEvent(type, data, fromConnection);
+ event.target = this;
+
+ // signal a "signal" event
+ // NOTE: trigger doesn't support defaultAction, and therefore preventDefault.
+ this.trigger(OT.Event.names.SIGNAL, event);
+
+ // signal an "signal:{type}" event" if there was a custom type
+ if (type) this.dispatchEvent(event);
+ }, this),
+
+ subscriberCreate: function(stream, subscriber, channelsToSubscribeTo, completion) {
+ return _socket.subscriberCreate(stream.id, subscriber.widgetId,
+ channelsToSubscribeTo, completion);
+ },
+
+ subscriberDestroy: function(stream, subscriber) {
+ return _socket.subscriberDestroy(stream.id, subscriber.widgetId);
+ },
+
+ subscriberUpdate: function(stream, subscriber, attributes) {
+ return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes);
+ },
+
+ subscriberChannelUpdate: function(stream, subscriber, channel, attributes) {
+ return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id,
+ attributes);
+ },
+
+ streamCreate: function(name, audioFallbackEnabled, channels, completion) {
+ _socket.streamCreate(
+ name,
+ audioFallbackEnabled,
+ channels,
+ OT.Config.get('bitrates', 'min', OT.APIKEY),
+ OT.Config.get('bitrates', 'max', OT.APIKEY),
+ completion
+ );
+ },
+
+ streamDestroy: function(streamId) {
+ _socket.streamDestroy(streamId);
+ },
+
+ streamChannelUpdate: function(stream, channel, attributes) {
+ _socket.streamChannelUpdate(stream.id, channel.id, attributes);
+ }
};
- OT.PublishingState.prototype.isAttemptingToPublish = function() {
- return OT.$.arrayIndexOf(
- [ 'GetUserMedia', 'BindingMedia', 'MediaBound', 'PublishingToSession' ],
- this.current) !== -1;
+
+/**
+* Sends a signal to each client or a specified client in the session. Specify a
+* to
property of the signal
parameter to limit the signal to
+* be sent to a specific client; otherwise the signal is sent to each client connected to
+* the session.
+* +* The following example sends a signal of type "foo" with a specified data payload ("hello") +* to all clients connected to the session: +*
+* session.signal({ +* type: "foo", +* data: "hello" +* }, +* function(error) { +* if (error) { +* console.log("signal error: " + error.message); +* } else { +* console.log("signal sent"); +* } +* } +* ); +*+*
+* Calling this method without specifying a recipient client (by setting the to
+* property of the signal
parameter) results in multiple signals sent (one to each
+* client in the session). For information on charges for signaling, see the
+* OpenTok pricing page.
+*
+* The following example sends a signal of type "foo" with a data payload ("hello") to a +* specific client connected to the session: +*
+* session.signal({ +* type: "foo", +* to: recipientConnection; // a Connection object +* data: "hello" +* }, +* function(error) { +* if (error) { +* console.log("signal error: " + error.message); +* } else { +* console.log("signal sent"); +* } +* } +* ); +*+*
+* Add an event handler for the signal
event to listen for all signals sent in
+* the session. Add an event handler for the signal:type
event to listen for
+* signals of a specified type only (replace type
, in signal:type
,
+* with the type of signal to listen for). The Session object dispatches these events. (See
+* events.)
+*
+* @param {Object} signal An object that contains the following properties defining the signal:
+*
data
— (String) The data to send. The limit to the length of data
+* string is 8kB. Do not set the data string to null
or
+* undefined
.to
— (Connection) A Connection
+* object corresponding to the client that the message is to be sent to. If you do not
+* specify this property, the signal is sent to all clients connected to the session.type
— (String) The type of the signal. You can use the type to
+* filter signals when setting an event handler for the signal:type
event
+* (where you replace type
with the type string). The maximum length of the
+* type
string is 128 characters, and it must contain only letters (A-Z and a-z),
+* numbers (0-9), '-', '_', and '~'.Each property is optional. If you set none of the properties, you will send a signal +* with no data or type to each client connected to the session.
+* +* @param {Function} completionHandler A function that is called when sending the signal +* succeeds or fails. This function takes one parameter —error
.
+* On success, the completionHandler
function is not passed any
+* arguments. On error, the function is passed an error
object, defined by the
+* Error class. The error
object has the following
+* properties:
+*
+* code
— (Number) An error code, which can be one of the following:
+* 400 | One of the signal properties — data, type, or to — +* is invalid. | +*
404 | The client specified by the to property is not connected to +* the session. | +*
413 | The type string exceeds the maximum length (128 bytes), +* or the data string exceeds the maximum size (8 kB). | +*
500 | You are not connected to the OpenTok session. | +*
message
— (String) A description of the error.Note that the completionHandler
success result (error == null
)
+* indicates that the options passed into the Session.signal()
method are valid
+* and the signal was sent. It does not indicate that the signal was successfully
+* received by any of the intended recipients.
+*
+* @method #signal
+* @memberOf Session
+* @see signal and signal:type events
+*/
+ this.signal = function(options, completion) {
+ var _options = options,
+ _completion = completion;
+
+ if (OT.$.isFunction(_options)) {
+ _completion = _options;
+ _options = null;
+ }
+
+ if (this.isNot('connected')) {
+ var notConnectedErrorMsg = 'Unable to send signal - you are not connected to the session.';
+ dispatchError(500, notConnectedErrorMsg, _completion);
+ return;
+ }
+
+ _socket.signal(_options, _completion, this.logEvent);
+ 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.');
+ }
};
- OT.PublishingState.prototype.isPublishing = function() {
- return this.current === 'Publishing';
+/**
+* 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); + } }; -})(window); -!(function() { +/** +* 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; + } - // The default constraints - var defaultConstraints = { - audio: true, - video: true + var notPermittedErrorMsg = 'This token does not allow forceUnpublish. ' + + 'The role must be at least `moderator` to enable this functionality'; + + if (permittedTo('forceUnpublish')) { + var stream = typeof streamOrStreamId === 'string' ? + this.streams.get(streamOrStreamId) : streamOrStreamId; + + _socket.forceUnpublish(stream.id, function(err) { + if (err) { + dispatchError(OT.ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH, + notPermittedErrorMsg, completionHandler); + } else if (completionHandler && OT.$.isFunction(completionHandler)) { + completionHandler.apply(null, arguments); + } + }); + } else { + // if this throws an error the handleJsException won't occur + dispatchError(OT.ExceptionCodes.UNABLE_TO_FORCE_UNPUBLISH, + notPermittedErrorMsg, completionHandler); + } }; + this.getStateManager = function() { + OT.warn('Fixme: Have not implemented session.getStateManager'); + }; + + this.isConnected = function() { + return this.is('connected'); + }; + + this.capabilities = new OT.Capabilities([]); + +/** + * Dispatched when an archive recording of the session starts. + * + * @name archiveStarted + * @event + * @memberof Session + * @see ArchiveEvent + * @see Archiving overview. + */ + +/** + * Dispatched when an archive recording of the session stops. + * + * @name archiveStopped + * @event + * @memberof Session + * @see ArchiveEvent + * @see Archiving overview. + */ + +/** + * Dispatched when a new client (including your own) has connected to the session, and for + * every client in the session when you first connect. (The Session object also dispatches + * a
sessionConnected
event when your local client connects.)
+ *
+ * @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 + */ + +/** + * Defines an event dispatched when property of a stream has changed. This can happen in + * in the following conditions: + *
+ *
videoDisabled
and videoEnabled
events in all
+ * conditions that cause the subscriber's stream to be disabled or enabled.
+ * videoDimensions
property of the Stream object has
+ * changed (see Stream.videoDimensions).
+ * videoType
property of the Stream object has changed.
+ * This can happen in a stream published by a mobile device. (See
+ * Stream.videoType.)
+ * 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
+ */
+};
+
+// tb_require('../helpers/helpers.js')
+// tb_require('../helpers/lib/get_user_media.js')
+// tb_require('../helpers/lib/widget_view.js')
+// tb_require('./analytics.js')
+// tb_require('./events.js')
+// tb_require('./system_requirements.js')
+// tb_require('./stylable_component.js')
+// tb_require('./stream.js')
+// tb_require('./connection.js')
+// tb_require('./publishing_state.js')
+// tb_require('./environment_loader.js')
+// tb_require('./audio_context.js')
+// tb_require('./chrome/chrome.js')
+// tb_require('./chrome/backing_bar.js')
+// tb_require('./chrome/name_panel.js')
+// tb_require('./chrome/mute_button.js')
+// tb_require('./chrome/archiving.js')
+// tb_require('./chrome/audio_level_meter.js')
+// tb_require('./peer_connection/publisher_peer_connection.js')
+// tb_require('./screensharing/register.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+// The default constraints
+var defaultConstraints = {
+ audio: true,
+ video: true
+};
+
/**
* The Publisher object provides the mechanism through which control of the
* published stream is accomplished. Calling the OT.initPublisher()
method
@@ -17582,4691 +22046,2068 @@ var SDPHelpers = {
* @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;
+OT.Publisher = function(options) {
+ // 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,
+ _widgetView,
+ _targetElement,
+ _stream,
+ _streamId,
+ _webRTCStream,
+ _session,
+ _peerConnections = {},
+ _loaded = false,
+ _publishStartTime,
+ _microphone,
+ _chrome,
+ _audioLevelMeter,
+ _properties,
+ _validResolutions,
+ _validFrameRates = [ 1, 7, 15, 30 ],
+ _prevStats,
+ _state,
+ _iceServers,
+ _audioLevelCapable = OT.$.hasCapabilities('webAudio'),
+ _audioLevelSampler,
+ _isScreenSharing = options && (
+ options.videoSource === 'screen' ||
+ options.videoSource === 'window' ||
+ options.videoSource === 'tab' ||
+ options.videoSource === 'application'
+ ),
+ _connectivityAttemptPinger,
+ _publisher = this;
+
+ _properties = OT.$.defaults(options || {}, {
+ publishAudio: _isScreenSharing ? false : true,
+ publishVideo: true,
+ mirror: _isScreenSharing ? false : true,
+ showControls: true,
+ fitMode: _isScreenSharing ? 'contain' : 'cover',
+ audioFallbackEnabled: _isScreenSharing ? false : true,
+ maxResolution: _isScreenSharing ? { width: 1920, height: 1920 } : undefined
+ });
+
+ _validResolutions = {
+ '320x240': {width: 320, height: 240},
+ '640x480': {width: 640, height: 480},
+ '1280x720': {width: 1280, height: 720}
+ };
+
+ if (_isScreenSharing) {
+ if (window.location.protocol !== 'https:') {
+ OT.warn('Screen Sharing typically requires pages to be loadever over HTTPS - unless this ' +
+ 'browser is configured locally to allow non-SSL pages, permission will be denied ' +
+ 'without user input.');
}
+ }
- var _guid = OT.Publisher.nextId(),
- _domId,
- _container,
- _targetElement,
- _stream,
- _streamId,
- _webRTCStream,
- _session,
- _peerConnections = {},
- _loaded = false,
- _publishProperties,
- _publishStartTime,
- _microphone,
- _chrome,
- _audioLevelMeter,
- _analytics = new OT.Analytics(),
- _validResolutions,
- _validFrameRates = [ 1, 7, 15, 30 ],
- _prevStats,
- _state,
- _iceServers,
- _audioLevelCapable = OT.$.hasCapabilities('webAudio'),
- _audioLevelSampler,
- _publisher = this;
-
- _validResolutions = {
- '320x240': {width: 320, height: 240},
- '640x480': {width: 640, height: 480},
- '1280x720': {width: 1280, height: 720}
- };
-
- _prevStats = {
- 'timeStamp' : OT.$.now()
- };
-
- OT.$.eventing(this);
-
- if(_audioLevelCapable) {
- _audioLevelSampler = new OT.AnalyserAudioLevelSampler(OT.audioContext());
-
- var audioLevelRunner = new OT.IntervalRunner(function() {
- _audioLevelSampler.sample(function(audioInputLevel) {
- OT.$.requestAnimationFrame(function() {
- _publisher.dispatchEvent(
- new OT.AudioLevelUpdatedEvent(audioInputLevel));
- });
- });
- }, 60);
-
- this.on({
- 'audioLevelUpdated:added': function(count) {
- if (count === 1) {
- audioLevelRunner.start();
- }
- },
- 'audioLevelUpdated:removed': function(count) {
- if (count === 0) {
- audioLevelRunner.stop();
- }
- }
- });
- }
-
- OT.StylableComponent(this, {
- showArchiveStatus: true,
- nameDisplayMode: 'auto',
- buttonDisplayMode: 'auto',
- audioLevelDisplayMode: 'auto',
- backgroundImageURI: null
- });
-
- /// Private Methods
- var logAnalyticsEvent = function(action, variation, payloadType, payload) {
- _analytics.logEvent({
- action: action,
- variation: variation,
- 'payload_type': payloadType,
- payload: payload,
- 'session_id': _session ? _session.sessionId : null,
- 'connection_id': _session &&
- _session.isConnected() ? _session.connection.connectionId : null,
- 'partner_id': _session ? _session.apiKey : OT.APIKEY,
- streamId: _stream ? _stream.id : null,
- 'widget_id': _guid,
- 'widget_type': 'Publisher'
- });
- },
-
- recordQOS = OT.$.bind(function(connection, parsedStats) {
- if(!_state.isPublishing()) {
- return;
- }
- var QoSBlob = {
- 'widget_type': 'Publisher',
- 'stream_type': 'WebRTC',
- sessionId: _session ? _session.sessionId : null,
- connectionId: _session && _session.isConnected() ?
- _session.connection.connectionId : null,
- partnerId: _session ? _session.apiKey : OT.APIKEY,
- streamId: _stream ? _stream.id : null,
- width: _container ? OT.$.width(_container.domElement) : undefined,
- height: _container ? OT.$.height(_container.domElement) : undefined,
- widgetId: _guid,
- version: OT.properties.version,
- 'media_server_name': _session ? _session.sessionInfo.messagingServer : null,
- p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
- duration: _publishStartTime ? new Date().getTime() - _publishStartTime.getTime() : 0,
- 'remote_connection_id': connection.id
- };
-
- _analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) );
- this.trigger('qos', parsedStats);
- }, this),
-
- /// Private Events
-
- stateChangeFailed = function(changeFailed) {
- OT.error('Publisher State Change Failed: ', changeFailed.message);
- OT.debug(changeFailed);
- },
-
- onLoaded = function() {
- if (_state.isDestroyed()) {
- // The publisher was destroyed before loading finished
- return;
- }
-
- OT.debug('OT.Publisher.onLoaded');
-
- _state.set('MediaBound');
-
- // If we have a session and we haven't created the stream yet then
- // wait until that is complete before hiding the loading spinner
- _container.loading(this.session ? !_stream : false);
-
- _loaded = true;
-
- _createChrome.call(this);
-
- this.trigger('initSuccess');
- this.trigger('loaded', this);
- },
-
- onLoadFailure = function(reason) {
- logAnalyticsEvent('publish', 'Failure', 'reason',
- 'Publisher PeerConnection Error: ' + reason);
-
- _state.set('Failed');
- this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.P2P_CONNECTION_FAILED,
- 'Publisher PeerConnection Error: ' + reason));
-
- OT.handleJsException('Publisher PeerConnection Error: ' + reason,
- OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
- session: _session,
- target: this
- });
- },
-
- onStreamAvailable = function(webOTStream) {
- OT.debug('OT.Publisher.onStreamAvailable');
-
- _state.set('BindingMedia');
-
- cleanupLocalStream();
- _webRTCStream = webOTStream;
-
- _microphone = new OT.Microphone(_webRTCStream, !_publishProperties.publishAudio);
- this.publishVideo(_publishProperties.publishVideo &&
- _webRTCStream.getVideoTracks().length > 0);
-
- this.accessAllowed = true;
- this.dispatchEvent(new OT.Event(OT.Event.names.ACCESS_ALLOWED, false));
-
- var videoContainerOptions = {
- muted: true,
- error: OT.$.bind(onVideoError, this)
- };
-
- _targetElement = _container.bindVideo(_webRTCStream,
- videoContainerOptions,
- OT.$.bind(function(err) {
- if (err) {
- onLoadFailure.call(this, err);
- return;
- }
-
- onLoaded.call(this);
- }, this));
-
- if(_audioLevelSampler && webOTStream.getAudioTracks().length > 0) {
- _audioLevelSampler.webOTStream = webOTStream;
- }
-
- },
-
- onStreamAvailableError = function(error) {
- OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
-
- _state.set('Failed');
- this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
- error.message));
-
- if (_container) _container.destroy();
-
- logAnalyticsEvent('publish', 'Failure', 'reason',
- 'GetUserMedia:Publisher failed to access camera/mic: ' + error.message);
-
- OT.handleJsException('Publisher failed to access camera/mic: ' + error.message,
- OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
- session: _session,
- target: this
- });
- },
-
- // The user has clicked the 'deny' button the the allow access dialog
- // (or it's set to always deny)
- onAccessDenied = function(error) {
- OT.error('OT.Publisher.onStreamAvailableError Permission Denied');
-
- _state.set('Failed');
- this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
- 'Publisher Access Denied: Permission Denied' +
- (error.message ? ': ' + error.message : '')));
-
- logAnalyticsEvent('publish', 'Failure', 'reason',
- 'GetUserMedia:Publisher Access Denied: Permission Denied');
-
- this.dispatchEvent(new OT.Event(OT.Event.names.ACCESS_DENIED));
- },
-
- accessDialogWasOpened = false,
-
- onAccessDialogOpened = function() {
-
- accessDialogWasOpened = true;
-
- logAnalyticsEvent('accessDialog', 'Opened', '', '');
-
- this.dispatchEvent(new OT.Event(OT.Event.names.ACCESS_DIALOG_OPENED, true));
- },
-
- onAccessDialogClosed = function() {
- logAnalyticsEvent('accessDialog', 'Closed', '', '');
-
- this.dispatchEvent( new OT.Event(OT.Event.names.ACCESS_DIALOG_CLOSED, false));
- },
-
- onVideoError = function(errorCode, errorReason) {
- OT.error('OT.Publisher.onVideoError');
-
- var message = errorReason + (errorCode ? ' (' + errorCode + ')' : '');
- logAnalyticsEvent('stream', null, 'reason',
- 'Publisher while playing stream: ' + message);
-
- _state.set('Failed');
-
- if (_state.isAttemptingToPublish()) {
- this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
- message));
- } else {
- this.trigger('error', message);
- }
-
- OT.handleJsException('Publisher error playing stream: ' + message,
- OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
- session: _session,
- target: this
- });
- },
-
- onPeerDisconnected = function(peerConnection) {
- OT.debug('OT.Subscriber has been disconnected from the Publisher\'s PeerConnection');
-
- this.cleanupSubscriber(peerConnection.remoteConnection().id);
- },
-
- onPeerConnectionFailure = function(code, reason, peerConnection, prefix) {
- logAnalyticsEvent('publish', 'Failure', 'reason|hasRelayCandidates',
- (prefix ? prefix : '') + [':Publisher PeerConnection with connection ' +
- (peerConnection && peerConnection.remoteConnection &&
- peerConnection.remoteConnection().id) + ' failed: ' +
- reason, peerConnection.hasRelayCandidates()
- ].join('|'));
-
- OT.handleJsException('Publisher PeerConnection Error: ' + reason,
- OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
- session: _session,
- target: this
- });
-
- // We don't call cleanupSubscriber as it also logs a
- // disconnected analytics event, which we don't want in this
- // instance. The duplication is crufty though and should
- // be tidied up.
-
- delete _peerConnections[peerConnection.remoteConnection().id];
- },
-
- /// Private Helpers
-
- // Assigns +stream+ to this publisher. The publisher listens
- // for a bunch of events on the stream so it can respond to
- // changes.
- assignStream = OT.$.bind(function(stream) {
- this.stream = _stream = stream;
- _stream.on('destroyed', this.disconnect, this);
-
- _state.set('Publishing');
- _container.loading(!_loaded);
- _publishStartTime = new Date();
-
- this.trigger('publishComplete', null, this);
-
- this.dispatchEvent(new OT.StreamEvent('streamCreated', stream, null, false));
-
- logAnalyticsEvent('publish', 'Success', 'streamType:streamId', 'WebRTC:' + _streamId);
- }, this),
-
- // Clean up our LocalMediaStream
- cleanupLocalStream = function() {
- if (_webRTCStream) {
- // Stop revokes our access cam and mic access for this instance
- // of localMediaStream.
- _webRTCStream.stop();
- _webRTCStream = null;
- }
- },
-
- createPeerConnectionForRemote = function(remoteConnection) {
- var peerConnection = _peerConnections[remoteConnection.id];
-
- if (!peerConnection) {
- var startConnectingTime = OT.$.now();
-
- logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
-
- // Cleanup our subscriber when they disconnect
- remoteConnection.on('destroyed',
- OT.$.bind(this.cleanupSubscriber, this, remoteConnection.id));
-
- peerConnection = _peerConnections[remoteConnection.id] = new OT.PublisherPeerConnection(
- remoteConnection,
- _session,
- _streamId,
- _webRTCStream
- );
-
- peerConnection.on({
- connected: function() {
- logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
- parseInt(OT.$.now() - startConnectingTime, 10),
- peerConnection.hasRelayCandidates()
- ].join('|'));
- },
- disconnected: onPeerDisconnected,
- error: onPeerConnectionFailure,
- qos: recordQOS
- }, this);
-
- peerConnection.init(_iceServers);
- }
-
- return peerConnection;
- },
-
- /// Chrome
-
- // If mode is false, then that is the mode. If mode is true then we'll
- // definitely display the button, but we'll defer the model to the
- // Publishers buttonDisplayMode style property.
- chromeButtonMode = function(mode) {
- if (mode === false) return 'off';
-
- var defaultMode = this.getStyle('buttonDisplayMode');
-
- // The default model is false, but it's overridden by +mode+ being true
- if (defaultMode === false) return 'on';
-
- // defaultMode is either true or auto.
- return defaultMode;
- },
-
- updateChromeForStyleChange = function(key, value) {
- if (!_chrome) return;
-
- switch(key) {
- case 'nameDisplayMode':
- _chrome.name.setDisplayMode(value);
- _chrome.backingBar.setNameMode(value);
- break;
-
- case 'showArchiveStatus':
- logAnalyticsEvent('showArchiveStatus', 'styleChange', 'mode', value ? 'on': 'off');
- _chrome.archive.setShowArchiveStatus(value);
- break;
-
- case 'buttonDisplayMode':
- _chrome.muteButton.setDisplayMode(value);
- _chrome.backingBar.setMuteMode(value);
- break;
-
- case 'audioLevelDisplayMode':
- _chrome.audioLevel.setDisplayMode(value);
- break;
-
- case 'backgroundImageURI':
- _container.setBackgroundImageURI(value);
- }
- },
-
- _createChrome = function() {
-
- if(!this.getStyle('showArchiveStatus')) {
- logAnalyticsEvent('showArchiveStatus', 'createChrome', 'mode', 'off');
- }
-
- var widgets = {
- backingBar: new OT.Chrome.BackingBar({
- nameMode: !_publishProperties.name ? 'off' : this.getStyle('nameDisplayMode'),
- muteMode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode'))
- }),
-
- name: new OT.Chrome.NamePanel({
- name: _publishProperties.name,
- mode: this.getStyle('nameDisplayMode')
- }),
-
- muteButton: new OT.Chrome.MuteButton({
- muted: _publishProperties.publishAudio === false,
- mode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode'))
- }),
-
- archive: new OT.Chrome.Archiving({
- show: this.getStyle('showArchiveStatus'),
- archiving: false
- })
- };
-
- if (_audioLevelCapable) {
- var audioLevelTransformer = new OT.AudioLevelTransformer();
-
- var audioLevelUpdatedHandler = function(evt) {
- _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
- };
-
- _audioLevelMeter = new OT.Chrome.AudioLevelMeter({
- mode: this.getStyle('audioLevelDisplayMode'),
- onActivate: function() {
- _publisher.on('audioLevelUpdated', audioLevelUpdatedHandler);
- },
- onPassivate: function() {
- _publisher.off('audioLevelUpdated', audioLevelUpdatedHandler);
- }
- });
-
- widgets.audioLevel = _audioLevelMeter;
- }
-
- _chrome = new OT.Chrome({
- parent: _container.domElement
- }).set(widgets).on({
- muted: OT.$.bind(this.publishAudio, this, false),
- unmuted: OT.$.bind(this.publishAudio, this, true)
- });
-
- if(_audioLevelMeter && this.getStyle('audioLevelDisplayMode') === 'auto') {
- _audioLevelMeter[_container.audioOnly() ? 'show' : 'hide']();
- }
- },
-
- reset = OT.$.bind(function() {
- if (_chrome) {
- _chrome.destroy();
- _chrome = null;
- }
-
- this.disconnect();
-
- _microphone = null;
-
- if (_targetElement) {
- _targetElement.destroy();
- _targetElement = null;
- }
-
- cleanupLocalStream();
-
- if (_container) {
- _container.destroy();
- _container = null;
- }
-
- if (_session) {
- this._.unpublishFromSession(_session, 'reset');
- }
-
- this.id = _domId = null;
- this.stream = _stream = null;
- _loaded = false;
-
- this.session = _session = null;
-
- if (!_state.isDestroyed()) _state.set('NotPublishing');
- }, this);
-
- var setAudioOnly = function(audioOnly) {
- if (_container) {
- _container.audioOnly(audioOnly);
- _container.showPoster(audioOnly);
- }
-
- if (_audioLevelMeter && _publisher.getStyle('audioLevelDisplayMode') === 'auto') {
- _audioLevelMeter[audioOnly ? 'show' : 'hide']();
- }
- };
-
- this.publish = function(targetElement, properties) {
- OT.debug('OT.Publisher: publish');
-
- if ( _state.isAttemptingToPublish() || _state.isPublishing() ) reset();
- _state.set('GetUserMedia');
-
- _publishProperties = OT.$.defaults(properties || {}, {
- publishAudio : true,
- publishVideo : true,
- mirror: true
- });
-
- if (!_publishProperties.constraints) {
- _publishProperties.constraints = OT.$.clone(defaultConstraints);
-
- if(_publishProperties.audioSource === null || _publishProperties.audioSource === false) {
- _publishProperties.constraints.audio = false;
- _publishProperties.publishAudio = false;
- } else {
- if(typeof _publishProperties.audioSource === 'object') {
- if(_publishProperties.audioSource.deviceId != null) {
- _publishProperties.audioSource = _publishProperties.audioSource.deviceId;
- } else {
- OT.warn('Invalid audioSource passed to Publisher. Expected either a device ID');
- }
- }
-
- if (_publishProperties.audioSource) {
- if (typeof _publishProperties.constraints.audio !== 'object') {
- _publishProperties.constraints.audio = {};
- }
- if (!_publishProperties.constraints.audio.mandatory) {
- _publishProperties.constraints.audio.mandatory = {};
- }
- if (!_publishProperties.constraints.audio.optional) {
- _publishProperties.constraints.audio.optional = [];
- }
- _publishProperties.constraints.audio.mandatory.sourceId =
- _publishProperties.audioSource;
- }
- }
-
- if(_publishProperties.videoSource === null || _publishProperties.videoSource === false) {
- _publishProperties.constraints.video = false;
- _publishProperties.publishVideo = false;
- } else {
-
- if(typeof _publishProperties.videoSource === 'object') {
- if(_publishProperties.videoSource.deviceId != null) {
- _publishProperties.videoSource = _publishProperties.videoSource.deviceId;
- } else {
- OT.warn('Invalid videoSource passed to Publisher. Expected either a device ID');
- }
- }
-
- if (_publishProperties.videoSource) {
- if (typeof _publishProperties.constraints.video !== 'object') {
- _publishProperties.constraints.video = {};
- }
- if (!_publishProperties.constraints.video.mandatory) {
- _publishProperties.constraints.video.mandatory = {};
- }
- if (!_publishProperties.constraints.video.optional) {
- _publishProperties.constraints.video.optional = [];
- }
- _publishProperties.constraints.video.mandatory.sourceId =
- _publishProperties.videoSource;
- }
-
- if (_publishProperties.resolution) {
- if (_publishProperties.resolution !== void 0 &&
- !_validResolutions.hasOwnProperty(_publishProperties.resolution)) {
- OT.warn('Invalid resolution passed to the Publisher. Got: ' +
- _publishProperties.resolution + ' expecting one of "' +
- OT.$.keys(_validResolutions).join('","') + '"');
- } else {
- _publishProperties.videoDimensions = _validResolutions[_publishProperties.resolution];
- if (typeof _publishProperties.constraints.video !== 'object') {
- _publishProperties.constraints.video = {};
- }
- if (!_publishProperties.constraints.video.mandatory) {
- _publishProperties.constraints.video.mandatory = {};
- }
- if (!_publishProperties.constraints.video.optional) {
- _publishProperties.constraints.video.optional = [];
- }
- _publishProperties.constraints.video.optional =
- _publishProperties.constraints.video.optional.concat([
- {minWidth: _publishProperties.videoDimensions.width},
- {maxWidth: _publishProperties.videoDimensions.width},
- {minHeight: _publishProperties.videoDimensions.height},
- {maxHeight: _publishProperties.videoDimensions.height}
- ]);
- }
- }
-
- if (_publishProperties.frameRate !== void 0 &&
- OT.$.arrayIndexOf(_validFrameRates, _publishProperties.frameRate) === -1) {
- OT.warn('Invalid frameRate passed to the publisher got: ' +
- _publishProperties.frameRate + ' expecting one of ' + _validFrameRates.join(','));
- delete _publishProperties.frameRate;
- } else if (_publishProperties.frameRate) {
- if (typeof _publishProperties.constraints.video !== 'object') {
- _publishProperties.constraints.video = {};
- }
- if (!_publishProperties.constraints.video.mandatory) {
- _publishProperties.constraints.video.mandatory = {};
- }
- if (!_publishProperties.constraints.video.optional) {
- _publishProperties.constraints.video.optional = [];
- }
- _publishProperties.constraints.video.optional =
- _publishProperties.constraints.video.optional.concat([
- { minFrameRate: _publishProperties.frameRate },
- { maxFrameRate: _publishProperties.frameRate }
- ]);
- }
- }
-
- } else {
- OT.warn('You have passed your own constraints not using ours');
- }
-
-
- if (_publishProperties.style) {
- this.setStyle(_publishProperties.style, null, true);
- }
-
- if (_publishProperties.name) {
- _publishProperties.name = _publishProperties.name.toString();
- }
-
- _publishProperties.classNames = 'OT_root OT_publisher';
-
- // Defer actually creating the publisher DOM nodes until we know
- // the DOM is actually loaded.
- OT.onLoad(function() {
- _container = new OT.WidgetView(targetElement, _publishProperties);
- this.id = _domId = _container.domId();
- this.element = _container.domElement;
-
- OT.$.shouldAskForDevices(OT.$.bind(function(devices) {
- if(!devices.video) {
- OT.warn('Setting video constraint to false, there are no video sources');
- _publishProperties.constraints.video = false;
- }
- if(!devices.audio) {
- OT.warn('Setting audio constraint to false, there are no audio sources');
- _publishProperties.constraints.audio = false;
- }
- OT.$.getUserMedia(
- _publishProperties.constraints,
- OT.$.bind(onStreamAvailable, this),
- OT.$.bind(onStreamAvailableError, this),
- OT.$.bind(onAccessDialogOpened, this),
- OT.$.bind(onAccessDialogClosed, this),
- OT.$.bind(onAccessDenied, this)
- );
- }, this));
-
- }, this);
-
- return this;
- };
-
- /**
- * Starts publishing audio (if it is currently not being published)
- * when the value
is true
; stops publishing audio
- * (if it is currently being published) when the value
is false
.
- *
- * @param {Boolean} value Whether to start publishing audio (true
)
- * or not (false
).
- *
- * @see OT.initPublisher()
- * @see Stream.hasAudio
- * @see StreamPropertyChangedEvent
- * @method #publishAudio
- * @memberOf Publisher
- */
- this.publishAudio = function(value) {
- _publishProperties.publishAudio = value;
-
- if (_microphone) {
- _microphone.muted(!value);
- }
-
- if (_chrome) {
- _chrome.muteButton.muted(!value);
- }
-
- if (_session && _stream) {
- _stream.setChannelActiveState('audio', value);
- }
-
- return this;
- };
-
- /**
- * Starts publishing video (if it is currently not being published)
- * when the value
is true
; stops publishing video
- * (if it is currently being published) when the value
is false
.
- *
- * @param {Boolean} value Whether to start publishing video (true
)
- * or not (false
).
- *
- * @see OT.initPublisher()
- * @see Stream.hasVideo
- * @see StreamPropertyChangedEvent
- * @method #publishVideo
- * @memberOf Publisher
- */
- this.publishVideo = function(value) {
- var oldValue = _publishProperties.publishVideo;
- _publishProperties.publishVideo = value;
-
- if (_session && _stream && _publishProperties.publishVideo !== oldValue) {
- _stream.setChannelActiveState('video', value);
- }
-
- // We currently do this event if the value of publishVideo has not changed
- // This is because the state of the video tracks enabled flag may not match
- // the value of publishVideo at this point. This will be tidied up shortly.
- if (_webRTCStream) {
- var videoTracks = _webRTCStream.getVideoTracks();
- for (var i=0, num=videoTracks.length; idestroyed
event when the DOM
- * element is removed.
- *
You can use the string as the value for a data URL scheme passed to the src parameter of - * an image file, as in the following:
- * - *- * var imgData = publisher.getImgData(); - * - * var img = document.createElement("img"); - * img.setAttribute("src", "data:image/png;base64," + imgData); - * var imgWin = window.open("about:blank", "Screenshot"); - * imgWin.document.write("<body></body>"); - * imgWin.document.body.appendChild(img); - *- * - * @method #getImgData - * @memberOf Publisher - * @return {String} The base-64 encoded string. Returns an empty string if there is no video. - */ - - this.getImgData = function() { - if (!_loaded) { - OT.error('OT.Publisher.getImgData: Cannot getImgData before the Publisher is publishing.'); - - return null; - } - - return _targetElement.imgData(); - }; - - - // API Compatibility layer for Flash Publisher, this could do with some tidyup. - this._ = { - publishToSession: OT.$.bind(function(session) { - // Add session property to Publisher - this.session = _session = session; - - var createStream = function() { - - var streamWidth, - streamHeight; - - // Bail if this.session is gone, it means we were unpublished - // before createStream could finish. - if (!_session) return; - - _state.set('PublishingToSession'); - - var onStreamRegistered = OT.$.bind(function(err, streamId, message) { - if (err) { - // @todo we should respect err.code here and translate it to the local - // client equivalent. - logAnalyticsEvent('publish', 'Failure', 'reason', - 'Publish:' + OT.ExceptionCodes.UNABLE_TO_PUBLISH + ':' + err.message); - if (_state.isAttemptingToPublish()) { - this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - err.message)); - } - return; - } - - this.streamId = _streamId = streamId; - _iceServers = OT.Raptor.parseIceServers(message); - }, this); - - // We set the streamWidth and streamHeight to be the minimum of the requested - // resolution and the actual resolution. - if (_publishProperties.videoDimensions) { - streamWidth = Math.min(_publishProperties.videoDimensions.width, - _targetElement.videoWidth() || 640); - streamHeight = Math.min(_publishProperties.videoDimensions.height, - _targetElement.videoHeight() || 480); - } else { - streamWidth = _targetElement.videoWidth() || 640; - streamHeight = _targetElement.videoHeight() || 480; - } - - session._.streamCreate( - _publishProperties && _publishProperties.name ? _publishProperties.name : '', - OT.VideoOrientation.ROTATED_NORMAL, - streamWidth, - streamHeight, - _publishProperties.publishAudio, - _publishProperties.publishVideo, - _publishProperties.frameRate, - onStreamRegistered - ); - }; - - if (_loaded) createStream.call(this); - else this.on('initSuccess', createStream, this); - - logAnalyticsEvent('publish', 'Attempt', 'streamType', 'WebRTC'); - - return this; - }, this), - - unpublishFromSession: OT.$.bind(function(session, reason) { - if (!_session || session.id !== _session.id) { - OT.warn('The publisher ' + _guid + ' is trying to unpublish from a session ' + - session.id + ' it is not attached to (it is attached to ' + - (_session && _session.id || 'no session') + ')'); - return this; - } - - if (session.isConnected() && this.stream) { - session._.streamDestroy(this.stream.id); - } - - // Disconnect immediately, rather than wait for the WebSocket to - // reply to our destroyStream message. - this.disconnect(); - this.session = _session = null; - - // We're back to being a stand-alone publisher again. - if (!_state.isDestroyed()) _state.set('MediaBound'); - - logAnalyticsEvent('unpublish', 'Success', 'sessionId', session.id); - - this._.streamDestroyed(reason); - - return this; - }, this), - - streamDestroyed: OT.$.bind(function(reason) { - if(OT.$.arrayIndexOf(['reset'], reason) < 0) { - var event = new OT.StreamEvent('streamDestroyed', _stream, reason, true); - var defaultAction = OT.$.bind(function() { - if(!event.isDefaultPrevented()) { - this.destroy(); - } - }, this); - this.dispatchEvent(event, defaultAction); - } - }, this), - - - archivingStatus: OT.$.bind(function(status) { - if(_chrome) { - _chrome.archive.setArchiving(status); - } - - return status; - }, this), - - webRtcStream: function() { - return _webRTCStream; - } - }; - - this.detectDevices = function() { - OT.warn('Fixme: Haven\'t implemented detectDevices'); - }; - - this.detectMicActivity = function() { - OT.warn('Fixme: Haven\'t implemented detectMicActivity'); - }; - - this.getEchoCancellationMode = function() { - OT.warn('Fixme: Haven\'t implemented getEchoCancellationMode'); - return 'fullDuplex'; - }; - - this.setMicrophoneGain = function() { - OT.warn('Fixme: Haven\'t implemented setMicrophoneGain'); - }; - - this.getMicrophoneGain = function() { - OT.warn('Fixme: Haven\'t implemented getMicrophoneGain'); - return 0.5; - }; - - this.setCamera = function() { - OT.warn('Fixme: Haven\'t implemented setCamera'); - }; - - this.setMicrophone = function() { - OT.warn('Fixme: Haven\'t implemented setMicrophone'); - }; - - - // Platform methods: - - this.guid = function() { - return _guid; - }; - - this.videoElement = function() { - return _targetElement.domElement(); - }; - - this.setStream = assignStream; - - this.isWebRTC = true; - - this.isLoading = function() { - return _container && _container.loading(); - }; - - this.videoWidth = function() { - return _targetElement.videoWidth(); - }; - - this.videoHeight = function() { - return _targetElement.videoHeight(); - }; - - // Make read-only: element, guid, _.webRtcStream - - this.on('styleValueChanged', updateChromeForStyleChange, this); - _state = new OT.PublishingState(stateChangeFailed); - - this.accessAllowed = false; - - /** - * Dispatched when the user has clicked the Allow button, granting the - * app access to the camera and microphone. The Publisher object has an - *
accessAllowed
property which indicates whether the user
- * has granted access to the camera and microphone.
- * @see Event
- * @name accessAllowed
- * @event
- * @memberof Publisher
- */
-
- /**
- * Dispatched when the user has clicked the Deny button, preventing the
- * app from having access to the camera and microphone.
- *
- * The default behavior of this event is to display a user interface element
- * in the Publisher object, indicating that that user has denied access to
- * the camera and microphone. Call the preventDefault()
method
- * method of the event object in the event listener to prevent this message
- * from being displayed.
- * @see Event
- * @name accessDenied
- * @event
- * @memberof Publisher
- */
-
- /**
- * Dispatched when the Allow/Deny dialog box is opened. (This is the dialog box in which
- * the user can grant the app access to the camera and microphone.)
- *
- * The default behavior of this event is to display a message in the browser that instructs
- * the user how to enable the camera and microphone. Call the preventDefault()
- * method of the event object in the event listener to prevent this message from being displayed.
- * @see Event
- * @name accessDialogOpened
- * @event
- * @memberof Publisher
- */
-
- /**
- * Dispatched when the Allow/Deny box is closed. (This is the dialog box in which the
- * user can grant the app access to the camera and microphone.)
- * @see Event
- * @name accessDialogClosed
- * @event
- * @memberof Publisher
- */
-
- /**
- * Dispatched periodically to indicate the publisher's audio level. The event is dispatched
- * up to 60 times per second, depending on the browser. The audioLevel
property
- * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
- * information.
- *
- * The following example adjusts the value of a meter element that shows volume of the - * publisher. Note that the audio level is adjusted logarithmically and a moving average - * is applied: - *
- *
- * var movingAvg = null; - * publisher.on('audioLevelUpdated', function(event) { - * if (movingAvg === null || movingAvg <= event.audioLevel) { - * movingAvg = event.audioLevel; - * } else { - * movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel; - * } - * - * // 1.5 scaling to map the -30 - 0 dBm range to [0,1] - * var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1; - * logLevel = Math.min(Math.max(logLevel, 0), 1); - * document.getElementById('publisherMeter').value = logLevel; - * }); - *- *
This example shows the algorithm used by the default audio level indicator displayed
- * in an audio-only Publisher.
- *
- * @name audioLevelUpdated
- * @event
- * @memberof Publisher
- * @see AudioLevelUpdatedEvent
- */
-
- /**
- * The publisher has started streaming to the session.
- * @name streamCreated
- * @event
- * @memberof Publisher
- * @see StreamEvent
- * @see Session.publish()
- */
-
- /**
- * The publisher has stopped streaming to the session. The default behavior is that
- * the Publisher object is removed from the HTML DOM). The Publisher object dispatches a
- * destroyed
event when the element is removed from the HTML DOM. If you call the
- * preventDefault()
method of the event object in the event listener, the default
- * behavior is prevented, and you can, optionally, retain the Publisher for reuse or clean it up
- * using your own code.
- * @name streamDestroyed
- * @event
- * @memberof Publisher
- * @see StreamEvent
- */
-
- /**
- * Dispatched when the Publisher element is removed from the HTML DOM. When this event
- * is dispatched, you may choose to adjust or remove HTML DOM elements related to the publisher.
- * @name destroyed
- * @event
- * @memberof Publisher
- */
+ _prevStats = {
+ 'timeStamp' : OT.$.now()
};
- // Helper function to generate unique publisher ids
- OT.Publisher.nextId = OT.$.uuid;
+ OT.$.eventing(this);
-})(window);
-!(function() {
+ if(!_isScreenSharing && _audioLevelCapable) {
+ _audioLevelSampler = new OT.AnalyserAudioLevelSampler(OT.audioContext());
-/**
- * The Subscriber object is a representation of the local video element that is playing back
- * a remote stream. The Subscriber object includes methods that let you disable and enable
- * local audio playback for the subscribed stream. The subscribe()
method of the
- * {@link Session} object returns a Subscriber object.
- *
- * @property {Element} element The HTML DOM element containing the Subscriber.
- * @property {String} id The DOM ID of the Subscriber.
- * @property {Stream} stream The stream to which you are subscribing.
- *
- * @class Subscriber
- * @augments EventDispatcher
- */
- OT.Subscriber = function(targetElement, options) {
- var _widgetId = OT.$.uuid(),
- _domId = targetElement || _widgetId,
- _container,
- _streamContainer,
- _chrome,
- _audioLevelMeter,
- _stream,
- _fromConnectionId,
- _peerConnection,
- _session = options.session,
- _subscribeStartTime,
- _startConnectingTime,
- _properties = OT.$.clone(options),
- _analytics = new OT.Analytics(),
- _audioVolume = 100,
- _state,
- _prevStats,
- _lastSubscribeToVideoReason,
- _audioLevelCapable = OT.$.hasCapabilities('audioOutputLevelStat') ||
- OT.$.hasCapabilities('webAudioCapableRemoteStream'),
- _audioLevelSampler,
- _audioLevelRunner,
- _frameRateRestricted = false,
- _subscriber = this;
-
- this.id = _domId;
- this.widgetId = _widgetId;
- this.session = _session;
-
- _prevStats = {
- timeStamp: OT.$.now()
- };
-
- if (!_session) {
- OT.handleJsException('Subscriber must be passed a session option', 2000, {
- session: _session,
- target: this
+ var audioLevelRunner = new OT.IntervalRunner(function() {
+ _audioLevelSampler.sample(function(audioInputLevel) {
+ OT.$.requestAnimationFrame(function() {
+ _publisher.dispatchEvent(
+ new OT.AudioLevelUpdatedEvent(audioInputLevel));
+ });
});
+ }, 60);
- return;
- }
-
- OT.$.eventing(this, false);
-
- if(_audioLevelCapable) {
- this.on({
- 'audioLevelUpdated:added': function(count) {
- if (count === 1 && _audioLevelRunner) {
- _audioLevelRunner.start();
- }
- },
- 'audioLevelUpdated:removed': function(count) {
- if (count === 0 && _audioLevelRunner) {
- _audioLevelRunner.stop();
- }
+ this.on({
+ 'audioLevelUpdated:added': function(count) {
+ if (count === 1) {
+ audioLevelRunner.start();
}
- });
- }
-
- OT.StylableComponent(this, {
- nameDisplayMode: 'auto',
- buttonDisplayMode: 'auto',
- audioLevelDisplayMode: 'auto',
- videoDisabledIndicatorDisplayMode: 'auto',
- backgroundImageURI: null,
- showArchiveStatus: true,
- showMicButton: true
+ },
+ 'audioLevelUpdated:removed': function(count) {
+ if (count === 0) {
+ audioLevelRunner.stop();
+ }
+ }
});
+ }
- var logAnalyticsEvent = function(action, variation, payloadType, payload) {
- /* jshint camelcase:false*/
- _analytics.logEvent({
- action: action,
- variation: variation,
- payload_type: payloadType,
- payload: payload,
- stream_id: _stream ? _stream.id : null,
- session_id: _session ? _session.sessionId : null,
- connection_id: _session && _session.isConnected() ?
- _session.connection.connectionId : null,
- partner_id: _session && _session.isConnected() ? _session.sessionInfo.partnerId : null,
- widget_id: _widgetId,
- widget_type: 'Subscriber'
+ /// Private Methods
+ var logAnalyticsEvent = function(action, variation, payload, throttle) {
+ OT.analytics.logEvent({
+ action: action,
+ variation: variation,
+ payload: payload,
+ 'sessionId': _session ? _session.sessionId : null,
+ 'connectionId': _session &&
+ _session.isConnected() ? _session.connection.connectionId : null,
+ 'partnerId': _session ? _session.apiKey : OT.APIKEY,
+ streamId: _stream ? _stream.id : null
+ }, throttle);
+ },
+
+ logConnectivityEvent = function(variation, payload) {
+ if (variation === 'Attempt' || !_connectivityAttemptPinger) {
+ _connectivityAttemptPinger = new OT.ConnectivityAttemptPinger({
+ action: 'Publish',
+ 'sessionId': _session ? _session.sessionId : null,
+ 'connectionId': _session &&
+ _session.isConnected() ? _session.connection.connectionId : null,
+ 'partnerId': _session ? _session.apiKey : OT.APIKEY,
+ streamId: _stream ? _stream.id : null
});
- },
+ }
+ if (variation === 'Failure' && payload.reason !== 'Non-fatal') {
+ // We don't want to log an invalid sequence in this case because it was a
+ // non-fatal failure
+ _connectivityAttemptPinger.setVariation(variation);
+ }
+ logAnalyticsEvent('Publish', variation, payload);
+ },
- recordQOS = OT.$.bind(function(parsedStats) {
- if(_state.isSubscribing() && _session && _session.isConnected()) {
- /*jshint camelcase:false */
- var QoSBlob = {
- widget_type: 'Subscriber',
- stream_type : 'WebRTC',
- width: _container ? OT.$.width(_container.domElement) : undefined,
- height: _container ? OT.$.height(_container.domElement) : undefined,
- session_id: _session ? _session.sessionId : null,
- connectionId: _session ? _session.connection.connectionId : null,
- media_server_name: _session ? _session.sessionInfo.messagingServer : null,
- p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
- partner_id: _session ? _session.apiKey : null,
- stream_id: _stream.id,
- widget_id: _widgetId,
- version: OT.properties.version,
- duration: parseInt(OT.$.now() - _subscribeStartTime, 10),
- remote_connection_id: _stream.connection.connectionId
- };
+ recordQOS = OT.$.bind(function(connection, parsedStats) {
+ var QoSBlob = {
+ streamType: 'WebRTC',
+ sessionId: _session ? _session.sessionId : null,
+ connectionId: _session && _session.isConnected() ?
+ _session.connection.connectionId : null,
+ partnerId: _session ? _session.apiKey : OT.APIKEY,
+ streamId: _stream ? _stream.id : null,
+ width: _widgetView ? Number(OT.$.width(_widgetView.domElement).replace('px', ''))
+ : undefined,
+ height: _widgetView ? Number(OT.$.height(_widgetView.domElement).replace('px', ''))
+ : undefined,
+ version: OT.properties.version,
+ mediaServerName: _session ? _session.sessionInfo.messagingServer : null,
+ p2pFlag: _session ? _session.sessionInfo.p2pEnabled : false,
+ duration: _publishStartTime ? new Date().getTime() - _publishStartTime.getTime() : 0,
+ remoteConnectionId: connection.id
+ };
+ OT.analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) );
+ this.trigger('qos', parsedStats);
+ }, this),
- _analytics.logQOS( OT.$.extend(QoSBlob, parsedStats) );
- this.trigger('qos', parsedStats);
- }
- }, this),
+ /// Private Events
+
+ stateChangeFailed = function(changeFailed) {
+ OT.error('Publisher State Change Failed: ', changeFailed.message);
+ OT.debug(changeFailed);
+ },
+
+ onLoaded = OT.$.bind(function() {
+ if (_state.isDestroyed()) {
+ // The publisher was destroyed before loading finished
+ return;
+ }
+
+ OT.debug('OT.Publisher.onLoaded');
+
+ _state.set('MediaBound');
+
+ // If we have a session and we haven't created the stream yet then
+ // wait until that is complete before hiding the loading spinner
+ _widgetView.loading(this.session ? !_stream : false);
+
+ _loaded = true;
+
+ createChrome.call(this);
+
+ this.trigger('initSuccess');
+ this.trigger('loaded', this);
+ }, this),
+
+ onLoadFailure = OT.$.bind(function(reason) {
+ var errorCode = OT.ExceptionCodes.P2P_CONNECTION_FAILED;
+ var payload = {
+ reason: 'Publisher PeerConnection Error: ',
+ code: errorCode,
+ message: reason
+ };
+ logConnectivityEvent('Failure', payload);
+
+ _state.set('Failed');
+ this.trigger('publishComplete', new OT.Error(errorCode,
+ 'Publisher PeerConnection Error: ' + reason));
+
+ OT.handleJsException('Publisher PeerConnection Error: ' + reason,
+ OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
+ session: _session,
+ target: this
+ });
+ }, this),
+
+ onStreamAvailable = OT.$.bind(function(webOTStream) {
+ OT.debug('OT.Publisher.onStreamAvailable');
+
+ _state.set('BindingMedia');
+
+ cleanupLocalStream();
+ _webRTCStream = webOTStream;
+
+ _microphone = new OT.Microphone(_webRTCStream, !_properties.publishAudio);
+ this.publishVideo(_properties.publishVideo &&
+ _webRTCStream.getVideoTracks().length > 0);
- stateChangeFailed = function(changeFailed) {
- OT.error('Subscriber State Change Failed: ', changeFailed.message);
- OT.debug(changeFailed);
- },
+ this.accessAllowed = true;
+ this.dispatchEvent(new OT.Event(OT.Event.names.ACCESS_ALLOWED, false));
- onLoaded = function() {
- if (_state.isSubscribing() || !_streamContainer) return;
+ var videoContainerOptions = {
+ muted: true,
+ error: onVideoError
+ };
- OT.debug('OT.Subscriber.onLoaded');
-
- _state.set('Subscribing');
- _subscribeStartTime = OT.$.now();
-
- logAnalyticsEvent('createPeerConnection', 'Success', 'pcc|hasRelayCandidates', [
- parseInt(_subscribeStartTime - _startConnectingTime, 10),
- _peerConnection && _peerConnection.hasRelayCandidates()
- ].join('|'));
-
- _container.loading(false);
-
- _createChrome.call(this);
- if(_frameRateRestricted) {
- _stream.setRestrictFrameRate(true);
+ _targetElement = _widgetView.bindVideo(_webRTCStream,
+ videoContainerOptions,
+ function(err) {
+ if (err) {
+ onLoadFailure(err);
+ return;
}
- this.trigger('subscribeComplete', null, this);
- this.trigger('loaded', this);
+ onLoaded();
+ });
- logAnalyticsEvent('subscribe', 'Success', 'streamId', _stream.id);
- },
+ if(_audioLevelSampler && _webRTCStream.getAudioTracks().length > 0) {
+ _audioLevelSampler.webRTCStream = _webRTCStream;
+ }
- onDisconnected = function() {
- OT.debug('OT.Subscriber has been disconnected from the Publisher\'s PeerConnection');
+ }, this),
- if (_state.isAttemptingToSubscribe()) {
- // subscribing error
- _state.set('Failed');
- this.trigger('subscribeComplete', new OT.Error(null, 'ClientDisconnected'));
+ onStreamAvailableError = OT.$.bind(function(error) {
+ OT.error('OT.Publisher.onStreamAvailableError ' + error.name + ': ' + error.message);
- } else if (_state.isSubscribing()) {
- _state.set('Failed');
+ _state.set('Failed');
+ this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
+ error.message));
- // we were disconnected after we were already subscribing
- // probably do nothing?
- }
+ if (_widgetView) _widgetView.destroy();
- this.disconnect();
- },
+ var payload = {
+ reason: 'GetUserMedia',
+ code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
+ message: 'Publisher failed to access camera/mic: ' + error.message
+ };
+ logConnectivityEvent('Failure', payload);
- onPeerConnectionFailure = OT.$.bind(function(reason, peerConnection, prefix) {
- if (_state.isAttemptingToSubscribe()) {
- // We weren't subscribing yet so this was a failure in setting
- // up the PeerConnection or receiving the initial stream.
- logAnalyticsEvent('createPeerConnection', 'Failure', 'reason|hasRelayCandidates', [
- 'Subscriber PeerConnection Error: ' + reason,
- _peerConnection && _peerConnection.hasRelayCandidates()
- ].join('|'));
+ OT.handleJsException(payload.reason,
+ payload.code, {
+ session: _session,
+ target: this
+ });
+ }, this),
- _state.set('Failed');
- this.trigger('subscribeComplete', new OT.Error(null, reason));
+ onScreenSharingError = OT.$.bind(function(error) {
+ OT.error('OT.Publisher.onScreenSharingError ' + error.message);
+ _state.set('Failed');
- } else if (_state.isSubscribing()) {
- // we were disconnected after we were already subscribing
- _state.set('Failed');
- this.trigger('error', reason);
- }
+ this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
+ 'Screensharing: ' + error.message));
- this.disconnect();
+ var payload = {
+ reason: 'ScreenSharing',
+ message:error.message
+ };
+ logConnectivityEvent('Failure', payload);
+ }, this),
- logAnalyticsEvent('subscribe', 'Failure', 'reason',
- (prefix ? prefix : '') + ':Subscriber PeerConnection Error: ' + reason);
+ // The user has clicked the 'deny' button the the allow access dialog
+ // (or it's set to always deny)
+ onAccessDenied = OT.$.bind(function(error) {
+ OT.error('OT.Publisher.onStreamAvailableError Permission Denied');
- OT.handleJsException('Subscriber PeerConnection Error: ' + reason,
- OT.ExceptionCodes.P2P_CONNECTION_FAILED, {
- session: _session,
- target: this
- }
+ _state.set('Failed');
+ var errorMessage = 'Publisher Access Denied: Permission Denied' +
+ (error.message ? ': ' + error.message : '');
+ var errorCode = OT.ExceptionCodes.UNABLE_TO_PUBLISH;
+ this.trigger('publishComplete', new OT.Error(errorCode, errorMessage));
+
+ var payload = {
+ reason: 'GetUserMedia',
+ code: errorCode,
+ message: errorMessage
+ };
+ logConnectivityEvent('Failure', payload);
+
+ this.dispatchEvent(new OT.Event(OT.Event.names.ACCESS_DENIED));
+ }, this),
+
+ onAccessDialogOpened = OT.$.bind(function() {
+ logAnalyticsEvent('accessDialog', 'Opened');
+
+ this.dispatchEvent(new OT.Event(OT.Event.names.ACCESS_DIALOG_OPENED, true));
+ }, this),
+
+ onAccessDialogClosed = OT.$.bind(function() {
+ logAnalyticsEvent('accessDialog', 'Closed');
+
+ this.dispatchEvent( new OT.Event(OT.Event.names.ACCESS_DIALOG_CLOSED, false));
+ }, this),
+
+ onVideoError = OT.$.bind(function(errorCode, errorReason) {
+ OT.error('OT.Publisher.onVideoError');
+
+ var message = errorReason + (errorCode ? ' (' + errorCode + ')' : '');
+ logAnalyticsEvent('stream', null, {reason:'Publisher while playing stream: ' + message});
+
+ _state.set('Failed');
+
+ if (_state.isAttemptingToPublish()) {
+ this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH,
+ message));
+ } else {
+ this.trigger('error', message);
+ }
+
+ OT.handleJsException('Publisher error playing stream: ' + message,
+ OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
+ session: _session,
+ target: this
+ });
+ }, this),
+
+ onPeerDisconnected = OT.$.bind(function(peerConnection) {
+ OT.debug('OT.Subscriber has been disconnected from the Publisher\'s PeerConnection');
+
+ this.cleanupSubscriber(peerConnection.remoteConnection().id);
+ }, this),
+
+ onPeerConnectionFailure = OT.$.bind(function(code, reason, peerConnection, prefix) {
+ var payload = {
+ reason: prefix ? prefix : 'PeerConnectionError',
+ code: OT.ExceptionCodes.UNABLE_TO_PUBLISH,
+ message: (prefix ? prefix : '') + ':Publisher PeerConnection with connection ' +
+ (peerConnection && peerConnection.remoteConnection &&
+ peerConnection.remoteConnection().id) + ' failed: ' + reason,
+ hasRelayCandidates: peerConnection.hasRelayCandidates()
+ };
+ if (_state.isPublishing()) {
+ // We're already publishing so this is a Non-fatal failure, must be p2p and one of our
+ // peerconnections failed
+ payload.reason = 'Non-fatal';
+ }
+ logConnectivityEvent('Failure', payload);
+
+ OT.handleJsException('Publisher PeerConnection Error: ' + reason,
+ OT.ExceptionCodes.UNABLE_TO_PUBLISH, {
+ session: _session,
+ target: this
+ });
+
+ // We don't call cleanupSubscriber as it also logs a
+ // disconnected analytics event, which we don't want in this
+ // instance. The duplication is crufty though and should
+ // be tidied up.
+
+ delete _peerConnections[peerConnection.remoteConnection().id];
+ }, this),
+
+ /// Private Helpers
+
+ // Assigns +stream+ to this publisher. The publisher listens
+ // for a bunch of events on the stream so it can respond to
+ // changes.
+ assignStream = OT.$.bind(function(stream) {
+ this.stream = _stream = stream;
+ _stream.on('destroyed', this.disconnect, this);
+
+ _state.set('Publishing');
+ _widgetView.loading(!_loaded);
+ _publishStartTime = new Date();
+
+ this.trigger('publishComplete', null, this);
+
+ this.dispatchEvent(new OT.StreamEvent('streamCreated', stream, null, false));
+
+ var payload = {
+ streamType: 'WebRTC',
+ };
+ logConnectivityEvent('Success', payload);
+ }, 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 = OT.$.bind(function(remoteConnection) {
+ var peerConnection = _peerConnections[remoteConnection.id];
+
+ if (!peerConnection) {
+ var startConnectingTime = OT.$.now();
+
+ logAnalyticsEvent('createPeerConnection', 'Attempt');
+
+ // Cleanup our subscriber when they disconnect
+ remoteConnection.on('destroyed',
+ OT.$.bind(this.cleanupSubscriber, this, remoteConnection.id));
+
+ peerConnection = _peerConnections[remoteConnection.id] = new OT.PublisherPeerConnection(
+ remoteConnection,
+ _session,
+ _streamId,
+ _webRTCStream
);
- _showError.call(this, reason);
- }, this),
- onRemoteStreamAdded = function(webOTStream) {
- OT.debug('OT.Subscriber.onRemoteStreamAdded');
-
- _state.set('BindingRemoteStream');
-
- // Disable the audio/video, if needed
- this.subscribeToAudio(_properties.subscribeToAudio);
-
- _lastSubscribeToVideoReason = 'loading';
- this.subscribeToVideo(_properties.subscribeToVideo, 'loading');
-
- var videoContainerOptions = {
- error: onPeerConnectionFailure,
- audioVolume: _audioVolume
- };
-
- // This is a workaround for a bug in Chrome where a track disabled on
- // the remote end doesn't fire loadedmetadata causing the subscriber to timeout
- // https://jira.tokbox.com/browse/OPENTOK-15605
- var browser = OT.$.browserVersion(),
- tracks,
- reenableVideoTrack = false;
- if (!_stream.hasVideo && browser.browser === 'Chrome' && browser.version >= 35) {
- tracks = webOTStream.getVideoTracks();
- if(tracks.length > 0) {
- tracks[0].enabled = false;
- reenableVideoTrack = tracks[0];
- }
- }
-
- _streamContainer = _container.bindVideo(webOTStream,
- videoContainerOptions,
- OT.$.bind(function(err) {
- if (err) {
- onPeerConnectionFailure(err.message || err, _peerConnection, 'VideoElement');
- return;
- }
-
- // Continues workaround for https://jira.tokbox.com/browse/OPENTOK-15605
- if (reenableVideoTrack != null && _properties.subscribeToVideo) {
- reenableVideoTrack.enabled = true;
- }
-
- _streamContainer.orientation({
- width: _stream.videoDimensions.width,
- height: _stream.videoDimensions.height,
- videoOrientation: _stream.videoDimensions.orientation
- });
-
- onLoaded.call(this, null);
- }, this));
-
- if (OT.$.hasCapabilities('webAudioCapableRemoteStream') && _audioLevelSampler &&
- webOTStream.getAudioTracks().length > 0) {
- _audioLevelSampler.webOTStream = webOTStream;
- }
-
- logAnalyticsEvent('createPeerConnection', 'StreamAdded', '', '');
- this.trigger('streamAdded', this);
- },
-
- onRemoteStreamRemoved = function(webOTStream) {
- OT.debug('OT.Subscriber.onStreamRemoved');
-
- if (_streamContainer.stream === webOTStream) {
- _streamContainer.destroy();
- _streamContainer = null;
- }
-
-
- this.trigger('streamRemoved', this);
- },
-
- streamDestroyed = function () {
- this.disconnect();
- },
-
- streamUpdated = function(event) {
-
- switch(event.changedProperty) {
- case 'videoDimensions':
- if (!_streamContainer) {
- // Ignore videoEmension updates before streamContainer is created OPENTOK-17253
- break;
- }
- _streamContainer.orientation({
- width: event.newValue.width,
- height: event.newValue.height,
- videoOrientation: event.newValue.orientation
- });
- break;
-
- case 'videoDisableWarning':
- _chrome.videoDisabledIndicator.setWarning(event.newValue);
- this.dispatchEvent(new OT.VideoDisableWarningEvent(
- event.newValue ? 'videoDisableWarning' : 'videoDisableWarningLifted'
- ));
- break;
-
- case 'hasVideo':
-
- setAudioOnly(!(_stream.hasVideo && _properties.subscribeToVideo));
-
- this.dispatchEvent(new OT.VideoEnabledChangedEvent(
- _stream.hasVideo ? 'videoEnabled' : 'videoDisabled', {
- reason: 'publishVideo'
- }));
- break;
-
- case 'hasAudio':
- // noop
- }
- },
-
- /// Chrome
-
- // If mode is false, then that is the mode. If mode is true then we'll
- // definitely display the button, but we'll defer the model to the
- // Publishers buttonDisplayMode style property.
- chromeButtonMode = function(mode) {
- if (mode === false) return 'off';
-
- var defaultMode = this.getStyle('buttonDisplayMode');
-
- // The default model is false, but it's overridden by +mode+ being true
- if (defaultMode === false) return 'on';
-
- // defaultMode is either true or auto.
- return defaultMode;
- },
-
- updateChromeForStyleChange = function(key, value/*, oldValue*/) {
- if (!_chrome) return;
-
- switch(key) {
- case 'nameDisplayMode':
- _chrome.name.setDisplayMode(value);
- _chrome.backingBar.setNameMode(value);
- break;
-
- case 'videoDisabledDisplayMode':
- _chrome.videoDisabledIndicator.setDisplayMode(value);
- break;
-
- case 'showArchiveStatus':
- _chrome.archive.setShowArchiveStatus(value);
- break;
-
- case 'buttonDisplayMode':
- _chrome.muteButton.setDisplayMode(value);
- _chrome.backingBar.setMuteMode(value);
- break;
-
- case 'audioLevelDisplayMode':
- _chrome.audioLevel.setDisplayMode(value);
- break;
-
- case 'backgroundImageURI':
- _container.setBackgroundImageURI(value);
- }
- },
-
- _createChrome = function() {
-
- var widgets = {
- backingBar: new OT.Chrome.BackingBar({
- nameMode: !_properties.name ? 'off' : this.getStyle('nameDisplayMode'),
- muteMode: chromeButtonMode.call(this, this.getStyle('showMuteButton'))
- }),
-
- name: new OT.Chrome.NamePanel({
- name: _properties.name,
- mode: this.getStyle('nameDisplayMode')
- }),
-
- muteButton: new OT.Chrome.MuteButton({
- muted: _properties.muted,
- mode: chromeButtonMode.call(this, this.getStyle('showMuteButton'))
- }),
-
- archive: new OT.Chrome.Archiving({
- show: this.getStyle('showArchiveStatus'),
- archiving: false
- })
- };
-
- if (_audioLevelCapable) {
- var audioLevelTransformer = new OT.AudioLevelTransformer();
-
- var audioLevelUpdatedHandler = function(evt) {
- _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
- };
-
- _audioLevelMeter = new OT.Chrome.AudioLevelMeter({
- mode: this.getStyle('audioLevelDisplayMode'),
- onActivate: function() {
- _subscriber.on('audioLevelUpdated', audioLevelUpdatedHandler);
- },
- onPassivate: function() {
- _subscriber.off('audioLevelUpdated', audioLevelUpdatedHandler);
- }
- });
-
- widgets.audioLevel = _audioLevelMeter;
- }
-
- widgets.videoDisabledIndicator = new OT.Chrome.VideoDisabledIndicator({
- mode: this.getStyle('videoDisabledDisplayMode')
- });
-
- _chrome = new OT.Chrome({
- parent: _container.domElement
- }).set(widgets).on({
- muted: function() {
- muteAudio.call(this, true);
+ peerConnection.on({
+ connected: function() {
+ var payload = {
+ pcc: parseInt(OT.$.now() - startConnectingTime, 10),
+ hasRelayCandidates: peerConnection.hasRelayCandidates()
+ };
+ logAnalyticsEvent('createPeerConnection', 'Success', payload);
},
-
- unmuted: function() {
- muteAudio.call(this, false);
- }
+ disconnected: onPeerDisconnected,
+ error: onPeerConnectionFailure,
+ qos: recordQOS
}, this);
- if(_audioLevelMeter && this.getStyle('audioLevelDisplayMode') === 'auto') {
- _audioLevelMeter[_container.audioOnly() ? 'show' : 'hide']();
- }
- },
+ peerConnection.init(_iceServers);
+ }
- _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.'
- );
+ return peerConnection;
+ }, this),
+
+ /// Chrome
+
+ // If mode is false, then that is the mode. If mode is true then we'll
+ // definitely display the button, but we'll defer the model to the
+ // Publishers buttonDisplayMode style property.
+ chromeButtonMode = function(mode) {
+ if (mode === false) return 'off';
+
+ var defaultMode = this.getStyle('buttonDisplayMode');
+
+ // The default model is false, but it's overridden by +mode+ being true
+ if (defaultMode === false) return 'on';
+
+ // defaultMode is either true or auto.
+ return defaultMode;
+ },
+
+ updateChromeForStyleChange = function(key, value) {
+ if (!_chrome) return;
+
+ switch(key) {
+ case 'nameDisplayMode':
+ _chrome.name.setDisplayMode(value);
+ _chrome.backingBar.setNameMode(value);
+ break;
+
+ case 'showArchiveStatus':
+ logAnalyticsEvent('showArchiveStatus', 'styleChange', {mode: value ? 'on': 'off'});
+ _chrome.archive.setShowArchiveStatus(value);
+ break;
+
+ case 'buttonDisplayMode':
+ _chrome.muteButton.setDisplayMode(value);
+ _chrome.backingBar.setMuteMode(value);
+ break;
+
+ case 'audioLevelDisplayMode':
+ _chrome.audioLevel.setDisplayMode(value);
+ break;
+
+ case 'backgroundImageURI':
+ _widgetView.setBackgroundImageURI(value);
+ }
+ },
+
+ createChrome = function() {
+
+ if(!this.getStyle('showArchiveStatus')) {
+ logAnalyticsEvent('showArchiveStatus', 'createChrome', {mode: 'off'});
+ }
+
+ var widgets = {
+ backingBar: new OT.Chrome.BackingBar({
+ nameMode: !_properties.name ? 'off' : this.getStyle('nameDisplayMode'),
+ muteMode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode'))
+ }),
+
+ name: new OT.Chrome.NamePanel({
+ name: _properties.name,
+ mode: this.getStyle('nameDisplayMode')
+ }),
+
+ muteButton: new OT.Chrome.MuteButton({
+ muted: _properties.publishAudio === false,
+ mode: chromeButtonMode.call(this, this.getStyle('buttonDisplayMode'))
+ }),
+
+ archive: new OT.Chrome.Archiving({
+ show: this.getStyle('showArchiveStatus'),
+ archiving: false
+ })
+ };
+
+ if (_audioLevelCapable) {
+ var audioLevelTransformer = new OT.AudioLevelTransformer();
+
+ var audioLevelUpdatedHandler = function(evt) {
+ _audioLevelMeter.setValue(audioLevelTransformer.transform(evt.audioLevel));
+ };
+
+ _audioLevelMeter = new OT.Chrome.AudioLevelMeter({
+ mode: this.getStyle('audioLevelDisplayMode'),
+ onActivate: function() {
+ _publisher.on('audioLevelUpdated', audioLevelUpdatedHandler);
+ },
+ onPassivate: function() {
+ _publisher.off('audioLevelUpdated', audioLevelUpdatedHandler);
+ }
+ });
+
+ widgets.audioLevel = _audioLevelMeter;
+ }
+
+ _chrome = new OT.Chrome({
+ parent: _widgetView.domElement
+ }).set(widgets).on({
+ muted: OT.$.bind(this.publishAudio, this, false),
+ unmuted: OT.$.bind(this.publishAudio, this, true)
+ });
+
+ if(_audioLevelMeter && this.getStyle('audioLevelDisplayMode') === 'auto') {
+ _audioLevelMeter[_widgetView.audioOnly() ? 'show' : 'hide']();
+ }
+ },
+
+ reset = OT.$.bind(function() {
+ if (_chrome) {
+ _chrome.destroy();
+ _chrome = null;
+ }
+
+ this.disconnect();
+
+ _microphone = null;
+
+ if (_targetElement) {
+ _targetElement.destroy();
+ _targetElement = null;
+ }
+
+ cleanupLocalStream();
+
+ if (_widgetView) {
+ _widgetView.destroy();
+ _widgetView = null;
+ }
+
+ if (_session) {
+ this._.unpublishFromSession(_session, 'reset');
+ }
+
+ this.id = _domId = null;
+ this.stream = _stream = null;
+ _loaded = false;
+
+ this.session = _session = null;
+
+ if (!_state.isDestroyed()) _state.set('NotPublishing');
+ }, this);
+
+ OT.StylableComponent(this, {
+ showArchiveStatus: true,
+ nameDisplayMode: 'auto',
+ buttonDisplayMode: 'auto',
+ audioLevelDisplayMode: _isScreenSharing ? 'off' : 'auto',
+ backgroundImageURI: null
+ }, _properties.showControls, function (payload) {
+ logAnalyticsEvent('SetStyle', 'Publisher', payload, 0.1);
+ });
+
+ var setAudioOnly = function(audioOnly) {
+ if (_widgetView) {
+ _widgetView.audioOnly(audioOnly);
+ _widgetView.showPoster(audioOnly);
+ }
+
+ if (_audioLevelMeter && _publisher.getStyle('audioLevelDisplayMode') === 'auto') {
+ _audioLevelMeter[audioOnly ? 'show' : 'hide']();
+ }
+ };
+
+ this.publish = function(targetElement) {
+ OT.debug('OT.Publisher: publish');
+
+ if ( _state.isAttemptingToPublish() || _state.isPublishing() ) reset();
+ _state.set('GetUserMedia');
+
+ if (!_properties.constraints) {
+ _properties.constraints = OT.$.clone(defaultConstraints);
+
+ if (_isScreenSharing) {
+ if (_properties.audioSource != null) {
+ OT.warn('Invalid audioSource passed to Publisher - when using screen sharing no ' +
+ 'audioSource may be used');
+ }
+ _properties.audioSource = null;
+ }
+
+ if(_properties.audioSource === null || _properties.audioSource === false) {
+ _properties.constraints.audio = false;
+ _properties.publishAudio = false;
+ } else {
+ if(typeof _properties.audioSource === 'object') {
+ if(_properties.audioSource.deviceId != null) {
+ _properties.audioSource = _properties.audioSource.deviceId;
+ } else {
+ OT.warn('Invalid audioSource passed to Publisher. Expected either a device ID');
+ }
+ }
+
+ if (_properties.audioSource) {
+ if (typeof _properties.constraints.audio !== 'object') {
+ _properties.constraints.audio = {};
+ }
+ if (!_properties.constraints.audio.mandatory) {
+ _properties.constraints.audio.mandatory = {};
+ }
+ if (!_properties.constraints.audio.optional) {
+ _properties.constraints.audio.optional = [];
+ }
+ _properties.constraints.audio.mandatory.sourceId =
+ _properties.audioSource;
+ }
+ }
+
+ if(_properties.videoSource === null || _properties.videoSource === false) {
+ _properties.constraints.video = false;
+ _properties.publishVideo = false;
+ } else {
+
+ if(typeof _properties.videoSource === 'object' &&
+ _properties.videoSource.deviceId == null) {
+ OT.warn('Invalid videoSource passed to Publisher. Expected either a device ' +
+ 'ID or device.');
+ _properties.videoSource = null;
+ }
+
+ var _setupVideoDefaults = function() {
+ if (typeof _properties.constraints.video !== 'object') {
+ _properties.constraints.video = {};
+ }
+ if (!_properties.constraints.video.mandatory) {
+ _properties.constraints.video.mandatory = {};
+ }
+ if (!_properties.constraints.video.optional) {
+ _properties.constraints.video.optional = [];
}
};
- var setAudioOnly = function(audioOnly) {
- if(_container) {
- _container.audioOnly(audioOnly);
- _container.showPoster(audioOnly);
- }
+ if (_properties.videoSource) {
+ _setupVideoDefaults();
- if (_audioLevelMeter && _subscriber.getStyle('audioLevelDisplayMode') === 'auto') {
- _audioLevelMeter[audioOnly ? 'show' : 'hide']();
- }
- };
-
- this.subscribe = function(stream) {
- OT.debug('OT.Subscriber: subscribe to ' + stream.id);
-
- if (_state.isSubscribing()) {
- // @todo error
- OT.error('OT.Subscriber.Subscribe: Cannot subscribe, already subscribing.');
- return false;
- }
-
- _state.set('Init');
-
- if (!stream) {
- // @todo error
- OT.error('OT.Subscriber: No stream parameter.');
- return false;
- }
-
- if (_stream) {
- // @todo error
- OT.error('OT.Subscriber: Already subscribed');
- return false;
- }
-
- this.stream = _stream = stream;
- this.streamId = _stream.id;
- _stream.on({
- updated: streamUpdated,
- destroyed: streamDestroyed
- }, this);
-
- _fromConnectionId = stream.connection.id;
- _properties.name = _properties.name || _stream.name;
- _properties.classNames = 'OT_root OT_subscriber';
-
- if (_properties.style) {
- this.setStyle(_properties.style, null, true);
- }
- if (_properties.audioVolume) {
- this.setAudioVolume(_properties.audioVolume);
- }
-
- _properties.subscribeToAudio = OT.$.castToBoolean(_properties.subscribeToAudio, true);
- _properties.subscribeToVideo = OT.$.castToBoolean(_properties.subscribeToVideo, true);
-
- _container = new OT.WidgetView(targetElement, _properties);
- this.id = _domId = _container.domId();
- this.element = _container.domElement;
-
- _startConnectingTime = OT.$.now();
-
- if (_stream.connection.id !== _session.connection.id) {
- logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
-
- _state.set('ConnectingToPeer');
-
- _peerConnection = new OT.SubscriberPeerConnection(_stream.connection, _session,
- _stream, this, _properties);
-
- _peerConnection.on({
- disconnected: onDisconnected,
- error: onPeerConnectionFailure,
- remoteStreamAdded: onRemoteStreamAdded,
- remoteStreamRemoved: onRemoteStreamRemoved,
- qos: recordQOS
- }, this);
-
- // initialize the peer connection AFTER we've added the event listeners
- _peerConnection.init();
-
- if (OT.$.hasCapabilities('audioOutputLevelStat')) {
- _audioLevelSampler = new OT.GetStatsAudioLevelSampler(_peerConnection, 'out');
- } else if (OT.$.hasCapabilities('webAudioCapableRemoteStream')) {
- _audioLevelSampler = new OT.AnalyserAudioLevelSampler(OT.audioContext());
+ var mandatory = _properties.constraints.video.mandatory;
+
+ if(_isScreenSharing) {
+ // this is handled by the extension helpers
+ } else if(_properties.videoSource.deviceId != null) {
+ mandatory.sourceId = _properties.videoSource.deviceId;
+ } else {
+ mandatory.sourceId = _properties.videoSource;
+ }
}
- if(_audioLevelSampler) {
- // sample with interval to minimise disturbance on animation loop but dispatch the
- // event with RAF since the main purpose is animation of a meter
- _audioLevelRunner = new OT.IntervalRunner(function() {
- _audioLevelSampler.sample(function(audioOutputLevel) {
- if (audioOutputLevel !== null) {
- OT.$.requestAnimationFrame(function() {
- _subscriber.dispatchEvent(
- new OT.AudioLevelUpdatedEvent(audioOutputLevel));
+ if (_properties.resolution) {
+ if (!_validResolutions.hasOwnProperty(_properties.resolution)) {
+ OT.warn('Invalid resolution passed to the Publisher. Got: ' +
+ _properties.resolution + ' expecting one of "' +
+ OT.$.keys(_validResolutions).join('","') + '"');
+ } else {
+ _properties.videoDimensions = _validResolutions[_properties.resolution];
+ _setupVideoDefaults();
+ if (OT.$.env.name === 'Chrome') {
+ _properties.constraints.video.optional =
+ _properties.constraints.video.optional.concat([
+ {minWidth: _properties.videoDimensions.width},
+ {maxWidth: _properties.videoDimensions.width},
+ {minHeight: _properties.videoDimensions.height},
+ {maxHeight: _properties.videoDimensions.height}
+ ]);
+ } else {
+ // This is not supported
+ }
+ }
+ }
+
+ if (_properties.maxResolution) {
+ _setupVideoDefaults();
+ if (_properties.maxResolution.width > 1920) {
+ OT.warn('Invalid maxResolution passed to the Publisher. maxResolution.width must ' +
+ 'be less than or equal to 1920');
+ _properties.maxResolution.width = 1920;
+ }
+ if (_properties.maxResolution.height > 1920) {
+ OT.warn('Invalid maxResolution passed to the Publisher. maxResolution.height must ' +
+ 'be less than or equal to 1920');
+ _properties.maxResolution.height = 1920;
+ }
+
+ _properties.videoDimensions = _properties.maxResolution;
+
+ if (OT.$.env.name === 'Chrome') {
+ _setupVideoDefaults();
+ _properties.constraints.video.mandatory.maxWidth =
+ _properties.videoDimensions.width;
+ _properties.constraints.video.mandatory.maxHeight =
+ _properties.videoDimensions.height;
+ } else {
+ // This is not suppoted
+ }
+ }
+
+ if (_properties.frameRate !== void 0 &&
+ OT.$.arrayIndexOf(_validFrameRates, _properties.frameRate) === -1) {
+ OT.warn('Invalid frameRate passed to the publisher got: ' +
+ _properties.frameRate + ' expecting one of ' + _validFrameRates.join(','));
+ delete _properties.frameRate;
+ } else if (_properties.frameRate) {
+ _setupVideoDefaults();
+ _properties.constraints.video.optional =
+ _properties.constraints.video.optional.concat([
+ { minFrameRate: _properties.frameRate },
+ { maxFrameRate: _properties.frameRate }
+ ]);
+ }
+
+ }
+
+ } else {
+ OT.warn('You have passed your own constraints not using ours');
+ }
+
+ if (_properties.style) {
+ this.setStyle(_properties.style, null, true);
+ }
+
+ if (_properties.name) {
+ _properties.name = _properties.name.toString();
+ }
+
+ _properties.classNames = 'OT_root OT_publisher';
+
+ // Defer actually creating the publisher DOM nodes until we know
+ // the DOM is actually loaded.
+ OT.onLoad(function() {
+ _widgetView = new OT.WidgetView(targetElement, _properties);
+ _publisher.id = _domId = _widgetView.domId();
+ _publisher.element = _widgetView.domElement;
+
+ _widgetView.on('videoDimensionsChanged', function(oldValue, newValue) {
+ if (_stream) {
+ _stream.setVideoDimensions(newValue.width, newValue.height);
+ }
+ _publisher.dispatchEvent(
+ new OT.VideoDimensionsChangedEvent(_publisher, oldValue, newValue)
+ );
+ });
+
+ _widgetView.on('mediaStopped', function() {
+ var event = new OT.MediaStoppedEvent(_publisher);
+
+ _publisher.dispatchEvent(event, function() {
+ if(!event.isDefaultPrevented()) {
+ if (_session) {
+ _publisher._.unpublishFromSession(_session, 'mediaStopped');
+ } else {
+ _publisher.destroy('mediaStopped');
+ }
+ }
+ });
+ });
+
+ OT.$.waterfall([
+ function(cb) {
+ if (_isScreenSharing) {
+ OT.checkScreenSharingCapability(function(response) {
+ if (!response.supported) {
+ onScreenSharingError(
+ new Error('Screen Sharing is not supported in this browser')
+ );
+ } else if (response.extensionRegistered === false) {
+ onScreenSharingError(
+ new Error('Screen Sharing suppor in this browser requires an extension, but ' +
+ 'one has not been registered.')
+ );
+ } else if (response.extensionInstalled === false) {
+ onScreenSharingError(
+ new Error('Screen Sharing suppor in this browser requires an extension, but ' +
+ 'the extension is not installed.')
+ );
+ } else {
+
+ var helper = OT.pickScreenSharingHelper();
+
+ if (helper.proto.getConstraintsShowsPermissionUI) {
+ onAccessDialogOpened();
+ }
+
+ helper.instance.getConstraints(options.videoSource, _properties.constraints,
+ function(err, constraints) {
+ if (helper.proto.getConstraintsShowsPermissionUI) {
+ onAccessDialogClosed();
+ }
+ if (err) {
+ if (err.message === 'PermissionDeniedError') {
+ onAccessDenied(err);
+ } else {
+ onScreenSharingError(err);
+ }
+ } else {
+ _properties.constraints = constraints;
+ cb();
+ }
});
}
});
- }, 60);
- }
- } else {
- logAnalyticsEvent('createPeerConnection', 'Attempt', '', '');
-
- var publisher = _session.getPublisherForStream(_stream);
- if(!(publisher && publisher._.webRtcStream())) {
- this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID'));
- return this;
- }
-
- // Subscribe to yourself edge-case
- onRemoteStreamAdded.call(this, publisher._.webRtcStream());
- }
-
- logAnalyticsEvent('subscribe', 'Attempt', 'streamId', _stream.id);
-
- return this;
- };
-
- this.destroy = function(reason, quiet) {
- if (_state.isDestroyed()) return;
-
- if(reason === 'streamDestroyed') {
- if (_state.isAttemptingToSubscribe()) {
- // We weren't subscribing yet so the stream was destroyed before we setup
- // the PeerConnection or receiving the initial stream.
- this.trigger('subscribeComplete', new OT.Error(null, 'InvalidStreamID'));
- }
- }
-
- _state.set('Destroyed');
-
- if(_audioLevelRunner) {
- _audioLevelRunner.stop();
- }
-
- this.disconnect();
-
- if (_chrome) {
- _chrome.destroy();
- _chrome = null;
- }
-
- if (_container) {
- _container.destroy();
- _container = null;
- this.element = null;
- }
-
- if (_stream && !_stream.destroyed) {
- logAnalyticsEvent('unsubscribe', null, 'streamId', _stream.id);
- }
-
- this.id = _domId = null;
- this.stream = _stream = null;
- this.streamId = null;
-
- this.session =_session = null;
- _properties = null;
-
- if (quiet !== true) {
- this.dispatchEvent(
- new OT.DestroyedEvent(
- OT.Event.names.SUBSCRIBER_DESTROYED,
- this,
- reason
- ),
- OT.$.bind(this.off, this)
- );
- }
-
- return this;
- };
-
- this.disconnect = function() {
- if (!_state.isDestroyed() && !_state.isFailed()) {
- // If we are already in the destroyed state then disconnect
- // has been called after (or from within) destroy.
- _state.set('NotSubscribing');
- }
-
- if (_streamContainer) {
- _streamContainer.destroy();
- _streamContainer = null;
- }
-
- if (_peerConnection) {
- _peerConnection.destroy();
- _peerConnection = null;
-
- logAnalyticsEvent('disconnect', 'PeerConnection', 'streamId', _stream.id);
- }
- };
-
- this.processMessage = function(type, fromConnection, message) {
- OT.debug('OT.Subscriber.processMessage: Received ' + type + ' message from ' +
- fromConnection.id);
- OT.debug(message);
-
- if (_fromConnectionId !== fromConnection.id) {
- _fromConnectionId = fromConnection.id;
- }
-
- if (_peerConnection) {
- _peerConnection.processMessage(type, message);
- }
- };
-
- this.disableVideo = function(active) {
- if (!active) {
- OT.warn('Due to high packet loss and low bandwidth, video has been disabled');
- } else {
- if (_lastSubscribeToVideoReason === 'auto') {
- OT.info('Video has been re-enabled');
- _chrome.videoDisabledIndicator.disableVideo(false);
- } else {
- OT.info('Video was not re-enabled because it was manually disabled');
- return;
- }
- }
- this.subscribeToVideo(active, 'auto');
- if(!active) {
- _chrome.videoDisabledIndicator.disableVideo(true);
- }
- logAnalyticsEvent('updateQuality', 'video', active ? 'videoEnabled' : 'videoDisabled', true);
- };
-
- /**
- * Return the base-64-encoded string of PNG data representing the Subscriber video.
- *
- *
You can use the string as the value for a data URL scheme passed to the src parameter of - * an image file, as in the following:
- * - *- * var imgData = subscriber.getImgData(); - * - * var img = document.createElement("img"); - * img.setAttribute("src", "data:image/png;base64," + imgData); - * var imgWin = window.open("about:blank", "Screenshot"); - * imgWin.document.write("<body></body>"); - * imgWin.document.body.appendChild(img); - *- * @method #getImgData - * @memberOf Subscriber - * @return {String} The base-64 encoded string. Returns an empty string if there is no video. - */ - this.getImgData = function() { - if (!this.isSubscribing()) { - OT.error('OT.Subscriber.getImgData: Cannot getImgData before the Subscriber ' + - 'is subscribing.'); - return null; - } - - return _streamContainer.imgData(); - }; - - /** - * Sets the audio volume, between 0 and 100, of the Subscriber. - * - *
You can set the initial volume when you call the Session.subscribe()
- * method. Pass a audioVolume
property of the properties
parameter
- * of the method.
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(OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()) { - _properties.subscribeMute = _properties.muted = _mute; - this.subscribeToAudio(_properties.subscribeToAudio); - } else { - if(_mute) { - _properties.premuteVolume = this.getAudioVolume(); - _properties.muted = true; - this.setAudioVolume(0); - } else if(_properties.premuteVolume || _properties.audioVolume) { - _properties.muted = false; - this.setAudioVolume(_properties.premuteVolume || _properties.audioVolume); - } - } - _properties.mute = _properties.mute; - }; - - var reasonMap = { - auto: 'quality', - publishVideo: 'publishVideo', - subscribeToVideo: 'subscribeToVideo' - }; - - - /** - * Toggles video on and off. Starts subscribing to video (if it is available and - * currently not being subscribed to) when the
value
is true
;
- * stops subscribing to video (if it is currently being subscribed to) when the
- * value
is false
.
- * - * Note: This method only affects the local playback of video. It has no impact on - * the video for other connections subscribing to the same stream. If the Publsher is not - * publishing video, enabling the Subscriber video will have no practical video. - *
- * - * @param {Boolean} value Whether to start subscribing to video (true
) or not
- * (false
).
- *
- * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
- * following:
- *
- * mySubscriber.subscribeToVideo(true).subscribeToAudio(false);- * - * @see subscribeToAudio() - * @see Session.subscribe() - * @see StreamPropertyChangedEvent - * - * @method #subscribeToVideo - * @memberOf Subscriber - */ - this.subscribeToVideo = function(pValue, reason) { - var value = OT.$.castToBoolean(pValue, true); - - setAudioOnly(!(value && _stream.hasVideo)); - - if ( value && _container && _container.video()) { - _container.loading(value); - _container.video().whenTimeIncrements(function() { - _container.loading(false); - }, this); - } - - if (_chrome && _chrome.videoDisabledIndicator) { - _chrome.videoDisabledIndicator.disableVideo(false); - } - - if (_peerConnection) { - _peerConnection.subscribeToVideo(value); - - if (_session && _stream && (value !== _properties.subscribeToVideo || - reason !== _lastSubscribeToVideoReason)) { - _stream.setChannelActiveState('video', value, reason); - } - } - - _properties.subscribeToVideo = value; - _lastSubscribeToVideoReason = reason; - - if (reason !== 'loading') { - this.dispatchEvent(new OT.VideoEnabledChangedEvent( - value ? 'videoEnabled' : 'videoDisabled', - { - reason: reasonMap[reason] || 'subscribeToVideo' - } - )); - } - - return this; - }; - - this.isSubscribing = function() { - return _state.isSubscribing(); - }; - - this.isWebRTC = true; - - this.isLoading = function() { - return _container && _container.loading(); - }; - - this.videoWidth = function() { - return _streamContainer.videoWidth(); - }; - - this.videoHeight = function() { - return _streamContainer.videoHeight(); - }; - - /** - * Restricts the frame rate of the Subscriber's video stream, when you pass in - *
true
. When you pass in false
, the frame rate of the video stream
- * is not restricted.
- * - * When the frame rate is restricted, the Subscriber video frame will update once or less per - * second. - *
- * This feature is only available in sessions that use the OpenTok Media Router (sessions with - * the media mode - * set to routed), not in sessions with the media mode set to relayed. In relayed sessions, - * calling this method has no effect. - *
- * Restricting the subscriber frame rate has the following benefits: - *
- * Reducing a subscriber's frame rate has no effect on the frame rate of the video in
- * other clients.
- *
- * @param {Boolean} value Whether to restrict the Subscriber's video frame rate
- * (true
) or not (false
).
- *
- * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the
- * following:
- *
- *
mySubscriber.restrictFrameRate(false).subscribeToAudio(true);- * - * @method #restrictFrameRate - * @memberOf Subscriber - */ - this.restrictFrameRate = function(val) { - OT.debug('OT.Subscriber.restrictFrameRate(' + val + ')'); - - logAnalyticsEvent('restrictFrameRate', val.toString(), 'streamId', _stream.id); - - if (_session.sessionInfo.p2pEnabled) { - OT.warn('OT.Subscriber.restrictFrameRate: Cannot restrictFrameRate on a P2P session'); - } - - if (typeof val !== 'boolean') { - OT.error('OT.Subscriber.restrictFrameRate: expected a boolean value got a ' + typeof val); - } else { - _frameRateRestricted = val; - _stream.setRestrictFrameRate(val); - } - return this; - }; - - this.on('styleValueChanged', updateChromeForStyleChange, this); - - this._ = { - archivingStatus: function(status) { - if(_chrome) { - _chrome.archive.setArchiving(status); - } - } - }; - - _state = new OT.SubscribingState(stateChangeFailed); - - /** - * Dispatched periodically to indicate the subscriber's audio level. The event is dispatched - * up to 60 times per second, depending on the browser. The
audioLevel
property
- * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
- * information.
- * - * The following example adjusts the value of a meter element that shows volume of the - * subscriber. Note that the audio level is adjusted logarithmically and a moving average - * is applied: - *
- * var movingAvg = null; - * subscriber.on('audioLevelUpdated', function(event) { - * if (movingAvg === null || movingAvg <= event.audioLevel) { - * movingAvg = event.audioLevel; - * } else { - * movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel; - * } - * - * // 1.5 scaling to map the -30 - 0 dBm range to [0,1] - * var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1; - * logLevel = Math.min(Math.max(logLevel, 0), 1); - * document.getElementById('subscriberMeter').value = logLevel; - * }); - *- *
This example shows the algorithm used by the default audio level indicator displayed - * in an audio-only Subscriber. - * - * @name audioLevelUpdated - * @event - * @memberof Subscriber - * @see AudioLevelUpdatedEvent - */ - - /** - * Dispatched when the video for the subscriber is disabled. - *
- * The reason
property defines the reason the video was disabled. This can be set to
- * one of the following values:
- *
- * - *
"publishVideo"
— The publisher stopped publishing video by calling
- * publishVideo(false)
."quality"
— The OpenTok Media Router stopped sending video
- * to the subscriber based on stream quality changes. This feature of the OpenTok Media
- * Router has a subscriber drop the video stream when connectivity degrades. (The subscriber
- * continues to receive the audio stream, if there is one.)
- *
- * Before sending this event, when the Subscriber's stream quality deteriorates to a level
- * that is low enough that the video stream is at risk of being disabled, the Subscriber
- * dispatches a videoDisableWarning
event.
- *
- * If connectivity improves to support video again, the Subscriber object dispatches
- * a videoEnabled
event, and the Subscriber resumes receiving video.
- *
- * By default, the Subscriber displays a video disabled indicator when a
- * videoDisabled
event with this reason is dispatched and removes the indicator
- * when the videoDisabled
event with this reason is dispatched. You can control
- * the display of this icon by calling the setStyle()
method of the Subscriber,
- * setting the videoDisabledDisplayMode
property(or you can set the style when
- * calling the Session.subscribe()
method, setting the style
property
- * of the properties
parameter).
- *
- * This feature is only available in sessions that use the OpenTok Media Router (sessions with - * the media mode - * set to routed), not in sessions with the media mode set to relayed. - *
"subscribeToVideo"
— The subscriber started or stopped subscribing to
- * video, by calling subscribeToVideo(false)
.
- * videoDisabled
event.
- *
- * By default, the Subscriber displays a video disabled warning indicator when this event
- * is dispatched (and the video is disabled). You can control the display of this icon by
- * calling the setStyle()
method and setting the
- * videoDisabledDisplayMode
property (or you can set the style when calling
- * the Session.subscribe()
method and setting the style
property
- * of the properties
parameter).
- *
- * This feature is only available in sessions that use the OpenTok Media Router (sessions with
- * the media mode
- * set to routed), not in sessions with the media mode set to relayed.
- *
- * @see Event
- * @see event:videoDisabled
- * @see event:videoDisableWarningLifted
- * @name videoDisableWarning
- * @event
- * @memberof Subscriber
- */
-
- /**
- * Dispatched when the OpenTok Media Router determines that the stream quality has improved
- * to the point at which the video being disabled is not an immediate risk. This event is
- * dispatched after the Subscriber object dispatches a videoDisableWarning
event.
- *
- * This feature is only available in sessions that use the OpenTok Media Router (sessions with - * the media mode - * set to routed), not in sessions with the media mode set to relayed. - * - * @see Event - * @see event:videoDisabled - * @see event:videoDisableWarning - * @name videoDisableWarningLifted - * @event - * @memberof Subscriber - */ - - /** - * Dispatched when the OpenTok Media Router resumes sending video to the subscriber - * after video was previously disabled. - *
- * The reason
property defines the reason the video was enabled. This can be set to
- * one of the following values:
- *
- * - *
"publishVideo"
— The publisher started publishing video by calling
- * publishVideo(true)
."quality"
— The OpenTok Media Router resumed sending video
- * to the subscriber based on stream quality changes. This feature of the OpenTok Media
- * Router has a subscriber drop the video stream when connectivity degrades and then resume
- * the video stream if the stream quality improves.
- * - * This feature is only available in sessions that use the OpenTok Media Router (sessions with - * the media mode - * set to routed), not in sessions with the media mode set to relayed. - *
"subscribeToVideo"
— The subscriber started or stopped subscribing to
- * video, by calling subscribeToVideo(false)
.
- *
- * To prevent video from resuming, in the videoEnabled
event listener,
- * call subscribeToVideo(false)
on the Subscriber object.
- *
- * @see VideoEnabledChangedEvent
- * @see event:videoDisabled
- * @name videoEnabled
- * @event
- * @memberof Subscriber
- */
-
- /**
- * Dispatched when the Subscriber element is removed from the HTML DOM. When this event is
- * dispatched, you may choose to adjust or remove HTML DOM elements related to the subscriber.
- * @see Event
- * @name destroyed
- * @event
- * @memberof Subscriber
- */
- };
-
-})(window);
-!(function() {
-
- var parseErrorFromJSONDocument,
- onGetResponseCallback,
- onGetErrorCallback;
-
- OT.SessionInfo = function(jsonDocument) {
- var sessionJSON = jsonDocument[0];
-
- OT.log('SessionInfo Response:');
- OT.log(jsonDocument);
-
- /*jshint camelcase:false*/
-
- this.sessionId = sessionJSON.session_id;
- this.partnerId = sessionJSON.partner_id;
- this.sessionStatus = sessionJSON.session_status;
-
- this.messagingServer = sessionJSON.messaging_server_url;
-
- this.messagingURL = sessionJSON.messaging_url;
- this.symphonyAddress = sessionJSON.symphony_address;
-
- this.p2pEnabled = !!(sessionJSON.properties &&
- sessionJSON.properties.p2p &&
- sessionJSON.properties.p2p.preference &&
- sessionJSON.properties.p2p.preference.value === 'enabled');
- };
-
- // Retrieves Session Info for +session+. The SessionInfo object will be passed
- // to the +onSuccess+ callback. The +onFailure+ callback will be passed an error
- // object and the DOMEvent that relates to the error.
- OT.SessionInfo.get = function(session, onSuccess, onFailure) {
- var sessionInfoURL = OT.properties.apiURL + '/session/' + session.id + '?extended=true',
-
- browser = OT.$.browserVersion(),
-
- startTime = OT.$.now(),
-
- options,
-
- validateRawSessionInfo = function(sessionInfo) {
- session.logEvent('Instrumentation', null, 'gsi', OT.$.now() - startTime);
- var error = parseErrorFromJSONDocument(sessionInfo);
- if (error === false) {
- onGetResponseCallback(session, onSuccess, sessionInfo);
} else {
- onGetErrorCallback(session, onFailure, error, JSON.stringify(sessionInfo));
+ OT.$.shouldAskForDevices(function(devices) {
+ if(!devices.video) {
+ OT.warn('Setting video constraint to false, there are no video sources');
+ _properties.constraints.video = false;
+ }
+ if(!devices.audio) {
+ OT.warn('Setting audio constraint to false, there are no audio sources');
+ _properties.constraints.audio = false;
+ }
+ cb();
+ });
}
- };
+ },
+ function() {
- if(browser.browser === 'IE' && browser.version < 10) {
- sessionInfoURL = sessionInfoURL + '&format=json&token=' + encodeURIComponent(session.token) +
- '&version=1&cache=' + OT.$.uuid();
- options = {
- xdomainrequest: true
- };
- }
- else {
- options = {
- headers: {
- 'X-TB-TOKEN-AUTH': session.token,
- 'X-TB-VERSION': 1
+ if (_state.isDestroyed()) {
+ return;
+ }
+
+ OT.$.getUserMedia(
+ _properties.constraints,
+ onStreamAvailable,
+ onStreamAvailableError,
+ onAccessDialogOpened,
+ onAccessDialogClosed,
+ onAccessDenied
+ );
}
- };
+
+ ]);
+
+ }, 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) {
+ _properties.publishAudio = value;
+
+ if (_microphone) {
+ _microphone.muted(!value);
}
- session.logEvent('getSessionInfo', 'Attempt', 'api_url', OT.properties.apiURL);
-
- OT.$.getJSON(sessionInfoURL, options, function(error, sessionInfo) {
- if(error) {
- var responseText = sessionInfo;
- onGetErrorCallback(session, onFailure,
- new OT.Error(error.target && error.target.status || error.code, error.message ||
- 'Could not connect to the OpenTok API Server.'), responseText);
- } else {
- validateRawSessionInfo(sessionInfo);
- }
- });
- };
-
- var messageServerToClientErrorCodes = {};
- messageServerToClientErrorCodes['404'] = OT.ExceptionCodes.INVALID_SESSION_ID;
- messageServerToClientErrorCodes['409'] = OT.ExceptionCodes.INVALID_SESSION_ID;
- messageServerToClientErrorCodes['400'] = OT.ExceptionCodes.INVALID_SESSION_ID;
- messageServerToClientErrorCodes['403'] = OT.ExceptionCodes.AUTHENTICATION_ERROR;
-
- // Return the error in +jsonDocument+, if there is one. Otherwise it will return
- // false.
- parseErrorFromJSONDocument = function(jsonDocument) {
- if(OT.$.isArray(jsonDocument)) {
-
- var errors = OT.$.filter(jsonDocument, function(node) {
- return node.error != null;
- });
-
- var numErrorNodes = errors.length;
- if(numErrorNodes === 0) {
- return false;
- }
-
- var errorCode = errors[0].error.code;
- if (messageServerToClientErrorCodes[errorCode.toString()]) {
- errorCode = messageServerToClientErrorCodes[errorCode];
- }
-
- return {
- code: errorCode,
- message: errors[0].error.errorMessage && errors[0].error.errorMessage.message
- };
- } else {
- return {
- code: null,
- message: 'Unknown error: getSessionInfo JSON response was badly formed'
- };
+ if (_chrome) {
+ _chrome.muteButton.muted(!value);
}
+
+ if (_session && _stream) {
+ _stream.setChannelActiveState('audio', value);
+ }
+
+ return this;
};
- onGetResponseCallback = function(session, onSuccess, rawSessionInfo) {
- session.logEvent('getSessionInfo', 'Success', 'api_url', OT.properties.apiURL);
-
- onSuccess( new OT.SessionInfo(rawSessionInfo) );
- };
-
- onGetErrorCallback = function(session, onFailure, error, responseText) {
- session.logEvent('Connect', 'Failure', 'errorMessage',
- 'GetSessionInfo:' + (error.code || 'No code') + ':' + error.message + ':' +
- (responseText || 'Empty responseText from API server'));
-
- onFailure(error, session);
- };
-
-})(window);
-!(function() {
- /**
- * A class defining properties of the capabilities
property of a
- * Session object. See Session.capabilities.
- *
- * All Capabilities properties are undefined until you have connected to a session
- * and the Session object has dispatched the sessionConnected
event.
- *
- * For more information on token roles, see the
- * generate_token()
- * method of the OpenTok server-side libraries.
- *
- * @class Capabilities
- *
- * @property {Number} forceDisconnect Specifies whether you can call
- * the Session.forceDisconnect()
method (1) or not (0). To call the
- * Session.forceDisconnect()
method,
- * the user must have a token that is assigned the role of moderator.
- * @property {Number} forceUnpublish Specifies whether you can call
- * the Session.forceUnpublish()
method (1) or not (0). To call the
- * Session.forceUnpublish()
method, the user must have a token that
- * is assigned the role of moderator.
- * @property {Number} publish Specifies whether you can publish to the session (1) or not (0).
- * The ability to publish is based on a few factors. To publish, the user must have a token that
- * is assigned a role that supports publishing. There must be a connected camera and microphone.
- * @property {Number} subscribe Specifies whether you can subscribe to streams
- * in the session (1) or not (0). Currently, this capability is available for all users on all
- * platforms.
- */
- OT.Capabilities = function(permissions) {
- this.publish = OT.$.arrayIndexOf(permissions, 'publish') !== -1 ? 1 : 0;
- this.subscribe = OT.$.arrayIndexOf(permissions, 'subscribe') !== -1 ? 1 : 0;
- this.forceUnpublish = OT.$.arrayIndexOf(permissions, 'forceunpublish') !== -1 ? 1 : 0;
- this.forceDisconnect = OT.$.arrayIndexOf(permissions, 'forcedisconnect') !== -1 ? 1 : 0;
- this.supportsWebRTC = OT.$.hasCapabilities('webrtc') ? 1 : 0;
-
- this.permittedTo = function(action) {
- return this.hasOwnProperty(action) && this[action] === 1;
- };
- };
-
-})(window);
-!(function(window) {
-
/**
- * The Session object returned by the OT.initSession()
method provides access to
- * much of the OpenTok functionality.
- *
- * @class Session
- * @augments EventDispatcher
- *
- * @property {Capabilities} capabilities A {@link Capabilities} object that includes information
- * about the capabilities of the client. All properties of the capabilities
object
- * are undefined until you have connected to a session and the Session object has dispatched the
- * sessionConnected
event.
- * @property {Connection} connection The {@link Connection} object for this session. The
- * connection property is only available once the Session object dispatches the sessionConnected
- * event. The Session object asynchronously dispatches a sessionConnected event in response
- * to a successful call to the connect() method. See: connect and
- * {@link Connection}.
- * @property {String} sessionId The session ID for this session. You pass this value into the
- * OT.initSession()
method when you create the Session object. (Note: a Session
- * object is not connected to the OpenTok server until you call the connect() method of the
- * object and the object dispatches a connected event. See {@link OT.initSession} and
- * {@link connect}).
- * For more information on sessions and session IDs, see
- * Session creation.
- */
- OT.Session = function(apiKey, sessionId) {
- OT.$.eventing(this);
-
- // Check that the client meets the minimum requirements, if they don't the upgrade
- // flow will be triggered.
- if (!OT.checkSystemRequirements()) {
- OT.upgradeSystemRequirements();
- return;
- }
-
- if(sessionId == null) {
- sessionId = apiKey;
- apiKey = null;
- }
-
- this.id = this.sessionId = sessionId;
-
- var _initialConnection = true,
- _apiKey = apiKey,
- _token,
- _sessionId = sessionId,
- _socket,
- _widgetId = OT.$.uuid(),
- _connectionId,
- _analytics = new OT.Analytics(),
- sessionConnectFailed,
- sessionDisconnectedHandler,
- connectionCreatedHandler,
- connectionDestroyedHandler,
- streamCreatedHandler,
- streamPropertyModifiedHandler,
- streamDestroyedHandler,
- archiveCreatedHandler,
- archiveDestroyedHandler,
- archiveUpdatedHandler,
- reset,
- disconnectComponents,
- destroyPublishers,
- destroySubscribers,
- connectMessenger,
- getSessionInfo,
- onSessionInfoResponse,
- permittedTo,
- dispatchError;
-
-
-
- var setState = OT.$.statable(this, [
- 'disconnected', 'connecting', 'connected', 'disconnecting'
- ], 'disconnected');
-
- this.connection = null;
- this.connections = new OT.Collection();
- this.streams = new OT.Collection();
- this.archives = new OT.Collection();
-
-
- //--------------------------------------
- // MESSAGE HANDLERS
- //--------------------------------------
-
- // The duplication of this and sessionConnectionFailed will go away when
- // session and messenger are refactored
- sessionConnectFailed = function(reason, code) {
- setState('disconnected');
-
- OT.error(reason);
-
- this.trigger('sessionConnectFailed',
- new OT.Error(code || OT.ExceptionCodes.CONNECT_FAILED, reason));
-
- OT.handleJsException(reason, code || OT.ExceptionCodes.CONNECT_FAILED, {
- session: this
- });
- };
-
- sessionDisconnectedHandler = function(event) {
- var reason = event.reason;
- if(reason === 'networkTimedout') {
- reason = 'networkDisconnected';
- this.logEvent('Connect', 'TimeOutDisconnect', 'reason', event.reason);
- } else {
- this.logEvent('Connect', 'Disconnected', 'reason', event.reason);
- }
-
- var publicEvent = new OT.SessionDisconnectEvent('sessionDisconnected', reason);
-
- reset.call(this);
- disconnectComponents.call(this, reason);
-
- var defaultAction = OT.$.bind(function() {
- // Publishers handle preventDefault'ing themselves
- destroyPublishers.call(this, publicEvent.reason);
- // Subscriers don't, destroy 'em if needed
- if (!publicEvent.isDefaultPrevented()) destroySubscribers.call(this, publicEvent.reason);
- }, this);
-
- this.dispatchEvent(publicEvent, defaultAction);
- };
-
- connectionCreatedHandler = function(connection) {
- // We don't broadcast events for the symphony connection
- if (connection.id.match(/^symphony\./)) return;
-
- this.dispatchEvent(new OT.ConnectionEvent(
- OT.Event.names.CONNECTION_CREATED,
- connection
- ));
- };
-
- connectionDestroyedHandler = function(connection, reason) {
- // We don't broadcast events for the symphony connection
- if (connection.id.match(/^symphony\./)) return;
-
- // Don't delete the connection if it's ours. This only happens when
- // we're about to receive a session disconnected and session disconnected
- // will also clean up our connection.
- if (connection.id === _socket.id()) return;
-
- this.dispatchEvent(
- new OT.ConnectionEvent(
- OT.Event.names.CONNECTION_DESTROYED,
- connection,
- reason
- )
- );
- };
-
- streamCreatedHandler = function(stream) {
- if(stream.connection.id !== this.connection.id) {
- this.dispatchEvent(new OT.StreamEvent(
- OT.Event.names.STREAM_CREATED,
- stream,
- null,
- false
- ));
- }
- };
-
- streamPropertyModifiedHandler = function(event) {
- var stream = event.target,
- propertyName = event.changedProperty,
- newValue = event.newValue;
-
- if (propertyName === 'videoDisableWarning' || propertyName === 'audioDisableWarning') {
- return; // These are not public properties, skip top level event for them.
- }
-
- if (propertyName === 'orientation') {
- propertyName = 'videoDimensions';
- newValue = {width: newValue.width, height: newValue.height};
- }
-
- this.dispatchEvent(new OT.StreamPropertyChangedEvent(
- OT.Event.names.STREAM_PROPERTY_CHANGED,
- stream,
- propertyName,
- event.oldValue,
- newValue
- ));
- };
-
- streamDestroyedHandler = function(stream, reason) {
-
- // if the stream is one of ours we delegate handling
- // to the publisher itself.
- if(stream.connection.id === this.connection.id) {
- OT.$.forEach(OT.publishers.where({ streamId: stream.id }), OT.$.bind(function(publisher) {
- publisher._.unpublishFromSession(this, reason);
- }, this));
- return;
- }
-
- var event = new OT.StreamEvent('streamDestroyed', stream, reason, true);
-
- var defaultAction = OT.$.bind(function() {
- if (!event.isDefaultPrevented()) {
- // If we are subscribed to any of the streams we should unsubscribe
- OT.$.forEach(OT.subscribers.where({streamId: stream.id}), function(subscriber) {
- if (subscriber.session.id === this.id) {
- if(subscriber.stream) {
- subscriber.destroy('streamDestroyed');
- }
- }
- }, this);
- } else {
- // @TODO Add a one time warning that this no longer cleans up the publisher
- }
- }, this);
-
- this.dispatchEvent(event, defaultAction);
- };
-
- archiveCreatedHandler = function(archive) {
- this.dispatchEvent(new OT.ArchiveEvent('archiveStarted', archive));
- };
-
- archiveDestroyedHandler = function(archive) {
- this.dispatchEvent(new OT.ArchiveEvent('archiveDestroyed', archive));
- };
-
- archiveUpdatedHandler = function(event) {
- var archive = event.target,
- propertyName = event.changedProperty,
- newValue = event.newValue;
-
- if(propertyName === 'status' && newValue === 'stopped') {
- this.dispatchEvent(new OT.ArchiveEvent('archiveStopped', archive));
- } else {
- this.dispatchEvent(new OT.ArchiveEvent('archiveUpdated', archive));
- }
- };
-
- // Put ourselves into a pristine state
- reset = function() {
- this.token = _token = null;
- setState('disconnected');
- this.connection = null;
- this.capabilities = new OT.Capabilities([]);
- this.connections.destroy();
- this.streams.destroy();
- this.archives.destroy();
- };
-
- disconnectComponents = function(reason) {
- OT.$.forEach(OT.publishers.where({session: this}), function(publisher) {
- publisher.disconnect(reason);
- });
-
- OT.$.forEach(OT.subscribers.where({session: this}), function(subscriber) {
- subscriber.disconnect();
- });
- };
-
- destroyPublishers = function(reason) {
- OT.$.forEach(OT.publishers.where({session: this}), function(publisher) {
- publisher._.streamDestroyed(reason);
- });
- };
-
- destroySubscribers = function(reason) {
- OT.$.forEach(OT.subscribers.where({session: this}), function(subscriber) {
- subscriber.destroy(reason);
- });
- };
-
- connectMessenger = function() {
- OT.debug('OT.Session: connecting to Raptor');
-
- var socketUrl = this.sessionInfo.messagingURL,
- symphonyUrl = OT.properties.symphonyAddresss || this.sessionInfo.symphonyAddress;
-
- _socket = new OT.Raptor.Socket(_widgetId, socketUrl, symphonyUrl,
- OT.SessionDispatcher(this));
-
- var analyticsPayload = [
- socketUrl, OT.$.userAgent(), OT.properties.version,
- window.externalHost ? 'yes' : 'no'
- ];
-
- _socket.connect(_token, this.sessionInfo, OT.$.bind(function(error, sessionState) {
- if (error) {
- _socket = void 0;
- analyticsPayload.splice(0,0,error.message);
- this.logEvent('Connect', 'Failure',
- 'reason|webSocketServerUrl|userAgent|sdkVersion|chromeFrame',
- analyticsPayload.map(function(e) { return e.replace('|', '\\|'); }).join('|'));
-
- sessionConnectFailed.call(this, error.message, error.code);
- return;
- }
-
- OT.debug('OT.Session: Received session state from Raptor', sessionState);
-
- this.connection = this.connections.get(_socket.id());
- if(this.connection) {
- this.capabilities = this.connection.permissions;
- }
-
- setState('connected');
-
- this.logEvent('Connect', 'Success',
- 'webSocketServerUrl|userAgent|sdkVersion|chromeFrame',
- OT.$.map(analyticsPayload, function(e) {
- return e.replace('|', '\\|');
- }).join('|'), {connectionId: this.connection.id});
-
- // Listen for our own connection's destroyed event so we know when we've been disconnected.
- this.connection.on('destroyed', sessionDisconnectedHandler, this);
-
- // Listen for connection updates
- this.connections.on({
- add: connectionCreatedHandler,
- remove: connectionDestroyedHandler
- }, this);
-
- // Listen for stream updates
- this.streams.on({
- add: streamCreatedHandler,
- remove: streamDestroyedHandler,
- update: streamPropertyModifiedHandler
- }, this);
-
- this.archives.on({
- add: archiveCreatedHandler,
- remove: archiveDestroyedHandler,
- update: archiveUpdatedHandler
- }, this);
-
- this.dispatchEvent(
- new OT.SessionConnectEvent(OT.Event.names.SESSION_CONNECTED), OT.$.bind(function() {
- this.connections._triggerAddEvents(); // { id: this.connection.id }
- this.streams._triggerAddEvents(); // { id: this.stream.id }
- this.archives._triggerAddEvents();
- }, this)
- );
-
- }, this));
- };
-
- getSessionInfo = function() {
- if (this.is('connecting')) {
- OT.SessionInfo.get(
- this,
- OT.$.bind(onSessionInfoResponse, this),
- OT.$.bind(function(error) {
- sessionConnectFailed.call(this, error.message +
- (error.code ? ' (' + error.code + ')' : ''), error.code);
- }, this)
- );
- }
- };
-
- onSessionInfoResponse = function(sessionInfo) {
- if (this.is('connecting')) {
- var overrides = OT.properties.sessionInfoOverrides;
- this.sessionInfo = sessionInfo;
- if (overrides != null && typeof overrides === 'object') {
- this.sessionInfo = OT.$.defaults(overrides, this.sessionInfo);
- console.log('is', this.sessionInfo);
- }
- if (this.sessionInfo.partnerId && this.sessionInfo.partnerId !== _apiKey) {
- this.apiKey = _apiKey = this.sessionInfo.partnerId;
-
- var reason = 'Authentication Error: The API key does not match the token or session.';
-
- this.logEvent('Connect', 'Failure', 'reason', 'GetSessionInfo:' +
- OT.ExceptionCodes.AUTHENTICATION_ERROR + ':' + reason);
-
- sessionConnectFailed.call(this, reason, OT.ExceptionCodes.AUTHENTICATION_ERROR);
- } else {
- connectMessenger.call(this);
- }
- }
- };
-
- // Check whether we have permissions to perform the action.
- permittedTo = OT.$.bind(function(action) {
- return this.capabilities.permittedTo(action);
- }, this);
-
- dispatchError = OT.$.bind(function(code, message, completionHandler) {
- OT.dispatchError(code, message, completionHandler, this);
- }, this);
-
- this.logEvent = function(action, variation, payloadType, payload, options) {
- /* jshint camelcase:false */
- var event = {
- action: action,
- variation: variation,
- payload_type: payloadType,
- payload: payload,
- session_id: _sessionId,
- partner_id: _apiKey,
- widget_id: _widgetId,
- widget_type: 'Controller'
- };
- if (this.connection && this.connection.id) _connectionId = event.connection_id =
- this.connection.id;
- else if (_connectionId) event.connection_id = _connectionId;
-
- if (options) event = OT.$.extend(options, event);
- _analytics.logEvent(event);
- };
-
- /**
- * Connects to an OpenTok session.
- *
- * Upon a successful connection, the completion handler (the second parameter of the method) is - * invoked without an error object passed in. (If there is an error connecting, the completion - * handler is invoked with an error object.) Make sure that you have successfully connected to the - * session before calling other methods of the Session object. - *
- *
- * The Session object dispatches a connectionCreated
event when any client
- * (including your own) connects to to the session.
- *
- * 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
(see the Error object).
- * On success, the completionHandler
function is not passed any
- * arguments. On error, the function is passed an error
object parameter
- * (see the Error object). The
- * error
object has two properties: code
(an integer) and
- * message
(a string), which identify the cause of the failure. The following
- * code adds a completionHandler
when calling the connect()
method:
- * - * session.connect(token, function (error) { - * if (error) { - * console.log(error.message); - * } else { - * console.log("Connected to session."); - * } - * }); - *- *
- * Note that upon connecting to the session, the Session object dispatches a
- * sessionConnected
event in addition to calling the completionHandler
.
- * The SessionConnectEvent object, which defines the sessionConnected
event,
- * includes connections
and streams
properties, which
- * list the connections and streams in the session when you connect.
- *
- * 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
- * (see the Error object). The
- * error
object has two properties: code
(an integer) and
- * message
(a string), which identify the cause of the failure. Calling
- * publish()
fails if the role assigned to your token is not "publisher" or
- * "moderator"; in this case error.code
is set to 1500. Calling
- * publish()
also fails the client fails to connect; in this case
- * error.code
is set to 1013. The following code adds a
- * completionHandler
when calling the publish()
method:
- * - * session.publish(publisher, null, function (error) { - * if (error) { - * console.log(error.message); - * } else { - * console.log("Publishing a stream."); - * } - * }); - *- * - * @returns The Publisher object for this stream. - * - * @method #publish - * @memberOf Session - */ - this.publish = function(publisher, properties, completionHandler) { - if(typeof publisher === 'function') { - completionHandler = publisher; - publisher = undefined; - } - if(typeof properties === 'function') { - completionHandler = properties; - properties = undefined; - } - if (this.isNot('connected')) { - /*jshint camelcase:false*/ - _analytics.logError(1010, 'OT.exception', - 'We need to be connected before you can publish', null, { - action: 'publish', - variation: 'Failure', - payload_type: 'reason', - payload: 'We need to be connected before you can publish', - session_id: _sessionId, - partner_id: _apiKey, - widgetId: _widgetId, - widget_type: 'Controller' - }); - - if (completionHandler && OT.$.isFunction(completionHandler)) { - dispatchError(OT.ExceptionCodes.NOT_CONNECTED, - 'We need to be connected before you can publish', completionHandler); - } - - return null; - } - - if (!permittedTo('publish')) { - this.logEvent('publish', 'Failure', 'reason', - 'This token does not allow publishing. The role must be at least `publisher` ' + - 'to enable this functionality'); - dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - 'This token does not allow publishing. The role must be at least `publisher` ' + - 'to enable this functionality', completionHandler); - return null; - } - - // If the user has passed in an ID of a element then we create a new publisher. - if (!publisher || typeof(publisher)==='string' || OT.$.isElementNode(publisher)) { - // Initiate a new Publisher with the new session credentials - publisher = OT.initPublisher(publisher, properties); - - } else if (publisher instanceof OT.Publisher){ - - // If the publisher already has a session attached to it we can - if ('session' in publisher && publisher.session && 'sessionId' in publisher.session) { - // send a warning message that we can't publish again. - if( publisher.session.sessionId === this.sessionId){ - OT.warn('Cannot publish ' + publisher.guid() + ' again to ' + - this.sessionId + '. Please call session.unpublish(publisher) first.'); - } else { - OT.warn('Cannot publish ' + publisher.guid() + ' publisher already attached to ' + - publisher.session.sessionId+ '. Please call session.unpublish(publisher) first.'); - } - } - - } else { - dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - 'Session.publish :: First parameter passed in is neither a ' + - 'string nor an instance of the Publisher', - completionHandler); - return; - } - - publisher.once('publishComplete', function(err) { - if (err) { - dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - 'Session.publish :: ' + err.message, - completionHandler); - return; - } - - if (completionHandler && OT.$.isFunction(completionHandler)) { - completionHandler.apply(null, arguments); - } - }); - - // Add publisher reference to the session - publisher._.publishToSession(this); - - // return the embed publisher - return publisher; - }; - - /** - * Ceases publishing the specified publisher's audio-video stream - * to the session. By default, the local representation of the audio-video stream is - * removed from the web page. Upon successful termination, the Session object on every - * connected web page dispatches - * a
streamDestroyed
event.
- *
- *
- *
- * To prevent the Publisher from being removed from the DOM, add an event listener for the
- * streamDestroyed
event dispatched by the Publisher object and call the
- * preventDefault()
method of the event object.
- *
- * Note: If you intend to reuse a Publisher object created using
- * OT.initPublisher()
to publish to different sessions sequentially, call
- * either Session.disconnect()
or Session.unpublish()
. Do not call
- * both. Then call the preventDefault()
method of the streamDestroyed
- * or sessionDisconnected
event object to prevent the Publisher object from being
- * removed from the page. Be sure to call preventDefault()
only if the
- * connection.connectionId
property of the Stream object in the event matches the
- * connection.connectionId
property of your Session object (to ensure that you are
- * preventing the default behavior for your published streams, not for other streams that you
- * subscribe to).
- *
- * 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:
- * audioLevelDisplayMode
(String) — How to display the audio level
- * indicator. Possible values are: "auto"
(the indicator is displayed when the
- * video is disabled), "off"
(the indicator is not displayed), and
- * "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
- * the background image when a video is not displayed. (A video may not be displayed if
- * you call subscribeToVideo(false)
on the Subscriber object). You can pass an
- * http or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
- * data
URI scheme (instead of http or https) and pass in base-64-encrypted
- * PNG data, such as that obtained from the
- * Subscriber.getImgData() method. For example,
- * you could set the property to "data:VBORw0KGgoAA..."
, where the portion of
- * the string after "data:"
is the result of a call to
- * Subscriber.getImgData()
. If the URL or the image data is invalid, the
- * property is ignored (the attempt to set the image fails silently).
- *
- * Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
- * you cannot set the backgroundImageURI
style to a string larger than
- * 32 kB. This is due to an IE 8 limitation on the size of URI strings. Due to this
- * limitation, you cannot set the backgroundImageURI
style to a string obtained
- * with the getImgData()
method.
- *
buttonDisplayMode
(String) — How to display the speaker controls
- * Possible values are: "auto"
(controls are displayed when the stream is first
- * displayed and when the user mouses over the display), "off"
(controls are not
- * displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
- * Possible values are: "auto"
(the name is displayed when the stream is first
- * displayed and when the user mouses over the display), "off"
(the name is not
- * displayed), and "on"
(the name is always displayed).videoDisabledDisplayMode
(String) Whether to display the video
- * disabled indicator and video disabled warning icons for a Subscriber. These icons
- * indicate that the video has been disabled (or is in risk of being disabled for
- * the warning icon) due to poor stream quality. This style only applies to the Subscriber
- * object. Possible values are: "auto"
(the icons are automatically when the
- * displayed video is disabled or in risk of being disabled due to poor stream quality),
- * "off"
(do not display the icons), and "on"
(display the
- * icons). The default setting is "auto"
subscribeToAudio
(Boolean) Whether to initially subscribe to audio
- * (if available) for the stream (default: true
).subscribeToVideo
(Boolean) Whether to initially subscribe to video
- * (if available) for the stream (default: true
).width
(Number) The desired width, in pixels, of the
- * displayed Subscriber video stream (default: 264). Note: Use the
- * height
and width
properties to set the dimensions
- * of the Subscriber video; do not set the height and width of the DOM element
- * (using CSS).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);
- },
-
- jsepCandidate: function(streamId, candidate) {
- return _socket.jsepCandidate(streamId, candidate);
- },
-
- jsepOffer: function(streamId, offerSdp) {
- return _socket.jsepOffer(streamId, offerSdp);
- },
-
- jsepOfferP2p: function(streamId, subscriberId, offerSdp) {
- return _socket.jsepOfferP2p(streamId, subscriberId, offerSdp);
- },
-
- jsepAnswer: function(streamId, answerSdp) {
- return _socket.jsepAnswer(streamId, answerSdp);
- },
-
- jsepAnswerP2p: function(streamId, subscriberId, answerSdp) {
- return _socket.jsepAnswerP2p(streamId, subscriberId, answerSdp);
- },
-
- // session.on("signal", function(SignalEvent))
- // session.on("signal:{type}", function(SignalEvent))
- dispatchSignal: OT.$.bind(function(fromConnection, type, data) {
- var event = new OT.SignalEvent(type, data, fromConnection);
- event.target = this;
-
- // signal a "signal" event
- // NOTE: trigger doesn't support defaultAction, and therefore preventDefault.
- this.trigger(OT.Event.names.SIGNAL, event);
-
- // signal an "signal:{type}" event" if there was a custom type
- if (type) this.dispatchEvent(event);
- }, this),
-
- subscriberCreate: function(stream, subscriber, channelsToSubscribeTo, completion) {
- return _socket.subscriberCreate(stream.id, subscriber.widgetId,
- channelsToSubscribeTo, completion);
- },
-
- subscriberDestroy: function(stream, subscriber) {
- return _socket.subscriberDestroy(stream.id, subscriber.widgetId);
- },
-
- subscriberUpdate: function(stream, subscriber, attributes) {
- return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes);
- },
-
- subscriberChannelUpdate: function(stream, subscriber, channel, attributes) {
- return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id,
- attributes);
- },
-
- streamCreate: OT.$.bind(function(name, orientation, encodedWidth, encodedHeight,
- hasAudio, hasVideo,
- frameRate, completion) {
-
- _socket.streamCreate(
- name,
- orientation,
- encodedWidth,
- encodedHeight,
- hasAudio,
- hasVideo,
- frameRate,
- OT.Config.get('bitrates', 'min', OT.APIKEY),
- OT.Config.get('bitrates', 'max', OT.APIKEY),
- completion
- );
- }, this),
-
- streamDestroy: function(streamId) {
- _socket.streamDestroy(streamId);
- },
-
- streamChannelUpdate: function(stream, channel, attributes) {
- _socket.streamChannelUpdate(stream.id, channel.id, attributes);
- }
- };
-
-
- /**
- * Sends a signal to each client or a specified client in the session. Specify a
- * to
property of the signal
parameter to limit the signal to
- * be sent to a specific client; otherwise the signal is sent to each client connected to
- * the session.
- * - * The following example sends a signal of type "foo" with a specified data payload ("hello") - * to all clients connected to the session: - *
- * session.signal({ - * type: "foo", - * data: "hello" - * }, - * function(error) { - * if (error) { - * console.log("signal error: " + error.message); - * } else { - * console.log("signal sent"); - * } - * } - * ); - *- *
- * Calling this method without specifying a recipient client (by setting the to
- * property of the signal
parameter) results in multiple signals sent (one to each
- * client in the session). For information on charges for signaling, see the
- * OpenTok pricing page.
- *
- * The following example sends a signal of type "foo" with a data payload ("hello") to a - * specific client connected to the session: - *
- * session.signal({ - * type: "foo", - * to: recipientConnection; // a Connection object - * data: "hello" - * }, - * function(error) { - * if (error) { - * console.log("signal error: " + error.message); - * } else { - * console.log("signal sent"); - * } - * } - * ); - *- *
- * Add an event handler for the signal
event to listen for all signals sent in
- * the session. Add an event handler for the signal:type
event to listen for
- * signals of a specified type only (replace type
, in signal:type
,
- * with the type of signal to listen for). The Session object dispatches these events. (See
- * events.)
- *
- * @param {Object} signal An object that contains the following properties defining the signal:
- *
data
— (String) The data to send. The limit to the length of data
- * string is 8kB. Do not set the data string to null
or
- * undefined
.to
— (Connection) A Connection
- * object corresponding to the client that the message is to be sent to. If you do not
- * specify this property, the signal is sent to all clients connected to the session.type
— (String) The type of the signal. You can use the type to
- * filter signals when setting an event handler for the signal:type
event
- * (where you replace type
with the type string). The maximum length of the
- * type
string is 128 characters, and it must contain only letters (A-Z and a-z),
- * numbers (0-9), '-', '_', and '~'.Each property is optional. If you set none of the properties, you will send a signal - * with no data or type to each client connected to the session.
- * - * @param {Function} completionHandler A function that is called when sending the signal - * succeeds or fails. This function takes one parameter —error
.
- * On success, the completionHandler
function is not passed any
- * arguments. On error, the function is passed an error
object, defined by the
- * Error class. The error
object has the following
- * properties:
- *
- * code
— (Number) An error code, which can be one of the following:
- * 400 | One of the signal properties — data, type, or to — - * is invalid. | - *
404 | The client specified by the to property is not connected to - * the session. | - *
413 | The type string exceeds the maximum length (128 bytes), - * or the data string exceeds the maximum size (8 kB). | - *
500 | You are not connected to the OpenTok session. | - *
message
— (String) A description of the error.Note that the completionHandler
success result (error == null
)
- * indicates that the options passed into the Session.signal()
method are valid
- * and the signal was sent. It does not indicate that the signal was successfully
- * received by any of the intended recipients.
- *
- * @method #signal
- * @memberOf Session
- * @see signal and signal:type events
- */
- this.signal = function(options, completion) {
- var _options = options,
- _completion = completion;
-
- if (OT.$.isFunction(_options)) {
- _completion = _options;
- _options = null;
- }
-
- if (this.isNot('connected')) {
- var notConnectedErrorMsg = 'Unable to send signal - you are not connected to the session.';
- dispatchError(500, notConnectedErrorMsg, _completion);
- return;
- }
-
- _socket.signal(_options, _completion);
- if (options && options.data && (typeof(options.data) !== 'string')) {
- OT.warn('Signaling of anything other than Strings is deprecated. ' +
- 'Please update the data property to be a string.');
- }
- this.logEvent('signal', 'send', 'type',
- (options && options.data) ? typeof(options.data) : 'null');
- };
-
- /**
- * Forces a remote connection to leave the session.
- *
- *
- * The forceDisconnect()
method is normally used as a moderation tool
- * to remove users from an ongoing session.
- *
- * When a connection is terminated using the forceDisconnect()
,
- * sessionDisconnected
, connectionDestroyed
and
- * streamDestroyed
events are dispatched in the same way as they
- * would be if the connection had terminated itself using the disconnect()
- * method. However, the reason
property of a {@link ConnectionEvent} or
- * {@link StreamEvent} object specifies "forceDisconnected"
as the reason
- * for the destruction of the connection and stream(s).
- *
- * While you can use the forceDisconnect()
method to terminate your own connection,
- * calling the disconnect()
method is simpler.
- *
- * The OT object dispatches an exception
event if the user's role
- * does not include permissions required to force other users to disconnect.
- * You define a user's role when you create the user token using the
- * generate_token()
method using
- * OpenTok
- * server-side libraries or the
- * Dashboard page.
- * See ExceptionEvent and OT.on().
- *
- * The application throws an error if the session is not connected. - *
- * - *
- * 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'); - }; - - this.isConnected = function() { - return this.is('connected'); - }; - - this.capabilities = new OT.Capabilities([]); - - /** - * Dispatched when an archive recording of the session starts. - * - * @name archiveStarted - * @event - * @memberof Session - * @see ArchiveEvent - * @see Archiving overview. - */ - - /** - * Dispatched when an archive recording of the session stops. - * - * @name archiveStopped - * @event - * @memberof Session - * @see ArchiveEvent - * @see Archiving overview. - */ - - /** - * A new client (including your own) has connected to the session. - * @name connectionCreated - * @event - * @memberof Session - * @see ConnectionEvent - * @see OT.initSession() - */ - - /** - * A client, other than your own, has disconnected from the session. - * @name connectionDestroyed - * @event - * @memberof Session - * @see ConnectionEvent - */ - - /** - * The page has connected to an OpenTok session. This event is dispatched asynchronously - * in response to a successful call to the
connect()
method of a Session
- * object. Before calling the connect()
method, initialize the session by
- * calling the OT.initSession()
method. For a code example and more details,
- * see Session.connect().
- * @name sessionConnected
- * @event
- * @memberof Session
- * @see SessionConnectEvent
- * @see Session.connect()
- * @see OT.initSession()
- */
-
- /**
- * The client has disconnected from the session. This event may be dispatched asynchronously
- * in response to a successful call to the disconnect()
method of the Session object.
- * The event may also be disptached if a session connection is lost inadvertantly, as in the case
- * of a lost network connection.
- *
- * The default behavior is that all Subscriber objects are unsubscribed and removed from the
- * HTML DOM. Each Subscriber object dispatches a
+ * The Publisher object dispatches a
- * 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
- *
- * For streams published by your own client, the Publisher object dispatches a
- *
- * For a code example and more details, see {@link StreamEvent}.
- * @name streamDestroyed
- * @event
- * @memberof Session
- * @see StreamEvent
- */
+ reset();
- /**
- * A stream has started or stopped publishing audio or video (see
- * Publisher.publishAudio() and
- * Publisher.publishVideo()); or the
- *
- * Note that a subscriber's video can be disabled or enabled for reasons other than the
- * publisher disabling or enabling it. A Subscriber object dispatches
- * You can register to receive all signals sent in the session, by adding an event handler
- * for the You can register for signals of a specfied type by adding an event handler for the
- *
- * You can register for signals of a specfied type by adding an event handler for the
- *
- * You can register to receive all signals sent in the session, by adding an event
- * handler for the 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:
+ * The following example adjusts the value of a meter element that shows volume of the
+ * publisher. Note that the audio level is adjusted logarithmically and a moving average
+ * is applied:
+ *
+ * This example shows the algorithm used by the default audio level indicator displayed
+ * in an audio-only Publisher.
+ *
+ * @name audioLevelUpdated
+ * @event
+ * @memberof Publisher
+ * @see AudioLevelUpdatedEvent
+ */
+
+/**
+ * The publisher has started streaming to the session.
+ * @name streamCreated
+ * @event
+ * @memberof Publisher
+ * @see StreamEvent
+ * @see Session.publish()
+ */
+
+/**
+ * The publisher has stopped streaming to the session. The default behavior is that
+ * the Publisher object is removed from the HTML DOM). The Publisher object dispatches a
+ *
+* Initializes and returns the local session object for a specified session ID.
+*
+* You connect to an OpenTok session using the
+* For an example, see Session.connect().
+*
+* Initializes and returns a Publisher object. You can then pass this Publisher
+* object to
+* Note: If you intend to reuse a Publisher object created using
+*
+* The application throws an error if an element with an ID set to the
+* 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.
+* 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
.
*
- * @name sessionDisconnected
- * @event
- * @memberof Session
- * @see Session.disconnect()
- * @see Session.forceDisconnect()
- * @see SessionDisconnectEvent
- */
+* @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 = _properties.publishVideo;
+ _properties.publishVideo = value;
+
+ if (_session && _stream && _properties.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.
+ * 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().
- * streamDestroyed
event.
- * videoDimensions
property of the Stream
- * object has changed (see Stream.videoDimensions).
- * videoDisabled
- * and videoEnabled
events in all conditions that cause the subscriber's stream
- * to be disabled or enabled.
- *
- * @name streamPropertyChanged
- * @event
- * @memberof Session
- * @see StreamPropertyChangedEvent
- * @see Publisher.publishAudio()
- * @see Publisher.publishVideo()
- * @see Stream.hasAudio
- * @see Stream.hasVideo
- * @see Stream.videoDimensions
- * @see Subscriber videoDisabled event
- * @see Subscriber videoEnabled event
- */
+ if (quiet !== true) {
+ this.dispatchEvent(
+ new OT.DestroyedEvent(
+ OT.Event.names.PUBLISHER_DESTROYED,
+ this,
+ reason
+ ),
+ OT.$.bind(this.off, this)
+ );
+ }
- /**
- * 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).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);
- * });
- *
- * 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).
- * 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);
- * });
- *
- * signal
event.
- *
- * @name signal:type
- * @event
- * @memberof Session
- * @see Session.signal()
- * @see SignalEvent
- * @see signal event
- */
+ return this;
};
-})(window);
-(function() {
-
- var txt = function(text) {
- return document.createTextNode(text);
+ /**
+ * @methodOf Publisher
+ * @private
+ */
+ this.disconnect = function() {
+ // Close the connection to each of our subscribers
+ for (var fromConnectionId in _peerConnections) {
+ this.cleanupSubscriber(fromConnectionId);
+ }
};
- var el = function(attr, children, tagName) {
- var el = OT.$.createElement(tagName || 'div', attr, children);
- el.on = OT.$.bind(OT.$.on, OT.$, el);
- return el;
+ this.cleanupSubscriber = function(fromConnectionId) {
+ var pc = _peerConnections[fromConnectionId];
+
+ if (pc) {
+ pc.destroy();
+ delete _peerConnections[fromConnectionId];
+
+ logAnalyticsEvent('disconnect', 'PeerConnection',
+ {subscriberConnection: fromConnectionId});
+ }
};
- function DevicePickerController(opts) {
- var destroyExistingPublisher,
- publisher,
- devicesById;
- this.change = OT.$.bind(function() {
- destroyExistingPublisher();
+ this.processMessage = function(type, fromConnection, message) {
+ OT.debug('OT.Publisher.processMessage: Received ' + type + ' from ' + fromConnection.id);
+ OT.debug(message);
- var settings;
+ switch (type) {
+ case 'unsubscribe':
+ this.cleanupSubscriber(message.content.connection.id);
+ break;
- this.pickedDevice = devicesById[opts.selectTag.value];
+ default:
+ var peerConnection = createPeerConnectionForRemote(fromConnection);
+ peerConnection.processMessage(type, message);
+ }
+ };
- if(!this.pickedDevice) {
- console.log('No device for', opts.mode, opts.selectTag.value);
- return;
- }
+ /**
+ * Returns the base-64-encoded string of PNG data representing the Publisher video.
+ *
+ *
+ * var imgData = publisher.getImgData();
+ *
+ * var img = document.createElement("img");
+ * img.setAttribute("src", "data:image/png;base64," + imgData);
+ * var imgWin = window.open("about:blank", "Screenshot");
+ * imgWin.document.write("<body></body>");
+ * imgWin.document.body.appendChild(img);
+ *
+ *
+ * @method #getImgData
+ * @memberOf Publisher
+ * @return {String} The base-64 encoded string. Returns an empty string if there is no video.
+ */
+
+ this.getImgData = function() {
+ if (!_loaded) {
+ OT.error('OT.Publisher.getImgData: Cannot getImgData before the Publisher is publishing.');
+
+ return null;
+ }
+
+ return _targetElement.imgData();
+ };
+
+
+ // API Compatibility layer for Flash Publisher, this could do with some tidyup.
+ this._ = {
+ publishToSession: OT.$.bind(function(session) {
+ // Add session property to Publisher
+ this.session = _session = session;
+
+ var createStream = function() {
+
+ var streamWidth,
+ streamHeight;
+
+ // Bail if this.session is gone, it means we were unpublished
+ // before createStream could finish.
+ if (!_session) return;
+
+ _state.set('PublishingToSession');
+
+ var onStreamRegistered = OT.$.bind(function(err, streamId, message) {
+ if (err) {
+ // @todo we should respect err.code here and translate it to the local
+ // client equivalent.
+ var errorCode = OT.ExceptionCodes.UNABLE_TO_PUBLISH;
+ var payload = {
+ reason: 'Publish',
+ code: errorCode,
+ message: err.message
+ };
+ logConnectivityEvent('Failure', payload);
+ if (_state.isAttemptingToPublish()) {
+ this.trigger('publishComplete', new OT.Error(errorCode, err.message));
+ }
+ return;
+ }
+
+ this.streamId = _streamId = streamId;
+ _iceServers = OT.Raptor.parseIceServers(message);
+ }, this);
+
+ // We set the streamWidth and streamHeight to be the minimum of the requested
+ // resolution and the actual resolution.
+ if (_properties.videoDimensions) {
+ streamWidth = Math.min(_properties.videoDimensions.width,
+ _targetElement.videoWidth() || 640);
+ streamHeight = Math.min(_properties.videoDimensions.height,
+ _targetElement.videoHeight() || 480);
+ } else {
+ streamWidth = _targetElement.videoWidth() || 640;
+ streamHeight = _targetElement.videoHeight() || 480;
+ }
+
+ var streamChannels = [];
+
+ if (!(_properties.videoSource === null || _properties.videoSource === false)) {
+ streamChannels.push(new OT.StreamChannel({
+ id: 'video1',
+ type: 'video',
+ active: _properties.publishVideo,
+ orientation: OT.VideoOrientation.ROTATED_NORMAL,
+ frameRate: _properties.frameRate,
+ width: streamWidth,
+ height: streamHeight,
+ source: _isScreenSharing ? 'screen' : 'camera',
+ fitMode: _properties.fitMode
+ }));
+ }
+
+ if (!(_properties.audioSource === null || _properties.audioSource === false)) {
+ streamChannels.push(new OT.StreamChannel({
+ id: 'audio1',
+ type: 'audio',
+ active: _properties.publishAudio
+ }));
+ }
+
+ session._.streamCreate(_properties.name || '', _properties.audioFallbackEnabled,
+ streamChannels, onStreamRegistered);
- settings = {
- insertMode: 'append',
- name: this.pickedDevice.label,
- audioSource: null,
- videoSource: null,
- width: 220,
- height: 165
};
- settings[opts.mode] = this.pickedDevice.deviceId;
+ if (_loaded) createStream.call(this);
+ else this.on('initSuccess', createStream, this);
- console.log('initPublisher', opts.previewTag, settings);
- var pub = OT.initPublisher(opts.previewTag, settings);
+ logConnectivityEvent('Attempt', {streamType: 'WebRTC'});
- pub.on({
- accessDialogOpened: function(event) {
- event.preventDefault();
- },
- accessDialogClosed: function() {
- },
- accessAllowed: function() {
- },
- accessDenied: function(event) {
- event.preventDefault();
- }
- });
+ return this;
+ }, this),
- publisher = pub;
- }, this);
-
- this.cleanup = destroyExistingPublisher = function() {
- if(publisher) {
- publisher.destroy();
- publisher = void 0;
+ unpublishFromSession: OT.$.bind(function(session, reason) {
+ if (!_session || session.id !== _session.id) {
+ OT.warn('The publisher ' + _guid + ' is trying to unpublish from a session ' +
+ session.id + ' it is not attached to (it is attached to ' +
+ (_session && _session.id || 'no session') + ')');
+ return this;
}
- };
- var disableSelector = function (opt, str) {
- opt.innerHTML = '';
- opt.appendChild(el({}, txt(str), 'option'));
- opt.setAttribute('disabled', '');
- };
-
- var addDevice = function (device) {
- devicesById[device.deviceId] = device;
- return el({ value: device.deviceId }, txt(device.label), 'option');
- };
-
- this.setDeviceList = OT.$.bind(function (devices) {
- opts.selectTag.innerHTML = '';
- devicesById = {};
- if(devices.length > 0) {
- devices.map(addDevice).map(OT.$.bind(opts.selectTag.appendChild, opts.selectTag));
- opts.selectTag.removeAttribute('disabled');
- } else {
- disableSelector(opts.selectTag, 'No devices');
+ if (session.isConnected() && this.stream) {
+ session._.streamDestroy(this.stream.id);
}
- this.change();
- }, this);
- this.setLoading = function() {
- disableSelector(opts.selectTag, 'Loading...');
- };
+ // Disconnect immediately, rather than wait for the WebSocket to
+ // reply to our destroyStream message.
+ this.disconnect();
+ this.session = _session = null;
- OT.$.on(opts.selectTag, 'change', this.change);
+ // We're back to being a stand-alone publisher again.
+ if (!_state.isDestroyed()) _state.set('MediaBound');
+
+ if(_connectivityAttemptPinger) {
+ _connectivityAttemptPinger.stop();
+ }
+ logAnalyticsEvent('unpublish', 'Success', {sessionId: session.id});
+
+ this._.streamDestroyed(reason);
+
+ return this;
+ }, this),
+
+ streamDestroyed: OT.$.bind(function(reason) {
+ if(OT.$.arrayIndexOf(['reset'], reason) < 0) {
+ var event = new OT.StreamEvent('streamDestroyed', _stream, reason, true);
+ var defaultAction = OT.$.bind(function() {
+ if(!event.isDefaultPrevented()) {
+ this.destroy();
+ }
+ }, this);
+ this.dispatchEvent(event, defaultAction);
+ }
+ }, this),
+
+
+ archivingStatus: OT.$.bind(function(status) {
+ if(_chrome) {
+ _chrome.archive.setArchiving(status);
+ }
+
+ return status;
+ }, this),
+
+ webRtcStream: function() {
+ return _webRTCStream;
+ }
+ };
+
+ this.detectDevices = function() {
+ OT.warn('Fixme: Haven\'t implemented detectDevices');
+ };
+
+ this.detectMicActivity = function() {
+ OT.warn('Fixme: Haven\'t implemented detectMicActivity');
+ };
+
+ this.getEchoCancellationMode = function() {
+ OT.warn('Fixme: Haven\'t implemented getEchoCancellationMode');
+ return 'fullDuplex';
+ };
+
+ this.setMicrophoneGain = function() {
+ OT.warn('Fixme: Haven\'t implemented setMicrophoneGain');
+ };
+
+ this.getMicrophoneGain = function() {
+ OT.warn('Fixme: Haven\'t implemented getMicrophoneGain');
+ return 0.5;
+ };
+
+ this.setCamera = function() {
+ OT.warn('Fixme: Haven\'t implemented setCamera');
+ };
+
+ this.setMicrophone = function() {
+ OT.warn('Fixme: Haven\'t implemented setMicrophone');
+ };
+
+
+ // Platform methods:
+
+ this.guid = function() {
+ return _guid;
+ };
+
+ this.videoElement = function() {
+ return _targetElement.domElement();
+ };
+
+ this.setStream = assignStream;
+
+ this.isWebRTC = true;
+
+ this.isLoading = function() {
+ return _widgetView && _widgetView.loading();
+ };
+
+ this.videoWidth = function() {
+ return _targetElement.videoWidth();
+ };
+
+ this.videoHeight = function() {
+ return _targetElement.videoHeight();
+ };
+
+ // Make read-only: element, guid, _.webRtcStream
+
+ this.on('styleValueChanged', updateChromeForStyleChange, this);
+ _state = new OT.PublishingState(stateChangeFailed);
+
+ this.accessAllowed = false;
+
+/**
+* Dispatched when the user has clicked the Allow button, granting the
+* app access to the camera and microphone. The Publisher object has an
+* accessAllowed
property which indicates whether the user
+* has granted access to the camera and microphone.
+* @see Event
+* @name accessAllowed
+* @event
+* @memberof Publisher
+*/
+
+/**
+* Dispatched when the user has clicked the Deny button, preventing the
+* app from having access to the camera and microphone.
+* @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.)
+* @see Event
+* @name accessDialogOpened
+* @event
+* @memberof Publisher
+*/
+
+/**
+* Dispatched when the Allow/Deny box is closed. (This is the dialog box in which the
+* user can grant the app access to the camera and microphone.)
+* @see Event
+* @name accessDialogClosed
+* @event
+* @memberof Publisher
+*/
+
+ /**
+ * Dispatched periodically to indicate the publisher's audio level. The event is dispatched
+ * up to 60 times per second, depending on the browser. The audioLevel
property
+ * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more
+ * information.
+ *
+ * var movingAvg = null;
+ * publisher.on('audioLevelUpdated', function(event) {
+ * if (movingAvg === null || movingAvg <= event.audioLevel) {
+ * movingAvg = event.audioLevel;
+ * } else {
+ * movingAvg = 0.7 * movingAvg + 0.3 * event.audioLevel;
+ * }
+ *
+ * // 1.5 scaling to map the -30 - 0 dBm range to [0,1]
+ * var logLevel = (Math.log(movingAvg) / Math.LN10) / 1.5 + 1;
+ * logLevel = Math.min(Math.max(logLevel, 0), 1);
+ * document.getElementById('publisherMeter').value = logLevel;
+ * });
+ *
+ * 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
+*/
+
+/**
+* Dispatched when the video dimensions of the video change. This can only occur in when the
+* stream.videoType
property is set to "screen"
(for a screen-sharing
+* video stream), and the user resizes the window being captured.
+* @name videoDimensionsChanged
+* @event
+* @memberof Publisher
+*/
+
+/**
+ * The user has stopped screen-sharing for the published stream. This event is only dispatched
+ * for screen-sharing video streams.
+ * @name mediaStopped
+ * @event
+ * @memberof Publisher
+ * @see StreamEvent
+ */
+};
+
+// Helper function to generate unique publisher ids
+OT.Publisher.nextId = OT.$.uuid;
+
+// tb_require('../../conf/properties.js')
+// tb_require('../ot.js')
+// tb_require('./session.js')
+// tb_require('./publisher.js')
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global OT */
+
+
+/**
+* The first step in using the OpenTok API is to call the OT.initSession()
+* method. Other methods of the OT object check for system requirements and set up error logging.
+*
+* @class OT
+*/
+
+/**
+* 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).
+* Session.publish()
to publish a stream to a session.
+* 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
.
+*
+* targetElement
value does not exist in the HTML DOM.
+*
true
) or not (false
). The audio-fallback
+* feature is available in sessions that use the the OpenTok Media Router. With the audio-fallback
+* feature enabled (the default), when the server determines that a stream's quality has degraded
+* significantly for a specific subscriber, it disables the video in that subscriber in order to
+* preserve audio quality. For streams that use a camera as a video source, the default setting is
+* true
(the audio-fallback feature is enabled). The default setting is
+* false
(the audio-fallback feature is disabled) for screen-sharing streams, which
+* have the videoSource
set to "screen"
in the
+* OT.initPublisher()
options. For more information, see the Subscriber
+* videoDisabled event and
+* the OpenTok Media
+* Router and media modes.
+* OT.initPublisher()
fails with an
+* error (error code 1500, "Unable to Publish") passed to the completion handler function.
+*
+* If you set this property to null
or false
, the browser does not
+* request access to the microphone, and no audio is published.
+*
+*
"cover"
— The video is cropped if its dimensions do not match those of
+* the DOM element. This is the default setting for screen-sharing videos.
+* "contain"
— The video is letter-boxed if its dimensions do not match
+* those of the DOM element. This is the default setting for videos publishing a camera feed.
+* If the publisher specifies a frame rate, the actual frame rate of the video stream
+* is set as the frameRate
property of the Stream object, though the actual frame rate
+* will vary based on changing network and system conditions. If the developer does not specify a
+* frame rate, this property is undefined.
+*
+* For sessions that use the OpenTok Media Router (sessions with +* the media mode +* set to routed, lowering the frame rate or lowering the resolution reduces +* the maximum bandwidth the stream can use. However, in sessions with the media mode set to +* relayed, lowering the frame rate or resolution may not reduce the stream's bandwidth. +*
+*
+* You can also restrict the frame rate of a Subscriber's video stream. To restrict the frame rate
+* a Subscriber, call the restrictFrameRate()
method of the subscriber, passing in
+* true
.
+* (See Subscriber.restrictFrameRate().)
+*
height
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.videoSource
property is set to
+* "screen"
(when the publisher is screen-sharing). The resolution of the
+* stream will match the captured screen region unless the region is greater than the
+* maxResolution
setting. Set this to an object that has two properties:
+* width
and height
(both numbers). The maximum value for each of
+* the width
and height
properties is 1920, and the minimum value
+* is 10.
+* true
+* (the video image is mirrored), except when the videoSource
property is set
+* to "screen"
(in which case the default is false
). This property
+* does not affect the display on subscribers' views of the video.
+* 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 sessions that use the OpenTok Media Router (sessions with the +* media mode +* set to routed, lowering the frame rate or lowering the resolution reduces the maximum bandwidth +* the stream can use. However, in sessions that have the media mode set to relayed, lowering the +* frame rate or resolution may not reduce the stream's bandwidth. +*
+*style
object includes
+* the following properties:
+* audioLevelDisplayMode
(String) — How to display the audio level
+* indicator. Possible values are: "auto"
(the indicator is displayed when the
+* video is disabled), "off"
(the indicator is not displayed), and
+* "on"
(the indicator is always displayed).backgroundImageURI
(String) — A URI for an image to display as
+* the background image when a video is not displayed. (A video may not be displayed if
+* you call publishVideo(false)
on the Publisher object). You can pass an http
+* or https URI to a PNG, JPEG, or non-animated GIF file location. You can also use the
+* data
URI scheme (instead of http or https) and pass in base-64-encrypted
+* PNG data, such as that obtained from the
+* Publisher.getImgData() method. For example,
+* you could set the property to "data:VBORw0KGgoAA..."
, where the portion of the
+* string after "data:"
is the result of a call to
+* Publisher.getImgData()
. If the URL or the image data is invalid, the property
+* is ignored (the attempt to set the image fails silently).
+*
+* Note that in Internet Explorer 8 (using the OpenTok Plugin for Internet Explorer),
+* you cannot set the backgroundImageURI
style to a string larger than 32 kB.
+* This is due to an IE 8 limitation on the size of URI strings. Due to this limitation,
+* you cannot set the backgroundImageURI
style to a string obtained with the
+* getImgData()
method.
+*
buttonDisplayMode
(String) — How to display the microphone controls
+* Possible values are: "auto"
(controls are displayed when the stream is first
+* displayed and when the user mouses over the display), "off"
(controls are not
+* displayed), and "on"
(controls are always displayed).nameDisplayMode
(String) Whether to display the stream name.
+* Possible values are: "auto"
(the name is displayed when the stream is first
+* displayed and when the user mouses over the display), "off"
(the name is not
+* displayed), and "on"
(the name is always displayed).OT.initPublisher()
fails with an
+* error (error code 1500, "Unable to Publish") passed to the completion handler function.
+*
+* If you set this property to null
or false
, the browser does not
+* request access to the camera, and no video is published. In a voice-only call, set this
+* property to null
or false
for each Publisher.
+*
+* Set this property to "screen"
to publish a screen-sharing stream. Call
+* OT.checkScreenSharingCapability() to check
+* if screen sharing is supported. When you set the videoSource
property to
+* "screen"
, the following are default values for other properties:
+* audioFallbackEnabled == false
,
+* maxResolution == {width: 1920, height: 1920}
, mirror == false
,
+* scaleMode == "fit"
. Also, the default scaleMode
setting for
+* subscribers to the stream is "fit"
.
+*
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 for audio input + * devices or
"videoInput"
for video input devices.
+ *
+ * The deviceId
property is a unique ID for the device. You can pass
+ * the deviceId
in as the audioSource
or videoSource
+ * property of the the options
parameter of the
+ * OT.initPublisher() method.
+ *
+ * The label
property identifies the device. The label
+ * property is set to an empty string if the user has not previously granted access to
+ * a camera and microphone. In HTTP, the user must have granted access to a camera and
+ * microphone in the current page (for example, in response to a call to
+ * OT.initPublisher()
). In HTTPS, the user must have previously granted access
+ * to the camera and microphone in the current page or in a page previously loaded from the
+ * domain.
+ *
+ *
+ * @see OT.initPublisher()
+ * @method OT.getDevices
+ * @memberof OT
+ */
+OT.getDevices = function(callback) {
+ OT.$.getMediaDevices(callback);
+};
+
+
+
+OT.reportIssue = function(){
+ OT.warn('ToDo: haven\'t yet implemented OT.reportIssue');
+};
+
+OT.components = {};
+
+
+/**
+ * This method is deprecated. Use on() or once() instead.
+ *
+ *
+ * Registers a method as an event listener for a specific event. + *
+ * + *
+ * The OT object dispatches one type of event an exception
event. The
+ * following code adds an event listener for the exception
event:
+ *
+ * OT.addEventListener("exception", exceptionHandler); + * + * function exceptionHandler(event) { + * alert("exception event. \n code == " + event.code + "\n message == " + event.message); + * } + *+ * + *
+ * If a handler is not registered for an event, the event is ignored locally. If the event + * listener function does not exist, the event is ignored locally. + *
+ *
+ * Throws an exception if the listener
name is invalid.
+ *
+ * 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
+ */
+
+
+// tb_require('./helpers/lib/css_loader.js')
+// tb_require('./ot/system_requirements.js')
+// tb_require('./ot/session.js')
+// tb_require('./ot/publisher.js')
+// tb_require('./ot/subscriber.js')
+// tb_require('./ot/archive.js')
+// tb_require('./ot/connection.js')
+// tb_require('./ot/stream.js')
+// We want this to be included at the end, just before footer.js
+
+/* jshint globalstrict: true, strict: false, undef: true, unused: true,
+ trailing: true, browser: true, smarttabs:true */
+/* global loadCSS, define */
+
+// Tidy up everything on unload
+OT.onUnload(function() {
+ OT.publishers.destroy();
+ OT.subscribers.destroy();
+ OT.sessions.destroy('unloaded');
+});
+
+loadCSS(OT.properties.cssURL);
// 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
@@ -22274,8 +24115,13 @@ var SDPHelpers = {
// 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; } );
- }
+if (typeof define === 'function' && define.amd) {
+ define( 'TB', [], function () { return TB; } );
+}
+// tb_require('./postscript.js')
-})(window);
+/* jshint ignore:start */
+})(window, window.OT);
+/* jshint ignore:end */
+
+})(window || exports);
\ No newline at end of file
diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
index 9a5642baec96..54781e191e49 100644
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -224,7 +224,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
* @private
*/
_onActiveRoomStateChanged: function() {
- this.setState(this.props.activeRoomStore.getStoreState());
+ var state = this.props.activeRoomStore.getStoreState();
+ this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions);
+ this.setState(state);
},
componentDidMount: function() {
@@ -283,6 +285,41 @@ loop.standaloneRoomViews = (function(mozL10n) {
}));
},
+ /**
+ * Specifically updates the local camera stream size and position, depending
+ * on the size and position of the remote video stream.
+ * This method gets called from `updateVideoContainer`, which is defined in
+ * the `MediaSetupMixin`.
+ *
+ * @param {Object} ratio Aspect ratio of the local camera stream
+ */
+ updateLocalCameraPosition: function(ratio) {
+ var node = this._getElement(".local");
+ var parent = node.offsetParent || this._getElement(".media");
+ // The local camera view should be a sixth of the size of its offset parent
+ // and positioned to overlap with the remote stream at a quarter of its width.
+ var parentWidth = parent.offsetWidth;
+ var targetWidth = parentWidth / 6;
+
+ node.style.right = "auto";
+ if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
+ targetWidth = 180;
+ node.style.left = "auto";
+ } else {
+ // Now position the local camera view correctly with respect to the remote
+ // video stream.
+ var remoteVideoDimensions = this.getRemoteVideoDimensions();
+ var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX);
+ // The horizontal offset of the stream, and the width of the resulting
+ // pillarbox, is determined by the height exponent of the aspect ratio.
+ // Therefore we multiply the width of the local camera view by the height
+ // ratio.
+ node.style.left = (offsetX - ((targetWidth * ratio.height) / 4)) + "px";
+ }
+ node.style.width = (targetWidth * ratio.width) + "px";
+ node.style.height = (targetWidth * ratio.height) + "px";
+ },
+
/**
* Checks if current room is active.
*
diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
index a5b0ba367abf..4aca9aba4f61 100644
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -224,7 +224,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
* @private
*/
_onActiveRoomStateChanged: function() {
- this.setState(this.props.activeRoomStore.getStoreState());
+ var state = this.props.activeRoomStore.getStoreState();
+ this.updateVideoDimensions(state.localVideoDimensions, state.remoteVideoDimensions);
+ this.setState(state);
},
componentDidMount: function() {
@@ -283,6 +285,41 @@ loop.standaloneRoomViews = (function(mozL10n) {
}));
},
+ /**
+ * Specifically updates the local camera stream size and position, depending
+ * on the size and position of the remote video stream.
+ * This method gets called from `updateVideoContainer`, which is defined in
+ * the `MediaSetupMixin`.
+ *
+ * @param {Object} ratio Aspect ratio of the local camera stream
+ */
+ updateLocalCameraPosition: function(ratio) {
+ var node = this._getElement(".local");
+ var parent = node.offsetParent || this._getElement(".media");
+ // The local camera view should be a sixth of the size of its offset parent
+ // and positioned to overlap with the remote stream at a quarter of its width.
+ var parentWidth = parent.offsetWidth;
+ var targetWidth = parentWidth / 6;
+
+ node.style.right = "auto";
+ if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
+ targetWidth = 180;
+ node.style.left = "auto";
+ } else {
+ // Now position the local camera view correctly with respect to the remote
+ // video stream.
+ var remoteVideoDimensions = this.getRemoteVideoDimensions();
+ var offsetX = (remoteVideoDimensions.streamWidth + remoteVideoDimensions.offsetX);
+ // The horizontal offset of the stream, and the width of the resulting
+ // pillarbox, is determined by the height exponent of the aspect ratio.
+ // Therefore we multiply the width of the local camera view by the height
+ // ratio.
+ node.style.left = (offsetX - ((targetWidth * ratio.height) / 4)) + "px";
+ }
+ node.style.width = (targetWidth * ratio.width) + "px";
+ node.style.height = (targetWidth * ratio.height) + "px";
+ },
+
/**
* Checks if current room is active.
*
diff --git a/browser/components/loop/test/desktop-local/roomViews_test.js b/browser/components/loop/test/desktop-local/roomViews_test.js
index 200c6f45a2e2..1bc48f5e8089 100644
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -68,7 +68,9 @@ describe("loop.roomViews", function () {
videoMuted: false,
failureReason: undefined,
used: false,
- foo: "bar"
+ foo: "bar",
+ localVideoDimensions: {},
+ remoteVideoDimensions: {}
});
});
diff --git a/browser/components/loop/test/functional/test_1_browser_call.py b/browser/components/loop/test/functional/test_1_browser_call.py
index de8d6dee7e75..0985e9ca5190 100644
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -129,7 +129,7 @@ class Test1BrowserCall(MarionetteTestCase):
def check_remote_video(self):
video_wrapper = self.wait_for_element_displayed(
By.CSS_SELECTOR,
- ".media .OT_subscriber .OT_video-container", 20)
+ ".media .OT_subscriber .OT_widget-container", 20)
video = self.wait_for_subelement_displayed(
video_wrapper, By.TAG_NAME, "video")
diff --git a/browser/components/loop/test/shared/activeRoomStore_test.js b/browser/components/loop/test/shared/activeRoomStore_test.js
index 8a9ffc65fcbf..3fa25151f54b 100644
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -282,6 +282,31 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
+ describe("#videoDimensionsChanged", function() {
+ it("should not contain any video dimensions at the very start", function() {
+ expect(store.getStoreState()).eql(store.getInitialStoreState());
+ });
+
+ it("should update the store with new video dimensions", function() {
+ var actionData = {
+ isLocal: true,
+ videoType: "camera",
+ dimensions: { width: 640, height: 480 }
+ };
+
+ store.videoDimensionsChanged(new sharedActions.VideoDimensionsChanged(actionData));
+
+ expect(store.getStoreState().localVideoDimensions)
+ .to.have.property(actionData.videoType, actionData.dimensions);
+
+ actionData.isLocal = false;
+ store.videoDimensionsChanged(new sharedActions.VideoDimensionsChanged(actionData));
+
+ expect(store.getStoreState().remoteVideoDimensions)
+ .to.have.property(actionData.videoType, actionData.dimensions);
+ });
+ });
+
describe("#setupRoomInfo", function() {
var fakeRoomInfo;
diff --git a/browser/components/loop/test/shared/mixins_test.js b/browser/components/loop/test/shared/mixins_test.js
index 7c6dbcc66585..bdbdd12f4ab5 100644
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -204,8 +204,16 @@ describe("loop.shared.mixins", function() {
}
});
+ sandbox.useFakeTimers();
+
rootObject = {
events: {},
+ setTimeout: function(func, timeout) {
+ return setTimeout(func, timeout);
+ },
+ clearTimeout: function(timer) {
+ return clearTimeout(timer);
+ },
addEventListener: function(eventName, listener) {
this.events[eventName] = listener;
},
@@ -244,20 +252,26 @@ describe("loop.shared.mixins", function() {
describe("resize", function() {
it("should update the width on the local stream element", function() {
localElement = {
+ offsetWidth: 100,
+ offsetHeight: 100,
style: { width: "0%" }
};
rootObject.events.resize();
+ sandbox.clock.tick(10);
expect(localElement.style.width).eql("100%");
});
it("should update the height on the remote stream element", function() {
remoteElement = {
+ offsetWidth: 100,
+ offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.resize();
+ sandbox.clock.tick(10);
expect(remoteElement.style.height).eql("100%");
});
@@ -266,24 +280,81 @@ describe("loop.shared.mixins", function() {
describe("orientationchange", function() {
it("should update the width on the local stream element", function() {
localElement = {
+ offsetWidth: 100,
+ offsetHeight: 100,
style: { width: "0%" }
};
rootObject.events.orientationchange();
+ sandbox.clock.tick(10);
expect(localElement.style.width).eql("100%");
});
it("should update the height on the remote stream element", function() {
remoteElement = {
+ offsetWidth: 100,
+ offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.orientationchange();
+ sandbox.clock.tick(10);
expect(remoteElement.style.height).eql("100%");
});
});
+
+
+ describe("Video stream dimensions", function() {
+ var localVideoDimensions = {
+ camera: {
+ width: 640,
+ height: 480
+ }
+ };
+ var remoteVideoDimensions = {
+ camera: {
+ width: 420,
+ height: 138
+ }
+ };
+
+ beforeEach(function() {
+ view.updateVideoDimensions(localVideoDimensions, remoteVideoDimensions);
+ });
+
+ it("should register video dimension updates correctly", function() {
+ expect(view._videoDimensionsCache.local.camera.width)
+ .eql(localVideoDimensions.camera.width);
+ expect(view._videoDimensionsCache.local.camera.height)
+ .eql(localVideoDimensions.camera.height);
+ expect(view._videoDimensionsCache.local.camera.aspectRatio.width).eql(1);
+ expect(view._videoDimensionsCache.local.camera.aspectRatio.height).eql(0.75);
+ expect(view._videoDimensionsCache.remote.camera.width)
+ .eql(remoteVideoDimensions.camera.width);
+ expect(view._videoDimensionsCache.remote.camera.height)
+ .eql(remoteVideoDimensions.camera.height);
+ expect(view._videoDimensionsCache.remote.camera.aspectRatio.width).eql(1);
+ expect(view._videoDimensionsCache.remote.camera.aspectRatio.height)
+ .eql(0.32857142857142857);
+ });
+
+ it("should fetch remote video stream dimensions correctly", function() {
+ remoteElement = {
+ offsetWidth: 600,
+ offsetHeight: 320
+ };
+
+ var remoteVideoDimensions = view.getRemoteVideoDimensions();
+ expect(remoteVideoDimensions.width).eql(remoteElement.offsetWidth);
+ expect(remoteVideoDimensions.height).eql(remoteElement.offsetHeight);
+ expect(remoteVideoDimensions.streamWidth).eql(534.8571428571429);
+ expect(remoteVideoDimensions.streamHeight).eql(remoteElement.offsetHeight);
+ expect(remoteVideoDimensions.offsetX).eql(32.571428571428555);
+ expect(remoteVideoDimensions.offsetY).eql(0);
+ });
+ });
});
});
diff --git a/browser/components/loop/test/shared/otSdkDriver_test.js b/browser/components/loop/test/shared/otSdkDriver_test.js
index 613e8db9f21d..b058af9a6609 100644
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -8,6 +8,7 @@ describe("loop.OTSdkDriver", function () {
var sharedActions = loop.shared.actions;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
+ var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES;
var sandbox;
var dispatcher, driver, publisher, sdk, session, sessionData;
var fakeLocalElement, fakeRemoteElement, publisherConfig, fakeEvent;
@@ -310,6 +311,44 @@ describe("loop.OTSdkDriver", function () {
});
});
+ describe("streamPropertyChanged", function() {
+ var fakeStream = {
+ connection: { id: "fake" },
+ videoType: "screen",
+ videoDimensions: {
+ width: 320,
+ height: 160
+ }
+ };
+
+ it("should not dispatch a VideoDimensionsChanged action for other properties", function() {
+ session.trigger("streamPropertyChanged", {
+ stream: fakeStream,
+ changedProperty: STREAM_PROPERTIES.HAS_AUDIO
+ });
+ session.trigger("streamPropertyChanged", {
+ stream: fakeStream,
+ changedProperty: STREAM_PROPERTIES.HAS_VIDEO
+ });
+
+ sinon.assert.notCalled(dispatcher.dispatch);
+ });
+
+ it("should dispatch a VideoDimensionsChanged action", function() {
+ session.connection = {
+ id: "localUser"
+ };
+ session.trigger("streamPropertyChanged", {
+ stream: fakeStream,
+ changedProperty: STREAM_PROPERTIES.VIDEO_DIMENSIONS
+ });
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "videoDimensionsChanged"))
+ })
+ });
+
describe("connectionCreated", function() {
beforeEach(function() {
session.connection = {
diff --git a/browser/components/places/tests/unit/test_421483.js b/browser/components/places/tests/unit/test_421483.js
index 2f3a4e7b7a9b..401bf66051ae 100644
--- a/browser/components/places/tests/unit/test_421483.js
+++ b/browser/components/places/tests/unit/test_421483.js
@@ -24,7 +24,7 @@ add_task(function smart_bookmarks_disabled() {
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, 0);
- do_log_info("check that pref has not been bumped up");
+ do_print("check that pref has not been bumped up");
do_check_eq(Services.prefs.getIntPref("browser.places.smartBookmarksVersion"), -1);
});
@@ -34,7 +34,7 @@ add_task(function create_smart_bookmarks() {
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_neq(smartBookmarkItemIds.length, 0);
- do_log_info("check that pref has been bumped up");
+ do_print("check that pref has been bumped up");
do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
});
@@ -42,14 +42,14 @@ add_task(function remove_smart_bookmark_and_restore() {
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
let smartBookmarksCount = smartBookmarkItemIds.length;
- do_log_info("remove one smart bookmark and restore");
+ do_print("remove one smart bookmark and restore");
PlacesUtils.bookmarks.removeItem(smartBookmarkItemIds[0]);
Services.prefs.setIntPref("browser.places.smartBookmarksVersion", 0);
gluesvc.ensurePlacesDefaultQueriesInitialized();
smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
- do_log_info("check that pref has been bumped up");
+ do_print("check that pref has been bumped up");
do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
});
@@ -57,7 +57,7 @@ add_task(function move_smart_bookmark_rename_and_restore() {
let smartBookmarkItemIds =
PlacesUtils.annotations.getItemsWithAnnotation(SMART_BOOKMARKS_ANNO);
let smartBookmarksCount = smartBookmarkItemIds.length;
- do_log_info("smart bookmark should be restored in place");
+ do_print("smart bookmark should be restored in place");
let parent = PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]);
let oldTitle = PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]);
// create a subfolder and move inside it
@@ -76,6 +76,6 @@ add_task(function move_smart_bookmark_rename_and_restore() {
do_check_eq(smartBookmarkItemIds.length, smartBookmarksCount);
do_check_eq(PlacesUtils.bookmarks.getFolderIdForItem(smartBookmarkItemIds[0]), newParent);
do_check_eq(PlacesUtils.bookmarks.getItemTitle(smartBookmarkItemIds[0]), oldTitle);
- do_log_info("check that pref has been bumped up");
+ do_print("check that pref has been bumped up");
do_check_true(Services.prefs.getIntPref("browser.places.smartBookmarksVersion") > 0);
});
diff --git a/browser/components/places/tests/unit/test_browserGlue_prefs.js b/browser/components/places/tests/unit/test_browserGlue_prefs.js
index 383936d88c6b..06ab6412412c 100644
--- a/browser/components/places/tests/unit/test_browserGlue_prefs.js
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -63,7 +63,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
function test_import()
{
- do_log_info("Import from bookmarks.html if importBookmarksHTML is true.");
+ do_print("Import from bookmarks.html if importBookmarksHTML is true.");
remove_all_bookmarks();
// Sanity check: we should not have any bookmark on the toolbar.
@@ -86,7 +86,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
run_next_test();
});
// Force nsBrowserGlue::_initPlaces().
- do_log_info("Simulate Places init");
+ do_print("Simulate Places init");
bg.QueryInterface(Ci.nsIObserver).observe(null,
TOPIC_BROWSERGLUE_TEST,
TOPICDATA_FORCE_PLACES_INIT);
@@ -94,8 +94,8 @@ function waitForImportAndSmartBookmarks(aCallback) {
function test_import_noSmartBookmarks()
{
- do_log_info("import from bookmarks.html, but don't create smart bookmarks \
- if they are disabled");
+ do_print("import from bookmarks.html, but don't create smart bookmarks \
+ if they are disabled");
remove_all_bookmarks();
// Sanity check: we should not have any bookmark on the toolbar.
@@ -119,7 +119,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
run_next_test();
});
// Force nsBrowserGlue::_initPlaces().
- do_log_info("Simulate Places init");
+ do_print("Simulate Places init");
bg.QueryInterface(Ci.nsIObserver).observe(null,
TOPIC_BROWSERGLUE_TEST,
TOPICDATA_FORCE_PLACES_INIT);
@@ -127,8 +127,8 @@ function waitForImportAndSmartBookmarks(aCallback) {
function test_import_autoExport_updatedSmartBookmarks()
{
- do_log_info("Import from bookmarks.html, but don't create smart bookmarks \
- if autoExportHTML is true and they are at latest version");
+ do_print("Import from bookmarks.html, but don't create smart bookmarks \
+ if autoExportHTML is true and they are at latest version");
remove_all_bookmarks();
// Sanity check: we should not have any bookmark on the toolbar.
@@ -154,7 +154,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
run_next_test();
});
// Force nsBrowserGlue::_initPlaces()
- do_log_info("Simulate Places init");
+ do_print("Simulate Places init");
bg.QueryInterface(Ci.nsIObserver).observe(null,
TOPIC_BROWSERGLUE_TEST,
TOPICDATA_FORCE_PLACES_INIT);
@@ -162,8 +162,8 @@ function waitForImportAndSmartBookmarks(aCallback) {
function test_import_autoExport_oldSmartBookmarks()
{
- do_log_info("Import from bookmarks.html, and create smart bookmarks if \
- autoExportHTML is true and they are not at latest version.");
+ do_print("Import from bookmarks.html, and create smart bookmarks if \
+ autoExportHTML is true and they are not at latest version.");
remove_all_bookmarks();
// Sanity check: we should not have any bookmark on the toolbar.
@@ -190,7 +190,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
run_next_test();
});
// Force nsBrowserGlue::_initPlaces()
- do_log_info("Simulate Places init");
+ do_print("Simulate Places init");
bg.QueryInterface(Ci.nsIObserver).observe(null,
TOPIC_BROWSERGLUE_TEST,
TOPICDATA_FORCE_PLACES_INIT);
@@ -198,8 +198,8 @@ function waitForImportAndSmartBookmarks(aCallback) {
function test_restore()
{
- do_log_info("restore from default bookmarks.html if \
- restore_default_bookmarks is true.");
+ do_print("restore from default bookmarks.html if \
+ restore_default_bookmarks is true.");
remove_all_bookmarks();
// Sanity check: we should not have any bookmark on the toolbar.
@@ -222,7 +222,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
run_next_test();
});
// Force nsBrowserGlue::_initPlaces()
- do_log_info("Simulate Places init");
+ do_print("Simulate Places init");
bg.QueryInterface(Ci.nsIObserver).observe(null,
TOPIC_BROWSERGLUE_TEST,
TOPICDATA_FORCE_PLACES_INIT);
@@ -231,8 +231,8 @@ function waitForImportAndSmartBookmarks(aCallback) {
function test_restore_import()
{
- do_log_info("setting both importBookmarksHTML and \
- restore_default_bookmarks should restore defaults.");
+ do_print("setting both importBookmarksHTML and \
+ restore_default_bookmarks should restore defaults.");
remove_all_bookmarks();
// Sanity check: we should not have any bookmark on the toolbar.
@@ -257,7 +257,7 @@ function waitForImportAndSmartBookmarks(aCallback) {
run_next_test();
});
// Force nsBrowserGlue::_initPlaces()
- do_log_info("Simulate Places init");
+ do_print("Simulate Places init");
bg.QueryInterface(Ci.nsIObserver).observe(null,
TOPIC_BROWSERGLUE_TEST,
TOPICDATA_FORCE_PLACES_INIT);
diff --git a/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js b/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
index c3ffd12beb4a..581693dba33e 100644
--- a/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
+++ b/browser/components/places/tests/unit/test_browserGlue_urlbar_defaultbehavior_migration.js
@@ -38,7 +38,7 @@ function setupBehaviorAndMigrate(aDefaultBehavior, aAutocompleteEnabled = true)
};
add_task(function*() {
- do_log_info("Migrate default.behavior = 0");
+ do_print("Migrate default.behavior = 0");
setupBehaviorAndMigrate(0);
Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
@@ -52,7 +52,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 1");
+ do_print("Migrate default.behavior = 1");
setupBehaviorAndMigrate(1);
Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
@@ -66,7 +66,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 2");
+ do_print("Migrate default.behavior = 2");
setupBehaviorAndMigrate(2);
Assert.equal(gGetBoolPref("browser.urlbar.suggest.history"), false,
@@ -80,7 +80,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 3");
+ do_print("Migrate default.behavior = 3");
setupBehaviorAndMigrate(3);
Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
@@ -94,7 +94,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 19");
+ do_print("Migrate default.behavior = 19");
setupBehaviorAndMigrate(19);
Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
@@ -108,7 +108,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 33");
+ do_print("Migrate default.behavior = 33");
setupBehaviorAndMigrate(33);
Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
@@ -122,7 +122,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 129");
+ do_print("Migrate default.behavior = 129");
setupBehaviorAndMigrate(129);
Assert.ok(gGetBoolPref("browser.urlbar.suggest.history"),
@@ -136,7 +136,7 @@ add_task(function*() {
});
add_task(function*() {
- do_log_info("Migrate default.behavior = 0, autocomplete.enabled = false");
+ do_print("Migrate default.behavior = 0, autocomplete.enabled = false");
setupBehaviorAndMigrate(0, false);
Assert.equal(gGetBoolPref("browser.urlbar.suggest.history"), false,
diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js
index ad3308f231e4..e64c304573ad 100644
--- a/browser/components/sessionstore/test/browser_crashedTabs.js
+++ b/browser/components/sessionstore/test/browser_crashedTabs.js
@@ -6,6 +6,15 @@
const PAGE_1 = "data:text/html,A%20regular,%20everyday,%20normal%20page.";
const PAGE_2 = "data:text/html,Another%20regular,%20everyday,%20normal%20page.";
+// Turn off tab animations for testing
+Services.prefs.setBoolPref("browser.tabs.animate", false);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("browser.tabs.animate");
+});
+
+// Allow tabs to restore on demand so we can test pending states
+Services.prefs.clearUserPref("browser.sessionstore.restore_on_demand");
+
/**
* Returns a Promise that resolves once a remote