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" /> - + 0 then we record its real size by taking its aspect + // ratio in account. Due to the 'contain' fit-mode, the stream will be + // centered inside the video element. + // We'll need to deal with more than one remote video stream whenever + // that becomes something we need to support. + if (width) { + remoteVideoDimensions = { + width: width, + height: node.offsetHeight + }; + var ratio = this._videoDimensionsCache.remote[videoType].aspectRatio; + var leadingAxis = Math.min(ratio.width, ratio.height) === ratio.width ? + "width" : "height"; + var slaveSize = remoteVideoDimensions[leadingAxis] + + (remoteVideoDimensions[leadingAxis] * (1 - ratio[leadingAxis])); + remoteVideoDimensions.streamWidth = leadingAxis === "width" ? + remoteVideoDimensions.width : slaveSize; + remoteVideoDimensions.streamHeight = leadingAxis === "height" ? + remoteVideoDimensions.height: slaveSize; + } + }, this); + + // Supply some sensible defaults for the remoteVideoDimensions if no remote + // stream is connected (yet). + if (!remoteVideoDimensions) { + var node = this._getElement(".remote"); + var width = node.offsetWidth; + var height = node.offsetHeight; + remoteVideoDimensions = { + width: width, + height: height, + streamWidth: width, + streamHeight: height + }; + } + + // Calculate the size of each individual letter- or pillarbox for convenience. + remoteVideoDimensions.offsetX = remoteVideoDimensions.width - + remoteVideoDimensions.streamWidth + if (remoteVideoDimensions.offsetX > 0) { + remoteVideoDimensions.offsetX /= 2; + } + remoteVideoDimensions.offsetY = remoteVideoDimensions.height - + remoteVideoDimensions.streamHeight; + if (remoteVideoDimensions.offsetY > 0) { + remoteVideoDimensions.offsetY /= 2; + } + + return remoteVideoDimensions; }, /** * Used to update the video container whenever the orientation or size of the * display area changes. + * + * Buffer the calls to this function to make sure we don't overflow the stack + * with update calls when many 'resize' event are fired, to prevent blocking + * the event loop. */ updateVideoContainer: function() { - var localStreamParent = this._getElement('.local .OT_publisher'); - var remoteStreamParent = this._getElement('.remote .OT_subscriber'); - if (localStreamParent) { - localStreamParent.style.width = "100%"; - } - if (remoteStreamParent) { - remoteStreamParent.style.height = "100%"; + if (this._bufferedUpdateVideo) { + rootObject.clearTimeout(this._bufferedUpdateVideo); + this._bufferedUpdateVideo = null; } + + this._bufferedUpdateVideo = rootObject.setTimeout(function() { + this._bufferedUpdateVideo = null; + var localStreamParent = this._getElement(".local .OT_publisher"); + var remoteStreamParent = this._getElement(".remote .OT_subscriber"); + if (localStreamParent) { + localStreamParent.style.width = "100%"; + } + if (remoteStreamParent) { + remoteStreamParent.style.height = "100%"; + } + + // Update the position and dimensions of the containers of local video + // streams, if necessary. The consumer of this mixin should implement the + // actual updating mechanism. + Object.keys(this._videoDimensionsCache.local).forEach(function(videoType) { + var ratio = this._videoDimensionsCache.local[videoType].aspectRatio + if (videoType == "camera" && this.updateLocalCameraPosition) { + this.updateLocalCameraPosition(ratio); + } + }, this); + }.bind(this), 0); }, /** @@ -198,15 +386,11 @@ loop.shared.mixins = (function() { // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445 return { insertMode: "append", + fitMode: "contain", width: "100%", height: "100%", publishVideo: options.publishVideo, - style: { - audioLevelDisplayMode: "off", - buttonDisplayMode: "off", - nameDisplayMode: "off", - videoDisabledDisplayMode: "off" - } + showControls: false, }; }, diff --git a/browser/components/loop/content/shared/js/otSdkDriver.js b/browser/components/loop/content/shared/js/otSdkDriver.js index baf439dcc27f..05d31dcf0e2e 100644 --- a/browser/components/loop/content/shared/js/otSdkDriver.js +++ b/browser/components/loop/content/shared/js/otSdkDriver.js @@ -9,6 +9,7 @@ loop.OTSdkDriver = (function() { var sharedActions = loop.shared.actions; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; + var STREAM_PROPERTIES = loop.shared.utils.STREAM_PROPERTIES; /** * This is a wrapper for the OT sdk. It is used to translate the SDK events into @@ -51,6 +52,7 @@ loop.OTSdkDriver = (function() { // the media. this.publisher = this.sdk.initPublisher(this.getLocalElement(), this.publisherConfig); + this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this)); this.publisher.on("accessAllowed", this._onPublishComplete.bind(this)); this.publisher.on("accessDenied", this._onPublishDenied.bind(this)); this.publisher.on("accessDialogOpened", @@ -91,6 +93,7 @@ loop.OTSdkDriver = (function() { this._onConnectionDestroyed.bind(this)); this.session.on("sessionDisconnected", this._onSessionDisconnected.bind(this)); + this.session.on("streamPropertyChanged", this._onStreamPropertyChanged.bind(this)); // This starts the actual session connection. this.session.connect(sessionData.apiKey, sessionData.sessionToken, @@ -102,12 +105,13 @@ loop.OTSdkDriver = (function() { */ disconnectSession: function() { if (this.session) { - this.session.off("streamCreated connectionDestroyed sessionDisconnected"); + this.session.off("streamCreated streamDestroyed connectionDestroyed " + + "sessionDisconnected streamPropertyChanged"); this.session.disconnect(); delete this.session; } if (this.publisher) { - this.publisher.off("accessAllowed accessDenied accessDialogOpened"); + this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated"); this.publisher.destroy(); delete this.publisher; } @@ -234,6 +238,14 @@ loop.OTSdkDriver = (function() { * https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html */ _onRemoteStreamCreated: function(event) { + if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) { + this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({ + isLocal: false, + videoType: event.stream.videoType, + dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS] + })); + } + this.session.subscribe(event.stream, this.getRemoteElement(), this.publisherConfig); @@ -243,6 +255,22 @@ loop.OTSdkDriver = (function() { } }, + /** + * Handles the event when the local stream is created. + * + * @param {StreamEvent} event The event details: + * https://tokbox.com/opentok/libraries/client/js/reference/StreamEvent.html + */ + _onLocalStreamCreated: function(event) { + if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) { + this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({ + isLocal: true, + videoType: event.stream.videoType, + dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS] + })); + } + }, + /** * Called from the sdk when the media access dialog is opened. * Prevents the default action, to prevent the SDK's "allow access" @@ -282,6 +310,19 @@ loop.OTSdkDriver = (function() { })); }, + /** + * Handles publishing of property changes to a stream. + */ + _onStreamPropertyChanged: function(event) { + if (event.changedProperty == STREAM_PROPERTIES.VIDEO_DIMENSIONS) { + this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({ + isLocal: event.stream.connection.id == this.session.connection.id, + videoType: event.stream.videoType, + dimensions: event.stream[STREAM_PROPERTIES.VIDEO_DIMENSIONS] + })); + } + }, + /** * Publishes the local stream if the session is connected * and the publisher is ready. diff --git a/browser/components/loop/content/shared/js/utils.js b/browser/components/loop/content/shared/js/utils.js index 25fe69d0a741..9906610af4bf 100644 --- a/browser/components/loop/content/shared/js/utils.js +++ b/browser/components/loop/content/shared/js/utils.js @@ -42,6 +42,12 @@ loop.shared.utils = (function(mozL10n) { UNKNOWN: "reason-unknown" }; + var STREAM_PROPERTIES = { + VIDEO_DIMENSIONS: "videoDimensions", + HAS_AUDIO: "hasAudio", + HAS_VIDEO: "hasVideo" + }; + /** * Format a given date into an l10n-friendly string. * @@ -138,6 +144,7 @@ loop.shared.utils = (function(mozL10n) { FAILURE_DETAILS: FAILURE_DETAILS, REST_ERRNOS: REST_ERRNOS, WEBSOCKET_REASONS: WEBSOCKET_REASONS, + STREAM_PROPERTIES: STREAM_PROPERTIES, Helper: Helper, composeCallUrlEmail: composeCallUrlEmail, formatDate: formatDate, diff --git a/browser/components/loop/content/shared/libs/sdk-content/css/ot.css b/browser/components/loop/content/shared/libs/sdk-content/css/ot.css index c4de63157a80..3195b3def2b5 100644 --- a/browser/components/loop/content/shared/libs/sdk-content/css/ot.css +++ b/browser/components/loop/content/shared/libs/sdk-content/css/ot.css @@ -11,16 +11,15 @@ /* Root OT object, this is where our CSS reset happens */ .OT_root, .OT_root * { - color: #ffffff; - margin: 0; - padding: 0; - border: 0; - font-size: 100%; - font-family: Arial, Helvetica, sans-serif; - vertical-align: baseline; + color: #ffffff; + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + font-family: Arial, Helvetica, sans-serif; + vertical-align: baseline; } - /** * Specific Element Reset */ @@ -31,10 +30,10 @@ .OT_root h4, .OT_root h5, .OT_root h6 { - color: #ffffff; - font-family: Arial, Helvetica, sans-serif; - font-size: 100%; - font-weight: bold; + color: #ffffff; + font-family: Arial, Helvetica, sans-serif; + font-size: 100%; + font-weight: bold; } .OT_root header { @@ -62,11 +61,11 @@ } .OT_root strong { - font-weight: bold; + font-weight: bold; } .OT_root em { - font-style: italic; + font-style: italic; } .OT_root a, @@ -74,46 +73,46 @@ .OT_root a:visited, .OT_root a:hover, .OT_root a:active { - font-family: Arial, Helvetica, sans-serif; + font-family: Arial, Helvetica, sans-serif; } - .OT_root ul, .OT_root ol { - margin: 1em 1em 1em 2em; + margin: 1em 1em 1em 2em; } .OT_root ol { - list-style: decimal outside; + list-style: decimal outside; } .OT_root ul { - list-style: disc outside; + list-style: disc outside; } .OT_root dl { - margin: 4px; + margin: 4px; } - .OT_root dl dt, - .OT_root dl dd { - float: left; - margin: 0; - padding: 0; - } - .OT_root dl dt { - clear: left; - text-align: right; - width: 50px; - } - .OT_root dl dd { - margin-left: 10px; - } +.OT_root dl dt, +.OT_root dl dd { + float: left; + margin: 0; + padding: 0; +} + +.OT_root dl dt { + clear: left; + text-align: right; + width: 50px; +} + +.OT_root dl dd { + margin-left: 10px; +} .OT_root img { - border: 0 none; + border: 0 none; } - /* Modal dialog styles */ /* Modal dialog styles */ @@ -166,7 +165,6 @@ text-align: center; } - .OT_dialog-messages-main { margin-bottom: 36px; line-height: 36px; @@ -302,37 +300,34 @@ /* Publisher and Subscriber styles */ .OT_publisher, .OT_subscriber { - position: relative; - min-width: 48px; - min-height: 48px; + position: relative; + min-width: 48px; + min-height: 48px; } -.OT_publisher video, -.OT_subscriber video, -.OT_publisher object, -.OT_subscriber object { - width: 100%; - height: 100%; -} +.OT_publisher .OT_video-element, +.OT_subscriber .OT_video-element { + display: block; + position: absolute; + width: 100%; -.OT_publisher object, -.OT_subscriber object { + transform-origin: 0 0; } /* Styles that are applied when the video element should be mirrored */ -.OT_publisher.OT_mirrored video{ - -webkit-transform: scale(-1, 1); - -moz-transform:scale(-1,1); +.OT_publisher.OT_mirrored .OT_video-element { + transform: scale(-1, 1); + transform-origin: 50% 50%; } .OT_subscriber_error { - background-color: #000; - color: #fff; - text-align: center; + background-color: #000; + color: #fff; + text-align: center; } .OT_subscriber_error > p { - padding: 20px; + padding: 20px; } /* The publisher/subscriber name/mute background */ @@ -346,67 +341,67 @@ .OT_subscriber .OT_archiving-status, .OT_publihser .OT_archiving-light-box, .OT_subscriber .OT_archiving-light-box { - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - -ms-box-sizing: border-box; - box-sizing: border-box; - top: 0; - left: 0; - right: 0; - display: block; - height: 34px; - position: absolute; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -ms-box-sizing: border-box; + box-sizing: border-box; + top: 0; + left: 0; + right: 0; + display: block; + height: 34px; + position: absolute; } .OT_publisher .OT_bar, .OT_subscriber .OT_bar { - background: rgba(0, 0, 0, 0.4); + background: rgba(0, 0, 0, 0.4); } .OT_publisher .OT_edge-bar-item, .OT_subscriber .OT_edge-bar-item { - z-index: 1; /* required to get audio level meter underneath */ + z-index: 1; /* required to get audio level meter underneath */ } /* The publisher/subscriber name panel/archiving status bar */ .OT_publisher .OT_name, .OT_subscriber .OT_name { - background-color: transparent; - color: #ffffff; - font-size: 15px; - line-height: 34px; - font-weight: normal; - padding: 0 4px 0 36px; + background-color: transparent; + color: #ffffff; + font-size: 15px; + line-height: 34px; + font-weight: normal; + padding: 0 4px 0 36px; } .OT_publisher .OT_archiving-status, .OT_subscriber .OT_archiving-status { - background: rgba(0, 0, 0, 0.4); - top: auto; - bottom: 0; - left: 34px; - padding: 0 4px; - color: rgba(255,255,255,0.8); - font-size: 15px; - line-height: 34px; - font-weight: normal; + background: rgba(0, 0, 0, 0.4); + top: auto; + bottom: 0; + left: 34px; + padding: 0 4px; + color: rgba(255, 255, 255, 0.8); + font-size: 15px; + line-height: 34px; + font-weight: normal; } .OT_micro .OT_archiving-status, .OT_micro:hover .OT_archiving-status, .OT_mini .OT_archiving-status, .OT_mini:hover .OT_archiving-status { - display: none; + display: none; } .OT_publisher .OT_archiving-light-box, .OT_subscriber .OT_archiving-light-box { - background: rgba(0, 0, 0, 0.4); - top: auto; - bottom: 0; - right: auto; - width: 34px; - height: 34px; + background: rgba(0, 0, 0, 0.4); + top: auto; + bottom: 0; + right: auto; + width: 34px; + height: 34px; } .OT_archiving-light { @@ -423,6 +418,7 @@ -moz-box-shadow: 0 0 5px 1px #575757; box-shadow: 0 0 5px 1px #575757; } + .OT_archiving-light.OT_active { background-color: #970d13; -webkit-animation: OT_pulse 1.3s ease-in; @@ -432,6 +428,7 @@ -moz-animation-iteration-count: infinite; -webkit-animation-iteration-count: infinite; } + @-moz-keyframes OT_pulse { 0% { -webkit-box-shadow: 0 0 0px 0px #c70019; @@ -463,6 +460,7 @@ box-shadow: 0 0 0px 0px #c70019; } } + @-webkit-keyframes OT_pulse { 0% { -webkit-box-shadow: 0 0 0px 0px #c70019; @@ -494,6 +492,7 @@ box-shadow: 0 0 0px 0px #c70019; } } + @-o-keyframes OT_pulse { 0% { -webkit-box-shadow: 0 0 0px 0px #c70019; @@ -525,6 +524,7 @@ box-shadow: 0 0 0px 0px #c70019; } } + @-ms-keyframes OT_pulse { 0% { -webkit-box-shadow: 0 0 0px 0px #c70019; @@ -556,6 +556,7 @@ box-shadow: 0 0 0px 0px #c70019; } } + @-webkit-keyframes OT_pulse { 0% { -webkit-box-shadow: 0 0 0px 0px #c70019; @@ -591,15 +592,15 @@ .OT_mini .OT_bar, .OT_bar.OT_mode-mini, .OT_bar.OT_mode-mini-auto { - bottom: 0; - height: auto; + bottom: 0; + height: auto; } .OT_mini .OT_name.OT_mode-off, .OT_mini .OT_name.OT_mode-on, .OT_mini .OT_name.OT_mode-auto, .OT_mini:hover .OT_name.OT_mode-auto { - display: none; + display: none; } .OT_publisher .OT_name, @@ -624,42 +625,42 @@ .OT_publisher .OT_mute, .OT_subscriber .OT_mute { - right: 0; - top: 0; - border-left: 1px solid rgba(255, 255, 255, 0.2); - height: 36px; - width: 37px; + right: 0; + top: 0; + border-left: 1px solid rgba(255, 255, 255, 0.2); + height: 36px; + width: 37px; } .OT_mini .OT_mute, -.OT_mute.OT_mode-mini, -.OT_mute.OT_mode-mini-auto { - top: 50%; - left: 50%; - right: auto; - margin-top: -18px; - margin-left: -18.5px; - border-left: none; +.OT_publisher.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold, +.OT_subscriber.OT_mini .OT_mute.OT_mode-auto.OT_mode-on-hold { + top: 50%; + left: 50%; + right: auto; + margin-top: -18px; + margin-left: -18.5px; + border-left: none; } .OT_publisher .OT_mute { - background-image: url(../images/rtc/mic-on.png); - background-position: 9px 5px; + background-image: url(../images/rtc/mic-on.png); + background-position: 9px 5px; } .OT_publisher .OT_mute.OT_active { - background-image: url(../images/rtc/mic-off.png); - background-position: 9px 4px; + background-image: url(../images/rtc/mic-off.png); + background-position: 9px 4px; } .OT_subscriber .OT_mute { - background-image: url(../images/rtc/speaker-on.png); - background-position: 8px 7px; + background-image: url(../images/rtc/speaker-on.png); + background-position: 8px 7px; } .OT_subscriber .OT_mute.OT_active { - background-image: url(../images/rtc/speaker-off.png); - background-position: 7px 7px; + background-image: url(../images/rtc/speaker-off.png); + background-position: 7px 7px; } /** @@ -672,17 +673,9 @@ /* Default display mode transitions for various chrome elements */ .OT_publisher .OT_edge-bar-item, .OT_subscriber .OT_edge-bar-item { - -ms-transition-property: top, bottom, opacity; - -ms-transition-duration: 0.5s; - -moz-transition-property: top, bottom, opacity; - -moz-transition-duration: 0.5s; - -webkit-transition-property: top, bottom, opacity; - -webkit-transition-duration: 0.5s; - -o-transition-property: top, bottom, opacity; - -o-transition-duration: 0.5s; - transition-property: top, bottom, opacity; - transition-duration: 0.5s; - transition-timing-function: ease-in; + transition-property: top, bottom, opacity; + transition-duration: 0.5s; + transition-timing-function: ease-in; } .OT_publisher .OT_edge-bar-item.OT_mode-off, @@ -691,14 +684,14 @@ .OT_subscriber .OT_edge-bar-item.OT_mode-auto, .OT_publisher .OT_edge-bar-item.OT_mode-mini-auto, .OT_subscriber .OT_edge-bar-item.OT_mode-mini-auto { - top: -25px; - opacity: 0; + top: -25px; + opacity: 0; } .OT_mini .OT_mute.OT_mode-auto, .OT_publisher .OT_mute.OT_mode-mini-auto, .OT_subscriber .OT_mute.OT_mode-mini-auto { - top: 50%; + top: 50%; } .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-off, @@ -707,8 +700,8 @@ .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto, .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto, .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-mini-auto { - top: auto; - bottom: -25px; + top: auto; + bottom: -25px; } .OT_publisher .OT_edge-bar-item.OT_mode-on, @@ -719,127 +712,148 @@ .OT_subscriber:hover .OT_edge-bar-item.OT_mode-auto, .OT_publisher:hover .OT_edge-bar-item.OT_mode-mini-auto, .OT_subscriber:hover .OT_edge-bar-item.OT_mode-mini-auto { - top: 0; - opacity: 1; + top: 0; + opacity: 1; } .OT_mini .OT_mute.OT_mode-on, .OT_mini:hover .OT_mute.OT_mode-auto, .OT_mute.OT_mode-mini, -.OT_root:hover .OT_mute.OT_mode-mini-auto { - top: 50%; +.OT_root:hover .OT_mute.OT_mode-mini-auto { + top: 50%; } .OT_publisher .OT_edge-bar-item.OT_edge-bottom.OT_mode-on, .OT_subscriber .OT_edge-bar-item.OT_edge-bottom.OT_mode-on, .OT_publisher:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto, .OT_subscriber:hover .OT_edge-bar-item.OT_edge-bottom.OT_mode-auto { - top: auto; - bottom: 0; - opacity: 1; + top: auto; + bottom: 0; + opacity: 1; } + /* Contains the video element, used to fix video letter-boxing */ -.OT_video-container { - position: absolute; - background-color: #000000; - overflow: hidden; +.OT_widget-container { + position: absolute; + background-color: #000000; + overflow: hidden; } .OT_hidden-audio { - position: absolute !important; - height: 1px !important; - width: 1px !important; + position: absolute !important; + height: 1px !important; + width: 1px !important; } /* Load animation */ .OT_root .OT_video-loading { - background: url('../images/rtc/loader.gif') no-repeat; - display:none; - position: absolute; - width: 32px; - height: 32px; - left: 50%; - top: 50%; - margin-left: -16px; - margin-top: -16px; + background: url('../images/rtc/loader.gif') no-repeat; + display: none; + position: absolute; + width: 32px; + height: 32px; + left: 50%; + top: 50%; + margin-left: -16px; + margin-top: -16px; } .OT_publisher.OT_loading .OT_video-loading, .OT_subscriber.OT_loading .OT_video-loading { - display: block; + display: block; } -.OT_publisher.OT_loading video, -.OT_subscriber.OT_loading video, -.OT_publisher.OT_loading object, -.OT_subscriber.OT_loading object { - display: none; +.OT_publisher.OT_loading .OT_video-element, +.OT_subscriber.OT_loading .OT_video-element { + /*display: none;*/ } +.OT_video-centering { + display: table; + width: 100%; + height: 100%; +} + +.OT_video-container { + display: table-cell; + vertical-align: middle; +} .OT_video-poster { - width: 100%; - height: 100%; - display: none; + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + display: none; - opacity: .25; - background-size: auto 76%; - background-repeat: no-repeat; - background-position: center bottom; - background-image: url(../images/rtc/audioonly-silhouette.svg); + opacity: .25; + + background-repeat: no-repeat; + background-image: url(../images/rtc/audioonly-silhouette.svg); +} + + +.OT_fit-mode-cover .OT_video-poster { + background-size: auto 76%; + background-position: center bottom; +} + +.OT_fit-mode-contain .OT_video-poster { + background-size: contain; + background-position: center; } .OT_audio-level-meter { - position: absolute; - width: 25%; - max-width: 224px; - min-width: 21px; - top: 0; - right: 0; - overflow: hidden; + position: absolute; + width: 25%; + max-width: 224px; + min-width: 21px; + top: 0; + right: 0; + overflow: hidden; } .OT_audio-level-meter:before { - /* makes the height of the container equals its width */ - content: ''; - display: block; - padding-top: 100%; + /* makes the height of the container equals its width */ + content: ''; + display: block; + padding-top: 100%; } .OT_audio-level-meter__bar { - position: absolute; - width: 192%; /* meter value can overflow of 8% */ - height: 192%; - top: -96% /* half of the size */; - right: -96%; - border-radius: 50%; + position: absolute; + width: 192%; /* meter value can overflow of 8% */ + height: 192%; + top: -96% /* half of the size */; + right: -96%; + border-radius: 50%; - background-color: rgba(0, 0, 0, .8); + background-color: rgba(0, 0, 0, .8); } .OT_audio-level-meter__audio-only-img { - position: absolute; - top: 22%; - right: 15%; - width: 40%; + position: absolute; + top: 22%; + right: 15%; + width: 40%; - opacity: .7; + opacity: .7; - background: url(../images/rtc/audioonly-headset.svg) no-repeat center; + background: url(../images/rtc/audioonly-headset.svg) no-repeat center; } .OT_audio-level-meter__audio-only-img:before { - /* makes the height of the container equals its width */ - content: ''; - display: block; - padding-top: 100%; + /* makes the height of the container equals its width */ + content: ''; + display: block; + padding-top: 100%; } .OT_audio-level-meter__value { - position: absolute; - border-radius: 50%; - background-image: radial-gradient(circle, rgba(151,206,0,1) 0%, rgba(151,206,0,0) 100%); + position: absolute; + border-radius: 50%; + background-image: radial-gradient(circle, rgba(151, 206, 0, 1) 0%, rgba(151, 206, 0, 0) 100%); } .OT_audio-level-meter.OT_mode-off { @@ -848,31 +862,31 @@ .OT_audio-level-meter.OT_mode-on, .OT_audio-only .OT_audio-level-meter.OT_mode-auto { - display: block; + display: block; } .OT_video-disabled-indicator { - opacity: 1; - border: none; - display: none; - position: absolute; - background-color: transparent; - background-repeat: no-repeat; - background-position:bottom right; - top: 0; - left: 0; - bottom: 3px; - right: 3px; + opacity: 1; + border: none; + display: none; + position: absolute; + background-color: transparent; + background-repeat: no-repeat; + background-position: bottom right; + top: 0; + left: 0; + bottom: 3px; + right: 3px; } .OT_video-disabled { - background-image: url(../images/rtc/video-disabled.png); + background-image: url(../images/rtc/video-disabled.png); } .OT_video-disabled-warning { - background-image: url(../images/rtc/video-disabled-warning.png); + background-image: url(../images/rtc/video-disabled-warning.png); } .OT_video-disabled-indicator.OT_active { - display: block; + display: block; } diff --git a/browser/components/loop/content/shared/libs/sdk-content/js/dynamic_config.min.js b/browser/components/loop/content/shared/libs/sdk-content/js/dynamic_config.min.js index e03661d4bec0..fffdb8736cdd 100644 --- a/browser/components/loop/content/shared/libs/sdk-content/js/dynamic_config.min.js +++ b/browser/components/loop/content/shared/libs/sdk-content/js/dynamic_config.min.js @@ -1,7 +1,7 @@ -/* - - Copyright (c) 2014 TokBox, Inc. - Released under the MIT license - http://opensource.org/licenses/MIT -*/ +/** + * @license + * Copyright (c) 2014 TokBox, Inc. + * Released under the MIT license + * http://opensource.org/licenses/MIT + */ !function(){TB.Config.replaceWith({global:{exceptionLogging:{enabled:!0,messageLimitPerPartner:100},iceServers:{enabled:!1},instrumentation:{enabled:!1,debugging:!1},tokshow:{textchat:!0}},partners:{change878:{instrumentation:{enabled:!0,debugging:!0}}}})}(TB); \ No newline at end of file diff --git a/browser/components/loop/content/shared/libs/sdk.js b/browser/components/loop/content/shared/libs/sdk.js index a3a701744cb3..531ed773f1da 100755 --- a/browser/components/loop/content/shared/libs/sdk.js +++ b/browser/components/loop/content/shared/libs/sdk.js @@ -1,66 +1,30 @@ /** - * @license OpenTok JavaScript Library v2.2.9.7 + * @license OpenTok JavaScript Library v2.4.0 54ae164 HEAD * http://www.tokbox.com/ * * Copyright (c) 2014 TokBox, Inc. * Released under the MIT license * http://opensource.org/licenses/MIT * - * Date: January 26 03:18:02 2015 + * Date: January 08 08:54:40 2015 */ -(function(window) { - if (!window.OT) window.OT = {}; - OT.properties = { - version: 'v2.2.9.7', // The current version (eg. v2.0.4) (This is replaced by gradle) - build: '59e99bc', // The current build hash (This is replaced by gradle) +!(function(window) { - // Whether or not to turn on debug logging by default - debug: 'false', - // The URL of the tokbox website - websiteURL: 'http://www.tokbox.com', +!(function(window, OTHelpers, undefined) { - // The URL of the CDN - cdnURL: 'http://static.opentok.com', - // The URL to use for logging - loggingURL: 'http://hlg.tokbox.com/prod', - // The anvil API URL - apiURL: 'http://anvil.opentok.com', - - // What protocol to use when connecting to the rumor web socket - messagingProtocol: 'wss', - // What port to use when connection to the rumor web socket - messagingPort: 443, - - // If this environment supports SSL - supportSSL: 'true', - // The CDN to use if we're using SSL - cdnURLSSL: 'https://static.opentok.com', - // The URL to use for logging - loggingURLSSL: 'https://hlg.tokbox.com/prod', - // The anvil API URL to use if we're using SSL - apiURLSSL: 'https://anvil.opentok.com', - - minimumVersion: { - firefox: parseFloat('29'), - chrome: parseFloat('34') - } - }; - -})(window); /** - * @license Common JS Helpers on OpenTok 0.2.0 3fa583f master + * @license Common JS Helpers on OpenTok 0.2.0 ef06638 2014Q4-2.2 * http://www.tokbox.com/ * - * Copyright (c) 2014 TokBox, Inc. - * Released under the MIT license - * http://opensource.org/licenses/MIT + * Copyright (c) 2015 TokBox, Inc. * - * Date: August 08 12:31:42 2014 + * Date: January 08 08:54:29 2015 * */ + // OT Helper Methods // // helpers.js <- the root file @@ -68,182 +32,359 @@ // (i.e. video, dom, etc) // // @example Getting a DOM element by it's id -// var element = OTHelpers('domId'); +// var element = OTHelpers('#domId'); // // /*jshint browser:true, smarttabs:true*/ -!(function(window, undefined) { +// Short cuts to commonly accessed prototypes +var prototypeSlice = Array.prototype.slice; - var OTHelpers = function(domId) { - return document.getElementById(domId); +// RegEx to detect CSS Id selectors +var detectIdSelectors = /^#([\w-]*)$/; + +// The top-level namespace, also performs basic DOMElement selecting. +// +// @example Get the DOM element with the id of 'domId' +// OTHelpers('#domId') +// +// @example Get all video elements +// OTHelpers('video') +// +// @example Get all elements with the class name of 'foo' +// OTHelpers('.foo') +// +// @example Get all elements with the class name of 'foo', +// and do something with the first. +// var collection = OTHelpers('.foo'); +// console.log(collection.first); +// +// +// The second argument is the context, that is document or parent Element, to +// select from. +// +// @example Get a video element within the element with the id of 'domId' +// OTHelpers('video', OTHelpers('#domId')) +// +// +// +// OTHelpers will accept any of the following and return a collection: +// OTHelpers() +// OTHelpers('css selector', optionalParentNode) +// OTHelpers(DomNode) +// OTHelpers([array of DomNode]) +// +// The collection is a ElementCollection object, see the ElementCollection docs for usage info. +// +var OTHelpers = function(selector, context) { + var results = []; + + if (typeof(selector) === 'string') { + var idSelector = detectIdSelectors.exec(selector); + context = context || document; + + if (idSelector && idSelector[1]) { + var element = context.getElementById(idSelector[1]); + if (element) results.push(element); + } + else { + results = context.querySelectorAll(selector); + } + } + else if (selector.nodeType || window.XMLHttpRequest && selector instanceof XMLHttpRequest) { + // allow OTHelpers(DOMNode) and OTHelpers(xmlHttpRequest) + results = [selector]; + context = selector; + } + else if (OTHelpers.isArray(selector)) { + results = selector.slice(); + context = null; + } + + return new ElementCollection(results, context); +}; + +// alias $ to OTHelpers for internal use. This is not exposed outside of OTHelpers. +var $ = OTHelpers; + +// A helper for converting a NodeList to a JS Array +var nodeListToArray = function nodeListToArray (nodes) { + if ($.env.name !== 'IE' || $.env.version > 9) { + return prototypeSlice.call(nodes); + } + + // IE 9 and below call use Array.prototype.slice.call against + // a NodeList, hence the following + var array = []; + + for (var i=0, num=nodes.length; i= length) { - return -1; - } + if (!this) { + throw new TypeError(); + } - if (pivot < 0) { - pivot = length - Math.abs(pivot); - } + length = this.length; - for (i = pivot; i < length; i++) { - if (this[i] === searchElement) { - return i; - } - } + if (length === 0 || pivot >= length) { return -1; - }; + } - OTHelpers.arrayIndexOf = function(array, searchElement, fromIndex) { - return _indexOf.call(array, searchElement, fromIndex); - }; + if (pivot < 0) { + pivot = length - Math.abs(pivot); + } - var _bind = Function.prototype.bind || function() { - var args = Array.prototype.slice.call(arguments), - ctx = args.shift(), - fn = this; - return function() { - return fn.apply(ctx, args.concat(Array.prototype.slice.call(arguments))); - }; - }; + for (i = pivot; i < length; i++) { + if (this[i] === searchElement) { + return i; + } + } + return -1; +}; - OTHelpers.bind = function() { - var args = Array.prototype.slice.call(arguments), - fn = args.shift(); - return _bind.apply(fn, args); - }; +OTHelpers.arrayIndexOf = function(array, searchElement, fromIndex) { + return _indexOf.call(array, searchElement, fromIndex); +}; - var _trim = String.prototype.trim || function() { - return this.replace(/^\s+|\s+$/g, ''); +var _bind = Function.prototype.bind || function() { + var args = prototypeSlice.call(arguments), + ctx = args.shift(), + fn = this; + return function() { + return fn.apply(ctx, args.concat(prototypeSlice.call(arguments))); }; +}; - OTHelpers.trim = function(str) { - return _trim.call(str); - }; +OTHelpers.bind = function() { + var args = prototypeSlice.call(arguments), + fn = args.shift(); + return _bind.apply(fn, args); +}; +var _trim = String.prototype.trim || function() { + return this.replace(/^\s+|\s+$/g, ''); +}; + +OTHelpers.trim = function(str) { + return _trim.call(str); +}; + +OTHelpers.noConflict = function() { OTHelpers.noConflict = function() { - OTHelpers.noConflict = function() { - return OTHelpers; - }; - window.OTHelpers = previousOTHelpers; return OTHelpers; }; + window.OTHelpers = previousOTHelpers; + return OTHelpers; +}; - OTHelpers.isNone = function(obj) { - return obj === undefined || obj === null; +OTHelpers.isNone = function(obj) { + return obj === void 0 || obj === null; +}; + +OTHelpers.isObject = function(obj) { + return obj === Object(obj); +}; + +OTHelpers.isFunction = function(obj) { + return !!obj && (obj.toString().indexOf('()') !== -1 || + Object.prototype.toString.call(obj) === '[object Function]'); +}; + +OTHelpers.isArray = OTHelpers.isFunction(Array.isArray) && Array.isArray || + function (vArg) { + return Object.prototype.toString.call(vArg) === '[object Array]'; }; - OTHelpers.isObject = function(obj) { - return obj === Object(obj); - }; +OTHelpers.isEmpty = function(obj) { + if (obj === null || obj === void 0) return true; + if (OTHelpers.isArray(obj) || typeof(obj) === 'string') return obj.length === 0; - OTHelpers.isFunction = function(obj) { - return !!obj && (obj.toString().indexOf('()') !== -1 || - Object.prototype.toString.call(obj) === '[object Function]'); - }; + // Objects without enumerable owned properties are empty. + for (var key in obj) { + if (obj.hasOwnProperty(key)) return false; + } - OTHelpers.isArray = OTHelpers.isFunction(Array.isArray) && Array.isArray || - function (vArg) { - return Object.prototype.toString.call(vArg) === '[object Array]'; - }; - - OTHelpers.isEmpty = function(obj) { - if (obj === null || obj === undefined) return true; - if (OTHelpers.isArray(obj) || typeof(obj) === 'string') return obj.length === 0; - - // Objects without enumerable owned properties are empty. - for (var key in obj) { - if (obj.hasOwnProperty(key)) return false; - } - - return true; - }; + return true; +}; // Extend a target object with the properties from one or // more source objects @@ -251,18 +392,18 @@ // @example: // dest = OTHelpers.extend(dest, source1, source2, source3); // - OTHelpers.extend = function(/* dest, source1[, source2, ..., , sourceN]*/) { - var sources = Array.prototype.slice.call(arguments), - dest = sources.shift(); +OTHelpers.extend = function(/* dest, source1[, source2, ..., , sourceN]*/) { + var sources = prototypeSlice.call(arguments), + dest = sources.shift(); - OTHelpers.forEach(sources, function(source) { - for (var key in source) { - dest[key] = source[key]; - } - }); + OTHelpers.forEach(sources, function(source) { + for (var key in source) { + dest[key] = source[key]; + } + }); - return dest; - }; + return dest; +}; // Ensures that the target object contains certain defaults. // @@ -271,28 +412,28 @@ // loading: true // loading by default // }); // - OTHelpers.defaults = function(/* dest, defaults1[, defaults2, ..., , defaultsN]*/) { - var sources = Array.prototype.slice.call(arguments), - dest = sources.shift(); +OTHelpers.defaults = function(/* dest, defaults1[, defaults2, ..., , defaultsN]*/) { + var sources = prototypeSlice.call(arguments), + dest = sources.shift(); - OTHelpers.forEach(sources, function(source) { - for (var key in source) { - if (dest[key] === void 0) dest[key] = source[key]; - } - }); + OTHelpers.forEach(sources, function(source) { + for (var key in source) { + if (dest[key] === void 0) dest[key] = source[key]; + } + }); - return dest; - }; + return dest; +}; - OTHelpers.clone = function(obj) { - if (!OTHelpers.isObject(obj)) return obj; - return OTHelpers.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj); - }; +OTHelpers.clone = function(obj) { + if (!OTHelpers.isObject(obj)) return obj; + return OTHelpers.isArray(obj) ? obj.slice() : OTHelpers.extend({}, obj); +}; // Handy do nothing function - OTHelpers.noop = function() {}; +OTHelpers.noop = function() {}; // Returns the number of millisceonds since the the UNIX epoch, this is functionally @@ -300,102 +441,32 @@ // // Where available, we use 'performance.now' which is more accurate and reliable, // otherwise we default to new Date().getTime(). - OTHelpers.now = (function() { - var performance = window.performance || {}, - navigationStart, - now = performance.now || - performance.mozNow || - performance.msNow || - performance.oNow || - performance.webkitNow; +OTHelpers.now = (function() { + var performance = window.performance || {}, + navigationStart, + now = performance.now || + performance.mozNow || + performance.msNow || + performance.oNow || + performance.webkitNow; - if (now) { - now = OTHelpers.bind(now, performance); - navigationStart = performance.timing.navigationStart; + if (now) { + now = OTHelpers.bind(now, performance); + navigationStart = performance.timing.navigationStart; - return function() { return navigationStart + now(); }; - } else { - return function() { return new Date().getTime(); }; - } - })(); - - var _browser = function() { - var userAgent = window.navigator.userAgent.toLowerCase(), - appName = window.navigator.appName, - navigatorVendor, - browser = 'unknown', - version = -1; - - if (userAgent.indexOf('opera') > -1 || userAgent.indexOf('opr') > -1) { - browser = 'Opera'; - - if (/opr\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { - version = parseFloat( RegExp.$1 ); - } - - } else if (userAgent.indexOf('firefox') > -1) { - browser = 'Firefox'; - - if (/firefox\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { - version = parseFloat( RegExp.$1 ); - } - - } else if (appName === 'Microsoft Internet Explorer') { - // IE 10 and below - browser = 'IE'; - - if (/msie ([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { - version = parseFloat( RegExp.$1 ); - } - - } else if (appName === 'Netscape' && userAgent.indexOf('trident') > -1) { - // IE 11+ - - browser = 'IE'; - - if (/trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { - version = parseFloat( RegExp.$1 ); - } - - } else if (userAgent.indexOf('chrome') > -1) { - browser = 'Chrome'; - - if (/chrome\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { - version = parseFloat( RegExp.$1 ); - } - - } else if ((navigatorVendor = window.navigator.vendor) && - navigatorVendor.toLowerCase().indexOf('apple') > -1) { - browser = 'Safari'; - - if (/version\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { - version = parseFloat( RegExp.$1 ); - } - } - - return { - browser: browser, - version: version, - iframeNeedsLoad: userAgent.indexOf('webkit') < 0 - }; - }(); - - OTHelpers.browser = function() { - return _browser.browser; - }; - - OTHelpers.browserVersion = function() { - return _browser; - }; - - - OTHelpers.canDefineProperty = true; - - try { - Object.defineProperty({}, 'x', {}); - } catch (err) { - OTHelpers.canDefineProperty = false; + return function() { return navigationStart + now(); }; + } else { + return function() { return new Date().getTime(); }; } +})(); + +OTHelpers.canDefineProperty = true; + +try { + Object.defineProperty({}, 'x', {}); +} catch (err) { + OTHelpers.canDefineProperty = false; +} // A helper for defining a number of getters at once. // @@ -409,105 +480,64 @@ // id: function() { return _sessionId; } // }); // - OTHelpers.defineGetters = function(self, getters, enumerable) { - var propsDefinition = {}; +OTHelpers.defineGetters = function(self, getters, enumerable) { + var propsDefinition = {}; - if (enumerable === void 0) enumerable = false; + if (enumerable === void 0) enumerable = false; - for (var key in getters) { - propsDefinition[key] = { - get: getters[key], - enumerable: enumerable - }; - } + for (var key in getters) { + propsDefinition[key] = { + get: getters[key], + enumerable: enumerable + }; + } - OTHelpers.defineProperties(self, propsDefinition); - }; + OTHelpers.defineProperties(self, propsDefinition); +}; - var generatePropertyFunction = function(object, getter, setter) { - if(getter && !setter) { - return function() { - return getter.call(object); - }; - } else if(getter && setter) { - return function(value) { - if(value !== void 0) { - setter.call(object, value); - } - return getter.call(object); - }; - } else { - return function(value) { - if(value !== void 0) { - setter.call(object, value); - } - }; - } - }; +var generatePropertyFunction = function(object, getter, setter) { + if(getter && !setter) { + return function() { + return getter.call(object); + }; + } else if(getter && setter) { + return function(value) { + if(value !== void 0) { + setter.call(object, value); + } + return getter.call(object); + }; + } else { + return function(value) { + if(value !== void 0) { + setter.call(object, value); + } + }; + } +}; - OTHelpers.defineProperties = function(object, getterSetters) { - for (var key in getterSetters) { - object[key] = generatePropertyFunction(object, getterSetters[key].get, - getterSetters[key].set); - } - }; +OTHelpers.defineProperties = function(object, getterSetters) { + for (var key in getterSetters) { + object[key] = generatePropertyFunction(object, getterSetters[key].get, + getterSetters[key].set); + } +}; // Polyfill Object.create for IE8 // // See https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create // - if (!Object.create) { - Object.create = function (o) { - if (arguments.length > 1) { - throw new Error('Object.create implementation only accepts the first parameter.'); - } - function F() {} - F.prototype = o; - return new F(); - }; - } - - OTHelpers.setCookie = function(key, value) { - try { - localStorage.setItem(key, value); - } catch (err) { - // Store in browser cookie - var date = new Date(); - date.setTime(date.getTime()+(365*24*60*60*1000)); - var expires = '; expires=' + date.toGMTString(); - document.cookie = key + '=' + value + expires + '; path=/'; +if (!Object.create) { + Object.create = function (o) { + if (arguments.length > 1) { + throw new Error('Object.create implementation only accepts the first parameter.'); } + function F() {} + F.prototype = o; + return new F(); }; - - OTHelpers.getCookie = function(key) { - var value; - - try { - value = localStorage.getItem('opentok_client_id'); - return value; - } catch (err) { - // Check browser cookies - var nameEQ = key + '='; - var ca = document.cookie.split(';'); - for(var i=0;i < ca.length;i++) { - var c = ca[i]; - while (c.charAt(0) === ' ') { - c = c.substring(1,c.length); - } - if (c.indexOf(nameEQ) === 0) { - value = c.substring(nameEQ.length,c.length); - } - } - - if (value) { - return value; - } - } - - return null; - }; - +} // These next bits are included from Underscore.js. The original copyright // notice is below. @@ -517,191 +547,65 @@ // (c) 2011-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. - // Invert the keys and values of an object. The values must be serializable. - OTHelpers.invert = function(obj) { - var result = {}; - for (var key in obj) if (obj.hasOwnProperty(key)) result[obj[key]] = key; - return result; - }; - - - // List of HTML entities for escaping. - var entityMap = { - escape: { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/' - } - }; - - entityMap.unescape = OTHelpers.invert(entityMap.escape); - - // Regexes containing the keys and values listed immediately above. - var entityRegexes = { - escape: new RegExp('[' + OTHelpers.keys(entityMap.escape).join('') + ']', 'g'), - unescape: new RegExp('(' + OTHelpers.keys(entityMap.unescape).join('|') + ')', 'g') - }; - - // Functions for escaping and unescaping strings to/from HTML interpolation. - OTHelpers.forEach(['escape', 'unescape'], function(method) { - OTHelpers[method] = function(string) { - if (string === null || string === undefined) return ''; - return ('' + string).replace(entityRegexes[method], function(match) { - return entityMap[method][match]; - }); - }; - }); - -// By default, Underscore uses ERB-style template delimiters, change the -// following template settings to use alternative delimiters. - OTHelpers.templateSettings = { - evaluate : /<%([\s\S]+?)%>/g, - interpolate : /<%=([\s\S]+?)%>/g, - escape : /<%-([\s\S]+?)%>/g - }; - -// When customizing `templateSettings`, if you don't want to define an -// interpolation, evaluation or escaping regex, we need one that is -// guaranteed not to match. - var noMatch = /(.)^/; - -// Certain characters need to be escaped so that they can be put into a -// string literal. - var escapes = { - '\'': '\'', - '\\': '\\', - '\r': 'r', - '\n': 'n', - '\t': 't', - '\u2028': 'u2028', - '\u2029': 'u2029' - }; - - var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; - -// JavaScript micro-templating, similar to John Resig's implementation. -// Underscore templating handles arbitrary delimiters, preserves whitespace, -// and correctly escapes quotes within interpolated code. - OTHelpers.template = function(text, data, settings) { - var render; - settings = OTHelpers.defaults({}, settings, OTHelpers.templateSettings); - - // Combine delimiters into one regular expression via alternation. - var matcher = new RegExp([ - (settings.escape || noMatch).source, - (settings.interpolate || noMatch).source, - (settings.evaluate || noMatch).source - ].join('|') + '|$', 'g'); - - // Compile the template source, escaping string literals appropriately. - var index = 0; - var source = '__p+=\''; - text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { - source += text.slice(index, offset) - .replace(escaper, function(match) { return '\\' + escapes[match]; }); - - if (escape) { - source += '\'+\n((__t=(' + escape + '))==null?\'\':OTHelpers.escape(__t))+\n\''; - } - if (interpolate) { - source += '\'+\n((__t=(' + interpolate + '))==null?\'\':__t)+\n\''; - } - if (evaluate) { - source += '\';\n' + evaluate + '\n__p+=\''; - } - index = offset + match.length; - return match; - }); - source += '\';\n'; - - // If a variable is not specified, place data values in local scope. - if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; - - source = 'var __t,__p=\'\',__j=Array.prototype.join,' + - 'print=function(){__p+=__j.call(arguments,\'\');};\n' + - source + 'return __p;\n'; - - try { - // evil is necessary for the new Function line - /*jshint evil:true */ - render = new Function(settings.variable || 'obj', source); - } catch (e) { - e.source = source; - throw e; - } - - if (data) return render(data); - var template = function(data) { - return render.call(this, data); - }; - - // Provide the compiled function source as a convenience for precompilation. - template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; - - return template; - }; - -})(window); +// Invert the keys and values of an object. The values must be serializable. +OTHelpers.invert = function(obj) { + var result = {}; + for (var key in obj) if (obj.hasOwnProperty(key)) result[obj[key]] = key; + return result; +}; /*jshint browser:true, smarttabs:true*/ // tb_require('../../helpers.js') -(function(window, OTHelpers, undefined) { +OTHelpers.statable = function(self, possibleStates, initialState, stateChanged, + stateChangedFailed) { + var previousState, + currentState = self.currentState = initialState; - OTHelpers.statable = function(self, possibleStates, initialState, stateChanged, - stateChangedFailed) { - var previousState, - currentState = self.currentState = initialState; - - var setState = function(state) { - if (currentState !== state) { - if (OTHelpers.arrayIndexOf(possibleStates, state) === -1) { - if (stateChangedFailed && OTHelpers.isFunction(stateChangedFailed)) { - stateChangedFailed('invalidState', state); - } - return; + var setState = function(state) { + if (currentState !== state) { + if (OTHelpers.arrayIndexOf(possibleStates, state) === -1) { + if (stateChangedFailed && OTHelpers.isFunction(stateChangedFailed)) { + stateChangedFailed('invalidState', state); } - - self.previousState = previousState = currentState; - self.currentState = currentState = state; - if (stateChanged && OTHelpers.isFunction(stateChanged)) stateChanged(state, previousState); + return; } - }; - - // Returns a number of states and returns true if the current state - // is any of them. - // - // @example - // if (this.is('connecting', 'connected')) { - // // do some stuff - // } - // - self.is = function (/* state0:String, state1:String, ..., stateN:String */) { - return OTHelpers.arrayIndexOf(arguments, currentState) !== -1; - }; - - - // Returns a number of states and returns true if the current state - // is none of them. - // - // @example - // if (this.isNot('connecting', 'connected')) { - // // do some stuff - // } - // - self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) { - return OTHelpers.arrayIndexOf(arguments, currentState) === -1; - }; - - return setState; + self.previousState = previousState = currentState; + self.currentState = currentState = state; + if (stateChanged && OTHelpers.isFunction(stateChanged)) stateChanged(state, previousState); + } }; -})(window, window.OTHelpers); + + // Returns a number of states and returns true if the current state + // is any of them. + // + // @example + // if (this.is('connecting', 'connected')) { + // // do some stuff + // } + // + self.is = function (/* state0:String, state1:String, ..., stateN:String */) { + return OTHelpers.arrayIndexOf(arguments, currentState) !== -1; + }; + + + // Returns a number of states and returns true if the current state + // is none of them. + // + // @example + // if (this.isNot('connecting', 'connected')) { + // // do some stuff + // } + // + self.isNot = function (/* state0:String, state1:String, ..., stateN:String */) { + return OTHelpers.arrayIndexOf(arguments, currentState) === -1; + }; + + return setState; +}; /*! * This is a modified version of Robert Kieffer awesome uuid.js library. @@ -720,7 +624,7 @@ /*global crypto:true, Uint32Array:true, Buffer:true */ /*jshint browser:true, smarttabs:true*/ -(function(window, OTHelpers, undefined) { +(function() { // Unique ID creation requires a high quality random # generator, but // Math.random() does not guarantee "cryptographic quality". So we feature @@ -843,202 +747,795 @@ OTHelpers.uuid = uuid; -}(window, window.OTHelpers)); -/*jshint browser:true, smarttabs:true*/ +}()); +/*jshint browser:true, smarttabs:true */ // tb_require('../helpers.js') -(function(window, OTHelpers, undefined) { - OTHelpers.useLogHelpers = function(on){ +var getErrorLocation; - // Log levels for OTLog.setLogLevel - on.DEBUG = 5; - on.LOG = 4; - on.INFO = 3; - on.WARN = 2; - on.ERROR = 1; - on.NONE = 0; +// Properties that we'll acknowledge from the JS Error object +var safeErrorProps = [ + 'description', + 'fileName', + 'lineNumber', + 'message', + 'name', + 'number', + 'stack' +]; - var _logLevel = on.NONE, - _logs = [], - _canApplyConsole = true; - try { - Function.prototype.bind.call(window.console.log, window.console); - } catch (err) { - _canApplyConsole = false; +// OTHelpers.Error +// +// A construct to contain error information that also helps with extracting error +// context, such as stack trace. +// +// @constructor +// @memberof OTHelpers +// @method Error +// +// @param {String} message +// Optional. The error message +// +// @param {Object} props +// Optional. A dictionary of properties containing extra Error info. +// +// +// @example Create a simple error with juts a custom message +// var error = new OTHelpers.Error('Something Broke!'); +// error.message === 'Something Broke!'; +// +// @example Create an Error with a message and a name +// var error = new OTHelpers.Error('Something Broke!', 'FooError'); +// error.message === 'Something Broke!'; +// error.name === 'FooError'; +// +// @example Create an Error with a message, name, and custom properties +// var error = new OTHelpers.Error('Something Broke!', 'FooError', { +// foo: 'bar', +// listOfImportantThings: [1,2,3,4] +// }); +// error.message === 'Something Broke!'; +// error.name === 'FooError'; +// error.foo === 'bar'; +// error.listOfImportantThings == [1,2,3,4]; +// +// @example Create an Error from a Javascript Error +// var error = new OTHelpers.Error(domSyntaxError); +// error.message === domSyntaxError.message; +// error.name === domSyntaxError.name === 'SyntaxError'; +// // ...continues for each properties of domSyntaxError +// +// @references +// * https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi +// * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Stack +// * http://www.w3.org/TR/dom/#interface-domerror +// +// +// @todo +// * update usage in OTMedia +// * replace error handling in OT.js +// * normalise stack behaviour under Chrome/Node/Safari with other browsers +// * unit test for stack parsing +// +// +OTHelpers.Error = function (message, name, props) { + switch (arguments.length) { + case 1: + if ($.isObject(message)) { + props = message; + name = void 0; + message = void 0; + } + // Otherwise it's the message + break; + + case 2: + if ($.isObject(name)) { + props = name; + name = void 0; + } + // Otherwise name is actually the name + + break; + } + + if ( props instanceof Error) { + // Special handling of this due to Chrome weirdness. It seems that + // properties of the Error object, and it's children, are not + // enumerable in Chrome? + for (var i = 0, num = safeErrorProps.length; i < num; ++i) { + this[safeErrorProps[i]] = props[safeErrorProps[i]]; + } + } + else if ( $.isObject(props)) { + // Use an custom properties that are provided + for (var key in props) { + if (props.hasOwnProperty(key)) { + this[key] = props[key]; + } + } + } + + // If any of the fundamental properties are missing then try and + // extract them. + if ( !(this.fileName && this.lineNumber && this.columnNumber && this.stack) ) { + var err = getErrorLocation(); + + if (!this.fileName && err.fileName) { + this.fileName = err.fileName; } - // Some objects can't be logged in the console, mostly these are certain - // types of native objects that are exposed to JS. This is only really a - // problem with IE, hence only the IE version does anything. - var makeLogArgumentsSafe = function(args) { return args; }; - - if (OTHelpers.browser() === 'IE') { - makeLogArgumentsSafe = function(args) { - return [toDebugString(Array.prototype.slice.apply(args))]; - }; + if (!this.lineNumber && err.lineNumber) { + this.lineNumber = err.lineNumber; } - // Generates a logging method for a particular method and log level. - // - // Attempts to handle the following cases: - // * the desired log method doesn't exist, call fallback (if available) instead - // * the console functionality isn't available because the developer tools (in IE) - // aren't open, call fallback (if available) - // * attempt to deal with weird IE hosted logging methods as best we can. - // - function generateLoggingMethod(method, level, fallback) { - return function() { - if (on.shouldLog(level)) { - var cons = window.console, - args = makeLogArgumentsSafe(arguments); - - // In IE, window.console may not exist if the developer tools aren't open - // This also means that cons and cons[method] can appear at any moment - // hence why we retest this every time. - if (cons && cons[method]) { - // the desired console method isn't a real object, which means - // that we can't use apply on it. We force it to be a real object - // using Function.bind, assuming that's available. - if (cons[method].apply || _canApplyConsole) { - if (!cons[method].apply) { - cons[method] = Function.prototype.bind.call(cons[method], cons); - } - - cons[method].apply(cons, args); - } - else { - // This isn't the same result as the above, but it's better - // than nothing. - cons[method](args); - } - } - else if (fallback) { - fallback.apply(on, args); - - // Skip appendToLogs, we delegate entirely to the fallback - return; - } - - appendToLogs(method, makeLogArgumentsSafe(arguments)); - } - }; + if (!this.columnNumber && err.columnNumber) { + this.columnNumber = err.columnNumber; } - on.log = generateLoggingMethod('log', on.LOG); + if (!this.stack && err.stack) { + this.stack = err.stack; + } + } - // Generate debug, info, warn, and error logging methods, these all fallback to on.log - on.debug = generateLoggingMethod('debug', on.DEBUG, on.log); - on.info = generateLoggingMethod('info', on.INFO, on.log); - on.warn = generateLoggingMethod('warn', on.WARN, on.log); - on.error = generateLoggingMethod('error', on.ERROR, on.log); + if (!this.message && message) this.message = message; + if (!this.name && name) this.name = name; +}; + +OTHelpers.Error.prototype.toString = +OTHelpers.Error.prototype.valueOf = function() { + var locationDetails = ''; + if (this.fileName) locationDetails += ' ' + this.fileName; + if (this.lineNumber) { + locationDetails += ' ' + this.lineNumber; + if (this.columnNumber) locationDetails += ':' + this.columnNumber; + } + + return '<' + (this.name ? this.name + ' ' : '') + this.message + locationDetails + '>'; +}; - on.setLogLevel = function(level) { - _logLevel = typeof(level) === 'number' ? level : 0; - on.debug('TB.setLogLevel(' + _logLevel + ')'); - return _logLevel; +// Normalise err.stack so that it is the same format as the other browsers +// We skip the first two frames so that we don't capture getErrorLocation() and +// the callee. +// +// Used by Environments that support the StackTrace API. (Chrome, Node, Opera) +// +var prepareStackTrace = function prepareStackTrace (_, stack){ + return $.map(stack.slice(2), function(frame) { + var _f = { + fileName: frame.getFileName(), + linenumber: frame.getLineNumber(), + columnNumber: frame.getColumnNumber() }; - on.getLogs = function() { - return _logs; - }; + if (frame.getFunctionName()) _f.functionName = frame.getFunctionName(); + if (frame.getMethodName()) _f.methodName = frame.getMethodName(); + if (frame.getThis()) _f.self = frame.getThis(); - // Determine if the level is visible given the current logLevel. - on.shouldLog = function(level) { - return _logLevel >= level; - }; + return _f; + }); +}; - // Format the current time nicely for logging. Returns the current - // local time. - function formatDateStamp() { - var now = new Date(); - return now.toLocaleTimeString() + now.getMilliseconds(); + +// Black magic to retrieve error location info for various environments +getErrorLocation = function getErrorLocation () { + var info = {}, + callstack, + errLocation, + err; + + switch ($.env.name) { + case 'Firefox': + case 'Safari': + case 'IE': + + if ($.env.name === 'IE') { + err = new Error(); } - - function toJson(object) { + else { try { - return JSON.stringify(object); - } catch(e) { - return object.toString(); + window.call.js.is.explody; + } + catch(e) { err = e; } + } + + callstack = err.stack.split('\n'); + + //Remove call to getErrorLocation() and the callee + callstack.shift(); + callstack.shift(); + + info.stack = callstack; + + if ($.env.name === 'IE') { + // IE also includes the error message in it's stack trace + info.stack.shift(); + + // each line begins with some amounts of spaces and 'at', we remove + // these to normalise with the other browsers. + info.stack = $.map(callstack, function(call) { + return call.replace(/^\s+at\s+/g, ''); + }); + } + + errLocation = /@(.+?):([0-9]+)(:([0-9]+))?$/.exec(callstack[0]); + if (errLocation) { + info.fileName = errLocation[1]; + info.lineNumber = parseInt(errLocation[2], 10); + if (errLocation.length > 3) info.columnNumber = parseInt(errLocation[4], 10); + } + break; + + case 'Chrome': + case 'Node': + case 'Opera': + var currentPST = Error.prepareStackTrace; + Error.prepareStackTrace = prepareStackTrace; + err = new Error(); + info.stack = err.stack; + Error.prepareStackTrace = currentPST; + + var topFrame = info.stack[0]; + info.lineNumber = topFrame.lineNumber; + info.columnNumber = topFrame.columnNumber; + info.fileName = topFrame.fileName; + if (topFrame.functionName) info.functionName = topFrame.functionName; + if (topFrame.methodName) info.methodName = topFrame.methodName; + if (topFrame.self) info.self = topFrame.self; + break; + + default: + err = new Error(); + if (err.stack) info.stack = err.stack.split('\n'); + break; + } + + if (err.message) info.message = err.message; + return info; +}; + + +/*jshint browser:true, smarttabs:true*/ +/* global process */ + +// tb_require('../helpers.js') + + +// OTHelpers.env +// +// Contains information about the current environment. +// * **OTHelpers.env.name** The name of the Environment (Chrome, FF, Node, etc) +// * **OTHelpers.env.version** Usually a Float, except in Node which uses a String +// * **OTHelpers.env.userAgent** The raw user agent +// * **OTHelpers.env.versionGreaterThan** A helper method that returns true if the +// current version is greater than the argument +// +// Example +// if (OTHelpers.env.versionGreaterThan('0.10.30')) { +// // do something +// } +// +(function() { + // @todo make exposing userAgent unnecessary + var version = -1; + + // Returns true if otherVersion is greater than the current environment + // version. + var versionGEThan = function versionGEThan (otherVersion) { + if (otherVersion === version) return true; + + if (typeof(otherVersion) === 'number' && typeof(version) === 'number') { + return otherVersion > version; + } + + // The versions have multiple components (i.e. 0.10.30) and + // must be compared piecewise. + // Note: I'm ignoring the case where one version has multiple + // components and the other doesn't. + var v1 = otherVersion.split('.'), + v2 = version.split('.'), + versionLength = (v1.length > v2.length ? v2 : v1).length; + + for (var i = 0; i < versionLength; ++i) { + if (parseInt(v1[i], 10) > parseInt(v2[i], 10)) { + return true; } } - function toDebugString(object) { - var components = []; - - if (typeof(object) === 'undefined') { - // noop - } - else if (object === null) { - components.push('NULL'); - } - else if (OTHelpers.isArray(object)) { - for (var i=0; i -1 || userAgent.indexOf('opr') > -1) { + name = 'Opera'; + + if (/opr\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { + version = parseFloat( RegExp.$1 ); + } + + } else if (userAgent.indexOf('firefox') > -1) { + name = 'Firefox'; + + if (/firefox\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { + version = parseFloat( RegExp.$1 ); + } + + } else if (appName === 'Microsoft Internet Explorer') { + // IE 10 and below + name = 'IE'; + + if (/msie ([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { + version = parseFloat( RegExp.$1 ); + } + + } else if (appName === 'Netscape' && userAgent.indexOf('trident') > -1) { + // IE 11+ + + name = 'IE'; + + if (/trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { + version = parseFloat( RegExp.$1 ); + } + + } else if (userAgent.indexOf('chrome') > -1) { + name = 'Chrome'; + + if (/chrome\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { + version = parseFloat( RegExp.$1 ); + } + + } else if ((navigatorVendor = window.navigator.vendor) && + navigatorVendor.toLowerCase().indexOf('apple') > -1) { + name = 'Safari'; + + if (/version\/([0-9]{1,}[\.0-9]{0,})/.exec(userAgent) !== null) { + version = parseFloat( RegExp.$1 ); + } + } + + return { + name: name, + version: version, + userAgent: window.navigator.userAgent, + iframeNeedsLoad: userAgent.indexOf('webkit') < 0, + versionGreaterThan: versionGEThan + }; + }(); + + + OTHelpers.env = env; + + OTHelpers.browser = function() { + return OTHelpers.env.name; + }; + + OTHelpers.browserVersion = function() { + return OTHelpers.env; + }; + +})(); +/*jshint browser:false, smarttabs:true*/ +/* global window, require */ + +// tb_require('../../helpers.js') +// tb_require('../environment.js') + +if (window.OTHelpers.env.name === 'Node') { + var request = require('request'); + + OTHelpers.request = function(url, options, callback) { + var completion = function(error, response, body) { + var event = {response: response, body: body}; + + // We need to detect things that Request considers a success, + // but we consider to be failures. + if (!error && response.statusCode >= 200 && + (response.statusCode < 300 || response.statusCode === 304) ) { + callback(null, event); + } else { + callback(error, event); + } + }; + + if (options.method.toLowerCase() === 'get') { + request.get(url, completion); + } + else { + request.post(url, options.body, completion); } }; - OTHelpers.useLogHelpers(OTHelpers); - OTHelpers.setLogLevel(OTHelpers.ERROR); + OTHelpers.getJSON = function(url, options, callback) { + var extendedHeaders = require('underscore').extend( + { + 'Accept': 'application/json' + }, + options.headers || {} + ); -})(window, window.OTHelpers); + request.get({ + url: url, + headers: extendedHeaders, + json: true + }, function(err, response) { + callback(err, response && response.body); + }); + }; +} +/*jshint browser:true, smarttabs:true*/ + +// tb_require('../../helpers.js') +// tb_require('../environment.js') + +function formatPostData(data) { //, contentType + // If it's a string, we assume it's properly encoded + if (typeof(data) === 'string') return data; + + var queryString = []; + + for (var key in data) { + queryString.push( + encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) + ); + } + + return queryString.join('&').replace(/\+/g, '%20'); +} + +if (window.OTHelpers.env.name !== 'Node') { + + OTHelpers.xdomainRequest = function(url, options, callback) { + /*global XDomainRequest*/ + var xdr = new XDomainRequest(), + _options = options || {}, + _method = _options.method.toLowerCase(); + + if(!_method) { + callback(new Error('No HTTP method specified in options')); + return; + } + + _method = _method.toUpperCase(); + + if(!(_method === 'GET' || _method === 'POST')) { + callback(new Error('HTTP method can only be ')); + return; + } + + function done(err, event) { + xdr.onload = xdr.onerror = xdr.ontimeout = function() {}; + xdr = void 0; + callback(err, event); + } + + + xdr.onload = function() { + done(null, { + target: { + responseText: xdr.responseText, + headers: { + 'content-type': xdr.contentType + } + } + }); + }; + + xdr.onerror = function() { + done(new Error('XDomainRequest of ' + url + ' failed')); + }; + + xdr.ontimeout = function() { + done(new Error('XDomainRequest of ' + url + ' timed out')); + }; + + xdr.open(_method, url); + xdr.send(options.body && formatPostData(options.body)); + + }; + + OTHelpers.request = function(url, options, callback) { + var request = new XMLHttpRequest(), + _options = options || {}, + _method = _options.method; + + if(!_method) { + callback(new Error('No HTTP method specified in options')); + return; + } + + // Setup callbacks to correctly respond to success and error callbacks. This includes + // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore + // by default. + if(callback) { + OTHelpers.on(request, 'load', function(event) { + var status = event.target.status; + + // We need to detect things that XMLHttpRequest considers a success, + // but we consider to be failures. + if ( status >= 200 && (status < 300 || status === 304) ) { + callback(null, event); + } else { + callback(event); + } + }); + + OTHelpers.on(request, 'error', callback); + } + + request.open(options.method, url, true); + + if (!_options.headers) _options.headers = {}; + + for (var name in _options.headers) { + request.setRequestHeader(name, _options.headers[name]); + } + + request.send(options.body && formatPostData(options.body)); + }; + + + OTHelpers.getJSON = function(url, options, callback) { + options = options || {}; + + var done = function(error, event) { + if(error) { + callback(error, event && event.target && event.target.responseText); + } else { + var response; + + try { + response = JSON.parse(event.target.responseText); + } catch(e) { + // Badly formed JSON + callback(e, event && event.target && event.target.responseText); + return; + } + + callback(null, response, event); + } + }; + + if(options.xdomainrequest) { + OTHelpers.xdomainRequest(url, { method: 'GET' }, done); + } else { + var extendedHeaders = OTHelpers.extend({ + 'Accept': 'application/json' + }, options.headers || {}); + + OTHelpers.get(url, OTHelpers.extend(options || {}, { + headers: extendedHeaders + }), done); + } + + }; + +} +/*jshint browser:true, smarttabs:true*/ + +// tb_require('../helpers.js') +// tb_require('./environment.js') + +OTHelpers.useLogHelpers = function(on){ + + // Log levels for OTLog.setLogLevel + on.DEBUG = 5; + on.LOG = 4; + on.INFO = 3; + on.WARN = 2; + on.ERROR = 1; + on.NONE = 0; + + var _logLevel = on.NONE, + _logs = [], + _canApplyConsole = true; + + try { + Function.prototype.bind.call(window.console.log, window.console); + } catch (err) { + _canApplyConsole = false; + } + + // Some objects can't be logged in the console, mostly these are certain + // types of native objects that are exposed to JS. This is only really a + // problem with IE, hence only the IE version does anything. + var makeLogArgumentsSafe = function(args) { return args; }; + + if (OTHelpers.env.name === 'IE') { + makeLogArgumentsSafe = function(args) { + return [toDebugString(prototypeSlice.apply(args))]; + }; + } + + // Generates a logging method for a particular method and log level. + // + // Attempts to handle the following cases: + // * the desired log method doesn't exist, call fallback (if available) instead + // * the console functionality isn't available because the developer tools (in IE) + // aren't open, call fallback (if available) + // * attempt to deal with weird IE hosted logging methods as best we can. + // + function generateLoggingMethod(method, level, fallback) { + return function() { + if (on.shouldLog(level)) { + var cons = window.console, + args = makeLogArgumentsSafe(arguments); + + // In IE, window.console may not exist if the developer tools aren't open + // This also means that cons and cons[method] can appear at any moment + // hence why we retest this every time. + if (cons && cons[method]) { + // the desired console method isn't a real object, which means + // that we can't use apply on it. We force it to be a real object + // using Function.bind, assuming that's available. + if (cons[method].apply || _canApplyConsole) { + if (!cons[method].apply) { + cons[method] = Function.prototype.bind.call(cons[method], cons); + } + + cons[method].apply(cons, args); + } + else { + // This isn't the same result as the above, but it's better + // than nothing. + cons[method](args); + } + } + else if (fallback) { + fallback.apply(on, args); + + // Skip appendToLogs, we delegate entirely to the fallback + return; + } + + appendToLogs(method, makeLogArgumentsSafe(arguments)); + } + }; + } + + on.log = generateLoggingMethod('log', on.LOG); + + // Generate debug, info, warn, and error logging methods, these all fallback to on.log + on.debug = generateLoggingMethod('debug', on.DEBUG, on.log); + on.info = generateLoggingMethod('info', on.INFO, on.log); + on.warn = generateLoggingMethod('warn', on.WARN, on.log); + on.error = generateLoggingMethod('error', on.ERROR, on.log); + + + on.setLogLevel = function(level) { + _logLevel = typeof(level) === 'number' ? level : 0; + on.debug('TB.setLogLevel(' + _logLevel + ')'); + return _logLevel; + }; + + on.getLogs = function() { + return _logs; + }; + + // Determine if the level is visible given the current logLevel. + on.shouldLog = function(level) { + return _logLevel >= level; + }; + + // Format the current time nicely for logging. Returns the current + // local time. + function formatDateStamp() { + var now = new Date(); + return now.toLocaleTimeString() + now.getMilliseconds(); + } + + function toJson(object) { + try { + return JSON.stringify(object); + } catch(e) { + return object.toString(); + } + } + + function toDebugString(object) { + var components = []; + + if (typeof(object) === 'undefined') { + // noop + } + else if (object === null) { + components.push('NULL'); + } + else if (OTHelpers.isArray(object)) { + for (var i=0; ion, 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 of this in the event - * handler function. - * - * @returns {EventDispatcher} The EventDispatcher object. - * - * @memberOf EventDispatcher - * @method #on - * @see off() - * @see once() - * @see Events - */ - self.on = function(eventNames, handlerOrContext, context) { - if (typeof(eventNames) === 'string' && handlerOrContext) { - addListeners(eventNames.split(' '), handlerOrContext, context); } else { - for (var name in eventNames) { - if (eventNames.hasOwnProperty(name)) { - addListeners([name], eventNames[name], handlerOrContext); - } + 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 of this in the event + * handler function. + * + * @returns {EventDispatcher} The EventDispatcher object. + * + * @memberOf EventDispatcher + * @method #on + * @see off() + * @see once() + * @see Events + */ + self.on = function(eventNames, handlerOrContext, context) { + if (typeof(eventNames) === 'string' && handlerOrContext) { + addListeners(eventNames.split(' '), handlerOrContext, context); + } + else { + for (var name in eventNames) { + if (eventNames.hasOwnProperty(name)) { + addListeners([name], eventNames[name], handlerOrContext); } - 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. + *

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

+ * Removes an event listener for a specific event. + *

+ * + *

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

+ * + * @param {String} type The string identifying the type of event. + * + * @param {Function} listener The event listener function to remove. + * + * @param {Object} context (Optional) If you specify a context, the event + * handler is removed for all specified events and event listeners that use the specified + context. (The context must match the context passed into + * addEventListener().) + * + * @memberOf EventDispatcher + * @method #removeEventListener + * @see off() + * @see Events + */ + // See 'off' for usage. + // @depreciated will become a private helper function in the future. + self.removeEventListener = function(eventName, handler, context) { + OTHelpers.warn('The removeEventListener() method is deprecated. Use off() instead.'); + removeListeners([eventName], handler, context); + }; + + + return self; +}; + +OTHelpers.eventing.Event = function() { + return function (type, cancelable) { + this.type = type; + this.cancelable = cancelable !== undefined ? cancelable : true; + + var _defaultPrevented = false; + + this.preventDefault = function() { + if (this.cancelable) { + _defaultPrevented = true; } else { - 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. - *

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

- * Removes an event listener for a specific event. - *

- * - *

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

- * - * @param {String} type The string identifying the type of event. - * - * @param {Function} listener The event listener function to remove. - * - * @param {Object} context (Optional) If you specify a context, the event - * handler is removed for all specified events and event listeners that use the specified - context. (The context must match the context passed into - * addEventListener().) - * - * @memberOf EventDispatcher - * @method #removeEventListener - * @see off() - * @see Events - */ - // See 'off' for usage. - // @depreciated will become a private helper function in the future. - self.removeEventListener = function(eventName, handler, context) { - OTHelpers.warn('The removeEventListener() method is deprecated. Use off() instead.'); - removeListeners([eventName], handler, context); - }; - - - - return self; + 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 = '' + + '' + + '' + + ''; - // This is necessary for IE, as it will not inherit it's doctype from - // the parent frame. - var frameContent = '' + - '' + - '' + - ''; + var wrappedCallback = function() { + var doc = domElement.contentDocument || domElement.contentWindow.document; - var wrappedCallback = function() { - var doc = domElement.contentDocument || domElement.contentWindow.document; + if (OTHelpers.env.iframeNeedsLoad) { + doc.body.style.backgroundColor = 'transparent'; + doc.body.style.border = 'none'; - if (OTHelpers.browserVersion().iframeNeedsLoad) { - doc.body.style.backgroundColor = 'transparent'; - doc.body.style.border = 'none'; - - if (OTHelpers.browser() !== 'IE') { - // Skip this for IE as we use the bookmarklet workaround - // for THAT browser. - doc.open(); - doc.write(frameContent); - doc.close(); - } + if (OTHelpers.env.name !== 'IE') { + // Skip this for IE as we use the bookmarklet workaround + // for THAT browser. + doc.open(); + doc.write(frameContent); + doc.close(); } - - callback( - domElement.contentWindow, - doc - ); - }; - - document.body.appendChild(domElement); - - if(OTHelpers.browserVersion().iframeNeedsLoad) { - if (OTHelpers.browser() === 'IE') { - // This works around some issues with IE and document.write. - // Basically this works by slightly abusing the bookmarklet/scriptlet - // functionality that all browsers support. - domElement.contentWindow.contents = frameContent; - /*jshint scripturl:true*/ - domElement.src = 'javascript:window["contents"]'; - /*jshint scripturl:false*/ - } - - OTHelpers.on(domElement, 'load', wrappedCallback); - } else { - setTimeout(wrappedCallback); } - this.close = function() { - OTHelpers.removeElement(domElement); - this.trigger('closed'); - this.element = domElement = null; - return this; - }; - - this.element = domElement; - + callback( + domElement.contentWindow, + doc + ); }; -})(window, window.OTHelpers); + document.body.appendChild(domElement); + + if(OTHelpers.env.iframeNeedsLoad) { + if (OTHelpers.env.name === 'IE') { + // This works around some issues with IE and document.write. + // Basically this works by slightly abusing the bookmarklet/scriptlet + // functionality that all browsers support. + domElement.contentWindow.contents = frameContent; + /*jshint scripturl:true*/ + domElement.src = 'javascript:window["contents"]'; + /*jshint scripturl:false*/ + } + + OTHelpers.on(domElement, 'load', wrappedCallback); + } else { + setTimeout(wrappedCallback, 0); + } + + this.close = function() { + OTHelpers.removeElement(domElement); + this.trigger('closed'); + this.element = domElement = null; + return this; + }; + + this.element = domElement; + +}; /* * getComputedStyle from @@ -2371,7 +3016,7 @@ /*jshint strict: false, eqnull: true, browser:true, smarttabs:true*/ -(function(window, OTHelpers, undefined) { +(function() { /*jshint eqnull: true, browser: true */ @@ -2464,95 +3109,433 @@ } }; -})(window, window.OTHelpers); +})(); -// DOM Attribute helpers helpers +/*jshint browser:true, smarttabs:true */ -/*jshint browser:true, smarttabs:true*/ +// tb_require('../helpers.js') +// tb_require('./callbacks.js') +// tb_require('./dom.js') + +var observeStyleChanges = function observeStyleChanges (element, stylesToObserve, onChange) { + var oldStyles = {}; + + var getStyle = function getStyle(style) { + switch (style) { + case 'width': + return $(element).width(); + + case 'height': + return $(element).height(); + + default: + return $(element).css(style); + } + }; + + // get the inital values + $.forEach(stylesToObserve, function(style) { + oldStyles[style] = getStyle(style); + }); + + var observer = new MutationObserver(function(mutations) { + var changeSet = {}; + + $.forEach(mutations, function(mutation) { + if (mutation.attributeName !== 'style') return; + + var isHidden = $.isDisplayNone(element); + + $.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 (!$.isEmpty(changeSet)) { + // Do this after so as to help avoid infinite loops of mutations. + $.callAsync(function() { + onChange.call(null, changeSet); + }); + } + }); + + observer.observe(element, { + attributes:true, + attributeFilter: ['style'], + childList:false, + characterData:false, + subtree:false + }); + + return observer; +}; + +var observeNodeOrChildNodeRemoval = function observeNodeOrChildNodeRemoval (element, onChange) { + var observer = new MutationObserver(function(mutations) { + var removedNodes = []; + + $.forEach(mutations, function(mutation) { + if (mutation.removedNodes.length) { + removedNodes = removedNodes.concat(prototypeSlice.call(mutation.removedNodes)); + } + }); + + if (removedNodes.length) { + // Do this after so as to help avoid infinite loops of mutations. + $.callAsync(function() { + onChange($(removedNodes)); + }); + } + }); + + observer.observe(element, { + attributes:false, + childList:true, + characterData:false, + subtree:true + }); + + return observer; +}; + +// 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(object).observeStyleChanges(, +// ['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; +// +ElementCollection.prototype.observeStyleChanges = function(stylesToObserve, onChange) { + var observers = []; + + this.forEach(function(element) { + observers.push( + observeStyleChanges(element, stylesToObserve, onChange) + ); + }); + + return observers; +}; + +// 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(object).observeNodeOrChildNodeRemoval(function(removedNodes) { +// OT.debug("Some child nodes were removed"); +// removedNodes.forEach(function(node) { +// OT.debug(node); +// }); +// }); +// +// Cleaning up +// // stop observing changes +// nodeObserver.disconnect(); +// nodeObserver = null; +// +ElementCollection.prototype.observeNodeOrChildNodeRemoval = function(onChange) { + var observers = []; + + this.forEach(function(element) { + observers.push( + observeNodeOrChildNodeRemoval(element, onChange) + ); + }); + + return observers; +}; + + +// @remove +OTHelpers.observeStyleChanges = function(element, stylesToObserve, onChange) { + return $(element).observeStyleChanges(stylesToObserve, onChange)[0]; +}; + +// @remove +OTHelpers.observeNodeOrChildNodeRemoval = function(element, onChange) { + return $(element).observeNodeOrChildNodeRemoval(onChange)[0]; +}; + +/*jshint browser:true, smarttabs:true */ // tb_require('../helpers.js') // tb_require('./dom.js') +// tb_require('./capabilities.js') -(function(window, OTHelpers, undefined) { +// Returns true if the client supports element.classList +OTHelpers.registerCapability('classList', function() { + return (typeof document !== 'undefined') && ('classList' in document.createElement('a')); +}); - OTHelpers.addClass = function(element, value) { - // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc - if (element.nodeType !== 1) { - return; + +function hasClass (element, className) { + if (!className) return false; + + if ($.hasCapabilities('classList')) { + return element.classList.contains(className); + } + + return element.className.indexOf(className) > -1; +} + +function toggleClasses (element, classNames) { + if (!classNames || classNames.length === 0) return; + + // Only bother targeting Element nodes, ignore Text Nodes, CDATA, etc + if (element.nodeType !== 1) { + return; + } + + var numClasses = classNames.length, + i = 0; + + if ($.hasCapabilities('classList')) { + for (; i 0) { return element.offsetWidth + 'px'; } - return OTHelpers.css(element, 'width'); + return $(element).css('width'); }, _height = function(element) { @@ -2560,58 +3543,58 @@ return element.offsetHeight + 'px'; } - return OTHelpers.css(element, 'height'); + return $(element).css('height'); }; - OTHelpers.width = function(element, newWidth) { + ElementCollection.prototype.width = function (newWidth) { if (newWidth) { - OTHelpers.css(element, 'width', newWidth); + this.css('width', newWidth); return this; } else { - if (OTHelpers.isDisplayNone(element)) { - // We can't get the width, probably since the element is hidden. - return OTHelpers.makeVisibleAndYield(element, function() { + if (this.isDisplayNone()) { + return this.makeVisibleAndYield(function(element) { return _width(element); - }); + })[0]; } else { - return _width(element); + return _width(this.get(0)); } } }; - OTHelpers.height = function(element, newHeight) { + ElementCollection.prototype.height = function (newHeight) { if (newHeight) { - OTHelpers.css(element, 'height', newHeight); + this.css('height', newHeight); return this; } else { - if (OTHelpers.isDisplayNone(element)) { + if (this.isDisplayNone()) { // We can't get the height, probably since the element is hidden. - return OTHelpers.makeVisibleAndYield(element, function() { + return this.makeVisibleAndYield(function(element) { return _height(element); - }); + })[0]; } else { - return _height(element); + return _height(this.get(0)); } } }; - // Centers +element+ within the window. You can pass through the width and height - // if you know it, if you don't they will be calculated for you. - OTHelpers.centerElement = function(element, width, height) { - if (!width) width = parseInt(OTHelpers.width(element), 10); - if (!height) height = parseInt(OTHelpers.height(element), 10); - - var marginLeft = -0.5 * width + 'px'; - var marginTop = -0.5 * height + 'px'; - OTHelpers.css(element, 'margin', marginTop + ' 0 0 ' + marginLeft); - OTHelpers.addClass(element, 'OT_centered'); + // @remove + OTHelpers.width = function(element, newWidth) { + var ret = $(element).width(newWidth); + return newWidth ? OTHelpers : ret; }; -})(window, window.OTHelpers); + // @remove + OTHelpers.height = function(element, newHeight) { + var ret = $(element).height(newHeight); + return newHeight ? OTHelpers : ret; + }; + +})(); + // CSS helpers helpers @@ -2621,12 +3604,12 @@ // tb_require('./dom.js') // tb_require('./getcomputedstyle.js') -(function(window, OTHelpers, undefined) { +(function() { var displayStateCache = {}, defaultDisplays = {}; - var defaultDisplayValueForElement = function(element) { + var defaultDisplayValueForElement = function (element) { if (defaultDisplays[element.ownerDocument] && defaultDisplays[element.ownerDocument][element.nodeName]) { return defaultDisplays[element.ownerDocument][element.nodeName]; @@ -2641,83 +3624,49 @@ element.ownerDocument.body.appendChild(testNode); defaultDisplay = defaultDisplays[element.ownerDocument][element.nodeName] = - OTHelpers.css(testNode, 'display'); + $(testNode).css('display'); - OTHelpers.removeElement(testNode); + $(testNode).remove(); testNode = null; return defaultDisplay; }; - var isHidden = function(element) { - var computedStyle = OTHelpers.getComputedStyle(element); + var isHidden = function (element) { + var computedStyle = $.getComputedStyle(element); return computedStyle.getPropertyValue('display') === 'none'; }; - OTHelpers.show = function(element) { - var display = element.style.display; + var setCssProperties = function (element, hash) { + var style = element.style; - if (display === '' || display === 'none') { - element.style.display = displayStateCache[element] || ''; - delete displayStateCache[element]; - } - - if (isHidden(element)) { - // It's still hidden so there's probably a stylesheet that declares this - // element as display:none; - displayStateCache[element] = 'none'; - - element.style.display = defaultDisplayValueForElement(element); - } - - return this; - }; - - OTHelpers.hide = function(element) { - if (element.style.display === 'none') return; - - displayStateCache[element] = element.style.display; - element.style.display = 'none'; - - return this; - }; - - OTHelpers.css = function(element, nameOrHash, value) { - if (typeof(nameOrHash) !== 'string') { - var style = element.style; - - for (var cssName in nameOrHash) { - if (nameOrHash.hasOwnProperty(cssName)) { - style[cssName] = nameOrHash[cssName]; - } + for (var cssName in hash) { + if (hash.hasOwnProperty(cssName)) { + style[cssName] = hash[cssName]; } - - return this; - - } else if (value !== undefined) { - element.style[nameOrHash] = value; - return this; - - } else { - // Normalise vendor prefixes from the form MozTranform to -moz-transform - // except for ms extensions, which are weird... - - var name = nameOrHash.replace( /([A-Z]|^ms)/g, '-$1' ).toLowerCase(), - computedStyle = OTHelpers.getComputedStyle(element), - currentValue = computedStyle.getPropertyValue(name); - - if (currentValue === '') { - currentValue = element.style[name]; - } - - return currentValue; } }; + var setCssProperty = function (element, name, value) { + element.style[name] = value; + }; -// Apply +styles+ to +element+ while executing +callback+, restoring the previous -// styles after the callback executes. - OTHelpers.applyCSS = function(element, styles, callback) { + var getCssProperty = function (element, unnormalisedName) { + // Normalise vendor prefixes from the form MozTranform to -moz-transform + // except for ms extensions, which are weird... + + var name = unnormalisedName.replace( /([A-Z]|^ms)/g, '-$1' ).toLowerCase(), + computedStyle = $.getComputedStyle(element), + currentValue = computedStyle.getPropertyValue(name); + + if (currentValue === '') { + currentValue = element.style[name]; + } + + return currentValue; + }; + + var applyCSS = function(element, styles, callback) { var oldStyles = {}, name, ret; @@ -2730,44 +3679,237 @@ // only want to pull values out of the style (domeElement.style) hash. oldStyles[name] = element.style[name]; - OTHelpers.css(element, name, styles[name]); + $(element).css(name, styles[name]); } } - ret = callback(); + ret = callback(element); // Restore the old styles for (name in styles) { if (styles.hasOwnProperty(name)) { - OTHelpers.css(element, name, oldStyles[name] || ''); + $(element).css(name, oldStyles[name] || ''); } } return ret; }; - // Make +element+ visible while executing +callback+. - OTHelpers.makeVisibleAndYield = function(element, callback) { - // find whether it's the element or an ancester that's display none and - // then apply to whichever it is - var targetElement = OTHelpers.findElementWithDisplayNone(element); - if (!targetElement) return; + ElementCollection.prototype.show = function() { + return this.forEach(function(element) { + var display = element.style.display; - return OTHelpers.applyCSS(targetElement, { - display: 'block', - visibility: 'hidden' - }, callback); + if (display === '' || display === 'none') { + element.style.display = displayStateCache[element] || ''; + delete displayStateCache[element]; + } + + if (isHidden(element)) { + // It's still hidden so there's probably a stylesheet that declares this + // element as display:none; + displayStateCache[element] = 'none'; + + element.style.display = defaultDisplayValueForElement(element); + } + }); }; -})(window, window.OTHelpers); + ElementCollection.prototype.hide = function() { + return this.forEach(function(element) { + if (element.style.display === 'none') return; -// AJAX helpers + displayStateCache[element] = element.style.display; + element.style.display = 'none'; + }); + }; + + ElementCollection.prototype.css = function(nameOrHash, value) { + if (this.length === 0) return; + + if (typeof(nameOrHash) !== 'string') { + + return this.forEach(function(element) { + setCssProperties(element, nameOrHash); + }); + + } else if (value !== undefined) { + + return this.forEach(function(element) { + setCssProperty(element, nameOrHash, value); + }); + + } else { + return getCssProperty(this.first, nameOrHash, value); + } + }; + + // Apply +styles+ to +element+ while executing +callback+, restoring the previous + // styles after the callback executes. + ElementCollection.prototype.applyCSS = function (styles, callback) { + var results = []; + + this.forEach(function(element) { + results.push(applyCSS(element, styles, callback)); + }); + + return results; + }; + + + // Make +element+ visible while executing +callback+. + ElementCollection.prototype.makeVisibleAndYield = function (callback) { + var hiddenVisually = { + display: 'block', + visibility: 'hidden' + }, + results = []; + + this.forEach(function(element) { + // find whether it's the element or an ancestor that's display none and + // then apply to whichever it is + var targetElement = $.findElementWithDisplayNone(element); + if (!targetElement) { + results.push(void 0); + } + else { + results.push( + applyCSS(targetElement, hiddenVisually, callback) + ); + } + }); + + return results; + }; + + + // @remove + OTHelpers.show = function(element) { + return $(element).show(); + }; + + // @remove + OTHelpers.hide = function(element) { + return $(element).hide(); + }; + + // @remove + OTHelpers.css = function(element, nameOrHash, value) { + return $(element).css(nameOrHash, value); + }; + + // @remove + OTHelpers.applyCSS = function(element, styles, callback) { + return $(element).applyCSS(styles, callback); + }; + + // @remove + OTHelpers.makeVisibleAndYield = function(element, callback) { + return $(element).makeVisibleAndYield(callback); + }; + +})(); + +// tb_require('../helpers.js') + +/**@licence + * Copyright (c) 2010 Caolan McMahon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + **/ + + +(function() { + + OTHelpers.setImmediate = (function() { + if (typeof process === 'undefined' || !(process.nextTick)) { + if (typeof setImmediate === 'function') { + return function (fn) { + // not a direct alias for IE10 compatibility + setImmediate(fn); + }; + } + return function (fn) { + setTimeout(fn, 0); + }; + } + if (typeof setImmediate !== 'undefined') { + return setImmediate; + } + return process.nextTick; + })(); + + OTHelpers.iterator = function(tasks) { + var makeCallback = function (index) { + var fn = function () { + if (tasks.length) { + tasks[index].apply(null, arguments); + } + return fn.next(); + }; + fn.next = function () { + return (index < tasks.length - 1) ? makeCallback(index + 1) : null; + }; + return fn; + }; + return makeCallback(0); + }; + + OTHelpers.waterfall = function(array, done) { + done = done || function () {}; + if (array.constructor !== Array) { + return done(new Error('First argument to waterfall must be an array of functions')); + } + + if (!array.length) { + return done(); + } + + var next = function(iterator) { + return function (err) { + if (err) { + done.apply(null, arguments); + done = function () {}; + } else { + var args = prototypeSlice.call(arguments, 1), + nextFn = iterator.next(); + if (nextFn) { + args.push(next(nextFn)); + } else { + args.push(done); + } + OTHelpers.setImmediate(function() { + iterator.apply(null, args); + }); + } + }; + }; + + next(OTHelpers.iterator(array))(); + }; + +})(); /*jshint browser:true, smarttabs:true*/ // tb_require('../helpers.js') -(function(window, OTHelpers, undefined) { +(function() { var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || @@ -2791,906 +3933,281 @@ } OTHelpers.requestAnimationFrame = requestAnimationFrame; -})(window, window.OTHelpers); +})(); +/*jshint browser:true, smarttabs:true*/ + +// tb_require('../helpers.js') + +(function() { + + // Singleton interval + var logQueue = [], + queueRunning = false; + + OTHelpers.Analytics = function(loggingUrl, debugFn) { + + var endPoint = loggingUrl + '/logging/ClientEvent', + endPointQos = loggingUrl + '/logging/ClientQos', + + reportedErrors = {}, + + send = function(data, isQos, callback) { + OTHelpers.post((isQos ? endPointQos : endPoint) + '?_=' + OTHelpers.uuid.v4(), { + body: data, + xdomainrequest: ($.env.name === 'IE' && $.env.version < 10), + headers: { + 'Content-Type': 'application/json' + } + }, 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) { + var debugMsg = 'Failed to send ClientEvent, moving on to the next item.'; + if (debugFn) { + debugFn(debugMsg); + } else { + console.log(debugMsg); + } + // 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; + }; + + // 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] 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] streamId ... + // @option options [String] section ... + // @option options [String] clientVersion ... + // + // 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 (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 = details ? details : null; + + reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ? + reportedErrors[errKey] + 1 : 1; + this.logEvent(OTHelpers.extend(options, { + action: type + '.' + code, + payload: payload + }), false); + }; + + // Log a client event to the analytics backend. + // + // @example Logs a client event called 'foo' + // this.logEvent({ + // action: 'foo', + // payload: 'bar', + // sessionId: sessionId, + // connectionId: connectionId + // }, false) + // + // @param [Hash] data the data to log the client event with. + // @param [Boolean] qos Whether this is a QoS event. + // + this.logEvent = function(data, qos, throttle) { + if (!qos) qos = false; + + if (throttle && !isNaN(throttle)) { + if (Math.random() > throttle) { + return; + } + } + + // remove properties that have null values: + for (var key in data) { + if (data.hasOwnProperty(key) && data[key] === null) { + delete data[key]; + } + } + + // TODO: catch error when stringifying an object that has a circular reference + data = JSON.stringify(data); + + var onComplete = function() { + // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] + // + ', payload: ' + data['payload'] + '}'); + }; + + post(data, onComplete, qos); + }; + + // Log a client QOS to the analytics backend. + // Log a client QOS to the analytics backend. + // @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] 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] streamId ... + // @option options [String] section ... + // @option options [String] clientVersion ... + // + this.logQOS = function(options) { + this.logEvent(options, true); + }; + }; + +})(); + // AJAX helpers /*jshint browser:true, smarttabs:true*/ // tb_require('../helpers.js') +// tb_require('./ajax/node.js') +// tb_require('./ajax/browser.js') -(function(window, OTHelpers, undefined) { - - function formatPostData(data) { //, contentType - // If it's a string, we assume it's properly encoded - if (typeof(data) === 'string') return data; - - var queryString = []; - - for (var key in data) { - queryString.push( - encodeURIComponent(key) + '=' + encodeURIComponent(data[key]) - ); - } - - return queryString.join('&').replace(/\+/g, '%20'); - } - - OTHelpers.getJSON = function(url, options, callback) { - options = options || {}; - - var done = function(error, event) { - if(error) { - callback(error, event && event.target && event.target.responseText); - } else { - var response; - - try { - response = JSON.parse(event.target.responseText); - } catch(e) { - // Badly formed JSON - callback(e, event && event.target && event.target.responseText); - return; - } - - callback(null, response, event); - } - }; - - if(options.xdomainrequest) { - OTHelpers.xdomainRequest(url, { method: 'GET' }, done); - } else { - var extendedHeaders = OTHelpers.extend({ - 'Accept': 'application/json' - }, options.headers || {}); - - OTHelpers.get(url, OTHelpers.extend(options || {}, { - headers: extendedHeaders - }), done); - } - - }; - - OTHelpers.xdomainRequest = function(url, options, callback) { - /*global XDomainRequest*/ - var xdr = new XDomainRequest(), - _options = options || {}, - _method = _options.method; - - if(!_method) { - callback(new Error('No HTTP method specified in options')); - return; - } - - _method = _method.toUpperCase(); - - if(!(_method === 'GET' || _method === 'POST')) { - callback(new Error('HTTP method can only be ')); - return; - } - - function done(err, event) { - xdr.onload = xdr.onerror = xdr.ontimeout = function() {}; - xdr = void 0; - callback(err, event); - } +OTHelpers.get = function(url, options, callback) { + var _options = OTHelpers.extend(options || {}, { + method: 'GET' + }); + OTHelpers.request(url, _options, callback); +}; - xdr.onload = function() { - done(null, { - target: { - responseText: xdr.responseText, - headers: { - 'content-type': xdr.contentType - } - } - }); - }; +OTHelpers.post = function(url, options, callback) { + var _options = OTHelpers.extend(options || {}, { + method: 'POST' + }); - xdr.onerror = function() { - done(new Error('XDomainRequest of ' + url + ' failed')); - }; - - xdr.ontimeout = function() { - done(new Error('XDomainRequest of ' + url + ' timed out')); - }; - - xdr.open(_method, url); - xdr.send(options.body && formatPostData(options.body)); - - }; - - OTHelpers.request = function(url, options, callback) { - var request = new XMLHttpRequest(), - _options = options || {}, - _method = _options.method; - - if(!_method) { - callback(new Error('No HTTP method specified in options')); - return; - } - - // Setup callbacks to correctly respond to success and error callbacks. This includes - // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore - // by default. - if(callback) { - OTHelpers.on(request, 'load', function(event) { - var status = event.target.status; - - // We need to detect things that XMLHttpRequest considers a success, - // but we consider to be failures. - if ( status >= 200 && status < 300 || status === 304 ) { - callback(null, event); - } else { - callback(event); - } - }); - - OTHelpers.on(request, 'error', callback); - } - - request.open(options.method, url, true); - - if (!_options.headers) _options.headers = {}; - - for (var name in _options.headers) { - request.setRequestHeader(name, _options.headers[name]); - } - - request.send(options.body && formatPostData(options.body)); - }; - - OTHelpers.get = function(url, options, callback) { - var _options = OTHelpers.extend(options || {}, { - method: 'GET' - }); + if(_options.xdomainrequest) { + OTHelpers.xdomainRequest(url, _options, callback); + } else { OTHelpers.request(url, _options, callback); - }; + } +}; - OTHelpers.post = function(url, options, callback) { - var _options = OTHelpers.extend(options || {}, { - method: 'POST' - }); - - if(_options.xdomainrequest) { - OTHelpers.xdomainRequest(url, _options, callback); - } else { - OTHelpers.request(url, _options, callback); - } - }; })(window, window.OTHelpers); -!(function(window) { - /* global OTHelpers */ - if (!window.OT) window.OT = {}; - - // Bring OTHelpers in as OT.$ - OT.$ = OTHelpers.noConflict(); - - // Allow events to be bound on OT - OT.$.eventing(OT); - - // REMOVE THIS POST IE MERGE - - OT.$.defineGetters = function(self, getters, enumerable) { - var propsDefinition = {}; - - if (enumerable === void 0) enumerable = false; - - for (var key in getters) { - if(!getters.hasOwnProperty(key)) { - continue; - } - propsDefinition[key] = { - get: getters[key], - enumerable: enumerable - }; - } - - Object.defineProperties(self, propsDefinition); - }; - - // STOP REMOVING HERE - - // OT.$.Modal was OT.Modal before the great common-js-helpers move - OT.Modal = OT.$.Modal; - - // Add logging methods - OT.$.useLogHelpers(OT); - - var _debugHeaderLogged = false, - _setLogLevel = OT.setLogLevel; - - // On the first time log level is set to DEBUG (or higher) show version info. - OT.setLogLevel = function(level) { - // Set OT.$ to the same log level - OT.$.setLogLevel(level); - var retVal = _setLogLevel.call(OT, level); - if (OT.shouldLog(OT.DEBUG) && !_debugHeaderLogged) { - OT.debug('OpenTok JavaScript library ' + OT.properties.version); - OT.debug('Release notes: ' + OT.properties.websiteURL + - '/opentok/webrtc/docs/js/release-notes.html'); - OT.debug('Known issues: ' + OT.properties.websiteURL + - '/opentok/webrtc/docs/js/release-notes.html#knownIssues'); - _debugHeaderLogged = true; - } - OT.debug('OT.setLogLevel(' + retVal + ')'); - return retVal; - }; - - var debugTrue = OT.properties.debug === 'true' || OT.properties.debug === true; - OT.setLogLevel(debugTrue ? OT.DEBUG : OT.ERROR); - - OT.$.userAgent = function() { - var userAgent = navigator.userAgent; - if (TBPlugin.isInstalled()) userAgent += '; TBPlugin ' + TBPlugin.version(); - return userAgent; - }; - - /** - * Sets the API log level. - *

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

- *

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

- *

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

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

- *

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

- * - * @name OT.setLogLevel - * @memberof OT - * @function - * @see OT.log() - */ - - /** - * Sends a string to the the debugger console (such as Firebug), if one exists. - * However, the function only logs to the console if you have set the log level - * to OT.LOG or OT.DEBUG, - * by calling OT.setLogLevel(OT.LOG) or OT.setLogLevel(OT.DEBUG). - * - * @param {String} message The string to log. - * - * @name OT.log - * @memberof OT - * @function - * @see OT.setLogLevel() - */ - -})(window); -!(function() { - - var 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(''); - - for (var name in params) { - if (params.hasOwnProperty(name)) { - objBits.push(''); - } - } - - objBits.push(''); - tmpContainer.innerHTML = objBits.join(''); - - _document.body.appendChild(tmpContainer); - - function firstElementChild(element) { - if(element.firstElementChild) { - return element.firstElementChild; - } - for(var i = 0, len = element.childNodes.length; i < len; ++i) { - if(element.childNodes[i].nodeType === 1) { - return element.childNodes[i]; - } - } - return null; - } - - o = firstElementChild(tmpContainer); - o.setAttribute('tb_callbackId', callbackId); - - pluginRefCounted = new PluginObject(o); - - _document.body.appendChild(o); - _document.body.removeChild(tmpContainer); - - objectTimeouts[callbackId] = setTimeout(function() { - clearObjectLoadTimeout(callbackId); - - completion.call(TBPlugin, 'The object with the mimeType of ' + - mimeType + ' timed out while loading.'); - - _document.body.removeChild(o); - }, 3000); - - return pluginRefCounted; -}; - - -// Creates the Media Capture controller. This exposes selectSources and is -// used in the private API. -// -// Only one Media Capture controller can exist at once, calling this method -// more than once will raise an exception. -// -var createMediaCaptureController = function createMediaCaptureController (completion) { - if (mediaCaptureObject) { - throw new Error('TBPlugin.createMediaCaptureController called multiple times!'); - } - - mediaCaptureObject = injectObject(pluginInfo.mimeType, false, {windowless: false}, completion); - - mediaCaptureObject.selectSources = function() { - return this._.selectSources.apply(this._, arguments); - }; - - return mediaCaptureObject; -}; - -// Create an instance of the publisher/subscriber/peerconnection object. -// Many of these can exist at once, but the +id+ of each must be unique -// within a single instance of scope (window or window-like thing). -// -var createPeerController = function createPeerController (completion) { - var o = injectObject(pluginInfo.mimeType, true, {windowless: true}, function(err, plugin) { - if (err) { - completion.call(TBPlugin, err); - return; - } - - plugins[plugin.id] = plugin; - completion.call(TBPlugin, null, plugin); - }); - - return o; -}; - // tb_require('./header.js') // tb_require('./shims.js') -// tb_require('./plugin_object.js') +// 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'); + + for (var name in params) { + if (params.hasOwnProperty(name)) { + objBits.push(''); + } + } + + objBits.push(''); + + scope.document.body.insertAdjacentHTML('beforeend', objBits.join('')); + plugin = new PluginProxy(lastElementChild(scope.document.body)); + plugin._.setAttribute('tbCallbackId', callbackId); + + return plugin; +}; + + +// Stops and cleans up after the plugin object load timeout. +var injectObject = function injectObject (mimeType, isVisible, params, completion) { + var callbackId = generateCallbackUUID(), + plugin; + + params.onload = callbackId; + params.userAgent = OTHelpers.env.userAgent.toLowerCase(); + + scope[callbackId] = function() { + clearObjectLoadTimeout(callbackId); + + plugin._.setAttribute('id', 'tb_plugin_' + plugin._.uuid); + + if (plugin._.removeAttribute !== void 0) { + plugin._.removeAttribute('tbCallbackId'); + } + else { + // Plugin is some kind of weird object that doesn't have removeAttribute on Safari? + plugin._.tbCallbackId = null; + } + + plugin.uuid = plugin._.uuid; + plugin.id = plugin._.id; + + plugin.onReady(function(err) { + if (err) { + OTPlugin.error('Error while starting up plugin ' + plugin.uuid + ': ' + err); + return; + } + + OTPlugin.debug('Plugin ' + plugin.id + ' is loaded'); + + if (completion && OTHelpers.isFunction(completion)) { + completion.call(OTPlugin, null, plugin); + } + }); + }; + + plugin = createPluginProxy(callbackId, mimeType, params, isVisible); + + objectTimeouts[callbackId] = setTimeout(function() { + clearObjectLoadTimeout(callbackId); + + completion.call(OTPlugin, 'The object with the mimeType of ' + + mimeType + ' timed out while loading.'); + + scope.document.body.removeChild(plugin._); + }, 3000); + + return plugin; +}; + + +// tb_require('./header.js') +// tb_require('./shims.js') +// tb_require('./proxy.js') +// tb_require('./embedding.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global injectObject, scope:true */ +/* exported createMediaCaptureController:true, createPeerController:true, + injectObject:true, plugins:true, mediaCaptureObject:true, + removeAllObjects:true */ + +var objectTimeouts = {}, + mediaCaptureObject, + plugins = {}; + + +// @todo bind destroy to unload, may need to coordinate with TB +// jshint -W098 +var removeAllObjects = function removeAllObjects () { + if (mediaCaptureObject) mediaCaptureObject.destroy(); + + for (var id in plugins) { + if (plugins.hasOwnProperty(id)) { + plugins[id].destroy(); + } + } +}; + +// 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('OTPlugin.createMediaCaptureController called multiple times!'); + } + + mediaCaptureObject = injectObject(OTPlugin.meta.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(OTPlugin.meta.mimeType, true, {windowless: true}, function(err, plugin) { + if (err) { + completion.call(OTPlugin, err); + return; + } + + plugins[plugin.id] = plugin; + completion.call(OTPlugin, null, plugin); + }); + + return o; +}; + +// tb_require('./header.js') +// tb_require('./shims.js') +// tb_require('./proxy.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT:true, OTPlugin:true, ActiveXObject:true, injectObject:true, curryCallAsync:true */ /* exported AutoUpdater:true */ @@ -4665,21 +5341,38 @@ var AutoUpdater; updaterMimeType, // <- cached version, use getInstallerMimeType instead installedVersion = -1; // <- cached version, use getInstallerMimeType instead - - var versionGreaterThan = function versionGreaterThan (version1,version2) { + var versionGreaterThan = function versionGreaterThan (version1, version2) { if (version1 === version2) return false; + if (version1 === -1) return version2; + if (version2 === -1) return version1; + if (version1.indexOf('.') === -1 && version2.indexOf('.') === -1) { + return version1 > version2; + } + + // The versions have multiple components (i.e. 0.10.30) and + // must be compared piecewise. + // Note: I'm ignoring the case where one version has multiple + // components and the other doesn't. var v1 = version1.split('.'), - v2 = version2.split('.'); - - v1 = parseFloat(parseInt(v1.shift(), 10) + '.' + - v1.map(function(vcomp) { return parseInt(vcomp, 10); }).join('')); - - v2 = parseFloat(parseInt(v2.shift(), 10) + '.' + - v2.map(function(vcomp) { return parseInt(vcomp, 10); }).join('')); + v2 = version2.split('.'), + versionLength = (v1.length > v2.length ? v2 : v1).length; - return v1 > v2; + for (var i = 0; i < versionLength; ++i) { + if (parseInt(v1[i], 10) > parseInt(v2[i], 10)) { + return true; + } + } + + // Special case, v1 has extra components but the initial components + // were identical, we assume this means newer but it might also mean + // that someone changed versioning systems. + if (i < v1.length) { + return true; + } + + return false; }; @@ -4692,12 +5385,12 @@ var AutoUpdater; } var activeXControlId = 'TokBox.otiePluginInstaller', + installPluginName = 'otiePluginInstaller', unversionedMimeType = 'application/x-otieplugininstaller', - plugin = navigator.plugins[activeXControlId]; + plugin = navigator.plugins[activeXControlId] || navigator.plugins[installPluginName]; installedVersion = -1; - if (plugin) { // Look through the supported mime-types for the version // There should only be one mime-type in our use case, and @@ -4705,10 +5398,11 @@ var AutoUpdater; // version. var numMimeTypes = plugin.length, extractVersion = new RegExp(unversionedMimeType.replace('-', '\\-') + - ',version=([0-9]+)', 'i'), + ',version=([0-9a-zA-Z-_.]+)', 'i'), mimeType, bits; + for (var i=0; i ', object); - } - else { - scope.OT.info('TB Plugin - ' + message); - } -}; + notifyReadyListeners = function notifyReadyListeners (err) { + var callback; - -/// Private API - -var isDomReady = function isDomReady () { - return (_document.readyState === 'complete' || - (_document.readyState === 'interactive' && _document.body)); + while ( (callback = readyCallbacks.pop()) && OTHelpers.isFunction(callback) ) { + callback.call(OTPlugin, err); + } }, onDomReady = function onDomReady () { - var callCompletionHandlers = function(err) { - var callback; - - while ( (callback = readyCallbacks.pop()) && OT.$.isFunction(callback) ) { - callback.call(TBPlugin, err); - } - }; - AutoUpdater.get(function(err, updater) { if (err) { - OT.error('Error while loading the AutoUpdater: ' + err); - callCompletionHandlers('Error while loading the AutoUpdater: ' + err); + OTPlugin.error('Error while loading the AutoUpdater: ' + err); + notifyReadyListeners('Error while loading the AutoUpdater: ' + err); return; } @@ -4948,54 +5645,51 @@ var isDomReady = function isDomReady () { err = 'The TB Plugin failed to load properly'; } - pluginReady = true; - callCompletionHandlers(err); + pluginIsReady = true; + notifyReadyListeners(err); - OT.onUnload(destroy); + OTHelpers.onDOMUnload(destroy); }); }); - }, - - waitForDomReady = function waitForDomReady () { - if (isDomReady()) { - onDomReady(); - } - else if (_document.addEventListener) { - _document.addEventListener('DOMContentLoaded', onDomReady, false); - } else if (_document.attachEvent) { - _document.attachEvent('onreadystatechange', function() { - if (_document.readyState === 'complete') onDomReady(); - }); - } - }, - - // @todo bind destroy to unload, may need to coordinate with TB - // jshint -W098 - destroy = function destroy () { - removeAllObjects(); }; +// tb_require('./header.js') +// tb_require('./shims.js') +// tb_require('./proxy.js') +// tb_require('./auto_updater.js') +// tb_require('./media_constraints.js') +// tb_require('./peer_connection.js') +// tb_require('./media_stream.js') +// tb_require('./video_container.js') +// tb_require('./rumor.js') +// tb_require('./lifecycle.js') -/// Public API +/* jshint globalstrict: true, strict: false, undef: true, + unused: true, trailing: true, browser: true, smarttabs:true */ +/* global AutoUpdater, + RumorSocket, + MediaConstraints, PeerConnection, MediaStream, + registerReadyListener, + mediaCaptureObject, createPeerController */ -TBPlugin.isInstalled = function isInstalled () { +OTPlugin.isInstalled = function isInstalled () { if (!this.isSupported()) return false; return AutoUpdater.isinstalled(); }; -TBPlugin.version = function version () { - return pluginInfo.version; +OTPlugin.version = function version () { + return OTPlugin.meta.version; }; -TBPlugin.installedVersion = function installedVersion () { +OTPlugin.installedVersion = function installedVersion () { return AutoUpdater.installedVersion(); }; -// Returns a URI to the TBPlugin installer that is paired with -// this version of TBPlugin.js. -TBPlugin.pathToInstaller = function pathToInstaller () { +// Returns a URI to the OTPlugin installer that is paired with +// this version of OTPlugin.js. +OTPlugin.pathToInstaller = function pathToInstaller () { return 'https://s3.amazonaws.com/otplugin.tokbox.com/v' + - pluginInfo.version + '/otiePluginMain.msi'; + OTPlugin.meta.version + '/otiePluginMain.msi'; }; // Trigger +callback+ when the plugin is ready @@ -5003,31 +5697,31 @@ TBPlugin.pathToInstaller = function pathToInstaller () { // Most of the public API cannot be called until // the plugin is ready. // -TBPlugin.ready = function ready (callback) { - if (TBPlugin.isReady()) { +OTPlugin.ready = function ready (callback) { + if (OTPlugin.isReady()) { var err; if (!mediaCaptureObject || !mediaCaptureObject.isValid()) { err = 'The TB Plugin failed to load properly'; } - callback.call(TBPlugin, err); + callback.call(OTPlugin, err); } else { - readyCallbacks.push(callback); + registerReadyListener(callback); } }; -// Helper function for TBPlugin.getUserMedia +// Helper function for OTPlugin.getUserMedia var _getUserMedia = function _getUserMedia(mediaConstraints, success, error) { createPeerController(function(err, plugin) { if (err) { - error.call(TBPlugin, err); + error.call(OTPlugin, err); return; } plugin._.getUserMedia(mediaConstraints.toHash(), function(streamJson) { - success.call(TBPlugin, MediaStream.fromJson(streamJson, plugin)); + success.call(OTPlugin, MediaStream.fromJson(streamJson, plugin)); }, error); }); }; @@ -5035,7 +5729,7 @@ var _getUserMedia = function _getUserMedia(mediaConstraints, success, error) { // Equivalent to: window.getUserMedia(constraints, success, error); // // Except that the constraints won't be identical -TBPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) { +OTPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) { var constraints = new MediaConstraints(userConstraints); if (constraints.screenSharing) { @@ -5049,7 +5743,7 @@ TBPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) mediaCaptureObject.selectSources(sources, function(captureDevices) { for (var key in captureDevices) { if (captureDevices.hasOwnProperty(key)) { - OT.debug(key + ' Capture Device: ' + captureDevices[key]); + OTPlugin.debug(key + ' Capture Device: ' + captureDevices[key]); } } @@ -5062,12 +5756,12 @@ TBPlugin.getUserMedia = function getUserMedia (userConstraints, success, error) } }; -TBPlugin.initRumorSocket = function(messagingURL, completion) { - TBPlugin.ready(function(error) { +OTPlugin.initRumorSocket = function(messagingURL, completion) { + OTPlugin.ready(function(error) { if(error) { completion(error); } else { - completion(null, new PluginRumorSocket(mediaCaptureObject, messagingURL)); + completion(null, new RumorSocket(mediaCaptureObject, messagingURL)); } }); }; @@ -5076,21 +5770,26 @@ TBPlugin.initRumorSocket = function(messagingURL, completion) { // Equivalent to: var pc = new window.RTCPeerConnection(iceServers, options); // // Except that it is async and takes a completion handler -TBPlugin.initPeerConnection = function initPeerConnection (iceServers, +OTPlugin.initPeerConnection = function initPeerConnection (iceServers, options, localStream, completion) { var gotPeerObject = function(err, plugin) { if (err) { - completion.call(TBPlugin, err); + completion.call(OTPlugin, err); return; } - debug('Got PeerConnection for ' + plugin.id); - var peerConnection = new PeerConnection(iceServers, options, plugin); + OTPlugin.debug('Got PeerConnection for ' + plugin.id); + PeerConnection.create(iceServers, options, plugin, function(err, peerConnection) { + if (err) { + completion.call(OTPlugin, err); + return; + } - completion.call(TBPlugin, null, peerConnection); + completion.call(OTPlugin, null, peerConnection); + }); }; // @fixme this is nasty and brittle. We need some way to use the same Object @@ -5108,3886 +5807,380 @@ TBPlugin.initPeerConnection = function initPeerConnection (iceServers, }; // A RTCSessionDescription like object exposed for native WebRTC compatability -TBPlugin.RTCSessionDescription = function RTCSessionDescription (options) { +OTPlugin.RTCSessionDescription = function RTCSessionDescription (options) { this.type = options.type; this.sdp = options.sdp; }; // A RTCIceCandidate like object exposed for native WebRTC compatability -TBPlugin.RTCIceCandidate = function RTCIceCandidate (options) { +OTPlugin.RTCIceCandidate = function RTCIceCandidate (options) { this.sdpMid = options.sdpMid; this.sdpMLineIndex = parseInt(options.sdpMLineIndex, 10); this.candidate = options.candidate; }; +// tb_require('./api.js') -// Make this available for now -TBPlugin.debug = debug; +/* global shim, OTHelpers, onDomReady */ shim(); -waitForDomReady(); +OTHelpers.onDOMLoad(onDomReady); -// tb_require('./tb_plugin.js') /* jshint ignore:start */ })(this); /* jshint ignore:end */ -!(function() { -/*global OT:true */ - var defaultAspectRatio = 4.0/3.0, - miniWidth = 128, - miniHeight = 128, - microWidth = 64, - microHeight = 64; - // This code positions the video element so that we don't get any letterboxing. - // It will take into consideration aspect ratios other than 4/3 but only when - // the video element is first created. If the aspect ratio changes at a later point - // this calculation will become incorrect. - function fixAspectRatio(element, width, height, desiredAspectRatio, rotated) { +/* jshint ignore:start */ +!(function(window, OT) { +/* jshint ignore:end */ - if (TBPlugin.isInstalled()) { - // The plugin will sort out it's own aspect ratio, so we - // only need to tell the container to expand to fit it's parent. +// tb_require('./header.js') - OT.$.css(element, { - width: '100%', - height: '100%', - left: 0, - top: 0 - }); +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ - return; - } +// 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 (!width) width = parseInt(OT.$.width(element.parentNode), 10); - else width = parseInt(width, 10); - - if (!height) height = parseInt(OT.$.height(element.parentNode), 10); - else height = parseInt(height, 10); - - if (width === 0 || height === 0) return; - - if (!desiredAspectRatio) desiredAspectRatio = defaultAspectRatio; - - var actualRatio = (width + 0.0)/height, - props; - - props = { - width: '100%', - height: '100%', - left: 0, - top: 0 - }; - - if (actualRatio > desiredAspectRatio) { - // Width is largest so we blow up the height so we don't have letterboxing - var newHeight = (actualRatio / desiredAspectRatio) * 100; - - props.height = newHeight + '%'; - props.top = '-' + ((newHeight - 100) / 2) + '%'; - } else if (actualRatio < desiredAspectRatio) { - // Height is largest, blow up the width - var newWidth = (desiredAspectRatio / actualRatio) * 100; - - props.width = newWidth + '%'; - props.left = '-' + ((newWidth - 100) / 2) + '%'; - } - - OT.$.css(element, props); - - var video = element.querySelector('video'); - if(video) { - if(rotated) { - var w = element.offsetWidth, - h = element.offsetHeight, - diff = w - h; - props = { - width: h + 'px', - height: w + 'px', - marginTop: -(diff / 2) + 'px', - marginLeft: (diff / 2) + 'px' - }; - OT.$.css(video, props); - } else { - OT.$.css(video, { width: '', height: '', marginTop: '', marginLeft: ''}); - } - } - } - - function fixMini(container, width, height) { - var w = parseInt(width, 10), - h = parseInt(height, 10); - - if(w < microWidth || h < microHeight) { - OT.$.addClass(container, 'OT_micro'); - } else { - OT.$.removeClass(container, 'OT_micro'); - } - if(w < miniWidth || h < miniHeight) { - OT.$.addClass(container, 'OT_mini'); - } else { - OT.$.removeClass(container, 'OT_mini'); - } - } - - var getOrCreateContainer = function getOrCreateContainer(elementOrDomId, insertMode) { - var container, - domId; - - if (elementOrDomId && elementOrDomId.nodeName) { - // It looks like we were given a DOM element. Grab the id or generate - // one if it doesn't have one. - container = elementOrDomId; - if (!container.getAttribute('id') || container.getAttribute('id').length === 0) { - container.setAttribute('id', 'OT_' + OT.$.uuid()); - } - - domId = container.getAttribute('id'); - } else { - // We may have got an id, try and get it's DOM element. - container = OT.$(elementOrDomId); - domId = elementOrDomId || ('OT_' + OT.$.uuid()); - } - - if (!container) { - container = OT.$.createElement('div', {id: domId}); - container.style.backgroundColor = '#000000'; - document.body.appendChild(container); - } else { - if(!(insertMode == null || insertMode === 'replace')) { - var placeholder = document.createElement('div'); - placeholder.id = ('OT_' + OT.$.uuid()); - if(insertMode === 'append') { - container.appendChild(placeholder); - container = placeholder; - } else if(insertMode === 'before') { - container.parentNode.insertBefore(placeholder, container); - container = placeholder; - } else if(insertMode === 'after') { - container.parentNode.insertBefore(placeholder, container.nextSibling); - container = placeholder; - } - } else { - OT.$.emptyElement(container); - } - } - - return container; - }; - - // Creates the standard container that the Subscriber and Publisher use to hold - // their video element and other chrome. - OT.WidgetView = function(targetElement, properties) { - var container = getOrCreateContainer(targetElement, properties && properties.insertMode), - videoContainer = document.createElement('div'), - oldContainerStyles = {}, - dimensionsObserver, - videoElement, - videoObserver, - posterContainer, - loadingContainer, - width, - height, - loading = true, - audioOnly = false; - - if (properties) { - width = properties.width; - height = properties.height; - - if (width) { - if (typeof(width) === 'number') { - width = width + 'px'; - } - } - - if (height) { - if (typeof(height) === 'number') { - height = height + 'px'; - } - } - - container.style.width = width ? width : '264px'; - container.style.height = height ? height : '198px'; - container.style.overflow = 'hidden'; - fixMini(container, width || '264px', height || '198px'); - - if (properties.mirror === undefined || properties.mirror) { - OT.$.addClass(container, 'OT_mirrored'); - } - } - - if (properties.classNames) OT.$.addClass(container, properties.classNames); - OT.$.addClass(container, 'OT_loading'); - - - OT.$.addClass(videoContainer, 'OT_video-container'); - videoContainer.style.width = container.style.width; - videoContainer.style.height = container.style.height; - container.appendChild(videoContainer); - fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight); - - loadingContainer = document.createElement('div'); - OT.$.addClass(loadingContainer, 'OT_video-loading'); - videoContainer.appendChild(loadingContainer); - - posterContainer = document.createElement('div'); - OT.$.addClass(posterContainer, 'OT_video-poster'); - videoContainer.appendChild(posterContainer); - - oldContainerStyles.width = container.offsetWidth; - oldContainerStyles.height = container.offsetHeight; - - if (!TBPlugin.isInstalled()) { - // Observe changes to the width and height and update the aspect ratio - dimensionsObserver = OT.$.observeStyleChanges(container, ['width', 'height'], - function(changeSet) { - var width = changeSet.width ? changeSet.width[1] : container.offsetWidth, - height = changeSet.height ? changeSet.height[1] : container.offsetHeight; - fixMini(container, width, height); - fixAspectRatio(videoContainer, width, height, videoElement ? - videoElement.aspectRatio() : null); - }); - - - // @todo observe if the video container or the video element get removed - // if they do we should do some cleanup - videoObserver = OT.$.observeNodeOrChildNodeRemoval(container, function(removedNodes) { - if (!videoElement) return; - - // This assumes a video element being removed is the main video element. This may - // not be the case. - var videoRemoved = OT.$.some(removedNodes, function(node) { - return node === videoContainer || node.nodeName === 'VIDEO'; - }); - - if (videoRemoved) { - videoElement.destroy(); - videoElement = null; - } - - if (videoContainer) { - OT.$.removeElement(videoContainer); - videoContainer = null; - } - - if (dimensionsObserver) { - dimensionsObserver.disconnect(); - dimensionsObserver = null; - } - - if (videoObserver) { - videoObserver.disconnect(); - videoObserver = null; - } - }); - } - - this.destroy = function() { - if (dimensionsObserver) { - dimensionsObserver.disconnect(); - dimensionsObserver = null; - } - - if (videoObserver) { - videoObserver.disconnect(); - videoObserver = null; - } - - if (videoElement) { - videoElement.destroy(); - videoElement = null; - } - - if (container) { - OT.$.removeElement(container); - container = null; - } - }; - - this.setBackgroundImageURI = function(bgImgURI) { - if (bgImgURI.substr(0, 5) !== 'http:' && bgImgURI.substr(0, 6) !== 'https:') { - if (bgImgURI.substr(0, 22) !== 'data:image/png;base64,') { - bgImgURI = 'data:image/png;base64,' + bgImgURI; - } - } - OT.$.css(posterContainer, 'backgroundImage', 'url(' + bgImgURI + ')'); - OT.$.css(posterContainer, 'backgroundSize', 'contain'); - OT.$.css(posterContainer, 'opacity', '1.0'); - }; - - if (properties && properties.style && properties.style.backgroundImageURI) { - this.setBackgroundImageURI(properties.style.backgroundImageURI); - } - - this.bindVideo = function(webRTCStream, options, completion) { - // remove the old video element if it exists - // @todo this might not be safe, publishers/subscribers use this as well... - if (videoElement) { - videoElement.destroy(); - videoElement = null; - } - - var onError = options && options.error ? options.error : void 0; - delete options.error; - - var video = new OT.VideoElement({ attributes: options }, onError); - - // Initialize the audio volume - if (options.audioVolume) video.setAudioVolume(options.audioVolume); - - // makes the incoming audio streams take priority (will impact only FF OS for now) - video.audioChannelType('telephony'); - - video.appendTo(videoContainer).bindToStream(webRTCStream, function(err) { - if (err) { - video.destroy(); - completion(err); - return; - } - - videoElement = video; - - videoElement.on({ - orientationChanged: function(){ - fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight, - videoElement.aspectRatio(), videoElement.isRotated()); - } - }); - - var fix = function() { - fixAspectRatio(videoContainer, container.offsetWidth, container.offsetHeight, - videoElement ? videoElement.aspectRatio() : null, - videoElement ? videoElement.isRotated() : null); - }; - - if(isNaN(videoElement.aspectRatio())) { - videoElement.on('streamBound', fix); - } else { - fix(); - } - - completion(null, video); - }); - - return video; - }; - - this.video = function() { return videoElement; }; - - - OT.$.defineProperties(this, { - showPoster: { - get: function() { - return !OT.$.isDisplayNone(posterContainer); - }, - set: function(newValue) { - if(newValue) { - OT.$.show(posterContainer); - } else { - OT.$.hide(posterContainer); - } - } - }, - - poster: { - get: function() { - return OT.$.css(posterContainer, 'backgroundImage'); - }, - set: function(src) { - OT.$.css(posterContainer, 'backgroundImage', 'url(' + src + ')'); - } - }, - - loading: { - get: function() { return loading; }, - set: function(l) { - loading = l; - - if (loading) { - OT.$.addClass(container, 'OT_loading'); - } else { - OT.$.removeClass(container, 'OT_loading'); - } - } - }, - - audioOnly: { - get: function() { return audioOnly; }, - set: function(a) { - audioOnly = a; - - if (audioOnly) { - OT.$.addClass(container, 'OT_audio-only'); - } else { - OT.$.removeClass(container, 'OT_audio-only'); - } - } - }, - - domId: { - get: function() { return container.getAttribute('id'); } - } - - }); - - this.domElement = container; - - this.addError = function(errorMsg, helpMsg, classNames) { - container.innerHTML = '

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

'; - OT.$.addClass(container, classNames || 'OT_subscriber_error'); - if(container.querySelector('p').offsetHeight > container.offsetHeight) { - container.querySelector('span').style.display = 'none'; - } - }; - }; - -})(window); -// Web OT Helpers -!(function(window) { - - var NativeRTCPeerConnection = (window.webkitRTCPeerConnection || - window.mozRTCPeerConnection); - - if (navigator.webkitGetUserMedia) { - /*global webkitMediaStream, webkitRTCPeerConnection*/ - // Stub for getVideoTracks for Chrome < 26 - if (!webkitMediaStream.prototype.getVideoTracks) { - webkitMediaStream.prototype.getVideoTracks = function() { - return this.videoTracks; - }; - } - - // Stubs for getAudioTracks for Chrome < 26 - if (!webkitMediaStream.prototype.getAudioTracks) { - webkitMediaStream.prototype.getAudioTracks = function() { - return this.audioTracks; - }; - } - - if (!webkitRTCPeerConnection.prototype.getLocalStreams) { - webkitRTCPeerConnection.prototype.getLocalStreams = function() { - return this.localStreams; - }; - } - - if (!webkitRTCPeerConnection.prototype.getRemoteStreams) { - webkitRTCPeerConnection.prototype.getRemoteStreams = function() { - return this.remoteStreams; - }; - } - - } else if (navigator.mozGetUserMedia) { - // Firefox < 23 doesn't support get Video/Audio tracks, we'll just stub them out for now. - /* global MediaStream */ - if (!MediaStream.prototype.getVideoTracks) { - MediaStream.prototype.getVideoTracks = function() { - return []; - }; - } - - if (!MediaStream.prototype.getAudioTracks) { - MediaStream.prototype.getAudioTracks = function() { - return []; - }; - } - - // This won't work as mozRTCPeerConnection is a weird internal Firefox - // object (a wrapped native object I think). - // if (!window.mozRTCPeerConnection.prototype.getLocalStreams) { - // window.mozRTCPeerConnection.prototype.getLocalStreams = function() { - // return this.localStreams; - // }; - // } - - // This won't work as mozRTCPeerConnection is a weird internal Firefox - // object (a wrapped native object I think). - // if (!window.mozRTCPeerConnection.prototype.getRemoteStreams) { - // window.mozRTCPeerConnection.prototype.getRemoteStreams = function() { - // return this.remoteStreams; - // }; - // } - } - - // The setEnabled method on MediaStreamTracks is a TBPlugin - // construct. In this particular instance it's easier to bring - // all the good browsers down to IE's level than bootstrap it up. - if (typeof window.MediaStreamTrack !== 'undefined') { - if (!window.MediaStreamTrack.prototype.setEnabled) { - window.MediaStreamTrack.prototype.setEnabled = function (enabled) { - this.enabled = OT.$.castToBoolean(enabled); - }; - } - } - - - OT.$.createPeerConnection = function (config, options, publishersWebRtcStream, completion) { - if (TBPlugin.isInstalled()) { - TBPlugin.initPeerConnection(config, options, - publishersWebRtcStream, completion); - } - else { - var pc; - - try { - pc = new NativeRTCPeerConnection(config, options); - } catch(e) { - completion(e.message); - return; - } - - completion(null, pc); - } - }; - - // Returns a String representing the supported WebRTC crypto scheme. The possible - // values are SDES_SRTP, DTLS_SRTP, and NONE; - // - // Broadly: - // * Firefox only supports DTLS - // * Older versions of Chrome (<= 24) only support SDES - // * Newer versions of Chrome (>= 25) support DTLS and SDES - // - OT.$.supportedCryptoScheme = function() { - if (!OT.$.hasCapabilities('webrtc')) return 'NONE'; - - var chromeVersion = window.navigator.userAgent.toLowerCase().match(/chrome\/([0-9\.]+)/i); - return chromeVersion && parseFloat(chromeVersion[1], 10) < 25 ? 'SDES_SRTP' : 'DTLS_SRTP'; - }; - -})(window); -// Web OT Helpers -!(function(window) { - - /* jshint globalstrict: true, strict: false, undef: true, unused: true, - trailing: true, browser: true, smarttabs:true */ - /* global TBPlugin, OT */ - - /// - // Capabilities - // - // Support functions to query browser/client Media capabilities. - // - - - // Indicates whether this client supports the getUserMedia - // API. - // - OT.$.registerCapability('getUserMedia', function() { - return !!(navigator.webkitGetUserMedia || navigator.mozGetUserMedia || TBPlugin.isInstalled()); - }); - - - // TODO Remove all PeerConnection stuff, that belongs to the messaging layer not the Media layer. - // Indicates whether this client supports the PeerConnection - // API. - // - // Chrome Issues: - // * The explicit prototype.addStream check is because webkitRTCPeerConnection was - // partially implemented, but not functional, in Chrome 22. - // - // Firefox Issues: - // * No real support before Firefox 19 - // * Firefox 19 has issues with generating Offers. - // * Firefox 20 doesn't interoperate with Chrome. - // - OT.$.registerCapability('PeerConnection', function() { - var browser = OT.$.browserVersion(); - - if (navigator.webkitGetUserMedia) { - return typeof(window.webkitRTCPeerConnection) === 'function' && - !!window.webkitRTCPeerConnection.prototype.addStream; - - } else if (navigator.mozGetUserMedia) { - if (typeof(window.mozRTCPeerConnection) === 'function' && browser.version > 20.0) { - try { - new window.mozRTCPeerConnection(); - return true; - } catch (err) { - return false; - } - } - } else { - return TBPlugin.isInstalled(); - } - }); - - - // Indicates whether this client supports WebRTC - // - // This is defined as: getUserMedia + PeerConnection + exceeds min browser version - // - OT.$.registerCapability('webrtc', function() { - var browser = OT.$.browserVersion(), - minimumVersions = OT.properties.minimumVersion || {}, - minimumVersion = minimumVersions[browser.browser.toLowerCase()]; - - if(minimumVersion && minimumVersion > browser.version) { - OT.debug('Support for', browser.browser, 'is disabled because we require', - minimumVersion, 'but this is', browser.version); - return false; - } - - - return OT.$.hasCapabilities('getUserMedia', 'PeerConnection'); - }); - - - // TODO Remove all transport stuff, that belongs to the messaging layer not the Media layer. - // Indicates if the browser supports bundle - // - // Broadly: - // * Firefox doesn't support bundle - // * Chrome support bundle - // * OT Plugin supports bundle - // - OT.$.registerCapability('bundle', function() { - return OT.$.hasCapabilities('webrtc') && - (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()); - }); - - - // Indicates if the browser supports rtcp mux - // - // Broadly: - // * Older versions of Firefox (<= 25) don't support rtcp mux - // * Older versions of Firefox (>= 26) support rtcp mux (not tested yet) - // * Chrome support rtcp mux - // * OT Plugin supports rtcp mux - // - OT.$.registerCapability('RTCPMux', function() { - return OT.$.hasCapabilities('webrtc') && - (OT.$.browser() === 'Chrome' || TBPlugin.isInstalled()); - }); - - - - // Indicates whether this browser supports the getMediaDevices (getSources) API. - // - OT.$.registerCapability('getMediaDevices', function() { - return OT.$.isFunction(window.MediaStreamTrack) && - OT.$.isFunction(window.MediaStreamTrack.getSources); - }); - -})(window); -// Web OT Helpers -!(function() { - - var nativeGetUserMedia, - vendorToW3CErrors, - gumNamesToMessages, - mapVendorErrorName, - parseErrorEvent, - areInvalidConstraints; - - // Handy cross-browser getUserMedia shim. Inspired by some code from Adam Barth - nativeGetUserMedia = (function() { - if (navigator.getUserMedia) { - return OT.$.bind(navigator.getUserMedia, navigator); - } else if (navigator.mozGetUserMedia) { - return OT.$.bind(navigator.mozGetUserMedia, navigator); - } else if (navigator.webkitGetUserMedia) { - return OT.$.bind(navigator.webkitGetUserMedia, navigator); - } else if (TBPlugin.isInstalled()) { - return OT.$.bind(TBPlugin.getUserMedia, TBPlugin); - } - })(); - - // Mozilla error strings and the equivalent W3C names. NOT_SUPPORTED_ERROR does not - // exist in the spec right now, so we'll include Mozilla's error description. - // Chrome TrackStartError is triggered when the camera is already used by another app (Windows) - vendorToW3CErrors = { - PERMISSION_DENIED: 'PermissionDeniedError', - NOT_SUPPORTED_ERROR: 'NotSupportedError', - MANDATORY_UNSATISFIED_ERROR: ' ConstraintNotSatisfiedError', - NO_DEVICES_FOUND: 'NoDevicesFoundError', - HARDWARE_UNAVAILABLE: 'HardwareUnavailableError', - TrackStartError: 'HardwareUnavailableError' - }; - - gumNamesToMessages = { - PermissionDeniedError: 'End-user denied permission to hardware devices', - PermissionDismissedError: 'End-user dismissed permission to hardware devices', - NotSupportedError: 'A constraint specified is not supported by the browser.', - ConstraintNotSatisfiedError: 'It\'s not possible to satisfy one or more constraints ' + - 'passed into the getUserMedia function', - OverconstrainedError: 'Due to changes in the environment, one or more mandatory ' + - 'constraints can no longer be satisfied.', - NoDevicesFoundError: 'No voice or video input devices are available on this machine.', - HardwareUnavailableError: 'The selected voice or video devices are unavailable. Verify ' + - 'that the chosen devices are not in use by another application.' - }; - - // Map vendor error strings to names and messages if possible - mapVendorErrorName = function mapVendorErrorName(vendorErrorName, vendorErrors) { - var errorName, errorMessage; - - if(vendorErrors.hasOwnProperty(vendorErrorName)) { - errorName = vendorErrors[vendorErrorName]; - } else { - // This doesn't map to a known error from the Media Capture spec, it's - // probably a custom vendor error message. - errorName = vendorErrorName; - } - - if(gumNamesToMessages.hasOwnProperty(errorName)) { - errorMessage = gumNamesToMessages[errorName]; - } else { - errorMessage = 'Unknown Error while getting user media'; - } - - return { - name: errorName, - message: errorMessage - }; - }; - - // Parse and normalise a getUserMedia error event from Chrome or Mozilla - // @ref http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-NavigatorUserMediaError - parseErrorEvent = function parseErrorObject(event) { - var error; - - if (OT.$.isObject(event) && event.name) { - error = mapVendorErrorName(event.name, vendorToW3CErrors); - error.constraintName = event.constraintName; - } else if (typeof event === 'string') { - error = mapVendorErrorName(event, vendorToW3CErrors); - } else { - error = { - message: 'Unknown Error type while getting media' - }; - } - - return error; - }; - - // Validates a Hash of getUserMedia constraints. Currently we only - // check to see if there is at least one non-false constraint. - areInvalidConstraints = function(constraints) { - if (!constraints || !OT.$.isObject(constraints)) return true; - - for (var key in constraints) { - if(!constraints.hasOwnProperty(key)) { - continue; - } - if (constraints[key]) return false; - } - - return true; - }; - - - // A wrapper for the builtin navigator.getUserMedia. In addition to the usual - // getUserMedia behaviour, this helper method also accepts a accessDialogOpened - // and accessDialogClosed callback. - // - // @memberof OT.$ - // @private - // - // @param {Object} constraints - // A dictionary of constraints to pass to getUserMedia. See - // http://dev.w3.org/2011/webrtc/editor/getusermedia.html#idl-def-MediaStreamConstraints - // in the Media Capture and Streams spec for more info. - // - // @param {function} success - // Called when getUserMedia completes successfully. The callback will be passed a WebRTC - // Stream object. - // - // @param {function} failure - // Called when getUserMedia fails to access a user stream. It will be passed an object - // with a code property representing the error that occurred. - // - // @param {function} accessDialogOpened - // Called when the access allow/deny dialog is opened. - // - // @param {function} accessDialogClosed - // Called when the access allow/deny dialog is closed. - // - // @param {function} accessDenied - // Called when access is denied to the camera/mic. This will be either because - // the user has clicked deny or because a particular origin is permanently denied. - // - OT.$.getUserMedia = function(constraints, success, failure, accessDialogOpened, - accessDialogClosed, accessDenied, customGetUserMedia) { - - var getUserMedia = nativeGetUserMedia; - - if(OT.$.isFunction(customGetUserMedia)) { - getUserMedia = customGetUserMedia; - } - - // All constraints are false, we don't allow this. This may be valid later - // depending on how/if we integrate data channels. - if (areInvalidConstraints(constraints)) { - OT.error('Couldn\'t get UserMedia: All constraints were false'); - // Using a ugly dummy-code for now. - failure.call(null, { - name: 'NO_VALID_CONSTRAINTS', - message: 'Video and Audio was disabled, you need to enabled at least one' - }); - - return; - } - - var triggerOpenedTimer = null, - displayedPermissionDialog = false, - - finaliseAccessDialog = function() { - if (triggerOpenedTimer) { - clearTimeout(triggerOpenedTimer); - } - - if (displayedPermissionDialog && accessDialogClosed) accessDialogClosed(); - }, - - triggerOpened = function() { - triggerOpenedTimer = null; - displayedPermissionDialog = true; - - if (accessDialogOpened) accessDialogOpened(); - }, - - onStream = function(stream) { - finaliseAccessDialog(); - success.call(null, stream); - }, - - onError = function(event) { - finaliseAccessDialog(); - var error = parseErrorEvent(event); - - // The error name 'PERMISSION_DENIED' is from an earlier version of the spec - if (error.name === 'PermissionDeniedError' || error.name === 'PermissionDismissedError') { - accessDenied.call(null, error); - } else { - failure.call(null, error); - } - }; - - try { - getUserMedia(constraints, onStream, onError); - } catch (e) { - OT.error('Couldn\'t get UserMedia: ' + e.toString()); - onError(); - return; - } - - // The 'remember me' functionality of WebRTC only functions over HTTPS, if - // we aren't on HTTPS then we should definitely be displaying the access - // dialog. - // - // If we are on HTTPS, we'll wait 500ms to see if we get a stream - // immediately. If we do then the user had clicked 'remember me'. Otherwise - // we assume that the accessAllowed dialog is visible. - // - // @todo benchmark and see if 500ms is a reasonable number. It seems like - // we should know a lot quicker. - // - if (location.protocol.indexOf('https') === -1) { - // Execute after, this gives the client a chance to bind to the - // accessDialogOpened event. - triggerOpenedTimer = setTimeout(triggerOpened, 100); - - } else { - // wait a second and then trigger accessDialogOpened - triggerOpenedTimer = setTimeout(triggerOpened, 500); - } - }; - -})(); -// Web OT Helpers -!(function(window) { - - /* jshint globalstrict: true, strict: false, undef: true, unused: true, - trailing: true, browser: true, smarttabs:true */ - /* global OT */ - - /// - // Device Helpers - // - // Support functions to enumerating and guerying device info - // - - var chromeToW3CDeviceKinds = { - audio: 'audioInput', - video: 'videoInput' - }; - - - OT.$.shouldAskForDevices = function(callback) { - var MST = window.MediaStreamTrack; - - if(MST != null && OT.$.isFunction(MST.getSources)) { - window.MediaStreamTrack.getSources(function(sources) { - var hasAudio = sources.some(function(src) { - return src.kind === 'audio'; - }); - - var hasVideo = sources.some(function(src) { - return src.kind === 'video'; - }); - - callback.call(null, { video: hasVideo, audio: hasAudio }); - }); - - } else { - // This environment can't enumerate devices anyway, so we'll memorise this result. - OT.$.shouldAskForDevices = function(callback) { - setTimeout(OT.$.bind(callback, null, { video: true, audio: true })); - }; - - OT.$.shouldAskForDevices(callback); - } - }; - - - OT.$.getMediaDevices = function(callback) { - if(OT.$.hasCapabilities('getMediaDevices')) { - window.MediaStreamTrack.getSources(function(sources) { - var filteredSources = OT.$.filter(sources, function(source) { - return chromeToW3CDeviceKinds[source.kind] != null; - }); - callback(void 0, OT.$.map(filteredSources, function(source) { - return { - deviceId: source.id, - label: source.label, - kind: chromeToW3CDeviceKinds[source.kind] - }; - })); - }); - } else { - callback(new Error('This browser does not support getMediaDevices APIs')); - } - }; - -})(window); -(function(window) { - - var VideoOrientationTransforms = { - 0: 'rotate(0deg)', - 270: 'rotate(90deg)', - 90: 'rotate(-90deg)', - 180: 'rotate(180deg)' - }; - - OT.VideoOrientation = { - ROTATED_NORMAL: 0, - ROTATED_LEFT: 270, - ROTATED_RIGHT: 90, - ROTATED_UPSIDE_DOWN: 180 - }; - - var DefaultAudioVolume = 50; - - var DEGREE_TO_RADIANS = Math.PI * 2 / 360; - - // - // - // var _videoElement = new OT.VideoElement({ - // fallbackText: 'blah' - // }, errorHandler); - // - // _videoElement.bindToStream(webRtcStream, completion); // => VideoElement - // _videoElement.appendTo(DOMElement) // => VideoElement - // - // _videoElement.domElement // => DomNode - // - // _videoElement.imgData // => PNG Data string - // - // _videoElement.orientation = OT.VideoOrientation.ROTATED_LEFT; - // - // _videoElement.unbindStream(); - // _videoElement.destroy() // => Completely cleans up and - // removes the video element - // - // - OT.VideoElement = function(/* optional */ options/*, optional errorHandler*/) { - var _options = OT.$.defaults( options && !OT.$.isFunction(options) ? options : {}, { - fallbackText: 'Sorry, Web RTC is not available in your browser' - }), - - errorHandler = OT.$.isFunction(arguments[arguments.length-1]) ? - arguments[arguments.length-1] : void 0, - - orientationHandler = OT.$.bind(function(orientation) { - this.trigger('orientationChanged', orientation); - }, this), - - _videoElement = TBPlugin.isInstalled() ? - new PluginVideoElement(_options, errorHandler, orientationHandler) : - new NativeDOMVideoElement(_options, errorHandler, orientationHandler), - _streamBound = false, - _stream, - _preInitialisedVolue; - - OT.$.eventing(this); - - // Public Properties - OT.$.defineProperties(this, { - - domElement: { - get: function() { - return _videoElement.domElement(); - } - }, - - videoWidth: { - get: function() { - return _videoElement['video' + (this.isRotated() ? 'Height' : 'Width')](); - } - }, - - videoHeight: { - get: function() { - return _videoElement['video' + (this.isRotated() ? 'Width' : 'Height')](); - } - }, - - aspectRatio: { - get: function() { - return (this.videoWidth() + 0.0) / this.videoHeight(); - } - }, - - isRotated: { - get: function() { - return _videoElement.isRotated(); - } - }, - - orientation: { - get: function() { - return _videoElement.orientation(); - }, - set: function(orientation) { - _videoElement.orientation(orientation); - } - }, - - audioChannelType: { - get: function() { - return _videoElement.audioChannelType(); - }, - set: function(type) { - _videoElement.audioChannelType(type); - } - } - }); - - // Public Methods - - this.imgData = function() { - return _videoElement.imgData(); - }; - - this.appendTo = function(parentDomElement) { - _videoElement.appendTo(parentDomElement); - return this; - }; - - this.bindToStream = function(webRtcStream, completion) { - _streamBound = false; - _stream = webRtcStream; - - _videoElement.bindToStream(webRtcStream, OT.$.bind(function(err) { - if (err) { - completion(err); - return; - } - - _streamBound = true; - - if (_preInitialisedVolue) { - this.setAudioVolume(_preInitialisedVolue); - _preInitialisedVolue = null; - } - - completion(null); - }, this)); - - return this; - }; - - this.unbindStream = function() { - if (!_stream) return this; - - _stream = null; - _videoElement.unbindStream(); - return this; - }; - - this.setAudioVolume = function (value) { - if (_streamBound) _videoElement.setAudioVolume( OT.$.roundFloat(value / 100, 2) ); - else _preInitialisedVolue = value; - - return this; - }; - - this.getAudioVolume = function () { - if (_streamBound) return parseInt(_videoElement.getAudioVolume() * 100, 10); - else return _preInitialisedVolue || 50; - }; - - - this.whenTimeIncrements = function (callback, context) { - _videoElement.whenTimeIncrements(callback, context); - return this; - }; - - this.destroy = function () { - // unbind all events so they don't fire after the object is dead - this.off(); - - _videoElement.destroy(); - return void 0; - }; - }; - - var PluginVideoElement = function PluginVideoElement (options, - errorHandler, - orientationChangedHandler) { - var _videoProxy, - _parentDomElement; - - canBeOrientatedMixin(this, - function() { return _videoProxy.domElement; }, - orientationChangedHandler); - - /// Public methods - - this.domElement = function() { - return _videoProxy ? _videoProxy.domElement : void 0; - }; - - this.videoWidth = function() { - return _videoProxy ? _videoProxy.getVideoWidth() : void 0; - }; - - this.videoHeight = function() { - return _videoProxy ? _videoProxy.getVideoHeight() : void 0; - }; - - this.imgData = function() { - return _videoProxy ? _videoProxy.getImgData() : null; - }; - - // Append the Video DOM element to a parent node - this.appendTo = function(parentDomElement) { - _parentDomElement = parentDomElement; - return this; - }; - - // Bind a stream to the video element. - this.bindToStream = function(webRtcStream, completion) { - if (!_parentDomElement) { - completion('The VideoElement must attached to a DOM node before a stream can be bound'); - return; - } - - _videoProxy = webRtcStream._.render(); - _videoProxy.appendTo(_parentDomElement); - _videoProxy.show(completion); - - return this; - }; - - // Unbind the currently bound stream from the video element. - this.unbindStream = function() { - // TODO: some way to tell TBPlugin to release that stream and controller - - if (_videoProxy) { - _videoProxy.destroy(); - _parentDomElement = null; - _videoProxy = null; - } - - return this; - }; - - this.setAudioVolume = function(value) { - if (_videoProxy) _videoProxy.setVolume(value); - }; - - this.getAudioVolume = function() { - // Return the actual volume of the DOM element - if (_videoProxy) return _videoProxy.getVolume(); - return DefaultAudioVolume; - }; - - // see https://wiki.mozilla.org/WebAPI/AudioChannels - // The audioChannelType is not currently supported in the plugin. - this.audioChannelType = function(/* type */) { - return 'unknown'; - }; - - this.whenTimeIncrements = function(callback, context) { - // exists for compatibility with NativeVideoElement - OT.$.callAsync(OT.$.bind(callback, context)); - }; - - this.destroy = function() { - this.unbindStream(); - - return void 0; - }; - }; - - - var NativeDOMVideoElement = function NativeDOMVideoElement (options, - errorHandler, - orientationChangedHandler) { - var _domElement, - _videoElementMovedWarning = false; - - - /// Private API - var _onVideoError = OT.$.bind(function(event) { - var reason = 'There was an unexpected problem with the Video Stream: ' + - videoElementErrorCodeToStr(event.target.error.code); - errorHandler(reason, this, 'VideoElement'); - }, this), - - // The video element pauses itself when it's reparented, this is - // unfortunate. This function plays the video again and is triggered - // on the pause event. - _playVideoOnPause = function() { - if(!_videoElementMovedWarning) { - OT.warn('Video element paused, auto-resuming. If you intended to do this, ' + - 'use publishVideo(false) or subscribeToVideo(false) instead.'); - - _videoElementMovedWarning = true; - } - - _domElement.play(); - }; - - - _domElement = createNativeVideoElement(options.fallbackText, options.attributes); - - _domElement.addEventListener('pause', _playVideoOnPause); - - canBeOrientatedMixin(this, function() { return _domElement; }, orientationChangedHandler); - - /// Public methods - - this.domElement = function() { - return _domElement; - }; - - this.videoWidth = function() { - return _domElement.videoWidth; - }; - - this.videoHeight = function() { - return _domElement.videoHeight; - }; - - this.imgData = function() { - var canvas = OT.$.createElement('canvas', { - width: _domElement.videoWidth, - height: _domElement.videoHeight, - style: { display: 'none' } - }); - - document.body.appendChild(canvas); - try { - canvas.getContext('2d').drawImage(_domElement, 0, 0, canvas.width, canvas.height); - } catch(err) { - OT.warn('Cannot get image data yet'); - return null; - } - var imgData = canvas.toDataURL('image/png'); - - OT.$.removeElement(canvas); - - return OT.$.trim(imgData.replace('data:image/png;base64,', '')); - }; - - // Append the Video DOM element to a parent node - this.appendTo = function(parentDomElement) { - parentDomElement.appendChild(_domElement); - return this; - }; - - // Bind a stream to the video element. - this.bindToStream = function(webRtcStream, completion) { - bindStreamToNativeVideoElement(_domElement, webRtcStream, function(err) { - if (err) { - completion(err); - return; - } - - _domElement.addEventListener('error', _onVideoError, false); - completion(null); - }); - - return this; - }; - - - // Unbind the currently bound stream from the video element. - this.unbindStream = function() { - if (_domElement) { - unbindNativeStream(_domElement); - } - - return this; - }; - - this.setAudioVolume = function(value) { - if (_domElement) _domElement.volume = value; - }; - - this.getAudioVolume = function() { - // Return the actual volume of the DOM element - if (_domElement) return _domElement.volume; - return DefaultAudioVolume; - }; - - // see https://wiki.mozilla.org/WebAPI/AudioChannels - // The audioChannelType is currently only available in Firefox. This property returns - // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel" - this.audioChannelType = function(type) { - if (type !== void 0) { - _domElement.mozAudioChannelType = type; - } - - if ('mozAudioChannelType' in _domElement) { - return _domElement.mozAudioChannelType; - } else { - return 'unknown'; - } - }; - - this.whenTimeIncrements = function(callback, context) { - if(_domElement) { - var lastTime, handler; - handler = OT.$.bind(function() { - if(!lastTime || lastTime >= _domElement.currentTime) { - lastTime = _domElement.currentTime; - } else { - _domElement.removeEventListener('timeupdate', handler, false); - callback.call(context, this); - } - }, this); - _domElement.addEventListener('timeupdate', handler, false); - } - }; - - this.destroy = function() { - this.unbindStream(); - - if (_domElement) { - // Unbind this first, otherwise it will trigger when the - // video element is removed from the DOM. - _domElement.removeEventListener('pause', _playVideoOnPause); - - OT.$.removeElement(_domElement); - _domElement = null; - } - - return void 0; - }; - }; - -/// Private Helper functions - - // A mixin to create the orientation API implementation on +self+ - // +getDomElementCallback+ is a function that the mixin will call when it wants to - // get the native Dom element for +self+. - // - // +initialOrientation+ sets the initial orientation (shockingly), it's currently unused - // so the initial value is actually undefined. - // - var canBeOrientatedMixin = function canBeOrientatedMixin (self, - getDomElementCallback, - orientationChangedHandler, - initialOrientation) { - var _orientation = initialOrientation; - - OT.$.defineProperties(self, { - isRotated: { - get: function() { - return this.orientation() && - (this.orientation().videoOrientation === 270 || - this.orientation().videoOrientation === 90); - } - }, - - orientation: { - get: function() { return _orientation; }, - set: function(orientation) { - _orientation = orientation; - - var transform = VideoOrientationTransforms[orientation.videoOrientation] || - VideoOrientationTransforms.ROTATED_NORMAL; - - switch(OT.$.browser()) { - case 'Chrome': - case 'Safari': - getDomElementCallback().style.webkitTransform = transform; - break; - - case 'IE': - if (OT.$.browserVersion().version >= 9) { - getDomElementCallback().style.msTransform = transform; - } - else { - // So this basically defines matrix that represents a rotation - // of a single vector in a 2d basis. - // - // R = [cos(Theta) -sin(Theta)] - // [sin(Theta) cos(Theta)] - // - // Where Theta is the number of radians to rotate by - // - // Then to rotate the vector v: - // v' = Rv - // - // We then use IE8 Matrix filter property, which takes - // a 2x2 rotation matrix, to rotate our DOM element. - // - var radians = orientation.videoOrientation * DEGREE_TO_RADIANS, - element = getDomElementCallback(), - costheta = Math.cos(radians), - sintheta = Math.sin(radians); - - // element.filters.item(0).M11 = costheta; - // element.filters.item(0).M12 = -sintheta; - // element.filters.item(0).M21 = sintheta; - // element.filters.item(0).M22 = costheta; - - element.style.filter = 'progid:DXImageTransform.Microsoft.Matrix(' + - 'M11='+costheta+',' + - 'M12='+(-sintheta)+',' + - 'M21='+sintheta+',' + - 'M22='+costheta+',SizingMethod=\'auto expand\')'; - } - - - break; - - default: - // The standard version, just Firefox, Opera, and IE > 9 - getDomElementCallback().style.transform = transform; - } - - orientationChangedHandler(_orientation); - - } - }, - - // see https://wiki.mozilla.org/WebAPI/AudioChannels - // The audioChannelType is currently only available in Firefox. This property returns - // "unknown" in other browser. The related HTML tag attribute is "mozaudiochannel" - audioChannelType: { - get: function() { - if ('mozAudioChannelType' in this.domElement) { - return this.domElement.mozAudioChannelType; - } else { - return 'unknown'; - } - }, - set: function(type) { - if ('mozAudioChannelType' in this.domElement) { - this.domElement.mozAudioChannelType = type; - } - } - } - }); - }; - - function createNativeVideoElement(fallbackText, attributes) { - var videoElement = document.createElement('video'); - videoElement.setAttribute('autoplay', ''); - videoElement.innerHTML = fallbackText; - - if (attributes) { - if (attributes.muted === true) { - delete attributes.muted; - videoElement.muted = 'true'; - } - - for (var key in attributes) { - if(!attributes.hasOwnProperty(key)) { - continue; - } - videoElement.setAttribute(key, attributes[key]); - } - } - - return videoElement; - } - - - // See http://www.w3.org/TR/2010/WD-html5-20101019/video.html#error-codes - var _videoErrorCodes = {}; - - // Checking for window.MediaError for IE compatibility, just so we don't throw - // exceptions when the script is included - if (window.MediaError) { - _videoErrorCodes[window.MediaError.MEDIA_ERR_ABORTED] = 'The fetching process for the media ' + - 'resource was aborted by the user agent at the user\'s request.'; - _videoErrorCodes[window.MediaError.MEDIA_ERR_NETWORK] = 'A network error of some description ' + - 'caused the user agent to stop fetching the media resource, after the resource was ' + - 'established to be usable.'; - _videoErrorCodes[window.MediaError.MEDIA_ERR_DECODE] = 'An error of some description ' + - 'occurred while decoding the media resource, after the resource was established to be ' + - ' usable.'; - _videoErrorCodes[window.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED] = 'The media resource ' + - 'indicated by the src attribute was not suitable.'; - } - - function videoElementErrorCodeToStr(errorCode) { - return _videoErrorCodes[parseInt(errorCode, 10)] || 'An unknown error occurred.'; - } - - function bindStreamToNativeVideoElement(videoElement, webRtcStream, completion) { - var cleanup, - onLoad, - onError, - onStoppedLoading, - timeout; - - // Note: onloadedmetadata doesn't fire in Chrome for audio only crbug.com/110938 - // After version 36 it will fire if the video track is disabled. - var browser = OT.$.browserVersion(), - needsDisabledAudioProtection = browser.browser === 'Chrome' && browser.version < 36; - - if (navigator.mozGetUserMedia || !(needsDisabledAudioProtection && - (webRtcStream.getVideoTracks().length > 0 && webRtcStream.getVideoTracks()[0].enabled))) { - - cleanup = function cleanup () { - clearTimeout(timeout); - videoElement.removeEventListener('loadedmetadata', onLoad, false); - videoElement.removeEventListener('error', onError, false); - webRtcStream.onended = null; - }; - - onLoad = function onLoad () { - cleanup(); - completion(null); - }; - - onError = function onError (event) { - cleanup(); - unbindNativeStream(videoElement); - completion('There was an unexpected problem with the Video Stream: ' + - videoElementErrorCodeToStr(event.target.error.code)); - }; - - onStoppedLoading = function onStoppedLoading () { - // The stream ended before we fully bound it. Maybe the other end called - // stop on it or something else went wrong. - cleanup(); - unbindNativeStream(videoElement); - completion('Stream ended while trying to bind it to a video element.'); - }; - - // Timeout if it takes too long - timeout = setTimeout(OT.$.bind(function() { - if (videoElement.currentTime === 0) { - cleanup(); - completion('The video stream failed to connect. Please notify the site ' + - 'owner if this continues to happen.'); - } else if (webRtcStream.ended === true) { - // The ended event should have fired by here, but support for it isn't - // always so awesome. - onStoppedLoading(); - } else { - - OT.warn('Never got the loadedmetadata event but currentTime > 0'); - onLoad(null); - } - }, this), 30000); - - videoElement.addEventListener('loadedmetadata', onLoad, false); - videoElement.addEventListener('error', onError, false); - webRtcStream.onended = onStoppedLoading; - } else { - OT.$.callAsync(completion, null); - } - - // The official spec way is 'srcObject', we are slowly converging there. - if (videoElement.srcObject !== void 0) { - videoElement.srcObject = webRtcStream; - } else if (videoElement.mozSrcObject !== void 0) { - videoElement.mozSrcObject = webRtcStream; - } else { - videoElement.src = window.URL.createObjectURL(webRtcStream); - } - - videoElement.play(); - } - - - function unbindNativeStream(videoElement) { - if (videoElement.srcObject !== void 0) { - videoElement.srcObject = null; - } else if (videoElement.mozSrcObject !== void 0) { - videoElement.mozSrcObject = null; - } else { - window.URL.revokeObjectURL(videoElement.src); - } - } - - -})(window); -// tb_require('../helpers/helpers.js') - -!(function() { - /* jshint globalstrict: true, strict: false, undef: true, unused: true, - trailing: true, browser: true, smarttabs:true */ - /* global OT */ - - var currentGuidStorage, - currentGuid; - - var isInvalidStorage = function isInvalidStorage (storageInterface) { - return !(OT.$.isFunction(storageInterface.get) && OT.$.isFunction(storageInterface.set)); - }; - - var getClientGuid = function getClientGuid (completion) { - if (currentGuid) { - completion(null, currentGuid); - return; - } - - // It's the first time that getClientGuid has been called - // in this page lifetime. Attempt to load any existing Guid - // from the storage - currentGuidStorage.get(completion); - }; - - OT.overrideGuidStorage = function (storageInterface) { - if (isInvalidStorage(storageInterface)) { - throw new Error('The storageInterface argument does not seem to be valid, ' + - 'it must implement get and set methods'); - } - - if (currentGuidStorage === storageInterface) { - return; - } - - currentGuidStorage = storageInterface; - - // If a client Guid has already been assigned to this client then - // let the new storage know about it so that it's in sync. - if (currentGuid) { - currentGuidStorage.set(currentGuid, function(error) { - if (error) { - OT.error('Failed to send initial Guid value (' + currentGuid + - ') to the newly assigned Guid Storage. The error was: ' + error); - // @todo error - } - }); - } - }; - - if (!OT._) OT._ = {}; - OT._.getClientGuid = function (completion) { - getClientGuid(function(error, guid) { - if (error) { - completion(error); - return; - } - - if (!guid) { - // Nothing came back, this client is entirely new. - // generate a new Guid and persist it - guid = OT.$.uuid(); - currentGuidStorage.set(guid, function(error) { - if (error) { - completion(error); - return; - } - - currentGuid = guid; - }); - } - else if (!currentGuid) { - currentGuid = guid; - } - - completion(null, currentGuid); - }); - }; - - - // Implement our default storage mechanism, which sets/gets a cookie - // called 'opentok_client_id' - OT.overrideGuidStorage({ - get: function(completion) { - completion(null, OT.$.getCookie('opentok_client_id')); - }, - - set: function(guid, completion) { - OT.$.setCookie('opentok_client_id', guid); - completion(null); - } - }); - -})(window); -!(function(window) { - - // Singleton interval - var logQueue = [], - queueRunning = false; - - - OT.Analytics = function() { - - var endPoint = OT.properties.loggingURL + '/logging/ClientEvent', - endPointQos = OT.properties.loggingURL + '/logging/ClientQos', - - reportedErrors = {}, - - // Map of camel-cased keys to underscored - camelCasedKeys, - - browser = OT.$.browserVersion(), - - send = function(data, isQos, callback) { - OT.$.post((isQos ? endPointQos : endPoint) + '?_=' + OT.$.uuid.v4(), { - body: data, - xdomainrequest: (browser.browser === 'IE' && browser.version < 10), - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - } - }, callback); - }, - - throttledPost = function() { - // Throttle logs so that they only happen 1 at a time - if (!queueRunning && logQueue.length > 0) { - queueRunning = true; - var curr = logQueue[0]; - - // Remove the current item and send the next log - var processNextItem = function() { - logQueue.shift(); - queueRunning = false; - throttledPost(); - }; - - if (curr) { - send(curr.data, curr.isQos, function(err) { - if(err) { - OT.debug('Failed to send ClientEvent, moving on to the next item.'); - // There was an error, move onto the next item - } else { - curr.onComplete(); - } - setTimeout(processNextItem, 50); - }); - } - } - }, - - post = function(data, onComplete, isQos) { - logQueue.push({ - data: data, - onComplete: onComplete, - isQos: isQos - }); - - throttledPost(); - }, - - shouldThrottleError = function(code, type, partnerId) { - if (!partnerId) return false; - - var errKey = [partnerId, type, code].join('_'), - //msgLimit = DynamicConfig.get('exceptionLogging', 'messageLimitPerPartner', partnerId); - msgLimit = 100; - if (msgLimit === null || msgLimit === undefined) return false; - return (reportedErrors[errKey] || 0) <= msgLimit; - }; - - camelCasedKeys = { - payloadType: 'payload_type', - partnerId: 'partner_id', - streamId: 'stream_id', - sessionId: 'session_id', - connectionId: 'connection_id', - widgetType: 'widget_type', - widgetId: 'widget_id', - avgAudioBitrate: 'avg_audio_bitrate', - avgVideoBitrate: 'avg_video_bitrate', - localCandidateType: 'local_candidate_type', - remoteCandidateType: 'remote_candidate_type', - transportType: 'transport_type' - }; - - // Log an error via ClientEvents. - // - // @param [String] code - // @param [String] type - // @param [String] message - // @param [Hash] details additional error details - // - // @param [Hash] options the options to log the client event with. - // @option options [String] action The name of the Event that we are logging. E.g. - // 'TokShowLoaded'. Required. - // @option options [String] variation Usually used for Split A/B testing, when you - // have multiple variations of the +_action+. - // @option options [String] payloadType A text description of the payload. Required. - // @option options [String] payload The payload. Required. - // @option options [String] sessionId The active OpenTok session, if there is one - // @option options [String] connectionId The active OpenTok connectionId, if there is one - // @option options [String] partnerId - // @option options [String] guid ... - // @option options [String] widgetId ... - // @option options [String] streamId ... - // @option options [String] section ... - // @option options [String] build ... - // - // Reports will be throttled to X reports (see exceptionLogging.messageLimitPerPartner - // from the dynamic config for X) of each error type for each partner. Reports can be - // disabled/enabled globally or on a per partner basis (per partner settings - // take precedence) using exceptionLogging.enabled. - // - this.logError = function(code, type, message, details, options) { - if (!options) options = {}; - var partnerId = options.partnerId; - - if (OT.Config.get('exceptionLogging', 'enabled', partnerId) !== true) { - return; - } - - if (shouldThrottleError(code, type, partnerId)) { - //OT.log('ClientEvents.error has throttled an error of type ' + type + '.' + - // code + ' for partner ' + (partnerId || 'No Partner Id')); - return; - } - - var errKey = [partnerId, type, code].join('_'), - - payload = this.escapePayload(OT.$.extend(details || {}, { - message: payload, - userAgent: OT.$.userAgent() - })); - - - reportedErrors[errKey] = typeof(reportedErrors[errKey]) !== 'undefined' ? - reportedErrors[errKey] + 1 : 1; - - return this.logEvent(OT.$.extend(options, { - action: type + '.' + code, - payloadType: payload[0], - payload: payload[1] - })); - }; - - // Log a client event to the analytics backend. - // - // @example Logs a client event called 'foo' - // OT.ClientEvents.log({ - // action: 'foo', - // payload_type: 'foo's payload', - // payload: 'bar', - // session_id: sessionId, - // connection_id: connectionId - // }) - // - // @param [Hash] options the options to log the client event with. - // @option options [String] action The name of the Event that we are logging. - // E.g. 'TokShowLoaded'. Required. - // @option options [String] variation Usually used for Split A/B testing, when - // you have multiple variations of the +_action+. - // @option options [String] payloadType A text description of the payload. Required. - // @option options [String] payload The payload. Required. - // @option options [String] session_id The active OpenTok session, if there is one - // @option options [String] connection_id The active OpenTok connectionId, if there is one - // @option options [String] partner_id - // @option options [String] guid ... - // @option options [String] widget_id ... - // @option options [String] stream_id ... - // @option options [String] section ... - // @option options [String] build ... - // - this.logEvent = function(options) { - var partnerId = options.partnerId; - - if (!options) options = {}; - - OT._.getClientGuid(function(error, guid) { - if (error) { - // @todo - return; - } - - // Set a bunch of defaults - var data = OT.$.extend({ - 'variation' : '', - 'guid' : guid, - 'widget_id' : '', - 'session_id': '', - 'connection_id': '', - 'stream_id' : '', - 'partner_id' : partnerId, - 'source' : window.location.href, - 'section' : '', - 'build' : '' - }, options), - - onComplete = function(){ - // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] - // + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}'); - }; - - // We camel-case our names, but the ClientEvents backend wants them - // underscored... - for (var key in camelCasedKeys) { - if (camelCasedKeys.hasOwnProperty(key) && data[key]) { - data[camelCasedKeys[key]] = data[key]; - delete data[key]; - } - } - - post(data, onComplete, false); - }); - }; - - // Log a client QOS to the analytics backend. - // - this.logQOS = function(options) { - var partnerId = options.partnerId; - - if (!options) options = {}; - - OT._.getClientGuid(function(error, guid) { - if (error) { - // @todo - return; - } - - // Set a bunch of defaults - var data = OT.$.extend({ - 'guid' : guid, - 'widget_id' : '', - 'session_id': '', - 'connection_id': '', - 'stream_id' : '', - 'partner_id' : partnerId, - 'source' : window.location.href, - 'build' : '', - 'duration' : 0 //in milliseconds - }, options), - - onComplete = function(){ - // OT.log('logged: ' + '{action: ' + data['action'] + ', variation: ' + data['variation'] - // + ', payload_type: ' + data['payload_type'] + ', payload: ' + data['payload'] + '}'); - }; - - // We camel-case our names, but the ClientEvents backend wants them - // underscored... - for (var key in camelCasedKeys) { - if (camelCasedKeys.hasOwnProperty(key)) { - if(data[key]) { - data[camelCasedKeys[key]] = data[key]; - } - delete data[key]; - } - } - - post(data, onComplete, true); - }); - }; - - // Converts +payload+ to two pipe seperated strings. Doesn't currently handle - // edgecases, e.g. escaping '\\|' will break stuff. - // - // *Note:* It strip any keys that have null values. - this.escapePayload = function(payload) { - var escapedPayload = [], - escapedPayloadDesc = []; - - for (var key in payload) { - if (payload.hasOwnProperty(key) && payload[key] !== null && payload[key] !== undefined) { - escapedPayload.push( payload[key] ? payload[key].toString().replace('|', '\\|') : '' ); - escapedPayloadDesc.push( key.toString().replace('|', '\\|') ); - } - } - - return [ - escapedPayloadDesc.join('|'), - escapedPayload.join('|') - ]; - }; - }; - -})(window); -!(function() { - - OT.$.registerCapability('audioOutputLevelStat', function() { - return OT.$.browserVersion().browser === 'Chrome'; - }); - - OT.$.registerCapability('webAudioCapableRemoteStream', function() { - return OT.$.browserVersion().browser === 'Firefox'; - }); - - OT.$.registerCapability('getStatsWithSingleParameter', function() { - return OT.$.browserVersion().browser === 'Chrome'; - }); - - OT.$.registerCapability('webAudio', function() { - return 'AudioContext' in window; - }); - -})(); -!(function(window) { - - // This is not obvious, so to prevent end-user frustration we'll let them know - // explicitly rather than failing with a bunch of permission errors. We don't - // handle this using an OT Exception as it's really only a development thing. - if (location.protocol === 'file:') { - /*global alert*/ - alert('You cannot test a page using WebRTC through the file system due to browser ' + - 'permissions. You must run it over a web server.'); - } - - if (!window.OT) window.OT = {}; - - if (!window.URL && window.webkitURL) { - window.URL = window.webkitURL; - } - - var _analytics = new OT.Analytics(); - - var // Global parameters used by upgradeSystemRequirements - _intervalId, - _lastHash = document.location.hash; - - -/** -* The first step in using the OpenTok API is to call the OT.initSession() -* method. Other methods of the OT object check for system requirements and set up error logging. -* -* @class OT -*/ - -/** -*

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

-*

-* You connect to an OpenTok session using the connect() method -* of the Session object returned by the OT.initSession() method. -* Note that calling OT.initSession() does not initiate communications -* with the cloud. It simply initializes the Session object that you can use to -* connect (and to perform other operations once connected). -*

-* -*

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

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

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

-*

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

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

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

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

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

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

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

    -*

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

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

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

    -*

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

    -*

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

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

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

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

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

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

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

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

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

-* @see OT.checkSystemRequirements() -* @method OT.upgradeSystemRequirements -* @memberof OT -*/ - OT.upgradeSystemRequirements = function(){ - // trigger after the OT environment has loaded - OT.onLoad( function() { - - if(TBPlugin.isSupported()) { - OT.Dialogs.Plugin.promptToInstall().on({ - download: function() { - window.location = TBPlugin.pathToInstaller(); - }, - refresh: function() { - location.reload(); - }, - closed: function() {} - }); - return; - } - - var id = '_upgradeFlash'; - - // Load the iframe over the whole page. - document.body.appendChild((function() { - var d = document.createElement('iframe'); - d.id = id; - d.style.position = 'absolute'; - d.style.position = 'fixed'; - d.style.height = '100%'; - d.style.width = '100%'; - d.style.top = '0px'; - d.style.left = '0px'; - d.style.right = '0px'; - d.style.bottom = '0px'; - d.style.zIndex = 1000; - try { - d.style.backgroundColor = 'rgba(0,0,0,0.2)'; - } catch (err) { - // Old IE browsers don't support rgba and we still want to show the upgrade message - // but we just make the background of the iframe completely transparent. - d.style.backgroundColor = 'transparent'; - d.setAttribute('allowTransparency', 'true'); - } - d.setAttribute('frameBorder', '0'); - d.frameBorder = '0'; - d.scrolling = 'no'; - d.setAttribute('scrolling', 'no'); - - var browser = OT.$.browserVersion(), - minimumBrowserVersion = OT.properties.minimumVersion[browser.browser.toLowerCase()], - isSupportedButOld = minimumBrowserVersion > browser.version; - d.src = OT.properties.assetURL + '/html/upgrade.html#' + - encodeURIComponent(isSupportedButOld ? 'true' : 'false') + ',' + - encodeURIComponent(JSON.stringify(OT.properties.minimumVersion)) + '|' + - encodeURIComponent(document.location.href); - - return d; - })()); - - // Now we need to listen to the event handler if the user closes this dialog. - // Since this is from an IFRAME within another domain we are going to listen to hash - // changes. The best cross browser solution is to poll for a change in the hashtag. - if (_intervalId) clearInterval(_intervalId); - _intervalId = setInterval(function(){ - var hash = document.location.hash, - re = /^#?\d+&/; - if (hash !== _lastHash && re.test(hash)) { - _lastHash = hash; - if (hash.replace(re, '') === 'close_window'){ - document.body.removeChild(document.getElementById(id)); - document.location.hash = ''; - } - } - }, 100); - }); - }; - - - OT.reportIssue = function(){ - OT.warn('ToDo: haven\'t yet implemented OT.reportIssue'); - }; - - OT.components = {}; - OT.sessions = {}; - - // namespaces - OT.rtc = {}; +var OT = window.OT || {}; // Define the APIKEY this is a global parameter which should not change - OT.APIKEY = (function(){ - // Script embed - var scriptSrc = (function(){ - var s = document.getElementsByTagName('script'); - s = s[s.length - 1]; - s = s.getAttribute('src') || s.src; - return s; - })(); - - var m = scriptSrc.match(/[\?\&]apikey=([^&]+)/i); - return m ? m[1] : ''; +OT.APIKEY = (function(){ + // Script embed + var scriptSrc = (function(){ + var s = document.getElementsByTagName('script'); + s = s[s.length - 1]; + s = s.getAttribute('src') || s.src; + return s; })(); - OT.HAS_REQUIREMENTS = 1; - OT.NOT_HAS_REQUIREMENTS = 0; + var m = scriptSrc.match(/[\?\&]apikey=([^&]+)/i); + return m ? m[1] : ''; +})(); -/** -* This method is deprecated. Use on() or once() instead. -* -*

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

-* -*

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

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

-* If a handler is not registered for an event, the event is ignored locally. If the event -* listener function does not exist, the event is ignored locally. -*

-*

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

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

-* Removes an event listener for a specific event. -*

-* -*

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

-* -* @param {String} type The string identifying the type of event. -* -* @param {Function} listener The event listener function to remove. -* -* @see off() -* @memberof OT -* @method removeEventListener -*/ +if (!window.OT) window.OT = OT; +if (!window.TB) window.TB = OT; + +// tb_require('../js/ot.js') + +OT.properties = { + version: 'v2.4.0', // The current version (eg. v2.0.4) (This is replaced by gradle) + build: '54ae164', // The current build hash (This is replaced by gradle) + + // Whether or not to turn on debug logging by default + debug: 'false', + // The URL of the tokbox website + websiteURL: 'http://www.tokbox.com', + + // The URL of the CDN + cdnURL: 'http://static.opentok.com', + // The URL to use for logging + loggingURL: 'http://hlg.tokbox.com/prod', + + // The anvil API URL + apiURL: 'http://anvil.opentok.com', + + // What protocol to use when connecting to the rumor web socket + messagingProtocol: 'wss', + // What port to use when connection to the rumor web socket + messagingPort: 443, + + // If this environment supports SSL + supportSSL: 'true', + // The CDN to use if we're using SSL + cdnURLSSL: 'https://static.opentok.com', + // The URL to use for logging + loggingURLSSL: 'https://hlg.tokbox.com/prod', + + // The anvil API URL to use if we're using SSL + apiURLSSL: 'https://anvil.opentok.com', + + minimumVersion: { + firefox: parseFloat('29'), + chrome: parseFloat('34') + } +}; + + +// tb_require('../ot.js') +// tb_require('../../conf/properties.js'); + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + + +// Mount OTHelpers on OT.$ +OT.$ = window.OTHelpers; + +// Allow events to be bound on OT +OT.$.eventing(OT); + +// REMOVE THIS POST IE MERGE + +OT.$.defineGetters = function(self, getters, enumerable) { + var propsDefinition = {}; + + if (enumerable === void 0) enumerable = false; + + for (var key in getters) { + if(!getters.hasOwnProperty(key)) { + continue; + } + + propsDefinition[key] = { + get: getters[key], + enumerable: enumerable + }; + } + + Object.defineProperties(self, propsDefinition); +}; + +// STOP REMOVING HERE + +// OT.$.Modal was OT.Modal before the great common-js-helpers move +OT.Modal = OT.$.Modal; + +// Add logging methods +OT.$.useLogHelpers(OT); + +var _debugHeaderLogged = false, + _setLogLevel = OT.setLogLevel; + +// On the first time log level is set to DEBUG (or higher) show version info. +OT.setLogLevel = function(level) { + // Set OT.$ to the same log level + OT.$.setLogLevel(level); + var retVal = _setLogLevel.call(OT, level); + if (OT.shouldLog(OT.DEBUG) && !_debugHeaderLogged) { + OT.debug('OpenTok JavaScript library ' + OT.properties.version); + OT.debug('Release notes: ' + OT.properties.websiteURL + + '/opentok/webrtc/docs/js/release-notes.html'); + OT.debug('Known issues: ' + OT.properties.websiteURL + + '/opentok/webrtc/docs/js/release-notes.html#knownIssues'); + _debugHeaderLogged = true; + } + OT.debug('OT.setLogLevel(' + retVal + ')'); + return retVal; +}; + +var debugTrue = OT.properties.debug === 'true' || OT.properties.debug === true; +OT.setLogLevel(debugTrue ? OT.DEBUG : OT.ERROR); + + +// Patch the userAgent to ref OTPlugin, if it's installed. +if (OTPlugin && OTPlugin.isInstalled()) { + OT.$.env.userAgent += '; OTPlugin ' + OTPlugin.version(); +} + +// @todo remove this +OT.$.userAgent = function() { return OT.$.env.userAgent; }; /** -* Adds an event handler function for one or more events. -* -*

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

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

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

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

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

-* -* @param {String} type The string identifying the type of event. -* @param {Function} handler The handler function to process the event. This function takes the event -* object as a parameter. -* @param {Object} context (Optional) Defines the value of this in the event handler -* function. -* -* @memberof OT -* @method on -* @see off() -* @see once() -* @see Events -*/ + * 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. + *
  • + *
+ *

+ * + * @name OT.setLogLevel + * @memberof OT + * @function + * @see OT.log() + */ /** -* 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 -*/ + * 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:

- * - * - * - * @method #preventDefault - * @memberof Event - */ - /** - * Whether the default event behavior has been prevented via a call to - * preventDefault() (true) or not (false). - * See preventDefault(). - * @method #isDefaultPrevented - * @return {Boolean} - * @memberof Event - */ +// 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.

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

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
- * code - * - * - * title - *
- * 1004 - * - * - * Authentication error - *
- * 1005 - * - * - * Invalid Session ID - *
- * 1006 - * - * - * Connect Failed - *
- * 1007 - * - * - * Connect Rejected - *
- * 1008 - * - * - * Connect Time-out - *
- * 1009 - * - * - * Security Error - *
- * 1010 - * - * - * Not Connected - *
- * 1011 - * - * - * Invalid Parameter - *
- * 1013 - * - * Connection Failed - *
- * 1014 - * - * API Response Failure - *
- * 1500 - * - * Unable to Publish - *
- * 1520 - * - * Unable to Force Disconnect - *
- * 1530 - * - * Unable to Force Unpublish - *
- * 1535 - * - * Force Unpublish on Invalid Stream - *
- * 2000 - * - * - * Internal Error - *
- * 2010 - * - * - * Report Issue Failure - *
- * - *

Check the message property for more details about the error.

- * - * @property {String} message The error message. - * - * @property {Object} target The object that the event pertains to. For an - * exception event, this will be an object other than the OT object - * (such as a Session object or a Publisher object). - * - * @property {String} title The error title. - * @augments Event - */ - OT.ExceptionEvent = function (type, message, title, code, component, target) { - OT.Event.call(this, type); - - this.message = message; - this.title = title; - this.code = code; - this.component = component; - this.target = target; - }; - - - OT.IssueReportedEvent = function (type, issueId) { - OT.Event.call(this, type); - this.issueId = issueId; - }; - - // Triggered when the JS dynamic config and the DOM have loaded. - OT.EnvLoadedEvent = function (type) { - OT.Event.call(this, type); - }; - - -/** - * Dispatched by the Session object when a client connects to or disconnects from a {@link Session}. - * For the local client, the Session object dispatches a "sessionConnected" or "sessionDisconnected" - * event, defined by the {@link SessionConnectEvent} and {@link SessionDisconnectEvent} classes. - * - *
Example
- * - *

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

- * - *
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
- * var sessionID = ""; // Replace with your own session ID.
- *                     // See https://dashboard.tokbox.com/projects
- * var token = ""; // Replace with a generated token that has been assigned the moderator role.
- *                 // See https://dashboard.tokbox.com/projects
- * var connectionCount = 0;
- *
- * var session = OT.initSession(apiKey, sessionID);
- * session.on("connectionCreated", function(event) {
- *    connectionCount++;
- *    displayConnectionCount();
- * });
- * session.on("connectionDestroyed", function(event) {
- *    connectionCount--;
- *    displayConnectionCount();
- * });
- * session.connect(token);
- *
- * function displayConnectionCount() {
- *     document.getElementById("connectionCountField").value = connectionCount.toString();
- * }
- * - *

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

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

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

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

- * - *

For a connectionCreated event, this string is undefined.

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

Example — streamCreated event dispatched - * by the Session object

- *

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

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

Example — streamDestroyed event dispatched - * by the Session object

- * - *

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

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

Example — streamCreated event dispatched - * by a Publisher object

- *

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

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

Example — streamDestroyed event - * dispatched by a Publisher object

- * - *

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

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

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

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

- * - *

For a streamCreated event, this string is undefined.

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

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

-*

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

-*

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

-* -*

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

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

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

- * @augments Event - */ - - var sessionConnectedConnectionsDeprecationWarningShown = false; - var sessionConnectedStreamsDeprecationWarningShown = false; - var sessionConnectedArchivesDeprecationWarningShown = false; - - OT.SessionConnectEvent = function (type) { - OT.Event.call(this, type, false); - if (OT.$.canDefineProperty) { - Object.defineProperties(this, { - connections: { - get: function() { - if(!sessionConnectedConnectionsDeprecationWarningShown) { - OT.warn('OT.SessionConnectedEvent no longer includes connections. Listen ' + - 'for connectionCreated events instead.'); - sessionConnectedConnectionsDeprecationWarningShown = true; - } - return []; - } - }, - streams: { - get: function() { - if(!sessionConnectedStreamsDeprecationWarningShown) { - OT.warn('OT.SessionConnectedEvent no longer includes streams. Listen for ' + - 'streamCreated events instead.'); - sessionConnectedConnectionsDeprecationWarningShown = true; - } - return []; - } - }, - archives: { - get: function() { - if(!sessionConnectedArchivesDeprecationWarningShown) { - OT.warn('OT.SessionConnectedEvent no longer includes archives. Listen for ' + - 'archiveStarted events instead.'); - sessionConnectedArchivesDeprecationWarningShown = true; - } - return []; - } - } - }); - } else { - this.connections = []; - this.streams = []; - this.archives = []; - } - }; - -/** - * The Session object dispatches SessionDisconnectEvent object when a session has disconnected. - * This event may be dispatched asynchronously in response to a successful call to the - * disconnect() method of the session object. - * - *

- * Example - *

- *

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

- *
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
- *  var sessionID = ""; // Replace with your own session ID.
- *                      // See https://dashboard.tokbox.com/projects
- *  var token = ""; // Replace with a generated token that has been assigned the moderator role.
- *                  // See https://dashboard.tokbox.com/projects
- *
- *  var session = OT.initSession(apiKey, sessionID);
- *  session.on("sessionDisconnected", function(event) {
- *      alert("The session disconnected. " + event.reason);
- *  });
- *  session.connect(token);
- *  
- * - * @property {String} reason A description of why the session disconnected. - * This property can have two values: - *

- *
    - *
  • "clientDisconnected" — A client disconnected from the session by calling - * the disconnect() method of the Session object or by closing the browser. - * ( See Session.disconnect().)
  • - *
  • "forceDisconnected" — A moderator has disconnected you from the session - * by calling the forceDisconnect() method of the Session object. (See - * Session.forceDisconnect().)
  • - *
  • "networkDisconnected" — The network connection terminated abruptly - * (for example, the client lost their internet connection).
  • - *
- * - * @class SessionDisconnectEvent - * @augments Event - */ - OT.SessionDisconnectEvent = function (type, reason, cancelable) { - OT.Event.call(this, type, cancelable); - this.reason = reason; - }; - -/** -* Prevents the default behavior associated with the event from taking place. -* -*

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

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

-* -*

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

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

- * @see Publisher.publishVideo()

- * @see Stream.videoDimensions

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

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

- * @augments Event - */ - OT.SignalEvent = function(type, data, from) { - OT.Event.call(this, type ? 'signal:' + type : OT.Event.names.SIGNAL, false); - this.data = data; - this.from = from; - }; - - OT.StreamUpdatedEvent = function (stream, key, oldValue, newValue) { - OT.Event.call(this, 'updated', false); - this.target = stream; - this.changedProperty = key; - this.oldValue = oldValue; - this.newValue = newValue; - }; - - OT.DestroyedEvent = function(type, target, reason) { - OT.Event.call(this, type, false); - this.target = target; - this.reason = reason; - }; - -/** - * Defines the event object for the videoDisabled and videoEnabled events - * dispatched by the Subscriber. - * - * @class VideoEnabledChangedEvent - * - * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable - * (true) or not (false). You can cancel the default behavior by - * calling the preventDefault() method of the event object in the callback - * function. (See preventDefault().) - * - * @property {String} reason The reason the video was disabled or enabled. This can be set to one of - * the following values: - * - *
    - * - *
  • "publishVideo" — The publisher started or stopped publishing video, - * by calling publishVideo(true) or publishVideo(false).
  • - * - *
  • "quality" — The OpenTok Media Router starts or stops sending video - * to the subscriber based on stream quality changes. This feature of the OpenTok Media - * Router has a subscriber drop the video stream when connectivity degrades. (The subscriber - * continues to receive the audio stream, if there is one.) - *

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

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

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

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

- * @see Subscriber videoEnabled event

- * @augments Event - */ - OT.VideoEnabledChangedEvent = function(type, properties) { - OT.Event.call(this, type, false); - this.reason = properties.reason; - }; - - OT.VideoDisableWarningEvent = function(type/*, properties*/) { - OT.Event.call(this, type, false); - }; - -/** - * Dispatched periodically by a Subscriber or Publisher object to indicate the audio - * level. This event is dispatched up to 60 times per second, depending on the browser. - * - * @property {String} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically - * for use in adjusting a user interface element, such as a volume meter. Use a moving average - * to smooth the data. - * - * @class AudioLevelUpdatedEvent - * @augments Event - */ - OT.AudioLevelUpdatedEvent = function(audioLevel) { - OT.Event.call(this, OT.Event.names.AUDIO_LEVEL_UPDATED, false); - this.audioLevel = audioLevel; - }; - -})(window); -/* jshint ignore:start */ // https://code.google.com/p/stringencoding/ // An implementation of http://encoding.spec.whatwg.org/#api +// 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.|Array.>)>} */ var indexes = global['encoding-indexes'] || {}; - /** - * @param {number} pointer The |pointer| to search for in the gb18030 index. - * @return {?number} The code point corresponding to |pointer| in |index|, - * or null if |code point| is not in the gb18030 index. - */ - function indexGB18030CodePointFor(pointer) { - if ((pointer > 39419 && pointer < 189000) || (pointer > 1237575)) { - return null; - } - var /** @type {number} */ offset = 0, - /** @type {number} */ code_point_offset = 0, - /** @type {Array.>} */ index = indexes['gb18030']; - var i; - for (i = 0; i < index.length; ++i) { - var entry = index[i]; - if (entry[0] <= pointer) { - offset = entry[0]; - code_point_offset = entry[1]; - } else { - break; - } - } - return code_point_offset + pointer - offset; - } - - /** - * @param {number} code_point The |code point| to locate in the gb18030 index. - * @return {number} The first pointer corresponding to |code point| in the - * gb18030 index. - */ - function indexGB18030PointerFor(code_point) { - var /** @type {number} */ offset = 0, - /** @type {number} */ pointer_offset = 0, - /** @type {Array.>} */ index = indexes['gb18030']; - var i; - for (i = 0; i < index.length; ++i) { - var entry = index[i]; - if (entry[1] <= code_point) { - offset = entry[1]; - pointer_offset = entry[0]; - } else { - break; - } - } - return pointer_offset + code_point - offset; - } // // 7. The encoding @@ -9749,10 +6900,10 @@ waitForDomReady(); */ function UTF8Decoder(options) { var fatal = options.fatal; - var /** @type {number} */ utf8_code_point = 0, - /** @type {number} */ utf8_bytes_needed = 0, - /** @type {number} */ utf8_bytes_seen = 0, - /** @type {number} */ utf8_lower_boundary = 0; + var utf8_code_point = 0, + utf8_bytes_needed = 0, + utf8_bytes_seen = 0, + utf8_lower_boundary = 0; /** * @param {ByteInputStream} byte_pointer The byte stream to decode. @@ -9871,1394 +7022,6 @@ waitForDomReady(); return new UTF8Decoder(options); }; - // - // 8. Legacy single-byte encodings - // - - /** - * @constructor - * @param {Array.} index The encoding index. - * @param {{fatal: boolean}} options - */ - function SingleByteDecoder(index, options) { - var fatal = options.fatal; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte) { - return EOF_code_point; - } - byte_pointer.offset(1); - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - var code_point = index[bite - 0x80]; - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - }; - } - - /** - * @constructor - * @param {Array.} index The encoding index. - * @param {{fatal: boolean}} options - */ - function SingleByteEncoder(index, options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - var pointer = indexPointerFor(code_point, index); - if (pointer === null) { - encoderError(code_point); - } - return output_byte_stream.emit(pointer + 0x80); - }; - } - - (function() { - ['ibm864', 'ibm866', 'iso-8859-2', 'iso-8859-3', 'iso-8859-4', - 'iso-8859-5', 'iso-8859-6', 'iso-8859-7', 'iso-8859-8', 'iso-8859-10', - 'iso-8859-13', 'iso-8859-14', 'iso-8859-15', 'iso-8859-16', 'koi8-r', - 'koi8-u', 'macintosh', 'windows-874', 'windows-1250', 'windows-1251', - 'windows-1252', 'windows-1253', 'windows-1254', 'windows-1255', - 'windows-1256', 'windows-1257', 'windows-1258', 'x-mac-cyrillic' - ].forEach(function(name) { - var encoding = name_to_encoding[name]; - var index = indexes[name]; - encoding.getDecoder = function(options) { - return new SingleByteDecoder(index, options); - }; - encoding.getEncoder = function(options) { - return new SingleByteEncoder(index, options); - }; - }); - }()); - - // - // 9. Legacy multi-byte Chinese (simplified) encodings - // - - // 9.1 gbk - - /** - * @constructor - * @param {boolean} gb18030 True if decoding gb18030, false otherwise. - * @param {{fatal: boolean}} options - */ - function GBKDecoder(gb18030, options) { - var fatal = options.fatal; - var /** @type {number} */ gbk_first = 0x00, - /** @type {number} */ gbk_second = 0x00, - /** @type {number} */ gbk_third = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte && gbk_first === 0x00 && - gbk_second === 0x00 && gbk_third === 0x00) { - return EOF_code_point; - } - if (bite === EOF_byte && - (gbk_first !== 0x00 || gbk_second !== 0x00 || gbk_third !== 0x00)) { - gbk_first = 0x00; - gbk_second = 0x00; - gbk_third = 0x00; - decoderError(fatal); - } - byte_pointer.offset(1); - var code_point; - if (gbk_third !== 0x00) { - code_point = null; - if (inRange(bite, 0x30, 0x39)) { - code_point = indexGB18030CodePointFor( - (((gbk_first - 0x81) * 10 + (gbk_second - 0x30)) * 126 + - (gbk_third - 0x81)) * 10 + bite - 0x30); - } - gbk_first = 0x00; - gbk_second = 0x00; - gbk_third = 0x00; - if (code_point === null) { - byte_pointer.offset(-3); - return decoderError(fatal); - } - return code_point; - } - if (gbk_second !== 0x00) { - if (inRange(bite, 0x81, 0xFE)) { - gbk_third = bite; - return null; - } - byte_pointer.offset(-2); - gbk_first = 0x00; - gbk_second = 0x00; - return decoderError(fatal); - } - if (gbk_first !== 0x00) { - if (inRange(bite, 0x30, 0x39) && gb18030) { - gbk_second = bite; - return null; - } - var lead = gbk_first; - var pointer = null; - gbk_first = 0x00; - var offset = bite < 0x7F ? 0x40 : 0x41; - if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFE)) { - pointer = (lead - 0x81) * 190 + (bite - offset); - } - code_point = pointer === null ? null : - indexCodePointFor(pointer, indexes['gbk']); - if (pointer === null) { - byte_pointer.offset(-1); - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - if (bite === 0x80) { - return 0x20AC; - } - if (inRange(bite, 0x81, 0xFE)) { - gbk_first = bite; - return null; - } - return decoderError(fatal); - }; - } - - /** - * @constructor - * @param {boolean} gb18030 True if decoding gb18030, false otherwise. - * @param {{fatal: boolean}} options - */ - function GBKEncoder(gb18030, options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - var pointer = indexPointerFor(code_point, indexes['gbk']); - if (pointer !== null) { - var lead = div(pointer, 190) + 0x81; - var trail = pointer % 190; - var offset = trail < 0x3F ? 0x40 : 0x41; - return output_byte_stream.emit(lead, trail + offset); - } - if (pointer === null && !gb18030) { - return encoderError(code_point); - } - pointer = indexGB18030PointerFor(code_point); - var byte1 = div(div(div(pointer, 10), 126), 10); - pointer = pointer - byte1 * 10 * 126 * 10; - var byte2 = div(div(pointer, 10), 126); - pointer = pointer - byte2 * 10 * 126; - var byte3 = div(pointer, 10); - var byte4 = pointer - byte3 * 10; - return output_byte_stream.emit(byte1 + 0x81, - byte2 + 0x30, - byte3 + 0x81, - byte4 + 0x30); - }; - } - - name_to_encoding['gbk'].getEncoder = function(options) { - return new GBKEncoder(false, options); - }; - name_to_encoding['gbk'].getDecoder = function(options) { - return new GBKDecoder(false, options); - }; - - // 9.2 gb18030 - name_to_encoding['gb18030'].getEncoder = function(options) { - return new GBKEncoder(true, options); - }; - name_to_encoding['gb18030'].getDecoder = function(options) { - return new GBKDecoder(true, options); - }; - - // 9.3 hz-gb-2312 - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function HZGB2312Decoder(options) { - var fatal = options.fatal; - var /** @type {boolean} */ hzgb2312 = false, - /** @type {number} */ hzgb2312_lead = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte && hzgb2312_lead === 0x00) { - return EOF_code_point; - } - if (bite === EOF_byte && hzgb2312_lead !== 0x00) { - hzgb2312_lead = 0x00; - return decoderError(fatal); - } - byte_pointer.offset(1); - if (hzgb2312_lead === 0x7E) { - hzgb2312_lead = 0x00; - if (bite === 0x7B) { - hzgb2312 = true; - return null; - } - if (bite === 0x7D) { - hzgb2312 = false; - return null; - } - if (bite === 0x7E) { - return 0x007E; - } - if (bite === 0x0A) { - return null; - } - byte_pointer.offset(-1); - return decoderError(fatal); - } - if (hzgb2312_lead !== 0x00) { - var lead = hzgb2312_lead; - hzgb2312_lead = 0x00; - var code_point = null; - if (inRange(bite, 0x21, 0x7E)) { - code_point = indexCodePointFor((lead - 1) * 190 + - (bite + 0x3F), indexes['gbk']); - } - if (bite === 0x0A) { - hzgb2312 = false; - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - if (bite === 0x7E) { - hzgb2312_lead = 0x7E; - return null; - } - if (hzgb2312) { - if (inRange(bite, 0x20, 0x7F)) { - hzgb2312_lead = bite; - return null; - } - if (bite === 0x0A) { - hzgb2312 = false; - } - return decoderError(fatal); - } - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - return decoderError(fatal); - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function HZGB2312Encoder(options) { - var fatal = options.fatal; - var hzgb2312 = false; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F) && hzgb2312) { - code_point_pointer.offset(-1); - hzgb2312 = false; - return output_byte_stream.emit(0x7E, 0x7D); - } - if (code_point === 0x007E) { - return output_byte_stream.emit(0x7E, 0x7E); - } - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - if (!hzgb2312) { - code_point_pointer.offset(-1); - hzgb2312 = true; - return output_byte_stream.emit(0x7E, 0x7B); - } - var pointer = indexPointerFor(code_point, indexes['gbk']); - if (pointer === null) { - return encoderError(code_point); - } - var lead = div(pointer, 190) + 1; - var trail = pointer % 190 - 0x3F; - if (!inRange(lead, 0x21, 0x7E) || !inRange(trail, 0x21, 0x7E)) { - return encoderError(code_point); - } - return output_byte_stream.emit(lead, trail); - }; - } - - name_to_encoding['hz-gb-2312'].getEncoder = function(options) { - return new HZGB2312Encoder(options); - }; - name_to_encoding['hz-gb-2312'].getDecoder = function(options) { - return new HZGB2312Decoder(options); - }; - - // - // 10. Legacy multi-byte Chinese (traditional) encodings - // - - // 10.1 big5 - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function Big5Decoder(options) { - var fatal = options.fatal; - var /** @type {number} */ big5_lead = 0x00, - /** @type {?number} */ big5_pending = null; - - /** - * @param {ByteInputStream} byte_pointer The byte steram to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - // NOTE: Hack to support emitting two code points - if (big5_pending !== null) { - var pending = big5_pending; - big5_pending = null; - return pending; - } - var bite = byte_pointer.get(); - if (bite === EOF_byte && big5_lead === 0x00) { - return EOF_code_point; - } - if (bite === EOF_byte && big5_lead !== 0x00) { - big5_lead = 0x00; - return decoderError(fatal); - } - byte_pointer.offset(1); - if (big5_lead !== 0x00) { - var lead = big5_lead; - var pointer = null; - big5_lead = 0x00; - var offset = bite < 0x7F ? 0x40 : 0x62; - if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0xA1, 0xFE)) { - pointer = (lead - 0x81) * 157 + (bite - offset); - } - if (pointer === 1133) { - big5_pending = 0x0304; - return 0x00CA; - } - if (pointer === 1135) { - big5_pending = 0x030C; - return 0x00CA; - } - if (pointer === 1164) { - big5_pending = 0x0304; - return 0x00EA; - } - if (pointer === 1166) { - big5_pending = 0x030C; - return 0x00EA; - } - var code_point = (pointer === null) ? null : - indexCodePointFor(pointer, indexes['big5']); - if (pointer === null) { - byte_pointer.offset(-1); - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - if (inRange(bite, 0x81, 0xFE)) { - big5_lead = bite; - return null; - } - return decoderError(fatal); - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function Big5Encoder(options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - var pointer = indexPointerFor(code_point, indexes['big5']); - if (pointer === null) { - return encoderError(code_point); - } - var lead = div(pointer, 157) + 0x81; - //if (lead < 0xA1) { - // return encoderError(code_point); - //} - var trail = pointer % 157; - var offset = trail < 0x3F ? 0x40 : 0x62; - return output_byte_stream.emit(lead, trail + offset); - }; - } - - name_to_encoding['big5'].getEncoder = function(options) { - return new Big5Encoder(options); - }; - name_to_encoding['big5'].getDecoder = function(options) { - return new Big5Decoder(options); - }; - - - // - // 11. Legacy multi-byte Japanese encodings - // - - // 11.1 euc.jp - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function EUCJPDecoder(options) { - var fatal = options.fatal; - var /** @type {number} */ eucjp_first = 0x00, - /** @type {number} */ eucjp_second = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte) { - if (eucjp_first === 0x00 && eucjp_second === 0x00) { - return EOF_code_point; - } - eucjp_first = 0x00; - eucjp_second = 0x00; - return decoderError(fatal); - } - byte_pointer.offset(1); - - var lead, code_point; - if (eucjp_second !== 0x00) { - lead = eucjp_second; - eucjp_second = 0x00; - code_point = null; - if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) { - code_point = indexCodePointFor((lead - 0xA1) * 94 + bite - 0xA1, - indexes['jis0212']); - } - if (!inRange(bite, 0xA1, 0xFE)) { - byte_pointer.offset(-1); - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - if (eucjp_first === 0x8E && inRange(bite, 0xA1, 0xDF)) { - eucjp_first = 0x00; - return 0xFF61 + bite - 0xA1; - } - if (eucjp_first === 0x8F && inRange(bite, 0xA1, 0xFE)) { - eucjp_first = 0x00; - eucjp_second = bite; - return null; - } - if (eucjp_first !== 0x00) { - lead = eucjp_first; - eucjp_first = 0x00; - code_point = null; - if (inRange(lead, 0xA1, 0xFE) && inRange(bite, 0xA1, 0xFE)) { - code_point = indexCodePointFor((lead - 0xA1) * 94 + bite - 0xA1, - indexes['jis0208']); - } - if (!inRange(bite, 0xA1, 0xFE)) { - byte_pointer.offset(-1); - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - if (bite === 0x8E || bite === 0x8F || (inRange(bite, 0xA1, 0xFE))) { - eucjp_first = bite; - return null; - } - return decoderError(fatal); - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function EUCJPEncoder(options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - if (code_point === 0x00A5) { - return output_byte_stream.emit(0x5C); - } - if (code_point === 0x203E) { - return output_byte_stream.emit(0x7E); - } - if (inRange(code_point, 0xFF61, 0xFF9F)) { - return output_byte_stream.emit(0x8E, code_point - 0xFF61 + 0xA1); - } - - var pointer = indexPointerFor(code_point, indexes['jis0208']); - if (pointer === null) { - return encoderError(code_point); - } - var lead = div(pointer, 94) + 0xA1; - var trail = pointer % 94 + 0xA1; - return output_byte_stream.emit(lead, trail); - }; - } - - name_to_encoding['euc-jp'].getEncoder = function(options) { - return new EUCJPEncoder(options); - }; - name_to_encoding['euc-jp'].getDecoder = function(options) { - return new EUCJPDecoder(options); - }; - - // 11.2 iso-2022-jp - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function ISO2022JPDecoder(options) { - var fatal = options.fatal; - /** @enum */ - var state = { - ASCII: 0, - escape_start: 1, - escape_middle: 2, - escape_final: 3, - lead: 4, - trail: 5, - Katakana: 6 - }; - var /** @type {number} */ iso2022jp_state = state.ASCII, - /** @type {boolean} */ iso2022jp_jis0212 = false, - /** @type {number} */ iso2022jp_lead = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite !== EOF_byte) { - byte_pointer.offset(1); - } - switch (iso2022jp_state) { - default: - case state.ASCII: - if (bite === 0x1B) { - iso2022jp_state = state.escape_start; - return null; - } - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - if (bite === EOF_byte) { - return EOF_code_point; - } - return decoderError(fatal); - - case state.escape_start: - if (bite === 0x24 || bite === 0x28) { - iso2022jp_lead = bite; - iso2022jp_state = state.escape_middle; - return null; - } - if (bite !== EOF_byte) { - byte_pointer.offset(-1); - } - iso2022jp_state = state.ASCII; - return decoderError(fatal); - - case state.escape_middle: - var lead = iso2022jp_lead; - iso2022jp_lead = 0x00; - if (lead === 0x24 && (bite === 0x40 || bite === 0x42)) { - iso2022jp_jis0212 = false; - iso2022jp_state = state.lead; - return null; - } - if (lead === 0x24 && bite === 0x28) { - iso2022jp_state = state.escape_final; - return null; - } - if (lead === 0x28 && (bite === 0x42 || bite === 0x4A)) { - iso2022jp_state = state.ASCII; - return null; - } - if (lead === 0x28 && bite === 0x49) { - iso2022jp_state = state.Katakana; - return null; - } - if (bite === EOF_byte) { - byte_pointer.offset(-1); - } else { - byte_pointer.offset(-2); - } - iso2022jp_state = state.ASCII; - return decoderError(fatal); - - case state.escape_final: - if (bite === 0x44) { - iso2022jp_jis0212 = true; - iso2022jp_state = state.lead; - return null; - } - if (bite === EOF_byte) { - byte_pointer.offset(-2); - } else { - byte_pointer.offset(-3); - } - iso2022jp_state = state.ASCII; - return decoderError(fatal); - - case state.lead: - if (bite === 0x0A) { - iso2022jp_state = state.ASCII; - return decoderError(fatal, 0x000A); - } - if (bite === 0x1B) { - iso2022jp_state = state.escape_start; - return null; - } - if (bite === EOF_byte) { - return EOF_code_point; - } - iso2022jp_lead = bite; - iso2022jp_state = state.trail; - return null; - - case state.trail: - iso2022jp_state = state.lead; - if (bite === EOF_byte) { - return decoderError(fatal); - } - var code_point = null; - var pointer = (iso2022jp_lead - 0x21) * 94 + bite - 0x21; - if (inRange(iso2022jp_lead, 0x21, 0x7E) && - inRange(bite, 0x21, 0x7E)) { - code_point = (iso2022jp_jis0212 === false) ? - indexCodePointFor(pointer, indexes['jis0208']) : - indexCodePointFor(pointer, indexes['jis0212']); - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - - case state.Katakana: - if (bite === 0x1B) { - iso2022jp_state = state.escape_start; - return null; - } - if (inRange(bite, 0x21, 0x5F)) { - return 0xFF61 + bite - 0x21; - } - if (bite === EOF_byte) { - return EOF_code_point; - } - return decoderError(fatal); - } - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function ISO2022JPEncoder(options) { - var fatal = options.fatal; - /** @enum */ - var state = { - ASCII: 0, - lead: 1, - Katakana: 2 - }; - var /** @type {number} */ iso2022jp_state = state.ASCII; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if ((inRange(code_point, 0x0000, 0x007F) || - code_point === 0x00A5 || code_point === 0x203E) && - iso2022jp_state !== state.ASCII) { - code_point_pointer.offset(-1); - iso2022jp_state = state.ASCII; - return output_byte_stream.emit(0x1B, 0x28, 0x42); - } - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - if (code_point === 0x00A5) { - return output_byte_stream.emit(0x5C); - } - if (code_point === 0x203E) { - return output_byte_stream.emit(0x7E); - } - if (inRange(code_point, 0xFF61, 0xFF9F) && - iso2022jp_state !== state.Katakana) { - code_point_pointer.offset(-1); - iso2022jp_state = state.Katakana; - return output_byte_stream.emit(0x1B, 0x28, 0x49); - } - if (inRange(code_point, 0xFF61, 0xFF9F)) { - return output_byte_stream.emit(code_point - 0xFF61 - 0x21); - } - if (iso2022jp_state !== state.lead) { - code_point_pointer.offset(-1); - iso2022jp_state = state.lead; - return output_byte_stream.emit(0x1B, 0x24, 0x42); - } - var pointer = indexPointerFor(code_point, indexes['jis0208']); - if (pointer === null) { - return encoderError(code_point); - } - var lead = div(pointer, 94) + 0x21; - var trail = pointer % 94 + 0x21; - return output_byte_stream.emit(lead, trail); - }; - } - - name_to_encoding['iso-2022-jp'].getEncoder = function(options) { - return new ISO2022JPEncoder(options); - }; - name_to_encoding['iso-2022-jp'].getDecoder = function(options) { - return new ISO2022JPDecoder(options); - }; - - // 11.3 shift_jis - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function ShiftJISDecoder(options) { - var fatal = options.fatal; - var /** @type {number} */ shiftjis_lead = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte && shiftjis_lead === 0x00) { - return EOF_code_point; - } - if (bite === EOF_byte && shiftjis_lead !== 0x00) { - shiftjis_lead = 0x00; - return decoderError(fatal); - } - byte_pointer.offset(1); - if (shiftjis_lead !== 0x00) { - var lead = shiftjis_lead; - shiftjis_lead = 0x00; - if (inRange(bite, 0x40, 0x7E) || inRange(bite, 0x80, 0xFC)) { - var offset = (bite < 0x7F) ? 0x40 : 0x41; - var lead_offset = (lead < 0xA0) ? 0x81 : 0xC1; - var code_point = indexCodePointFor((lead - lead_offset) * 188 + - bite - offset, indexes['jis0208']); - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - byte_pointer.offset(-1); - return decoderError(fatal); - } - if (inRange(bite, 0x00, 0x80)) { - return bite; - } - if (inRange(bite, 0xA1, 0xDF)) { - return 0xFF61 + bite - 0xA1; - } - if (inRange(bite, 0x81, 0x9F) || inRange(bite, 0xE0, 0xFC)) { - shiftjis_lead = bite; - return null; - } - return decoderError(fatal); - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function ShiftJISEncoder(options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x0080)) { - return output_byte_stream.emit(code_point); - } - if (code_point === 0x00A5) { - return output_byte_stream.emit(0x5C); - } - if (code_point === 0x203E) { - return output_byte_stream.emit(0x7E); - } - if (inRange(code_point, 0xFF61, 0xFF9F)) { - return output_byte_stream.emit(code_point - 0xFF61 + 0xA1); - } - var pointer = indexPointerFor(code_point, indexes['jis0208']); - if (pointer === null) { - return encoderError(code_point); - } - var lead = div(pointer, 188); - var lead_offset = lead < 0x1F ? 0x81 : 0xC1; - var trail = pointer % 188; - var offset = trail < 0x3F ? 0x40 : 0x41; - return output_byte_stream.emit(lead + lead_offset, trail + offset); - }; - } - - name_to_encoding['shift_jis'].getEncoder = function(options) { - return new ShiftJISEncoder(options); - }; - name_to_encoding['shift_jis'].getDecoder = function(options) { - return new ShiftJISDecoder(options); - }; - - // - // 12. Legacy multi-byte Korean encodings - // - - // 12.1 euc-kr - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function EUCKRDecoder(options) { - var fatal = options.fatal; - var /** @type {number} */ euckr_lead = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte && euckr_lead === 0) { - return EOF_code_point; - } - if (bite === EOF_byte && euckr_lead !== 0) { - euckr_lead = 0x00; - return decoderError(fatal); - } - byte_pointer.offset(1); - if (euckr_lead !== 0x00) { - var lead = euckr_lead; - var pointer = null; - euckr_lead = 0x00; - - if (inRange(lead, 0x81, 0xC6)) { - var temp = (26 + 26 + 126) * (lead - 0x81); - if (inRange(bite, 0x41, 0x5A)) { - pointer = temp + bite - 0x41; - } else if (inRange(bite, 0x61, 0x7A)) { - pointer = temp + 26 + bite - 0x61; - } else if (inRange(bite, 0x81, 0xFE)) { - pointer = temp + 26 + 26 + bite - 0x81; - } - } - - if (inRange(lead, 0xC7, 0xFD) && inRange(bite, 0xA1, 0xFE)) { - pointer = (26 + 26 + 126) * (0xC7 - 0x81) + (lead - 0xC7) * 94 + - (bite - 0xA1); - } - - var code_point = (pointer === null) ? null : - indexCodePointFor(pointer, indexes['euc-kr']); - if (pointer === null) { - byte_pointer.offset(-1); - } - if (code_point === null) { - return decoderError(fatal); - } - return code_point; - } - - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - - if (inRange(bite, 0x81, 0xFD)) { - euckr_lead = bite; - return null; - } - - return decoderError(fatal); - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function EUCKREncoder(options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - var pointer = indexPointerFor(code_point, indexes['euc-kr']); - if (pointer === null) { - return encoderError(code_point); - } - var lead, trail; - if (pointer < ((26 + 26 + 126) * (0xC7 - 0x81))) { - lead = div(pointer, (26 + 26 + 126)) + 0x81; - trail = pointer % (26 + 26 + 126); - var offset = trail < 26 ? 0x41 : trail < 26 + 26 ? 0x47 : 0x4D; - return output_byte_stream.emit(lead, trail + offset); - } - pointer = pointer - (26 + 26 + 126) * (0xC7 - 0x81); - lead = div(pointer, 94) + 0xC7; - trail = pointer % 94 + 0xA1; - return output_byte_stream.emit(lead, trail); - }; - } - - name_to_encoding['euc-kr'].getEncoder = function(options) { - return new EUCKREncoder(options); - }; - name_to_encoding['euc-kr'].getDecoder = function(options) { - return new EUCKRDecoder(options); - }; - - // 12.2 iso-2022-kr - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function ISO2022KRDecoder(options) { - var fatal = options.fatal; - /** @enum */ - var state = { - ASCII: 0, - escape_start: 1, - escape_middle: 2, - escape_end: 3, - lead: 4, - trail: 5 - }; - var /** @type {number} */ iso2022kr_state = state.ASCII, - /** @type {number} */ iso2022kr_lead = 0x00; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite !== EOF_byte) { - byte_pointer.offset(1); - } - switch (iso2022kr_state) { - default: - case state.ASCII: - if (bite === 0x0E) { - iso2022kr_state = state.lead; - return null; - } - if (bite === 0x0F) { - return null; - } - if (bite === 0x1B) { - iso2022kr_state = state.escape_start; - return null; - } - if (inRange(bite, 0x00, 0x7F)) { - return bite; - } - if (bite === EOF_byte) { - return EOF_code_point; - } - return decoderError(fatal); - case state.escape_start: - if (bite === 0x24) { - iso2022kr_state = state.escape_middle; - return null; - } - if (bite !== EOF_byte) { - byte_pointer.offset(-1); - } - iso2022kr_state = state.ASCII; - return decoderError(fatal); - case state.escape_middle: - if (bite === 0x29) { - iso2022kr_state = state.escape_end; - return null; - } - if (bite === EOF_byte) { - byte_pointer.offset(-1); - } else { - byte_pointer.offset(-2); - } - iso2022kr_state = state.ASCII; - return decoderError(fatal); - case state.escape_end: - if (bite === 0x43) { - iso2022kr_state = state.ASCII; - return null; - } - if (bite === EOF_byte) { - byte_pointer.offset(-2); - } else { - byte_pointer.offset(-3); - } - iso2022kr_state = state.ASCII; - return decoderError(fatal); - case state.lead: - if (bite === 0x0A) { - iso2022kr_state = state.ASCII; - return decoderError(fatal, 0x000A); - } - if (bite === 0x0E) { - return null; - } - if (bite === 0x0F) { - iso2022kr_state = state.ASCII; - return null; - } - if (bite === EOF_byte) { - return EOF_code_point; - } - iso2022kr_lead = bite; - iso2022kr_state = state.trail; - return null; - case state.trail: - iso2022kr_state = state.lead; - if (bite === EOF_byte) { - return decoderError(fatal); - } - var code_point = null; - if (inRange(iso2022kr_lead, 0x21, 0x46) && - inRange(bite, 0x21, 0x7E)) { - code_point = indexCodePointFor((26 + 26 + 126) * - (iso2022kr_lead - 1) + - 26 + 26 + bite - 1, - indexes['euc-kr']); - } else if (inRange(iso2022kr_lead, 0x47, 0x7E) && - inRange(bite, 0x21, 0x7E)) { - code_point = indexCodePointFor((26 + 26 + 126) * (0xC7 - 0x81) + - (iso2022kr_lead - 0x47) * 94 + - (bite - 0x21), - indexes['euc-kr']); - } - if (code_point !== null) { - return code_point; - } - return decoderError(fatal); - } - }; - } - - /** - * @constructor - * @param {{fatal: boolean}} options - */ - function ISO2022KREncoder(options) { - var fatal = options.fatal; - /** @enum */ - var state = { - ASCII: 0, - lead: 1 - }; - var /** @type {boolean} */ iso2022kr_initialization = false, - /** @type {number} */ iso2022kr_state = state.ASCII; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - if (!iso2022kr_initialization) { - iso2022kr_initialization = true; - output_byte_stream.emit(0x1B, 0x24, 0x29, 0x43); - } - code_point_pointer.offset(1); - if (inRange(code_point, 0x0000, 0x007F) && - iso2022kr_state !== state.ASCII) { - code_point_pointer.offset(-1); - iso2022kr_state = state.ASCII; - return output_byte_stream.emit(0x0F); - } - if (inRange(code_point, 0x0000, 0x007F)) { - return output_byte_stream.emit(code_point); - } - if (iso2022kr_state !== state.lead) { - code_point_pointer.offset(-1); - iso2022kr_state = state.lead; - return output_byte_stream.emit(0x0E); - } - var pointer = indexPointerFor(code_point, indexes['euc-kr']); - if (pointer === null) { - return encoderError(code_point); - } - var lead, trail; - if (pointer < (26 + 26 + 126) * (0xC7 - 0x81)) { - lead = div(pointer, (26 + 26 + 126)) + 1; - trail = pointer % (26 + 26 + 126) - 26 - 26 + 1; - if (!inRange(lead, 0x21, 0x46) || !inRange(trail, 0x21, 0x7E)) { - return encoderError(code_point); - } - return output_byte_stream.emit(lead, trail); - } - pointer = pointer - (26 + 26 + 126) * (0xC7 - 0x81); - lead = div(pointer, 94) + 0x47; - trail = pointer % 94 + 0x21; - if (!inRange(lead, 0x47, 0x7E) || !inRange(trail, 0x21, 0x7E)) { - return encoderError(code_point); - } - return output_byte_stream.emit(lead, trail); - }; - } - - name_to_encoding['iso-2022-kr'].getEncoder = function(options) { - return new ISO2022KREncoder(options); - }; - name_to_encoding['iso-2022-kr'].getDecoder = function(options) { - return new ISO2022KRDecoder(options); - }; - - - // - // 13. Legacy utf-16 encodings - // - - // 13.1 utf-16 - - /** - * @constructor - * @param {boolean} utf16_be True if big-endian, false if little-endian. - * @param {{fatal: boolean}} options - */ - function UTF16Decoder(utf16_be, options) { - var fatal = options.fatal; - var /** @type {?number} */ utf16_lead_byte = null, - /** @type {?number} */ utf16_lead_surrogate = null; - /** - * @param {ByteInputStream} byte_pointer The byte stream to decode. - * @return {?number} The next code point decoded, or null if not enough - * data exists in the input stream to decode a complete code point. - */ - this.decode = function(byte_pointer) { - var bite = byte_pointer.get(); - if (bite === EOF_byte && utf16_lead_byte === null && - utf16_lead_surrogate === null) { - return EOF_code_point; - } - if (bite === EOF_byte && (utf16_lead_byte !== null || - utf16_lead_surrogate !== null)) { - return decoderError(fatal); - } - byte_pointer.offset(1); - if (utf16_lead_byte === null) { - utf16_lead_byte = bite; - return null; - } - var code_point; - if (utf16_be) { - code_point = (utf16_lead_byte << 8) + bite; - } else { - code_point = (bite << 8) + utf16_lead_byte; - } - utf16_lead_byte = null; - if (utf16_lead_surrogate !== null) { - var lead_surrogate = utf16_lead_surrogate; - utf16_lead_surrogate = null; - if (inRange(code_point, 0xDC00, 0xDFFF)) { - return 0x10000 + (lead_surrogate - 0xD800) * 0x400 + - (code_point - 0xDC00); - } - byte_pointer.offset(-2); - return decoderError(fatal); - } - if (inRange(code_point, 0xD800, 0xDBFF)) { - utf16_lead_surrogate = code_point; - return null; - } - if (inRange(code_point, 0xDC00, 0xDFFF)) { - return decoderError(fatal); - } - return code_point; - }; - } - - /** - * @constructor - * @param {boolean} utf16_be True if big-endian, false if little-endian. - * @param {{fatal: boolean}} options - */ - function UTF16Encoder(utf16_be, options) { - var fatal = options.fatal; - /** - * @param {ByteOutputStream} output_byte_stream Output byte stream. - * @param {CodePointInputStream} code_point_pointer Input stream. - * @return {number} The last byte emitted. - */ - this.encode = function(output_byte_stream, code_point_pointer) { - function convert_to_bytes(code_unit) { - var byte1 = code_unit >> 8; - var byte2 = code_unit & 0x00FF; - if (utf16_be) { - return output_byte_stream.emit(byte1, byte2); - } - return output_byte_stream.emit(byte2, byte1); - } - var code_point = code_point_pointer.get(); - if (code_point === EOF_code_point) { - return EOF_byte; - } - code_point_pointer.offset(1); - if (inRange(code_point, 0xD800, 0xDFFF)) { - encoderError(code_point); - } - if (code_point <= 0xFFFF) { - return convert_to_bytes(code_point); - } - var lead = div((code_point - 0x10000), 0x400) + 0xD800; - var trail = ((code_point - 0x10000) % 0x400) + 0xDC00; - convert_to_bytes(lead); - return convert_to_bytes(trail); - }; - } - - name_to_encoding['utf-16'].getEncoder = function(options) { - return new UTF16Encoder(false, options); - }; - name_to_encoding['utf-16'].getDecoder = function(options) { - return new UTF16Decoder(false, options); - }; - - // 13.2 utf-16be - name_to_encoding['utf-16be'].getEncoder = function(options) { - return new UTF16Encoder(true, options); - }; - name_to_encoding['utf-16be'].getDecoder = function(options) { - return new UTF16Decoder(true, options); - }; - // NOTE: currently unused /** @@ -11304,7 +7067,8 @@ waitForDomReady(); // Implementation of Text Encoding Web API // - /** @const */ var DEFAULT_ENCODING = 'utf-8'; + /** @const */ + var DEFAULT_ENCODING = 'utf-8'; /** * @constructor @@ -11465,397 +7229,249 @@ waitForDomReady(); global['TextEncoder'] = global['TextEncoder'] || TextEncoder; global['TextDecoder'] = global['TextDecoder'] || TextDecoder; + + /* jshint ignore:end */ + }(this)); -/* jshint ignore:end */ -!(function() { - // Rumor Messaging for JS - // - // https://tbwiki.tokbox.com/index.php/Rumor_:_Messaging_FrameWork - // - // @todo Rumor { - // Add error codes for all the error cases - // Add Dependability commands - // } +// tb_require('../../../helpers/helpers.js') +// tb_require('./encoding.js') +// tb_require('./rumor.js') - 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, +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT, TextEncoder, TextDecoder */ - // 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, +// +// +// @references +// * https://tbwiki.tokbox.com/index.php/Rumor_Message_Packet +// * https://tbwiki.tokbox.com/index.php/Rumor_Protocol +// +OT.Rumor.Message = function (type, toAddress, headers, data) { + this.type = type; + this.toAddress = toAddress; + this.headers = headers; + this.data = data; - // This 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.transactionId = this.headers['TRANSACTION-ID']; + this.status = this.headers.STATUS; + this.isError = !(this.status && this.status[0] === '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, +OT.Rumor.Message.prototype.serialize = function () { + var offset = 8, + cBuf = 7, + address = [], + headerKey = [], + headerVal = [], + strArray, + dataView, + i, + j; - // This will be the message used by the server to notify an address that a - // client disconnected. - DISCONNECT: 4, + // The number of addresses + cBuf++; - //Enhancements to support Keepalives - PING: 7, - PONG: 8, - STATUS: 9 + // Write out the address. + for (i = 0; i < this.toAddress.length; i++) { + /*jshint newcap:false */ + address.push(new TextEncoder('utf-8').encode(this.toAddress[i])); + cBuf += 2; + cBuf += address[i].length; + } + + // The number of parameters + cBuf++; + + // Write out the params + i = 0; + + for (var key in this.headers) { + if(!this.headers.hasOwnProperty(key)) { + continue; } + headerKey.push(new TextEncoder('utf-8').encode(key)); + headerVal.push(new TextEncoder('utf-8').encode(this.headers[key])); + cBuf += 4; + cBuf += headerKey[i].length; + cBuf += headerVal[i].length; + + i++; + } + + dataView = new TextEncoder('utf-8').encode(this.data); + cBuf += dataView.length; + + // Let's allocate a binary blob of this size + var buffer = new ArrayBuffer(cBuf); + var uint8View = new Uint8Array(buffer, 0, cBuf); + + // We don't include the header in the lenght. + cBuf -= 4; + + // Write out size (in network order) + uint8View[0] = (cBuf & 0xFF000000) >>> 24; + uint8View[1] = (cBuf & 0x00FF0000) >>> 16; + uint8View[2] = (cBuf & 0x0000FF00) >>> 8; + uint8View[3] = (cBuf & 0x000000FF) >>> 0; + + // Write out reserved bytes + uint8View[4] = 0; + uint8View[5] = 0; + + // Write out message type + uint8View[6] = this.type; + uint8View[7] = this.toAddress.length; + + // Now just copy over the encoded values.. + for (i = 0; i < address.length; i++) { + strArray = address[i]; + uint8View[offset++] = strArray.length >> 8 & 0xFF; + uint8View[offset++] = strArray.length >> 0 & 0xFF; + for (j = 0; j < strArray.length; j++) { + uint8View[offset++] = strArray[j]; + } + } + + uint8View[offset++] = headerKey.length; + + // Write out the params + for (i = 0; i < headerKey.length; i++) { + strArray = headerKey[i]; + uint8View[offset++] = strArray.length >> 8 & 0xFF; + uint8View[offset++] = strArray.length >> 0 & 0xFF; + for (j = 0; j < strArray.length; j++) { + uint8View[offset++] = strArray[j]; + } + + strArray = headerVal[i]; + uint8View[offset++] = strArray.length >> 8 & 0xFF; + uint8View[offset++] = strArray.length >> 0 & 0xFF; + for (j = 0; j < strArray.length; j++) { + uint8View[offset++] = strArray[j]; + } + } + + // And finally the data + for (i = 0; i < dataView.length; i++) { + uint8View[offset++] = dataView[i]; + } + + return buffer; +}; + +function toArrayBuffer(buffer) { + var ab = new ArrayBuffer(buffer.length); + var view = new Uint8Array(ab); + for (var i = 0; i < buffer.length; ++i) { + view[i] = buffer[i]; + } + return ab; +} + +OT.Rumor.Message.deserialize = function (buffer) { + + if(typeof Buffer !== 'undefined' && + Buffer.isBuffer(buffer)) { + buffer = toArrayBuffer(buffer); + } + var cBuf = 0, + type, + offset = 8, + uint8View = new Uint8Array(buffer), + strView, + headerlen, + headers, + keyStr, + valStr, + length, + i; + + // Write out size (in network order) + cBuf += uint8View[0] << 24; + cBuf += uint8View[1] << 16; + cBuf += uint8View[2] << 8; + cBuf += uint8View[3] << 0; + + type = uint8View[6]; + var address = []; + + for (i = 0; i < uint8View[7]; i++) { + length = uint8View[offset++] << 8; + length += uint8View[offset++]; + strView = new Uint8Array(buffer, offset, length); + /*jshint newcap:false */ + address[i] = new TextDecoder('utf-8').decode(strView); + offset += length; + } + + headerlen = uint8View[offset++]; + headers = {}; + + for (i = 0; i < headerlen; i++) { + length = uint8View[offset++] << 8; + length += uint8View[offset++]; + strView = new Uint8Array(buffer, offset, length); + keyStr = new TextDecoder('utf-8').decode(strView); + offset += length; + + length = uint8View[offset++] << 8; + length += uint8View[offset++]; + strView = new Uint8Array(buffer, offset, length); + valStr = new TextDecoder('utf-8').decode(strView); + headers[keyStr] = valStr; + offset += length; + } + + var dataView = new Uint8Array(buffer, offset); + var data = new TextDecoder('utf-8').decode(dataView); + + return new OT.Rumor.Message(type, address, headers, data); +}; + + +OT.Rumor.Message.Connect = function (uniqueId, notifyDisconnectAddress) { + var headers = { + uniqueId: uniqueId, + notifyDisconnectAddress: notifyDisconnectAddress }; -}(this)); -!(function(OT) { + return new OT.Rumor.Message(OT.Rumor.MessageType.CONNECT, [], headers, ''); +}; - var WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000, +OT.Rumor.Message.Disconnect = function () { + return new OT.Rumor.Message(OT.Rumor.MessageType.DISCONNECT, [], {}, ''); +}; - // Magic Connectivity Timeout Constant: We wait 9*the keep alive interval, - // on the third keep alive we trigger the timeout if we haven't received the - // server pong. - WEB_SOCKET_CONNECTIVITY_TIMEOUT = 5*WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100, +OT.Rumor.Message.Subscribe = function(topics) { + return new OT.Rumor.Message(OT.Rumor.MessageType.SUBSCRIBE, topics, {}, ''); +}; - wsCloseErrorCodes; +OT.Rumor.Message.Unsubscribe = function(topics) { + return new OT.Rumor.Message(OT.Rumor.MessageType.UNSUBSCRIBE, topics, {}, ''); +}; +OT.Rumor.Message.Publish = function(topics, message, headers) { + return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, headers||{}, message || ''); +}; +// This message is used to implement keepalives on the persistent +// socket connection between the client and server. Every time the +// client sends a PING to the server, the server will respond with +// a PONG. +OT.Rumor.Message.Ping = function() { + return new OT.Rumor.Message(OT.Rumor.MessageType.PING, [], {}, ''); +}; - // https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Close_codes - // http://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.CloseCodes.html - wsCloseErrorCodes = { - 1002: 'The endpoint is terminating the connection due to a protocol error. ' + - '(CLOSE_PROTOCOL_ERROR)', - 1003: 'The connection is being terminated because the endpoint received data of ' + - 'a type it cannot accept (for example, a text-only endpoint received binary data). ' + - '(CLOSE_UNSUPPORTED)', - 1004: 'The endpoint is terminating the connection because a data frame was received ' + - 'that is too large. (CLOSE_TOO_LARGE)', - 1005: 'Indicates that no status code was provided even though one was expected. ' + - '(CLOSE_NO_STATUS)', - 1006: 'Used to indicate that a connection was closed abnormally (that is, with no ' + - 'close frame being sent) when a status code is expected. (CLOSE_ABNORMAL)', - 1007: 'Indicates that an endpoint is terminating the connection because it has received ' + - 'data within a message that was not consistent with the type of the message (e.g., ' + - 'non-UTF-8 [RFC3629] data within a text message)', - 1008: 'Indicates that an endpoint is terminating the connection because it has received a ' + - 'message that violates its policy. This is a generic status code that can be returned ' + - 'when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a ' + - 'need to hide specific details about the policy', - 1009: 'Indicates that an endpoint is terminating the connection because it has received a ' + - 'message that is too big for it to process', - 1011: 'Indicates that a server is terminating the connection because it encountered an ' + - 'unexpected condition that prevented it from fulfilling the request', +// tb_require('../../../helpers/helpers.js') +// tb_require('./rumor.js') +// tb_require('./message.js') - // .... codes in the 4000-4999 range are available for use by applications. - 4001: 'Connectivity loss was detected as it was too long since the socket received the ' + - 'last PONG message' - }; +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ - OT.Rumor.SocketError = function(code, message) { - this.code = code; - this.message = message; - }; - - // The NativeSocket bit is purely to make testing simpler, it defaults to WebSocket - // so in normal operation you would omit it. - OT.Rumor.Socket = function(messagingURL, notifyDisconnectAddress, NativeSocket) { - - var states = ['disconnected', 'error', 'connected', 'connecting', 'disconnecting'], - webSocket, - id, - onOpen, - onError, - onClose, - onMessage, - connectCallback, - connectTimeout, - lastMessageTimestamp, // The timestamp of the last message received - keepAliveTimer; // Timer for the connectivity checks - - - //// Private API - var stateChanged = function(newState) { - switch (newState) { - case 'disconnected': - case 'error': - webSocket = null; - if (onClose) { - var error; - if(hasLostConnectivity()) { - error = new Error(wsCloseErrorCodes[4001]); - error.code = 4001; - } - onClose(error); - } - break; - } - }, - - setState = OT.$.statable(this, states, 'disconnected', stateChanged), - - validateCallback = function validateCallback (name, callback) { - if (callback === null || !OT.$.isFunction(callback) ) { - throw new Error('The Rumor.Socket ' + name + - ' callback must be a valid function or null'); - } - }, - - error = OT.$.bind(function error (errorMessage) { - OT.error('Rumor.Socket: ' + errorMessage); - - var socketError = new OT.Rumor.SocketError(null, errorMessage || 'Unknown Socket Error'); - - if (connectTimeout) clearTimeout(connectTimeout); - - setState('error'); - - if (this.previousState === 'connecting' && connectCallback) { - connectCallback(socketError, null); - connectCallback = null; - } - - if (onError) onError(socketError); - }, this), - - hasLostConnectivity = function hasLostConnectivity () { - if (!lastMessageTimestamp) return false; - - return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT; - }, - - sendKeepAlive = OT.$.bind(function() { - if (!this.is('connected')) return; - - if ( hasLostConnectivity() ) { - webSocketDisconnected({code: 4001}); - } - else { - webSocket.send(OT.Rumor.Message.Ping()); - keepAliveTimer = setTimeout(sendKeepAlive, WEB_SOCKET_KEEP_ALIVE_INTERVAL); - } - }, this), - - // Returns true if we think the DOM has been unloaded - // It detects this by looking for the OT global, which - // should always exist until the DOM is cleaned up. - isDOMUnloaded = function isDOMUnloaded () { - return !window.OT; - }; - - - //// Private Event Handlers - var webSocketConnected = OT.$.bind(function webSocketConnected () { - if (connectTimeout) clearTimeout(connectTimeout); - if (this.isNot('connecting')) { - OT.debug('webSocketConnected reached in state other than connecting'); - return; - } - - // Connect to Rumor by registering our connection id and the - // app server address to notify if we disconnect. - // - // We don't need to wait for a reply to this message. - webSocket.send(OT.Rumor.Message.Connect(id, notifyDisconnectAddress)); - - setState('connected'); - if (connectCallback) { - connectCallback(null, id); - connectCallback = null; - } - - if (onOpen) onOpen(id); - - keepAliveTimer = setTimeout(function() { - lastMessageTimestamp = OT.$.now(); - sendKeepAlive(); - }, WEB_SOCKET_KEEP_ALIVE_INTERVAL); - }, this), - - webSocketConnectTimedOut = function webSocketConnectTimedOut () { - var webSocketWas = webSocket; - error('Timed out while waiting for the Rumor socket to connect.'); - // This will prevent a socket eventually connecting - // But call it _after_ the error just in case any of - // the callbacks fire synchronously, breaking the error - // handling code. - try { - webSocketWas.close(); - } catch(x) {} - }, - - webSocketError = function webSocketError () {}, - // var errorMessage = 'Unknown Socket Error'; - // @fixme We MUST be able to do better than this! - - // All errors seem to result in disconnecting the socket, the close event - // has a close reason and code which gives some error context. This, - // combined with the fact that the errorEvent argument contains no - // error info at all, means we'll delay triggering the error handlers - // until the socket is closed. - // error(errorMessage); - - webSocketDisconnected = OT.$.bind(function webSocketDisconnected (closeEvent) { - if (connectTimeout) clearTimeout(connectTimeout); - if (keepAliveTimer) clearTimeout(keepAliveTimer); - - if (isDOMUnloaded()) { - // Sometimes we receive the web socket close event after - // the DOM has already been partially or fully unloaded - // if that's the case here then it's not really safe, or - // desirable, to continue. - return; - } - - if (closeEvent.code !== 1000 && closeEvent.code !== 1001) { - var reason = closeEvent.reason || closeEvent.message; - if (!reason && wsCloseErrorCodes.hasOwnProperty(closeEvent.code)) { - reason = wsCloseErrorCodes[closeEvent.code]; - } - - error('Rumor Socket Disconnected: ' + reason); - } - - if (this.isNot('error')) setState('disconnected'); - }, this), - - webSocketReceivedMessage = function webSocketReceivedMessage (msg) { - lastMessageTimestamp = OT.$.now(); - - if (onMessage) { - if (msg.type !== OT.Rumor.MessageType.PONG) { - onMessage(msg); - } - } - }; - - - //// Public API - - this.publish = function (topics, message, headers) { - webSocket.send(OT.Rumor.Message.Publish(topics, message, headers)); - }; - - this.subscribe = function(topics) { - webSocket.send(OT.Rumor.Message.Subscribe(topics)); - }; - - this.unsubscribe = function(topics) { - webSocket.send(OT.Rumor.Message.Unsubscribe(topics)); - }; - - this.connect = function (connectionId, complete) { - if (this.is('connecting', 'connected')) { - complete(new OT.Rumor.SocketError(null, - 'Rumor.Socket cannot connect when it is already connecting or connected.')); - return; - } - - id = connectionId; - connectCallback = complete; - - setState('connecting'); - - var TheWebSocket = NativeSocket || window.WebSocket; - - var events = { - onOpen: webSocketConnected, - onClose: webSocketDisconnected, - onError: webSocketError, - onMessage: webSocketReceivedMessage - }; - - try { - if(typeof TheWebSocket !== 'undefined') { - webSocket = new OT.Rumor.NativeSocket(TheWebSocket, messagingURL, events); - } else { - webSocket = new OT.Rumor.PluginSocket(messagingURL, events); - } - - connectTimeout = setTimeout(webSocketConnectTimedOut, OT.Rumor.Socket.CONNECT_TIMEOUT); - } - catch(e) { - OT.error(e); - - // @todo add an actual error message - error('Could not connect to the Rumor socket, possibly because of a blocked port.'); - } - }; - - this.disconnect = function(drainSocketBuffer) { - if (connectTimeout) clearTimeout(connectTimeout); - if (keepAliveTimer) clearTimeout(keepAliveTimer); - - if (!webSocket) { - if (this.isNot('error')) setState('disconnected'); - return; - } - - if (webSocket.isClosed()) { - if (this.isNot('error')) setState('disconnected'); - } - else { - if (this.is('connected')) { - // Look! We are nice to the rumor server ;-) - webSocket.send(OT.Rumor.Message.Disconnect()); - } - - // Wait until the socket is ready to close - webSocket.close(drainSocketBuffer); - } - }; - - - - OT.$.defineProperties(this, { - id: { - get: function() { return id; } - }, - - onOpen: { - set: function(callback) { - validateCallback('onOpen', callback); - onOpen = callback; - }, - - get: function() { return onOpen; } - }, - - onError: { - set: function(callback) { - validateCallback('onError', callback); - onError = callback; - }, - - get: function() { return onError; } - }, - - onClose: { - set: function(callback) { - validateCallback('onClose', callback); - onClose = callback; - }, - - get: function() { return onClose; } - }, - - onMessage: { - set: function(callback) { - validateCallback('onMessage', callback); - onMessage = callback; - }, - - get: function() { return onMessage; } - } - }); - }; - - // The number of ms to wait for the websocket to connect - OT.Rumor.Socket.CONNECT_TIMEOUT = 15000; - -}(window.OT, this)); !(function() { var BUFFER_DRAIN_INTERVAL = 100, @@ -11931,1126 +7547,771 @@ waitForDomReady(); }(this)); -!(function() { - OT.Rumor.PluginSocket = function(messagingURL, events) { - - var webSocket, - state = 'initializing'; - - TBPlugin.initRumorSocket(messagingURL, OT.$.bind(function(err, rumorSocket) { - if(err) { - state = 'closed'; - events.onClose({ code: 4999 }); - } else if(state === 'initializing') { - webSocket = rumorSocket; - - webSocket.onOpen(function() { - state = 'open'; - events.onOpen(); - }); - webSocket.onClose(function(error) { - state = 'closed'; /* CLOSED */ - events.onClose({ code: error }); - }); - webSocket.onError(function(error) { - state = 'closed'; /* CLOSED */ - events.onError(error); - /* native websockets seem to do this, so should we */ - events.onClose({ code: error }); - }); - - webSocket.onMessage(function(type, addresses, headers, payload) { - var msg = new OT.Rumor.Message(type, addresses, headers, payload); - events.onMessage(msg); - }); - - webSocket.open(); - } else { - this.close(); - } - }, this)); - - this.close = function() { - if(state === 'initializing' || state === 'closed') { - state = 'closed'; - return; - } - - webSocket.close(1000, ''); - }; - - this.send = function(msg) { - if(state === 'open') { - webSocket.send(msg); - } - }; - - this.isClosed = function() { - return state === 'closed'; - }; - - }; - -}(this)); -!(function() { - - /*global TextEncoder, TextDecoder */ - - // - // - // @references - // * https://tbwiki.tokbox.com/index.php/Rumor_Message_Packet - // * https://tbwiki.tokbox.com/index.php/Rumor_Protocol - // - OT.Rumor.Message = function (type, toAddress, headers, data) { - this.type = type; - this.toAddress = toAddress; - this.headers = headers; - this.data = data; - - this.transactionId = this.headers['TRANSACTION-ID']; - this.status = this.headers.STATUS; - this.isError = !(this.status && this.status[0] === '2'); - }; - - OT.Rumor.Message.prototype.serialize = function () { - var offset = 8, - cBuf = 7, - address = [], - headerKey = [], - headerVal = [], - strArray, - dataView, - i, - j; - - // The number of addresses - cBuf++; - - // Write out the address. - for (i = 0; i < this.toAddress.length; i++) { - /*jshint newcap:false */ - address.push(new TextEncoder('utf-8').encode(this.toAddress[i])); - cBuf += 2; - cBuf += address[i].length; - } - - // The number of parameters - cBuf++; - - // Write out the params - i = 0; - - for (var key in this.headers) { - if(!this.headers.hasOwnProperty(key)) { - continue; - } - headerKey.push(new TextEncoder('utf-8').encode(key)); - headerVal.push(new TextEncoder('utf-8').encode(this.headers[key])); - cBuf += 4; - cBuf += headerKey[i].length; - cBuf += headerVal[i].length; - - i++; - } - - dataView = new TextEncoder('utf-8').encode(this.data); - cBuf += dataView.length; - - // Let's allocate a binary blob of this size - var buffer = new ArrayBuffer(cBuf); - var uint8View = new Uint8Array(buffer, 0, cBuf); - - // We don't include the header in the lenght. - cBuf -= 4; - - // Write out size (in network order) - uint8View[0] = (cBuf & 0xFF000000) >>> 24; - uint8View[1] = (cBuf & 0x00FF0000) >>> 16; - uint8View[2] = (cBuf & 0x0000FF00) >>> 8; - uint8View[3] = (cBuf & 0x000000FF) >>> 0; - - // Write out reserved bytes - uint8View[4] = 0; - uint8View[5] = 0; - - // Write out message type - uint8View[6] = this.type; - uint8View[7] = this.toAddress.length; - - // Now just copy over the encoded values.. - for (i = 0; i < address.length; i++) { - strArray = address[i]; - uint8View[offset++] = strArray.length >> 8 & 0xFF; - uint8View[offset++] = strArray.length >> 0 & 0xFF; - for (j = 0; j < strArray.length; j++) { - uint8View[offset++] = strArray[j]; - } - } - - uint8View[offset++] = headerKey.length; - - // Write out the params - for (i = 0; i < headerKey.length; i++) { - strArray = headerKey[i]; - uint8View[offset++] = strArray.length >> 8 & 0xFF; - uint8View[offset++] = strArray.length >> 0 & 0xFF; - for (j = 0; j < strArray.length; j++) { - uint8View[offset++] = strArray[j]; - } - - strArray = headerVal[i]; - uint8View[offset++] = strArray.length >> 8 & 0xFF; - uint8View[offset++] = strArray.length >> 0 & 0xFF; - for (j = 0; j < strArray.length; j++) { - uint8View[offset++] = strArray[j]; - } - } - - // And finally the data - for (i = 0; i < dataView.length; i++) { - uint8View[offset++] = dataView[i]; - } - - return buffer; - }; - - function toArrayBuffer(buffer) { - var ab = new ArrayBuffer(buffer.length); - var view = new Uint8Array(ab); - for (var i = 0; i < buffer.length; ++i) { - view[i] = buffer[i]; - } - return ab; - } - - OT.Rumor.Message.deserialize = function (buffer) { - - if(typeof Buffer !== 'undefined' && - Buffer.isBuffer(buffer)) { - buffer = toArrayBuffer(buffer); - } - var cBuf = 0, - type, - offset = 8, - uint8View = new Uint8Array(buffer), - strView, - headerlen, - headers, - keyStr, - valStr, - length, - i; - - // Write out size (in network order) - cBuf += uint8View[0] << 24; - cBuf += uint8View[1] << 16; - cBuf += uint8View[2] << 8; - cBuf += uint8View[3] << 0; - - type = uint8View[6]; - var address = []; - - for (i = 0; i < uint8View[7]; i++) { - length = uint8View[offset++] << 8; - length += uint8View[offset++]; - strView = new Uint8Array(buffer, offset, length); - /*jshint newcap:false */ - address[i] = new TextDecoder('utf-8').decode(strView); - offset += length; - } - - headerlen = uint8View[offset++]; - headers = {}; - - for (i = 0; i < headerlen; i++) { - length = uint8View[offset++] << 8; - length += uint8View[offset++]; - strView = new Uint8Array(buffer, offset, length); - keyStr = new TextDecoder('utf-8').decode(strView); - offset += length; - - length = uint8View[offset++] << 8; - length += uint8View[offset++]; - strView = new Uint8Array(buffer, offset, length); - valStr = new TextDecoder('utf-8').decode(strView); - headers[keyStr] = valStr; - offset += length; - } - - var dataView = new Uint8Array(buffer, offset); - var data = new TextDecoder('utf-8').decode(dataView); - - return new OT.Rumor.Message(type, address, headers, data); - }; - - - OT.Rumor.Message.Connect = function (uniqueId, notifyDisconnectAddress) { - var headers = { - uniqueId: uniqueId, - notifyDisconnectAddress: notifyDisconnectAddress - }; - - return new OT.Rumor.Message(OT.Rumor.MessageType.CONNECT, [], headers, ''); - }; - - OT.Rumor.Message.Disconnect = function () { - return new OT.Rumor.Message(OT.Rumor.MessageType.DISCONNECT, [], {}, ''); - }; - - OT.Rumor.Message.Subscribe = function(topics) { - return new OT.Rumor.Message(OT.Rumor.MessageType.SUBSCRIBE, topics, {}, ''); - }; - - OT.Rumor.Message.Unsubscribe = function(topics) { - return new OT.Rumor.Message(OT.Rumor.MessageType.UNSUBSCRIBE, topics, {}, ''); - }; - - OT.Rumor.Message.Publish = function(topics, message, headers) { - return new OT.Rumor.Message(OT.Rumor.MessageType.MESSAGE, topics, headers||{}, message || ''); - }; - - // This message is used to implement keepalives on the persistent - // socket connection between the client and server. Every time the - // client sends a PING to the server, the server will respond with - // a PONG. - OT.Rumor.Message.Ping = function() { - return new OT.Rumor.Message(OT.Rumor.MessageType.PING, [], {}, ''); - }; - -}(this)); -!(function() { - - // Rumor Messaging for JS - // - // https://tbwiki.tokbox.com/index.php/Raptor_Messages_(Sent_as_a_RumorMessage_payload_in_JSON) - // - // @todo Raptor { - // Look at disconnection cleanup: i.e. subscriber + publisher cleanup - // Add error codes for all the error cases - // Write unit tests for SessionInfo - // Write unit tests for Session - // Make use of the new DestroyedEvent - // Remove dependency on OT.properties - // OT.Capabilities must be part of the Raptor namespace - // Add Dependability commands - // Think about noConflict, or whether we should just use the OT namespace - // Think about how to expose OT.publishers, OT.subscribers, and OT.sessions if messaging was - // being included as a component - // Another solution to the problem of having publishers/subscribers/etc would be to make - // Raptor Socket a separate component from Dispatch (dispatch being more business logic) - // Look at the coupling of OT.sessions to OT.Raptor.Socket - // } - // - // @todo Raptor Docs { - // Document payload formats for incoming messages (what are the payloads for - // STREAM CREATED/MODIFIED for example) - // Document how keepalives work - // Document all the Raptor actions and types - // Document the session connect flow (including error cases) - // } - - OT.Raptor = { - Actions: { - //General - CONNECT: 100, - CREATE: 101, - UPDATE: 102, - DELETE: 103, - STATE: 104, - - //Moderation - FORCE_DISCONNECT: 105, - FORCE_UNPUBLISH: 106, - SIGNAL: 107, - - //Archives - CREATE_ARCHIVE: 108, - CLOSE_ARCHIVE: 109, - START_RECORDING_SESSION: 110, - STOP_RECORDING_SESSION: 111, - START_RECORDING_STREAM: 112, - STOP_RECORDING_STREAM: 113, - LOAD_ARCHIVE: 114, - START_PLAYBACK: 115, - STOP_PLAYBACK: 116, - - //AppState - APPSTATE_PUT: 117, - APPSTATE_DELETE: 118, - - // JSEP - OFFER: 119, - ANSWER: 120, - PRANSWER: 121, - CANDIDATE: 122, - SUBSCRIBE: 123, - UNSUBSCRIBE: 124, - QUERY: 125, - SDP_ANSWER: 126, - - //KeepAlive - PONG: 127, - REGISTER: 128, //Used for registering streams. - - QUALITY_CHANGED: 129 - }, - - Types: { - //RPC - RPC_REQUEST: 100, - RPC_RESPONSE: 101, - - //EVENT - STREAM: 102, - ARCHIVE: 103, - CONNECTION: 104, - APPSTATE: 105, - CONNECTIONCOUNT: 106, - MODERATION: 107, - SIGNAL: 108, - SUBSCRIBER: 110, - - //JSEP Protocol - JSEP: 109 - } - }; - -}(this)); -!(function() { - - - OT.Raptor.serializeMessage = function (message) { - return JSON.stringify(message); - }; - - - // Deserialising a Raptor message mainly means doing a JSON.parse on it. - // We do decorate the final message with a few extra helper properies though. - // - // These include: - // * typeName: A human readable version of the Raptor type. E.g. STREAM instead of 102 - // * actionName: A human readable version of the Raptor action. E.g. CREATE instead of 101 - // * signature: typeName and actionName combined. This is mainly for debugging. E.g. A type - // of 102 and an action of 101 would result in a signature of "STREAM:CREATE" - // - OT.Raptor.deserializeMessage = function (msg) { - if (msg.length === 0) return {}; - - var message = JSON.parse(msg), - bits = message.uri.substr(1).split('/'); - - // Remove the Raptor protocol version - bits.shift(); - if (bits[bits.length-1] === '') bits.pop(); - - message.params = {}; - for (var i=0, numBits=bits.length ; i 6) { - message.resource = bits[bits.length-4] + '_' + bits[bits.length-2]; - } else { - message.resource = bits[bits.length-2]; - } - } - else { - if (bits[bits.length-1] === 'channel' && bits.length > 5) { - message.resource = bits[bits.length-3] + '_' + bits[bits.length-1]; - } else { - message.resource = bits[bits.length-1]; - } - } - - message.signature = message.resource + '#' + message.method; - return message; - }; - - OT.Raptor.unboxFromRumorMessage = function (rumorMessage) { - var message = OT.Raptor.deserializeMessage(rumorMessage.data); - message.transactionId = rumorMessage.transactionId; - message.fromAddress = rumorMessage.headers['X-TB-FROM-ADDRESS']; - - return message; - }; - - OT.Raptor.parseIceServers = function (message) { - try { - return JSON.parse(message.data).content.iceServers; - } catch (e) { - return []; - } - }; - - OT.Raptor.Message = {}; - - - OT.Raptor.Message.connections = {}; - - OT.Raptor.Message.connections.create = function (apiKey, sessionId, connectionId) { - return OT.Raptor.serializeMessage({ - method: 'create', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId, - content: { - userAgent: OT.$.userAgent() - } - }); - }; - - OT.Raptor.Message.connections.destroy = function (apiKey, sessionId, connectionId) { - return OT.Raptor.serializeMessage({ - method: 'delete', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId, - content: {} - }); - }; - - - OT.Raptor.Message.sessions = {}; - - OT.Raptor.Message.sessions.get = function (apiKey, sessionId) { - return OT.Raptor.serializeMessage({ - method: 'read', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId, - content: {} - }); - }; - - - OT.Raptor.Message.streams = {}; - - OT.Raptor.Message.streams.get = function (apiKey, sessionId, streamId) { - return OT.Raptor.serializeMessage({ - method: 'read', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, - content: {} - }); - }; - - OT.Raptor.Message.streams.create = function (apiKey, sessionId, streamId, name, videoOrientation, - videoWidth, videoHeight, hasAudio, hasVideo, frameRate, minBitrate, maxBitrate) { - var channels = []; - - if (hasAudio !== void 0) { - channels.push({ - id: 'audio1', - type: 'audio', - active: hasAudio - }); - } - - if (hasVideo !== void 0) { - var channel = { - id: 'video1', - type: 'video', - active: hasVideo, - width: videoWidth, - height: videoHeight, - orientation: videoOrientation - }; - if (frameRate) channel.frameRate = frameRate; - channels.push(channel); - } - - var messageContent = { - id: streamId, - name: name, - channel: channels - }; - - if (minBitrate) messageContent.minBitrate = minBitrate; - if (maxBitrate) messageContent.maxBitrate = maxBitrate; - - return OT.Raptor.serializeMessage({ - method: 'create', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, - content: messageContent - }); - }; - - OT.Raptor.Message.streams.destroy = function (apiKey, sessionId, streamId) { - return OT.Raptor.serializeMessage({ - method: 'delete', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, - content: {} - }); - }; - - OT.Raptor.Message.streams.offer = function (apiKey, sessionId, streamId, offerSdp) { - return OT.Raptor.serializeMessage({ - method: 'offer', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, - content: { - sdp: offerSdp - } - }); - }; - - OT.Raptor.Message.streams.answer = function (apiKey, sessionId, streamId, answerSdp) { - return OT.Raptor.serializeMessage({ - method: 'answer', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, - content: { - sdp: answerSdp - } - }); - }; - - OT.Raptor.Message.streams.candidate = function (apiKey, sessionId, streamId, candidate) { - return OT.Raptor.serializeMessage({ - method: 'candidate', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, - content: candidate - }); - }; - - OT.Raptor.Message.streamChannels = {}; - OT.Raptor.Message.streamChannels.update = - function (apiKey, sessionId, streamId, channelId, attributes) { - return OT.Raptor.serializeMessage({ - method: 'update', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + - streamId + '/channel/' + channelId, - content: attributes - }); - }; - - - OT.Raptor.Message.subscribers = {}; - - OT.Raptor.Message.subscribers.create = - function (apiKey, sessionId, streamId, subscriberId, connectionId, channelsToSubscribeTo) { - var content = { - id: subscriberId, - connection: connectionId, - keyManagementMethod: OT.$.supportedCryptoScheme(), - bundleSupport: OT.$.hasCapabilities('bundle'), - rtcpMuxSupport: OT.$.hasCapabilities('RTCPMux') - }; - if (channelsToSubscribeTo) content.channel = channelsToSubscribeTo; - - return OT.Raptor.serializeMessage({ - method: 'create', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId, - content: content - }); - }; - - OT.Raptor.Message.subscribers.destroy = function (apiKey, sessionId, streamId, subscriberId) { - return OT.Raptor.serializeMessage({ - method: 'delete', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId, - content: {} - }); - }; - - OT.Raptor.Message.subscribers.update = - function (apiKey, sessionId, streamId, subscriberId, attributes) { - return OT.Raptor.serializeMessage({ - method: 'update', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId, - content: attributes - }); - }; - - - OT.Raptor.Message.subscribers.candidate = - function (apiKey, sessionId, streamId, subscriberId, candidate) { - return OT.Raptor.serializeMessage({ - method: 'candidate', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId, - content: candidate - }); - }; - - OT.Raptor.Message.subscribers.offer = - function (apiKey, sessionId, streamId, subscriberId, offerSdp) { - return OT.Raptor.serializeMessage({ - method: 'offer', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId, - content: { - sdp: offerSdp - } - }); - }; - - OT.Raptor.Message.subscribers.answer = - function (apiKey, sessionId, streamId, subscriberId, answerSdp) { - return OT.Raptor.serializeMessage({ - method: 'answer', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId, - content: { - sdp: answerSdp - } - }); - }; - - - OT.Raptor.Message.subscriberChannels = {}; - - OT.Raptor.Message.subscriberChannels.update = - function (apiKey, sessionId, streamId, subscriberId, channelId, attributes) { - return OT.Raptor.serializeMessage({ - method: 'update', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - '/stream/' + streamId + '/subscriber/' + subscriberId + '/channel/' + channelId, - content: attributes - }); - }; - - - OT.Raptor.Message.signals = {}; - - OT.Raptor.Message.signals.create = function (apiKey, sessionId, toAddress, type, data) { - var content = {}; - if (type !== void 0) content.type = type; - if (data !== void 0) content.data = data; - - return OT.Raptor.serializeMessage({ - method: 'signal', - uri: '/v2/partner/' + apiKey + '/session/' + sessionId + - (toAddress !== void 0 ? '/connection/' + toAddress : '') + '/signal/' + OT.$.uuid(), - content: content - }); - }; - -}(this)); -!(function() { - - var MAX_SIGNAL_DATA_LENGTH = 8192, - MAX_SIGNAL_TYPE_LENGTH = 128; - - // - // Error Codes: - // 413 - Type too long - // 400 - Type is invalid - // 413 - Data too long - // 400 - Data is invalid (can't be parsed as JSON) - // 429 - Rate limit exceeded - // 500 - Websocket connection is down - // 404 - To connection does not exist - // 400 - To is invalid - // - OT.Signal = function(sessionId, fromConnectionId, options) { - var isInvalidType = function(type) { - // Our format matches the unreserved characters from the URI RFC: - // http://www.ietf.org/rfc/rfc3986 - return !/^[a-zA-Z0-9\-\._~]+$/.exec(type); - }, - - validateTo = function(toAddress) { - if (!toAddress) { - return { - code: 400, - reason: 'The signal type was null or an empty String. Either set it to a non-empty ' + - 'String value or omit it' - }; - } - - if ( !(toAddress instanceof OT.Connection || toAddress instanceof OT.Session) ) { - return { - code: 400, - reason: 'The To field was invalid' - }; - } - - return null; - }, - - validateType = function(type) { - var error = null; - - if (type === null || type === void 0) { - error = { - code: 400, - reason: 'The signal type was null or undefined. Either set it to a String value or ' + - 'omit it' - }; - } - else if (type.length > MAX_SIGNAL_TYPE_LENGTH) { - error = { - code: 413, - reason: 'The signal type was too long, the maximum length of it is ' + - MAX_SIGNAL_TYPE_LENGTH + ' characters' - }; - } - else if ( isInvalidType(type) ) { - error = { - code: 400, - reason: 'The signal type was invalid, it can only contain letters, ' + - 'numbers, \'-\', \'_\', and \'~\'.' - }; - } - - return error; - }, - - validateData = function(data) { - var error = null; - if (data === null || data === void 0) { - error = { - code: 400, - reason: 'The signal data was null or undefined. Either set it to a String value or ' + - 'omit it' - }; - } - else { - try { - if (JSON.stringify(data).length > MAX_SIGNAL_DATA_LENGTH) { - error = { - code: 413, - reason: 'The data field was too long, the maximum size of it is ' + - MAX_SIGNAL_DATA_LENGTH + ' characters' - }; +// tb_require('../../../helpers/helpers.js') +// tb_require('./message.js') +// tb_require('./native_socket.js') +// tb_require('./plugin_socket.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +var WEB_SOCKET_KEEP_ALIVE_INTERVAL = 9000, + + // Magic Connectivity Timeout Constant: We wait 9*the keep alive interval, + // on the third keep alive we trigger the timeout if we haven't received the + // server pong. + WEB_SOCKET_CONNECTIVITY_TIMEOUT = 5*WEB_SOCKET_KEEP_ALIVE_INTERVAL - 100, + + wsCloseErrorCodes; + +// https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent#Close_codes +// http://docs.oracle.com/javaee/7/api/javax/websocket/CloseReason.CloseCodes.html +wsCloseErrorCodes = { + 1002: 'The endpoint is terminating the connection due to a protocol error. ' + + '(CLOSE_PROTOCOL_ERROR)', + 1003: 'The connection is being terminated because the endpoint received data of ' + + 'a type it cannot accept (for example, a text-only endpoint received binary data). ' + + '(CLOSE_UNSUPPORTED)', + 1004: 'The endpoint is terminating the connection because a data frame was received ' + + 'that is too large. (CLOSE_TOO_LARGE)', + 1005: 'Indicates that no status code was provided even though one was expected. ' + + '(CLOSE_NO_STATUS)', + 1006: 'Used to indicate that a connection was closed abnormally (that is, with no ' + + 'close frame being sent) when a status code is expected. (CLOSE_ABNORMAL)', + 1007: 'Indicates that an endpoint is terminating the connection because it has received ' + + 'data within a message that was not consistent with the type of the message (e.g., ' + + 'non-UTF-8 [RFC3629] data within a text message)', + 1008: 'Indicates that an endpoint is terminating the connection because it has received a ' + + 'message that violates its policy. This is a generic status code that can be returned ' + + 'when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a ' + + 'need to hide specific details about the policy', + 1009: 'Indicates that an endpoint is terminating the connection because it has received a ' + + 'message that is too big for it to process', + 1011: 'Indicates that a server is terminating the connection because it encountered an ' + + 'unexpected condition that prevented it from fulfilling the request', + + // .... codes in the 4000-4999 range are available for use by applications. + 4001: 'Connectivity loss was detected as it was too long since the socket received the ' + + 'last PONG message' +}; + +OT.Rumor.SocketError = function(code, message) { + this.code = code; + this.message = message; +}; + +// The NativeSocket bit is purely to make testing simpler, it defaults to WebSocket +// so in normal operation you would omit it. +OT.Rumor.Socket = function(messagingURL, notifyDisconnectAddress, NativeSocket) { + + var states = ['disconnected', 'error', 'connected', 'connecting', 'disconnecting'], + webSocket, + id, + onOpen, + onError, + onClose, + onMessage, + connectCallback, + connectTimeout, + lastMessageTimestamp, // The timestamp of the last message received + keepAliveTimer; // Timer for the connectivity checks + + + //// Private API + var stateChanged = function(newState) { + switch (newState) { + case 'disconnected': + case 'error': + webSocket = null; + if (onClose) { + var error; + if(hasLostConnectivity()) { + error = new Error(wsCloseErrorCodes[4001]); + error.code = 4001; + } + onClose(error); } - } - catch(e) { - error = {code: 400, reason: 'The data field was not valid JSON'}; - } + break; + } + }, + + setState = OT.$.statable(this, states, 'disconnected', stateChanged), + + validateCallback = function validateCallback (name, callback) { + if (callback === null || !OT.$.isFunction(callback) ) { + throw new Error('The Rumor.Socket ' + name + + ' callback must be a valid function or null'); + } + }, + + error = OT.$.bind(function error (errorMessage) { + OT.error('Rumor.Socket: ' + errorMessage); + + var socketError = new OT.Rumor.SocketError(null, errorMessage || 'Unknown Socket Error'); + + if (connectTimeout) clearTimeout(connectTimeout); + + setState('error'); + + if (this.previousState === 'connecting' && connectCallback) { + connectCallback(socketError, void 0); + connectCallback = null; } - return error; + if (onError) onError(socketError); + }, this), + + hasLostConnectivity = function hasLostConnectivity () { + if (!lastMessageTimestamp) return false; + + return (OT.$.now() - lastMessageTimestamp) >= WEB_SOCKET_CONNECTIVITY_TIMEOUT; + }, + + sendKeepAlive = OT.$.bind(function() { + if (!this.is('connected')) return; + + if ( hasLostConnectivity() ) { + webSocketDisconnected({code: 4001}); + } + else { + webSocket.send(OT.Rumor.Message.Ping()); + keepAliveTimer = setTimeout(sendKeepAlive, WEB_SOCKET_KEEP_ALIVE_INTERVAL); + } + }, this), + + // Returns true if we think the DOM has been unloaded + // It detects this by looking for the OT global, which + // should always exist until the DOM is cleaned up. + isDOMUnloaded = function isDOMUnloaded () { + return !window.OT; }; - this.toRaptorMessage = function() { - var to = this.to; - - if (to && typeof(to) !== 'string') { - to = to.id; - } - - return OT.Raptor.Message.signals.create(OT.APIKEY, sessionId, to, this.type, this.data); - }; - - this.toHash = function() { - return options; - }; - - - this.error = null; - - if (options) { - if (options.hasOwnProperty('data')) { - this.data = OT.$.clone(options.data); - this.error = validateData(this.data); - } - - if (options.hasOwnProperty('to')) { - this.to = options.to; - - if (!this.error) { - this.error = validateTo(this.to); - } - } - - if (options.hasOwnProperty('type')) { - if (!this.error) { - this.error = validateType(options.type); - } - this.type = options.type; - } - } - - this.valid = this.error === null; - }; - -}(this)); -!(function() { - - function SignalError(code, reason) { - this.code = code; - this.reason = reason; - } - - // The Dispatcher bit is purely to make testing simpler, it defaults to a new OT.Raptor.Dispatcher - // so in normal operation you would omit it. - OT.Raptor.Socket = function(widgetId, messagingSocketUrl, symphonyUrl, dispatcher) { - var _states = ['disconnected', 'connecting', 'connected', 'error', 'disconnecting'], - _sessionId, - _token, - _rumor, - _dispatcher, - _completion; - - - //// Private API - var setState = OT.$.statable(this, _states, 'disconnected'), - - onConnectComplete = function onConnectComplete(error) { - if (error) { - setState('error'); - } - else { - setState('connected'); - } - - _completion.apply(null, arguments); - }, - - onClose = OT.$.bind(function onClose (err) { - var reason = this.is('disconnecting') ? 'clientDisconnected' : 'networkDisconnected'; - - if(err && err.code === 4001) { - reason = 'networkTimedout'; - } - - setState('disconnected'); - - _dispatcher.onClose(reason); - }, this), - - onError = function onError () {}; - // @todo what does having an error mean? Are they always fatal? Are we disconnected now? - - - //// Public API - - this.connect = function (token, sessionInfo, completion) { - if (!this.is('disconnected', 'error')) { - OT.warn('Cannot connect the Raptor Socket as it is currently connected. You should ' + - 'disconnect first.'); - return; - } - - setState('connecting'); - _sessionId = sessionInfo.sessionId; - _token = token; - _completion = completion; - - var connectionId = OT.$.uuid(), - rumorChannel = '/v2/partner/' + OT.APIKEY + '/session/' + _sessionId; - - _rumor = new OT.Rumor.Socket(messagingSocketUrl, symphonyUrl); - _rumor.onClose(onClose); - _rumor.onMessage(OT.$.bind(_dispatcher.dispatch, _dispatcher)); - - _rumor.connect(connectionId, OT.$.bind(function(error) { - if (error) { - error.message = 'WebSocketConnection:' + error.code + ':' + error.message; - onConnectComplete(error); + //// Private Event Handlers + var webSocketConnected = OT.$.bind(function webSocketConnected () { + if (connectTimeout) clearTimeout(connectTimeout); + if (this.isNot('connecting')) { + OT.debug('webSocketConnected reached in state other than connecting'); return; } - // we do this here to avoid getting connect errors twice - _rumor.onError(onError); + // Connect to Rumor by registering our connection id and the + // app server address to notify if we disconnect. + // + // We don't need to wait for a reply to this message. + webSocket.send(OT.Rumor.Message.Connect(id, notifyDisconnectAddress)); - OT.debug('Raptor Socket connected. Subscribing to ' + - rumorChannel + ' on ' + messagingSocketUrl); + setState('connected'); + if (connectCallback) { + connectCallback(void 0, id); + connectCallback = null; + } - _rumor.subscribe([rumorChannel]); + if (onOpen) onOpen(id); - //connect to session - var connectMessage = OT.Raptor.Message.connections.create(OT.APIKEY, - _sessionId, _rumor.id()); - this.publish(connectMessage, {'X-TB-TOKEN-AUTH': _token}, OT.$.bind(function(error) { - if (error) { - error.message = 'ConnectToSession:' + error.code + - ':Received error response to connection create message.'; - onConnectComplete(error); - return; + keepAliveTimer = setTimeout(function() { + lastMessageTimestamp = OT.$.now(); + sendKeepAlive(); + }, WEB_SOCKET_KEEP_ALIVE_INTERVAL); + }, this), + + webSocketConnectTimedOut = function webSocketConnectTimedOut () { + var webSocketWas = webSocket; + error('Timed out while waiting for the Rumor socket to connect.'); + // This will prevent a socket eventually connecting + // But call it _after_ the error just in case any of + // the callbacks fire synchronously, breaking the error + // handling code. + try { + webSocketWas.close(); + } catch(x) {} + }, + + webSocketError = function webSocketError () {}, + // var errorMessage = 'Unknown Socket Error'; + // @fixme We MUST be able to do better than this! + + // All errors seem to result in disconnecting the socket, the close event + // has a close reason and code which gives some error context. This, + // combined with the fact that the errorEvent argument contains no + // error info at all, means we'll delay triggering the error handlers + // until the socket is closed. + // error(errorMessage); + + webSocketDisconnected = OT.$.bind(function webSocketDisconnected (closeEvent) { + if (connectTimeout) clearTimeout(connectTimeout); + if (keepAliveTimer) clearTimeout(keepAliveTimer); + + if (isDOMUnloaded()) { + // Sometimes we receive the web socket close event after + // the DOM has already been partially or fully unloaded + // if that's the case here then it's not really safe, or + // desirable, to continue. + return; + } + + if (closeEvent.code !== 1000 && closeEvent.code !== 1001) { + var reason = closeEvent.reason || closeEvent.message; + if (!reason && wsCloseErrorCodes.hasOwnProperty(closeEvent.code)) { + reason = wsCloseErrorCodes[closeEvent.code]; } - this.publish( OT.Raptor.Message.sessions.get(OT.APIKEY, _sessionId), - function (error) { - if (error) { - error.message = 'GetSessionState:' + error.code + - ':Received error response to session read'; - } - onConnectComplete.apply(null, arguments); - }); - }, this)); - }, this)); - }; - - - this.disconnect = function (drainSocketBuffer) { - if (this.is('disconnected')) return; - - setState('disconnecting'); - _rumor.disconnect(drainSocketBuffer); - }; - - // Publishs +message+ to the Symphony app server. - // - // The completion handler is optional, as is the headers - // dict, but if you provide the completion handler it must - // be the last argument. - // - this.publish = function (message, headers, completion) { - if (_rumor.isNot('connected')) { - OT.error('OT.Raptor.Socket: cannot publish until the socket is connected.' + message); - return; - } - - var transactionId = OT.$.uuid(), - _headers = {}, - _completion; - - // Work out if which of the optional arguments (headers, completion) - // have been provided. - if (headers) { - if (OT.$.isFunction(headers)) { - _headers = {}; - _completion = headers; - } - else { - _headers = headers; - } - } - if (!_completion && completion && OT.$.isFunction(completion)) _completion = completion; - - - if (_completion) _dispatcher.registerCallback(transactionId, _completion); - - OT.debug('OT.Raptor.Socket Publish (ID:' + transactionId + ') '); - OT.debug(message); - - _rumor.publish([symphonyUrl], message, OT.$.extend(_headers, { - 'Content-Type': 'application/x-raptor+v2', - 'TRANSACTION-ID': transactionId, - 'X-TB-FROM-ADDRESS': _rumor.id() - })); - - return transactionId; - }; - - // Register a new stream against _sessionId - this.streamCreate = function(name, orientation, encodedWidth, encodedHeight, - hasAudio, hasVideo, frameRate, minBitrate, maxBitrate, completion) { - var streamId = OT.$.uuid(), - message = OT.Raptor.Message.streams.create( OT.APIKEY, - _sessionId, - streamId, - name, - orientation, - encodedWidth, - encodedHeight, - hasAudio, - hasVideo, - frameRate, - minBitrate, - maxBitrate); - - this.publish(message, function(error, message) { - completion(error, streamId, message); - }); - }; - - this.streamDestroy = function(streamId) { - this.publish( OT.Raptor.Message.streams.destroy(OT.APIKEY, _sessionId, streamId) ); - }; - - this.streamChannelUpdate = function(streamId, channelId, attributes) { - this.publish( OT.Raptor.Message.streamChannels.update(OT.APIKEY, _sessionId, - streamId, channelId, attributes) ); - }; - - this.subscriberCreate = function(streamId, subscriberId, channelsToSubscribeTo, completion) { - this.publish( OT.Raptor.Message.subscribers.create(OT.APIKEY, _sessionId, - streamId, subscriberId, _rumor.id(), channelsToSubscribeTo), completion ); - }; - - this.subscriberDestroy = function(streamId, subscriberId) { - this.publish( OT.Raptor.Message.subscribers.destroy(OT.APIKEY, _sessionId, - streamId, subscriberId) ); - }; - - this.subscriberUpdate = function(streamId, subscriberId, attributes) { - this.publish( OT.Raptor.Message.subscribers.update(OT.APIKEY, _sessionId, - streamId, subscriberId, attributes) ); - }; - - this.subscriberChannelUpdate = function(streamId, subscriberId, channelId, attributes) { - this.publish( OT.Raptor.Message.subscriberChannels.update(OT.APIKEY, _sessionId, - streamId, subscriberId, channelId, attributes) ); - }; - - this.forceDisconnect = function(connectionIdToDisconnect, completion) { - this.publish( OT.Raptor.Message.connections.destroy(OT.APIKEY, _sessionId, - connectionIdToDisconnect), completion ); - }; - - this.forceUnpublish = function(streamIdToUnpublish, completion) { - this.publish( OT.Raptor.Message.streams.destroy(OT.APIKEY, _sessionId, - streamIdToUnpublish), completion ); - }; - - this.jsepCandidate = function(streamId, candidate) { - this.publish( - OT.Raptor.Message.streams.candidate(OT.APIKEY, _sessionId, streamId, candidate) - ); - }; - - this.jsepCandidateP2p = function(streamId, subscriberId, candidate) { - this.publish( - OT.Raptor.Message.subscribers.candidate(OT.APIKEY, _sessionId, streamId, - subscriberId, candidate) - ); - }; - - this.jsepOffer = function(streamId, offerSdp) { - this.publish( OT.Raptor.Message.streams.offer(OT.APIKEY, _sessionId, streamId, offerSdp) ); - }; - - this.jsepOfferP2p = function(streamId, subscriberId, offerSdp) { - this.publish( OT.Raptor.Message.subscribers.offer(OT.APIKEY, _sessionId, streamId, - subscriberId, offerSdp) ); - }; - - this.jsepAnswer = function(streamId, answerSdp) { - this.publish( OT.Raptor.Message.streams.answer(OT.APIKEY, _sessionId, streamId, answerSdp) ); - }; - - this.jsepAnswerP2p = function(streamId, subscriberId, answerSdp) { - this.publish( OT.Raptor.Message.subscribers.answer(OT.APIKEY, _sessionId, streamId, - subscriberId, answerSdp) ); - }; - - this.signal = function(options, completion) { - var signal = new OT.Signal(_sessionId, _rumor.id(), options || {}); - - if (!signal.valid) { - if (completion && OT.$.isFunction(completion)) { - completion( new SignalError(signal.error.code, signal.error.reason), signal.toHash() ); + error('Rumor Socket Disconnected: ' + reason); } - return; - } + if (this.isNot('error')) setState('disconnected'); + }, this), - this.publish( signal.toRaptorMessage(), function(err) { - var error; - if (err) error = new SignalError(err.code, err.message); + webSocketReceivedMessage = function webSocketReceivedMessage (msg) { + lastMessageTimestamp = OT.$.now(); - if (completion && OT.$.isFunction(completion)) completion(error, signal.toHash()); - }); - }; + if (onMessage) { + if (msg.type !== OT.Rumor.MessageType.PONG) { + onMessage(msg); + } + } + }; - this.id = function() { - return _rumor && _rumor.id(); - }; - if(dispatcher == null) { - dispatcher = new OT.Raptor.Dispatcher(); - } - _dispatcher = dispatcher; + //// Public API + + this.publish = function (topics, message, headers) { + webSocket.send(OT.Rumor.Message.Publish(topics, message, headers)); }; -}(this)); + this.subscribe = function(topics) { + webSocket.send(OT.Rumor.Message.Subscribe(topics)); + }; + + this.unsubscribe = function(topics) { + webSocket.send(OT.Rumor.Message.Unsubscribe(topics)); + }; + + this.connect = function (connectionId, complete) { + if (this.is('connecting', 'connected')) { + complete(new OT.Rumor.SocketError(null, + 'Rumor.Socket cannot connect when it is already connecting or connected.')); + return; + } + + id = connectionId; + connectCallback = complete; + + setState('connecting'); + + var TheWebSocket = NativeSocket || window.WebSocket; + + var events = { + onOpen: webSocketConnected, + onClose: webSocketDisconnected, + onError: webSocketError, + onMessage: webSocketReceivedMessage + }; + + try { + if(typeof TheWebSocket !== 'undefined') { + webSocket = new OT.Rumor.NativeSocket(TheWebSocket, messagingURL, events); + } else { + webSocket = new OT.Rumor.PluginSocket(messagingURL, events); + } + + connectTimeout = setTimeout(webSocketConnectTimedOut, OT.Rumor.Socket.CONNECT_TIMEOUT); + } + catch(e) { + OT.error(e); + + // @todo add an actual error message + error('Could not connect to the Rumor socket, possibly because of a blocked port.'); + } + }; + + this.disconnect = function(drainSocketBuffer) { + if (connectTimeout) clearTimeout(connectTimeout); + if (keepAliveTimer) clearTimeout(keepAliveTimer); + + if (!webSocket) { + if (this.isNot('error')) setState('disconnected'); + return; + } + + if (webSocket.isClosed()) { + if (this.isNot('error')) setState('disconnected'); + } + else { + if (this.is('connected')) { + // Look! We are nice to the rumor server ;-) + webSocket.send(OT.Rumor.Message.Disconnect()); + } + + // Wait until the socket is ready to close + webSocket.close(drainSocketBuffer); + } + }; + + + + OT.$.defineProperties(this, { + id: { + get: function() { return id; } + }, + + onOpen: { + set: function(callback) { + validateCallback('onOpen', callback); + onOpen = callback; + }, + + get: function() { return onOpen; } + }, + + onError: { + set: function(callback) { + validateCallback('onError', callback); + onError = callback; + }, + + get: function() { return onError; } + }, + + onClose: { + set: function(callback) { + validateCallback('onClose', callback); + onClose = callback; + }, + + get: function() { return onClose; } + }, + + onMessage: { + set: function(callback) { + validateCallback('onMessage', callback); + onMessage = callback; + }, + + get: function() { return onMessage; } + } + }); +}; + +// The number of ms to wait for the websocket to connect +OT.Rumor.Socket.CONNECT_TIMEOUT = 15000; + + +// tb_require('../../../helpers/helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +// Rumor Messaging for JS +// +// https://tbwiki.tokbox.com/index.php/Raptor_Messages_(Sent_as_a_RumorMessage_payload_in_JSON) +// +// @todo Raptor { +// Look at disconnection cleanup: i.e. subscriber + publisher cleanup +// Add error codes for all the error cases +// Write unit tests for SessionInfo +// Write unit tests for Session +// Make use of the new DestroyedEvent +// Remove dependency on OT.properties +// OT.Capabilities must be part of the Raptor namespace +// Add Dependability commands +// Think about noConflict, or whether we should just use the OT namespace +// Think about how to expose OT.publishers, OT.subscribers, and OT.sessions if messaging was +// being included as a component +// Another solution to the problem of having publishers/subscribers/etc would be to make +// Raptor Socket a separate component from Dispatch (dispatch being more business logic) +// Look at the coupling of OT.sessions to OT.Raptor.Socket +// } +// +// @todo Raptor Docs { +// Document payload formats for incoming messages (what are the payloads for +// STREAM CREATED/MODIFIED for example) +// Document how keepalives work +// Document all the Raptor actions and types +// Document the session connect flow (including error cases) +// } + +OT.Raptor = { + Actions: { + //General + CONNECT: 100, + CREATE: 101, + UPDATE: 102, + DELETE: 103, + STATE: 104, + + //Moderation + FORCE_DISCONNECT: 105, + FORCE_UNPUBLISH: 106, + SIGNAL: 107, + + //Archives + CREATE_ARCHIVE: 108, + CLOSE_ARCHIVE: 109, + START_RECORDING_SESSION: 110, + STOP_RECORDING_SESSION: 111, + START_RECORDING_STREAM: 112, + STOP_RECORDING_STREAM: 113, + LOAD_ARCHIVE: 114, + START_PLAYBACK: 115, + STOP_PLAYBACK: 116, + + //AppState + APPSTATE_PUT: 117, + APPSTATE_DELETE: 118, + + // JSEP + OFFER: 119, + ANSWER: 120, + PRANSWER: 121, + CANDIDATE: 122, + SUBSCRIBE: 123, + UNSUBSCRIBE: 124, + QUERY: 125, + SDP_ANSWER: 126, + + //KeepAlive + PONG: 127, + REGISTER: 128, //Used for registering streams. + + QUALITY_CHANGED: 129 + }, + + Types: { + //RPC + RPC_REQUEST: 100, + RPC_RESPONSE: 101, + + //EVENT + STREAM: 102, + ARCHIVE: 103, + CONNECTION: 104, + APPSTATE: 105, + CONNECTIONCOUNT: 106, + MODERATION: 107, + SIGNAL: 108, + SUBSCRIBER: 110, + + //JSEP Protocol + JSEP: 109 + } +}; + +// tb_require('../../../helpers/helpers.js') +// tb_require('./raptor.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +OT.Raptor.serializeMessage = function (message) { + return JSON.stringify(message); +}; + + +// Deserialising a Raptor message mainly means doing a JSON.parse on it. +// We do decorate the final message with a few extra helper properies though. +// +// These include: +// * typeName: A human readable version of the Raptor type. E.g. STREAM instead of 102 +// * actionName: A human readable version of the Raptor action. E.g. CREATE instead of 101 +// * signature: typeName and actionName combined. This is mainly for debugging. E.g. A type +// of 102 and an action of 101 would result in a signature of "STREAM:CREATE" +// +OT.Raptor.deserializeMessage = function (msg) { + if (msg.length === 0) return {}; + + var message = JSON.parse(msg), + bits = message.uri.substr(1).split('/'); + + // Remove the Raptor protocol version + bits.shift(); + if (bits[bits.length-1] === '') bits.pop(); + + message.params = {}; + for (var i=0, numBits=bits.length ; i 6) { + message.resource = bits[bits.length-4] + '_' + bits[bits.length-2]; + } else { + message.resource = bits[bits.length-2]; + } + } + else { + if (bits[bits.length-1] === 'channel' && bits.length > 5) { + message.resource = bits[bits.length-3] + '_' + bits[bits.length-1]; + } else { + message.resource = bits[bits.length-1]; + } + } + + message.signature = message.resource + '#' + message.method; + return message; +}; + +OT.Raptor.unboxFromRumorMessage = function (rumorMessage) { + var message = OT.Raptor.deserializeMessage(rumorMessage.data); + message.transactionId = rumorMessage.transactionId; + message.fromAddress = rumorMessage.headers['X-TB-FROM-ADDRESS']; + + return message; +}; + +OT.Raptor.parseIceServers = function (message) { + try { + return JSON.parse(message.data).content.iceServers; + } catch (e) { + return []; + } +}; + +OT.Raptor.Message = {}; + + +OT.Raptor.Message.offer = function (uri, offerSdp) { + return OT.Raptor.serializeMessage({ + method: 'offer', + uri: uri, + content: { + sdp: offerSdp + } + }); +}; + + +OT.Raptor.Message.connections = {}; + +OT.Raptor.Message.connections.create = function (apiKey, sessionId, connectionId) { + return OT.Raptor.serializeMessage({ + method: 'create', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId, + content: { + userAgent: OT.$.env.userAgent + } + }); +}; + +OT.Raptor.Message.connections.destroy = function (apiKey, sessionId, connectionId) { + return OT.Raptor.serializeMessage({ + method: 'delete', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/connection/' + connectionId, + content: {} + }); +}; + + +OT.Raptor.Message.sessions = {}; + +OT.Raptor.Message.sessions.get = function (apiKey, sessionId) { + return OT.Raptor.serializeMessage({ + method: 'read', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId, + content: {} + }); +}; + + +OT.Raptor.Message.streams = {}; + +OT.Raptor.Message.streams.get = function (apiKey, sessionId, streamId) { + return OT.Raptor.serializeMessage({ + method: 'read', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, + content: {} + }); +}; + +OT.Raptor.Message.streams.channelFromOTChannel = function(channel) { + var raptorChannel = { + id: channel.id, + type: channel.type, + active: channel.active + }; + + if (channel.type === 'video') { + raptorChannel.width = channel.width; + raptorChannel.height = channel.height; + raptorChannel.orientation = channel.orientation; + raptorChannel.frameRate = channel.frameRate; + if (channel.source !== 'default') { + raptorChannel.source = channel.source; + } + raptorChannel.fitMode = channel.fitMode; + } + + return raptorChannel; +}; + +OT.Raptor.Message.streams.create = function (apiKey, sessionId, streamId, name, + audioFallbackEnabled, channels, minBitrate, maxBitrate) { + var messageContent = { + id: streamId, + name: name, + audioFallbackEnabled: audioFallbackEnabled, + channel: OT.$.map(channels, function(channel) { + return OT.Raptor.Message.streams.channelFromOTChannel(channel); + }) + }; + + if (minBitrate) messageContent.minBitrate = minBitrate; + if (maxBitrate) messageContent.maxBitrate = maxBitrate; + + return OT.Raptor.serializeMessage({ + method: 'create', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, + content: messageContent + }); +}; + +OT.Raptor.Message.streams.destroy = function (apiKey, sessionId, streamId) { + return OT.Raptor.serializeMessage({ + method: 'delete', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, + content: {} + }); +}; + + +OT.Raptor.Message.streams.answer = function (apiKey, sessionId, streamId, answerSdp) { + return OT.Raptor.serializeMessage({ + method: 'answer', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, + content: { + sdp: answerSdp + } + }); +}; + +OT.Raptor.Message.streams.candidate = function (apiKey, sessionId, streamId, candidate) { + return OT.Raptor.serializeMessage({ + method: 'candidate', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + streamId, + content: candidate + }); +}; + +OT.Raptor.Message.streamChannels = {}; +OT.Raptor.Message.streamChannels.update = + function (apiKey, sessionId, streamId, channelId, attributes) { + return OT.Raptor.serializeMessage({ + method: 'update', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + '/stream/' + + streamId + '/channel/' + channelId, + content: attributes + }); +}; + + +OT.Raptor.Message.subscribers = {}; + +OT.Raptor.Message.subscribers.create = + function (apiKey, sessionId, streamId, subscriberId, connectionId, channelsToSubscribeTo) { + var content = { + id: subscriberId, + connection: connectionId, + keyManagementMethod: OT.$.supportedCryptoScheme(), + bundleSupport: OT.$.hasCapabilities('bundle'), + rtcpMuxSupport: OT.$.hasCapabilities('RTCPMux') + }; + if (channelsToSubscribeTo) content.channel = channelsToSubscribeTo; + + return OT.Raptor.serializeMessage({ + method: 'create', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + '/stream/' + streamId + '/subscriber/' + subscriberId, + content: content + }); +}; + +OT.Raptor.Message.subscribers.destroy = function (apiKey, sessionId, streamId, subscriberId) { + return OT.Raptor.serializeMessage({ + method: 'delete', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + '/stream/' + streamId + '/subscriber/' + subscriberId, + content: {} + }); +}; + +OT.Raptor.Message.subscribers.update = + function (apiKey, sessionId, streamId, subscriberId, attributes) { + return OT.Raptor.serializeMessage({ + method: 'update', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + '/stream/' + streamId + '/subscriber/' + subscriberId, + content: attributes + }); +}; + + +OT.Raptor.Message.subscribers.candidate = + function (apiKey, sessionId, streamId, subscriberId, candidate) { + return OT.Raptor.serializeMessage({ + method: 'candidate', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + '/stream/' + streamId + '/subscriber/' + subscriberId, + content: candidate + }); +}; + + +OT.Raptor.Message.subscribers.answer = + function (apiKey, sessionId, streamId, subscriberId, answerSdp) { + return OT.Raptor.serializeMessage({ + method: 'answer', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + '/stream/' + streamId + '/subscriber/' + subscriberId, + content: { + sdp: answerSdp + } + }); +}; + + +OT.Raptor.Message.subscriberChannels = {}; + +OT.Raptor.Message.subscriberChannels.update = + function (apiKey, sessionId, streamId, subscriberId, channelId, attributes) { + return OT.Raptor.serializeMessage({ + method: 'update', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + '/stream/' + streamId + '/subscriber/' + subscriberId + '/channel/' + channelId, + content: attributes + }); +}; + + +OT.Raptor.Message.signals = {}; + +OT.Raptor.Message.signals.create = function (apiKey, sessionId, toAddress, type, data) { + var content = {}; + if (type !== void 0) content.type = type; + if (data !== void 0) content.data = data; + + return OT.Raptor.serializeMessage({ + method: 'signal', + uri: '/v2/partner/' + apiKey + '/session/' + sessionId + + (toAddress !== void 0 ? '/connection/' + toAddress : '') + '/signal/' + OT.$.uuid(), + content: content + }); +}; + +// tb_require('../../../helpers/helpers.js') +// tb_require('./message.js') + + !(function() { - /*global EventEmitter, util*/ + /* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ + /* global OT, EventEmitter, util */ // Connect error codes and reasons that Raptor can return. var connectErrorReasons; @@ -13064,7 +8325,7 @@ waitForDomReady(); OT.Raptor.Dispatcher = function () { - if(OT.isNodeModule) { + if(OT.$.env.name === 'Node') { EventEmitter.call(this); } else { OT.$.eventing(this, true); @@ -13074,7 +8335,7 @@ waitForDomReady(); this.callbacks = {}; }; - if(OT.isNodeModule) { + if(OT.$.env.name === 'Node') { util.inherits(OT.Raptor.Dispatcher, EventEmitter); } @@ -13322,7 +8583,7 @@ waitForDomReady(); case 'created': this.emit('archive#created', message.content); break; - + case 'updated': this.emit('archive#updated', message.params.archive, message.content); break; @@ -13330,12 +8591,21 @@ waitForDomReady(); }; }(this)); + +// tb_require('../../../helpers/helpers.js') +// tb_require('./message.js') +// tb_require('./dispatch.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + (function(window) { // @todo hide these - OT.publishers = new OT.Collection('guid'); // Publishers are id'd by their guid - OT.subscribers = new OT.Collection('widgetId'); // Subscribers are id'd by their widgetId - OT.sessions = new OT.Collection(); + OT.publishers = new OT.$.Collection('guid'); // Publishers are id'd by their guid + OT.subscribers = new OT.$.Collection('widgetId'); // Subscribers are id'd by their widgetId + OT.sessions = new OT.$.Collection(); function parseStream(dict, session) { var channel = dict.channel.map(function(channel) { @@ -13376,20 +8646,78 @@ waitForDomReady(); return archive; } - var sessionRead, - sessionReadQueue = [], - // streams for which corresponding connectionCreated events have not been dispatched: - unconnectedStreams = {}; + var DelayedEventQueue = function DelayedEventQueue (eventDispatcher) { + var queue = []; - function sessionReadQueuePush(type, args) { - var triggerArgs = ['signal']; - triggerArgs.push.apply(triggerArgs, args); - sessionReadQueue.push(triggerArgs); - } + this.enqueue = function enqueue (/* arg1, arg2, ..., argN */) { + queue.push( Array.prototype.slice.call(arguments) ); + }; + + this.triggerAll = function triggerAll () { + var event; + + // Array.prototype.shift is actually pretty inefficient for longer Arrays, + // this is because after the first element is removed it reshuffles every + // remaining element up one (1). This involves way too many allocations and + // deallocations as the queue size increases. + // + // A more efficient version could be written by keeping an index to the current + // 'first' element in the Array and increasing that by one whenever an element + // is removed. The elements that are infront of the index have been 'deleted'. + // Periodically the front of the Array could be spliced off to reclaim the space. + // + // 1. http://www.ecma-international.org/ecma-262/5.1/#sec-15.4.4.9 + // + // + // TLDR: Array.prototype.shift is O(n), where n is the array length, + // instead of the expected O(1). You can implement your own shift that runs + // in amortised constant time. + // + // @todo benchmark and see if we should actually care about shift's performance + // for our common queue sizes. + // + while( (event = queue.shift()) ) { + eventDispatcher.trigger.apply(eventDispatcher, event); + } + }; + }; + + var DelayedSessionEvents = function(dispatcher) { + var eventQueues = {}; + + this.enqueue = function enqueue (/* key, arg1, arg2, ..., argN */) { + var key = arguments[0]; + var eventArgs = Array.prototype.slice.call(arguments, 1); + if (!eventQueues[key]) { + eventQueues[key] = new DelayedEventQueue(dispatcher); + } + eventQueues[key].enqueue.apply(eventQueues[key], eventArgs); + }; + + this.triggerConnectionCreated = function triggerConnectionCreated (connection) { + if (eventQueues['connectionCreated' + connection.id]) { + eventQueues['connectionCreated' + connection.id].triggerAll(); + } + }; + + this.triggerSessionConnected = function triggerSessionConnected (connections) { + if (eventQueues.sessionConnected) { + eventQueues.sessionConnected.triggerAll(); + } + + OT.$.forEach(connections, function(connection) { + this.triggerConnectionCreated(connection); + }); + }; + }; + + var unconnectedStreams = {}; window.OT.SessionDispatcher = function(session) { - var dispatcher = new OT.Raptor.Dispatcher(); + var dispatcher = new OT.Raptor.Dispatcher(), + sessionStateReceived = false, + delayedSessionEvents = new DelayedSessionEvents(dispatcher); dispatcher.on('close', function(reason) { @@ -13406,8 +8734,36 @@ waitForDomReady(); } connection.destroy( reason ); - }); + + // This method adds connections to the session both on a connection#created and + // on a session#read. In the case of session#read sessionRead is set to true and + // we include our own connection. + var addConnection = function (connection, sessionRead) { + connection = OT.Connection.fromHash(connection); + if (sessionRead || session.connection && connection.id !== session.connection.id) { + session.connections.add( connection ); + delayedSessionEvents.triggerConnectionCreated(connection); + } + + OT.$.forEach(OT.$.keys(unconnectedStreams), function(streamId) { + var stream = unconnectedStreams[streamId]; + if (stream && connection.id === stream.connection.id) { + // dispatch streamCreated event now that the connectionCreated has been dispatched + parseAndAddStreamToSession(stream, session); + delete unconnectedStreams[stream.id]; + + var payload = { + debug: sessionRead ? 'connection came in session#read' : + 'connection came in connection#created', + streamId : stream.id, + }; + session.logEvent('streamCreated', 'warning', payload); + } + }); + + return connection; + }; dispatcher.on('session#read', function(content, transactionId) { @@ -13419,9 +8775,8 @@ waitForDomReady(); state.archives = []; OT.$.forEach(content.connection, function(connectionParams) { - connection = OT.Connection.fromHash(connectionParams); + connection = addConnection(connectionParams, true); state.connections.push(connection); - session.connections.add(connection); }); OT.$.forEach(content.stream, function(streamParams) { @@ -13436,28 +8791,12 @@ waitForDomReady(); dispatcher.triggerCallback(transactionId, null, state); - sessionRead = true; - for (var i = 0; i < sessionReadQueue.length; ++i) { - dispatcher.trigger.apply(dispatcher, sessionReadQueue[i]); - } - sessionReadQueue = []; - + sessionStateReceived = true; + delayedSessionEvents.triggerSessionConnected(session.connections); }); dispatcher.on('connection#created', function(connection) { - connection = OT.Connection.fromHash(connection); - if (session.connection && connection.id !== session.connection.id) { - session.connections.add( connection ); - } - - OT.$.forEach(OT.$.keys(unconnectedStreams), function(streamId) { - var stream = unconnectedStreams[streamId]; - if (stream && connection.id === stream.connection.id) { - // dispatch streamCreated event now that the connectionCreated has been dispatched - parseAndAddStreamToSession(stream, session); - delete unconnectedStreams[stream.id]; - } - }); + addConnection(connection); }); dispatcher.on('connection#deleted', function(connection, reason) { @@ -13471,6 +8810,12 @@ waitForDomReady(); stream = parseAndAddStreamToSession(stream, session); } else { unconnectedStreams[stream.id] = stream; + + var payload = { + type : 'eventOrderError -- streamCreated event before connectionCreated', + streamId : stream.id, + }; + session.logEvent('streamCreated', 'warning', payload); } if (stream.publisher) { @@ -13668,12 +9013,20 @@ waitForDomReady(); }); dispatcher.on('signal', function(fromAddress, signalType, data) { - if (sessionRead) { - var fromConnection = session.connections.get(fromAddress); - session._.dispatchSignal(fromConnection, signalType, data); + var fromConnection = session.connections.get(fromAddress); + if (session.connection && fromAddress === session.connection.connectionId) { + if (sessionStateReceived) { + session._.dispatchSignal(fromConnection, signalType, data); + } else { + delayedSessionEvents.enqueue('sessionConnected', + 'signal', fromAddress, signalType, data); + } } else { - if (!sessionRead) { - sessionReadQueuePush('signal', arguments); + if (session.connections.get(fromAddress)) { + session._.dispatchSignal(fromConnection, signalType, data); + } else { + delayedSessionEvents.enqueue('connectionCreated' + fromAddress, + 'signal', fromAddress, signalType, data); } } }); @@ -13700,135 +9053,8106 @@ waitForDomReady(); }; })(window); -!(function() { - // Helper to synchronise several startup tasks and then dispatch a unified - // 'envLoaded' event. - // - // This depends on: - // * OT - // * OT.Config - // - function EnvironmentLoader() { - var _configReady = false, +// tb_require('../../helpers/helpers.js') - // If the plugin is installed, then we should wait for it to - // be ready as well. - _pluginSupported = TBPlugin.isSupported(), - _pluginLoadAttemptComplete = _pluginSupported ? TBPlugin.isReady() : true, +/* global OT, Promise */ - isReady = function() { - return !OT.$.isDOMUnloaded() && OT.$.isReady() && - _configReady && _pluginLoadAttemptComplete; - }, +function httpTest(config) { - onLoaded = function() { - if (isReady()) { - OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_LOADED)); - } - }, + function otRequest(url, options, callback) { + var request = new XMLHttpRequest(), + _options = options || {}, + _method = _options.method; + + if (!_method) { + callback(new OT.$.Error('No HTTP method specified in options')); + return; + } + + // Setup callbacks to correctly respond to success and error callbacks. This includes + // interpreting the responses HTTP status, which XmlHttpRequest seems to ignore + // by default. + if (callback) { + OTHelpers.on(request, 'load', function(event) { + var status = event.target.status; + + // We need to detect things that XMLHttpRequest considers a success, + // but we consider to be failures. + if (status >= 200 && (status < 300 || status === 304)) { + callback(null, event); + } else { + callback(event); + } + }); + + OTHelpers.on(request, 'error', callback); + } + + request.open(options.method, url, true); + + if (!_options.headers) _options.headers = {}; + + for (var name in _options.headers) { + request.setRequestHeader(name, _options.headers[name]); + } + + return request; + } - onDomReady = function() { - OT.$.onDOMUnload(onDomUnload); + var _httpConfig = config.httpConfig; - // The Dynamic Config won't load until the DOM is ready - OT.Config.load(OT.properties.configURL); + function timeout(delay) { + return new Promise(function(resolve) { + setTimeout(function() { + resolve(); + }, delay); + }); + } - onLoaded(); - }, + function generateRandom10DigitNumber() { + var min = 1000000000; + var max = 9999999999; + return min + Math.floor(Math.random() * (max - min)); + } - onDomUnload = function() { - // Disconnect the session first, this will prevent the plugin - // from locking up during browser unload. - // if (_pluginSupported) { - // var sessions = OT.sessions.where(); - // for (var i=0; i} + */ + function doDownload() { - OT.publishers.destroy(); - OT.subscribers.destroy(); - OT.sessions.destroy('unloaded'); + var xhr; + var startTs; + var loadedLength = 0; + var downloadPromise = new Promise(function(resolve, reject) { + xhr = otRequest([_httpConfig.downloadUrl, '?x=', generateRandom10DigitNumber()].join(''), + {method: 'get'}, function(error) { + if (error) { + reject(new OT.$.Error('Connection to the HTTP server failed (' + + error.target.status + ')', 1006)); + } else { + resolve(); + } + }); - OT.dispatchEvent(new OT.EnvLoadedEvent(OT.Event.names.ENV_UNLOADED)); - }, + xhr.addEventListener('loadstart', function() { + startTs = OT.$.now(); + }); + xhr.addEventListener('progress', function(evt) { + loadedLength = evt.loaded; + }); - onPluginReady = function(err) { - // We mark the plugin as ready so as not to stall the environment - // loader. In this case though, TBPlugin is not supported. - _pluginLoadAttemptComplete = true; + xhr.send(); + }); - if (err) { - OT.debug('TB Plugin failed to load or was not installed'); - } - - onLoaded(); - }, - - configLoaded = function() { - _configReady = true; - OT.Config.off('dynamicConfigChanged', configLoaded); - OT.Config.off('dynamicConfigLoadFailed', configLoadFailed); - - onLoaded(); - }, - - configLoadFailed = function() { - configLoaded(); + return Promise.race([ + downloadPromise, + timeout(_httpConfig.duration * 1000) + ]) + .then(function() { + xhr.abort(); + return { + byteDownloaded: loadedLength, + duration: OT.$.now() - startTs }; + }); + } + function doUpload() { + var payload = new Array(_httpConfig.uploadSize * _httpConfig.uploadCount).join('a'); - OT.Config.on('dynamicConfigChanged', configLoaded); - OT.Config.on('dynamicConfigLoadFailed', configLoadFailed); + var xhr; + var startTs; + var loadedLength = 0; + var uploadPromise = new Promise(function(resolve, reject) { + xhr = otRequest(_httpConfig.uploadUrl, {method: 'post'}, function(error) { + if (error) { + reject(new OT.$.Error('Connection to the HTTP server failed (' + + error.target.status + ')', 1006)); + } else { + resolve(); + } + }); - OT.$.onDOMLoad(onDomReady); + xhr.upload.addEventListener('loadstart', function() { + startTs = OT.$.now(); + }); + xhr.upload.addEventListener('progress', function(evt) { + loadedLength = evt.loaded; + }); - // If the plugin should work on this platform then - // see if it loads. - if (_pluginSupported) TBPlugin.ready(onPluginReady); + xhr.send(payload); + }); - this.onLoad = function(cb, context) { - if (isReady()) { - cb.call(context); - return; + return Promise.race([ + uploadPromise, + timeout(_httpConfig.duration * 1000) + ]) + .then(function() { + xhr.abort(); + return { + byteUploaded: loadedLength, + duration: OT.$.now() - startTs + }; + }); + } + + return Promise.all([doDownload(), doUpload()]) + .then(function(values) { + var downloadStats = values[0]; + var uploadStats = values[1]; + + return { + downloadBandwidth: 1000 * (downloadStats.byteDownloaded * 8) / downloadStats.duration, + uploadBandwidth: 1000 * (uploadStats.byteUploaded * 8) / uploadStats.duration + }; + }); +} + +OT.httpTest = httpTest; + +// tb_require('../../helpers/helpers.js') + +/* exported SDPHelpers */ + +// 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: / [/] +// * m= / +// +// References: +// * https://tools.ietf.org/html/rfc4566 +// * http://en.wikipedia.org/wiki/Session_Description_Protocol +// +var SDPHelpers = { + // Search through sdpLines to find the Media Line of type +mediaType+. + getMLineIndex: function getMLineIndex(sdpLines, mediaType) { + var targetMLine = 'm=' + mediaType; + + // Find the index of the media line for +type+ + return OT.$.findIndex(sdpLines, function(line) { + if (line.indexOf(targetMLine) !== -1) { + return true; } - OT.on(OT.Event.names.ENV_LOADED, cb, context); - }; + return false; + }); + }, - this.onUnload = function(cb, context) { - if (this.isUnloaded()) { - cb.call(context); - return; + // Extract the payload types for a give Media Line. + // + getMLinePayloadTypes: function getMLinePayloadTypes (mediaLine, mediaType) { + var mLineSelector = new RegExp('^m=' + mediaType + + ' \\d+(/\\d+)? [a-zA-Z0-9/]+(( [a-zA-Z0-9/]+)+)$', 'i'); + + // Get all payload types that the line supports + var payloadTypes = mediaLine.match(mLineSelector); + if (!payloadTypes || payloadTypes.length < 2) { + // Error, invalid M line? + return []; + } + + return OT.$.trim(payloadTypes[2]).split(' '); + }, + + removeTypesFromMLine: function removeTypesFromMLine (mediaLine, payloadTypes) { + return OT.$.trim( + mediaLine.replace(new RegExp(' ' + payloadTypes.join(' |'), 'ig') , ' ') + .replace(/\s+/g, ' ') ); + }, + + + // 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--; } - OT.on(OT.Event.names.ENV_UNLOADED, cb, context); - }; + // remove this one + return false; + }); - this.isUnloaded = function() { - return OT.$.isDOMUnloaded(); + 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); + } +}; + + + +// tb_require('../../helpers/helpers.js') + +function isVideoStat(stat) { + // Chrome implementation only has this property for RTP video stat + return stat.hasOwnProperty('googFrameWidthReceived') || + stat.hasOwnProperty('googFrameWidthInput') || + stat.mediaType === 'video'; +} + +function isAudioStat(stat) { + // Chrome implementation only has this property for RTP audio stat + return stat.hasOwnProperty('audioInputLevel') || + stat.hasOwnProperty('audioOutputLevel') || + stat.mediaType === 'audio'; +} + +function isInboundStat(stat) { + return stat.hasOwnProperty('bytesReceived'); +} + +function parseStatCategory(stat) { + var statCategory = { + packetsLost: 0, + packetsReceived: 0, + bytesReceived: 0 + }; + + if (stat.hasOwnProperty('packetsReceived')) { + statCategory.packetsReceived = parseInt(stat.packetsReceived, 10); + } + if (stat.hasOwnProperty('packetsLost')) { + statCategory.packetsLost = parseInt(stat.packetsLost, 10); + } + if (stat.hasOwnProperty('bytesReceived')) { + statCategory.bytesReceived += parseInt(stat.bytesReceived, 10); + } + + return statCategory; +} + +function normalizeTimestamp(timestamp) { + if (OT.$.isObject(timestamp) && 'getTime' in timestamp) { + // Chrome as of 39 delivers a "kind of Date" object for timestamps + // we duck check it and get the timestamp + return timestamp.getTime(); + } else { + return timestamp; + } +} + +var getStatsHelpers = {}; +getStatsHelpers.isVideoStat = isVideoStat; +getStatsHelpers.isAudioStat = isAudioStat; +getStatsHelpers.isInboundStat = isInboundStat; +getStatsHelpers.parseStatCategory = parseStatCategory; +getStatsHelpers.normalizeTimestamp = normalizeTimestamp; + +OT.getStatsHelpers = getStatsHelpers; + +// tb_require('../../helpers/helpers.js') + +/** + * + * @returns {function(RTCPeerConnection, + * function(DOMError, Array.<{id: string=, type: string=, timestamp: number}>))} + */ +function getStatsAdapter() { + + /// +// Get Stats using the older API. Used by all current versions +// of Chrome. +// + function getStatsOldAPI(peerConnection, completion) { + + peerConnection.getStats(function(rtcStatsReport) { + + var stats = []; + rtcStatsReport.result().forEach(function(rtcStat) { + + var stat = {}; + + rtcStat.names().forEach(function(name) { + stat[name] = rtcStat.stat(name); + }); + + // fake the structure of the "new" RTC stat object + stat.id = rtcStat.id; + stat.type = rtcStat.type; + stat.timestamp = rtcStat.timestamp; + stats.push(stat); + }); + + completion(null, stats); + }); + } + +/// +// Get Stats using the newer API. +// + function getStatsNewAPI(peerConnection, completion) { + + peerConnection.getStats(null, function(rtcStatsReport) { + + var stats = []; + rtcStatsReport.forEach(function(rtcStats) { + stats.push(rtcStats); + }); + + completion(null, stats); + }, completion); + } + + if (OT.$.browserVersion().name === 'Firefox' || OTPlugin.isInstalled()) { + return getStatsNewAPI; + } else { + return getStatsOldAPI; + } +} + +OT.getStatsAdpater = getStatsAdapter; + +// tb_require('../../helpers/helpers.js') +// tb_require('../peer_connection/get_stats_adapter.js') +// tb_require('../peer_connection/get_stats_helpers.js') + +/* global OT, Promise */ + +/** + * @returns {Promise.<{packetLostRation: number, roundTripTime: number}>} + */ +function webrtcTest(config) { + + var _getStats = OT.getStatsAdpater(); + var _mediaConfig = config.mediaConfig; + var _localStream = config.localStream; + + // todo copied from peer_connection.js + // Normalise these + var NativeRTCSessionDescription, + NativeRTCIceCandidate; + if (!OTPlugin.isInstalled()) { + // order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless + NativeRTCSessionDescription = (window.mozRTCSessionDescription || + window.RTCSessionDescription); + NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate); + } + else { + NativeRTCSessionDescription = OTPlugin.RTCSessionDescription; + NativeRTCIceCandidate = OTPlugin.RTCIceCandidate; + } + + + function isCandidateRelay(candidate) { + return candidate.candidate.indexOf('relay') !== -1; + } + + /** + * Create video a element attaches it to the body and put it visually outside the body. + * + * @returns {OT.VideoElement} + */ + function createVideoElementForTest() { + var videoElement = new OT.VideoElement({attributes: {muted: true}}); + videoElement.domElement().style.position = 'absolute'; + videoElement.domElement().style.top = '-9999%'; + videoElement.appendTo(document.body); + return videoElement; + } + + function createPeerConnectionForTest() { + return new Promise(function(resolve, reject) { + OT.$.createPeerConnection({ + iceServers: _mediaConfig.iceServers + }, {}, + null, + function(error, pc) { + if (error) { + reject(new OT.$.Error('createPeerConnection failed', 1600, error)); + } else { + resolve(pc); + } + } + ); + }); + } + + function createOffer(pc) { + return new Promise(function(resolve, reject) { + pc.createOffer(resolve, reject); + }); + } + + function attachMediaStream(videoElement, webRtcStream) { + return new Promise(function(resolve, reject) { + videoElement.bindToStream(webRtcStream, function(error) { + if (error) { + reject(new OT.$.Error('bindToStream failed', 1600, error)); + } else { + resolve(); + } + }); + }); + } + + function addIceCandidate(pc, candidate) { + return new Promise(function(resolve, reject) { + pc.addIceCandidate(new NativeRTCIceCandidate({ + sdpMLineIndex: candidate.sdpMLineIndex, + candidate: candidate.candidate + }), resolve, reject); + }); + } + + function setLocalDescription(pc, offer) { + return new Promise(function(resolve, reject) { + pc.setLocalDescription(offer, resolve, function(error) { + reject(new OT.$.Error('setLocalDescription failed', 1600, error)); + }); + }); + } + + function setRemoteDescription(pc, offer) { + return new Promise(function(resolve, reject) { + pc.setRemoteDescription(offer, resolve, function(error) { + reject(new OT.$.Error('setRemoteDescription failed', 1600, error)); + }); + }); + } + + function createAnswer(pc) { + return new Promise(function(resolve, reject) { + pc.createAnswer(resolve, function(error) { + reject(new OT.$.Error('createAnswer failed', 1600, error)); + }); + }); + } + + function getStats(pc) { + return new Promise(function(resolve, reject) { + _getStats(pc, function(error, stats) { + if (error) { + reject(new OT.$.Error('geStats failed', 1600, error)); + } else { + resolve(stats); + } + }); + }); + } + + function createOnIceCandidateListener(pc) { + return function(event) { + if (event.candidate && isCandidateRelay(event.candidate)) { + addIceCandidate(pc, event.candidate)['catch'](function() { + OT.warn('An error occurred while adding a ICE candidate during webrtc test'); + }); + } }; } - var EnvLoader = new EnvironmentLoader(); + /** + * @returns {Promise.<{packetLostRation: number, roundTripTime: number}>} + */ + function collectPeerConnectionStats(localPc, remotePc) { - OT.onLoad = function(cb, context) { - EnvLoader.onLoad(cb, context); + var SAMPLING_DELAY = 1000; + + return new Promise(function(resolve) { + + var collectionActive = true; + + var statsSamples = { + startTs: OT.$.now(), + packetLostRatioSamplesCount: 0, + packetLostRatio: 0, + roundTripTimeSamplesCount: 0, + roundTripTime: 0, + bytesReceived: 0 + }; + + function calculateBandwidth() { + return 1000 * statsSamples.bytesReceived * 8 / (OT.$.now() - statsSamples.startTs); + } + + function sample() { + + Promise.all([ + getStats(localPc).then(function(stats) { + OT.$.forEach(stats, function(stat) { + if (OT.getStatsHelpers.isVideoStat(stat)) { + var rtt = null; + + if (stat.hasOwnProperty('googRtt')) { + rtt = parseInt(stat.googRtt, 10); + } else if (stat.hasOwnProperty('mozRtt')) { + rtt = stat.mozRtt; + } + + if (rtt !== null && rtt > -1) { + statsSamples.roundTripTimeSamplesCount++; + statsSamples.roundTripTime += rtt; + } + } + }); + }), + + getStats(remotePc).then(function(stats) { + OT.$.forEach(stats, function(stat) { + if (OT.getStatsHelpers.isVideoStat(stat)) { + if (stat.hasOwnProperty('packetsReceived') && + stat.hasOwnProperty('packetsLost')) { + + var packetLost = parseInt(stat.packetsLost, 10); + var packetsReceived = parseInt(stat.packetsReceived, 10); + if (packetLost >= 0 && packetsReceived > 0) { + statsSamples.packetLostRatioSamplesCount++; + statsSamples.packetLostRatio += packetLost * 100 / packetsReceived; + } + } + + if (stat.hasOwnProperty('bytesReceived')) { + statsSamples.bytesReceived += parseInt(stat.bytesReceived, 10); + } + } + }); + }) + ]) + .then(function() { + // wait and trigger another round of collection + setTimeout(function() { + if (collectionActive) { + sample(); + } + }, SAMPLING_DELAY); + }); + } + + // start the sampling "loop" + sample(); + + function stopCollectStats() { + collectionActive = false; + + var pcStats = { + packetLostRatio: statsSamples.packetLostRatioSamplesCount > 0 ? + statsSamples.packetLostRatio /= statsSamples.packetLostRatioSamplesCount * 100 : null, + roundTripTime: statsSamples.roundTripTimeSamplesCount > 0 ? + statsSamples.roundTripTime /= statsSamples.roundTripTimeSamplesCount : null, + bandwidth: calculateBandwidth() + }; + + resolve(pcStats); + } + + // sample for the nominal delay + // if the bandwidth is bellow the threshold at the end we give an extra time + setTimeout(function() { + + if (calculateBandwidth() < _mediaConfig.thresholdBitsPerSecond) { + // give an extra delay in case it was transient bandwidth problem + setTimeout(stopCollectStats, _mediaConfig.extendedDuration * 1000); + } else { + stopCollectStats(); + } + + }, _mediaConfig.duration * 1000); + }); + } + + return Promise + .all([createPeerConnectionForTest(), createPeerConnectionForTest()]) + .then(function(pcs) { + + var localPc = pcs[0], + remotePc = pcs[1]; + + var localVideo = createVideoElementForTest(), + remoteVideo = createVideoElementForTest(); + + attachMediaStream(localVideo, _localStream); + localPc.addStream(_localStream); + + var remoteStream; + remotePc.onaddstream = function(evt) { + remoteStream = evt.stream; + attachMediaStream(remoteVideo, remoteStream); + }; + + localPc.onicecandidate = createOnIceCandidateListener(remotePc); + remotePc.onicecandidate = createOnIceCandidateListener(localPc); + + function dispose() { + localVideo.destroy(); + remoteVideo.destroy(); + localPc.close(); + remotePc.close(); + } + + return createOffer(localPc) + .then(function(offer) { + return Promise.all([ + setLocalDescription(localPc, offer), + setRemoteDescription(remotePc, offer) + ]); + }) + .then(function() { + return createAnswer(remotePc); + }) + .then(function(answer) { + return Promise.all([ + setLocalDescription(remotePc, answer), + setRemoteDescription(localPc, answer) + ]); + }) + .then(function() { + return collectPeerConnectionStats(localPc, remotePc); + }) + .then(function(value) { + dispose(); + return value; + }, function(error) { + dispose(); + throw error; + }); + }); +} + +OT.webrtcTest = webrtcTest; + +// tb_require('../../helpers/helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +// Manages N Chrome elements +OT.Chrome = function(properties) { + var _visible = false, + _widgets = {}, + + // Private helper function + _set = function(name, widget) { + widget.parent = this; + widget.appendTo(properties.parent); + + _widgets[name] = widget; + + this[name] = widget; + }; + + if (!properties.parent) { + // @todo raise an exception + return; + } + + OT.$.eventing(this); + + this.destroy = function() { + this.off(); + this.hideWhileLoading(); + + for (var name in _widgets) { + _widgets[name].destroy(); + } }; - OT.onUnload = function(cb, context) { - EnvLoader.onUnload(cb, context); + this.showAfterLoading = function() { + _visible = true; + + for (var name in _widgets) { + _widgets[name].showAfterLoading(); + } }; - OT.isUnloaded = function() { - return EnvLoader.isUnloaded(); + this.hideWhileLoading = function() { + _visible = false; + + for (var name in _widgets) { + _widgets[name].hideWhileLoading(); + } + }; + + + // Adds the widget to the chrome and to the DOM. Also creates a accessor + // property for it on the chrome. + // + // @example + // chrome.set('foo', new FooWidget()); + // chrome.foo.setDisplayMode('on'); + // + // @example + // chrome.set({ + // foo: new FooWidget(), + // bar: new BarWidget() + // }); + // chrome.foo.setDisplayMode('on'); + // + this.set = function(widgetName, widget) { + if (typeof(widgetName) === 'string' && widget) { + _set.call(this, widgetName, widget); + + } else { + for (var name in widgetName) { + if (widgetName.hasOwnProperty(name)) { + _set.call(this, name, widgetName[name]); + } + } + } + return this; + }; + +}; + + +// tb_require('../../../helpers/helpers.js') +// tb_require('../chrome.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +if (!OT.Chrome.Behaviour) OT.Chrome.Behaviour = {}; + +// A mixin to encapsulate the basic widget behaviour. This needs a better name, +// it's not actually a widget. It's actually "Behaviour that can be applied to +// an object to make it support the basic Chrome widget workflow"...but that would +// probably been too long a name. +OT.Chrome.Behaviour.Widget = function(widget, options) { + var _options = options || {}, + _mode, + _previousOnMode = 'auto', + _loadingMode; + + // + // @param [String] mode + // 'on', 'off', or 'auto' + // + widget.setDisplayMode = function(mode) { + var newMode = mode || 'auto'; + if (_mode === newMode) return; + + OT.$.removeClass(this.domElement, 'OT_mode-' + _mode); + OT.$.addClass(this.domElement, 'OT_mode-' + newMode); + + if (newMode === 'off') { + _previousOnMode = _mode; + } + _mode = newMode; + }; + + widget.getDisplayMode = function() { + return _mode; + }; + + widget.show = function() { + if (_mode !== _previousOnMode) { + this.setDisplayMode(_previousOnMode); + if (_options.onShow) _options.onShow(); + } + return this; + }; + + widget.showAfterLoading = function() { + this.setDisplayMode(_loadingMode); + }; + + widget.hide = function() { + if (_mode !== 'off') { + this.setDisplayMode('off'); + if (_options.onHide) _options.onHide(); + } + return this; + }; + + widget.hideWhileLoading = function() { + _loadingMode = _mode; + this.setDisplayMode('off'); + }; + + widget.destroy = function() { + if (_options.onDestroy) _options.onDestroy(this.domElement); + if (this.domElement) OT.$.removeElement(this.domElement); + + return widget; + }; + + widget.appendTo = function(parent) { + // create the element under parent + this.domElement = OT.$.createElement(_options.nodeName || 'div', + _options.htmlAttributes, + _options.htmlContent); + + if (_options.onCreate) _options.onCreate(this.domElement); + + widget.setDisplayMode(_options.mode); + + if (_options.mode === 'auto') { + // if the mode is auto we hold the "on mode" for 2 seconds + // this will let the proper widgets nicely fade away and help discoverability + OT.$.addClass(widget.domElement, 'OT_mode-on-hold'); + setTimeout(function() { + OT.$.removeClass(widget.domElement, 'OT_mode-on-hold'); + }, 2000); + } + + + // add the widget to the parent + parent.appendChild(this.domElement); + + return widget; + }; +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('./behaviour/widget.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +OT.Chrome.VideoDisabledIndicator = function(options) { + var _videoDisabled = false, + _warning = false, + updateClasses; + + updateClasses = OT.$.bind(function(domElement) { + if (_videoDisabled) { + OT.$.addClass(domElement, 'OT_video-disabled'); + } else { + OT.$.removeClass(domElement, 'OT_video-disabled'); + } + if(_warning) { + OT.$.addClass(domElement, 'OT_video-disabled-warning'); + } else { + OT.$.removeClass(domElement, 'OT_video-disabled-warning'); + } + if ((_videoDisabled || _warning) && + (this.getDisplayMode() === 'auto' || this.getDisplayMode() === 'on')) { + OT.$.addClass(domElement, 'OT_active'); + } else { + OT.$.removeClass(domElement, 'OT_active'); + } + }, this); + + this.disableVideo = function(value) { + _videoDisabled = value; + if(value === true) { + _warning = false; + } + updateClasses(this.domElement); + }; + + this.setWarning = function(value) { + _warning = value; + updateClasses(this.domElement); + }; + + // Mixin common widget behaviour + OT.Chrome.Behaviour.Widget(this, { + mode: options.mode || 'auto', + nodeName: 'div', + htmlAttributes: { + className: 'OT_video-disabled-indicator' + } + }); + + var parentSetDisplayMode = OT.$.bind(this.setDisplayMode, this); + this.setDisplayMode = function(mode) { + parentSetDisplayMode(mode); + updateClasses(this.domElement); + }; +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('./behaviour/widget.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +// NamePanel Chrome Widget +// +// mode (String) +// Whether to display the name. Possible values are: "auto" (the name is displayed +// when the stream is first displayed and when the user mouses over the display), +// "off" (the name is not displayed), and "on" (the name is displayed). +// +// displays a name +// can be shown/hidden +// can be destroyed +OT.Chrome.NamePanel = function(options) { + var _name = options.name; + + if (!_name || OT.$.trim(_name).length === '') { + _name = null; + + // THere's no name, just flip the mode off + options.mode = 'off'; + } + + this.setName = OT.$.bind(function(name) { + if (!_name) this.setDisplayMode('auto'); + _name = name; + this.domElement.innerHTML = _name; + }); + + // Mixin common widget behaviour + OT.Chrome.Behaviour.Widget(this, { + mode: options.mode, + nodeName: 'h1', + htmlContent: _name, + htmlAttributes: { + className: 'OT_name OT_edge-bar-item' + } + }); +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('./behaviour/widget.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +OT.Chrome.MuteButton = function(options) { + var _onClickCb, + _muted = options.muted || false, + updateClasses, + attachEvents, + detachEvents, + onClick; + + updateClasses = OT.$.bind(function() { + if (_muted) { + OT.$.addClass(this.domElement, 'OT_active'); + } else { + OT.$.removeClass(this.domElement, 'OT_active '); + } + }, this); + + // Private Event Callbacks + attachEvents = function(elem) { + _onClickCb = OT.$.bind(onClick, this); + OT.$.on(elem, 'click', _onClickCb); + }; + + detachEvents = function(elem) { + _onClickCb = null; + OT.$.off(elem, 'click', _onClickCb); + }; + + onClick = function() { + _muted = !_muted; + + updateClasses(); + + if (_muted) { + this.parent.trigger('muted', this); + } else { + this.parent.trigger('unmuted', this); + } + + return false; + }; + + OT.$.defineProperties(this, { + muted: { + get: function() { return _muted; }, + set: function(muted) { + _muted = muted; + updateClasses(); + } + } + }); + + // Mixin common widget behaviour + var classNames = _muted ? 'OT_edge-bar-item OT_mute OT_active' : 'OT_edge-bar-item OT_mute'; + OT.Chrome.Behaviour.Widget(this, { + mode: options.mode, + nodeName: 'button', + htmlContent: 'Mute', + htmlAttributes: { + className: classNames + }, + onCreate: OT.$.bind(attachEvents, this), + onDestroy: OT.$.bind(detachEvents, this) + }); +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('./behaviour/widget.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +// BackingBar Chrome Widget +// +// nameMode (String) +// Whether or not the name panel is being displayed +// Possible values are: "auto" (the name is displayed +// when the stream is first displayed and when the user mouses over the display), +// "off" (the name is not displayed), and "on" (the name is displayed). +// +// muteMode (String) +// Whether or not the mute button is being displayed +// Possible values are: "auto" (the mute button is displayed +// when the stream is first displayed and when the user mouses over the display), +// "off" (the mute button is not displayed), and "on" (the mute button is displayed). +// +// displays a backing bar +// can be shown/hidden +// can be destroyed +OT.Chrome.BackingBar = function(options) { + var _nameMode = options.nameMode, + _muteMode = options.muteMode; + + function getDisplayMode() { + if(_nameMode === 'on' || _muteMode === 'on') { + return 'on'; + } else if(_nameMode === 'mini' || _muteMode === 'mini') { + return 'mini'; + } else if(_nameMode === 'mini-auto' || _muteMode === 'mini-auto') { + return 'mini-auto'; + } else if(_nameMode === 'auto' || _muteMode === 'auto') { + return 'auto'; + } else { + return 'off'; + } + } + + // Mixin common widget behaviour + OT.Chrome.Behaviour.Widget(this, { + mode: getDisplayMode(), + nodeName: 'div', + htmlContent: '', + htmlAttributes: { + className: 'OT_bar OT_edge-bar-item' + } + }); + + this.setNameMode = function(nameMode) { + _nameMode = nameMode; + this.setDisplayMode(getDisplayMode()); + }; + + this.setMuteMode = function(muteMode) { + _muteMode = muteMode; + this.setDisplayMode(getDisplayMode()); + }; + +}; + + +// tb_require('../../helpers/helpers.js') +// tb_require('./behaviour/widget.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + + +OT.Chrome.AudioLevelMeter = function(options) { + + var widget = this, + _meterBarElement, + _voiceOnlyIconElement, + _meterValueElement, + _value, + _maxValue = options.maxValue || 1, + _minValue = options.minValue || 0; + + function onCreate() { + _meterBarElement = OT.$.createElement('div', { + className: 'OT_audio-level-meter__bar' + }, ''); + _meterValueElement = OT.$.createElement('div', { + className: 'OT_audio-level-meter__value' + }, ''); + _voiceOnlyIconElement = OT.$.createElement('div', { + className: 'OT_audio-level-meter__audio-only-img' + }, ''); + + widget.domElement.appendChild(_meterBarElement); + widget.domElement.appendChild(_voiceOnlyIconElement); + widget.domElement.appendChild(_meterValueElement); + } + + function updateView() { + var percentSize = _value * 100 / (_maxValue - _minValue); + _meterValueElement.style.width = _meterValueElement.style.height = 2 * percentSize + '%'; + _meterValueElement.style.top = _meterValueElement.style.right = -percentSize + '%'; + } + + // Mixin common widget behaviour + var widgetOptions = { + mode: options ? options.mode : 'auto', + nodeName: 'div', + htmlAttributes: { + className: 'OT_audio-level-meter' + }, + onCreate: onCreate + }; + + OT.Chrome.Behaviour.Widget(this, widgetOptions); + + // override + var _setDisplayMode = OT.$.bind(widget.setDisplayMode, widget); + widget.setDisplayMode = function(mode) { + _setDisplayMode(mode); + if (mode === 'off') { + if (options.onPassivate) options.onPassivate(); + } else { + if (options.onActivate) options.onActivate(); + } + }; + + widget.setValue = function(value) { + _value = value; + updateView(); + }; +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('./behaviour/widget.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + + +// Archving Chrome Widget +// +// mode (String) +// Whether to display the archving widget. Possible values are: "on" (the status is displayed +// when archiving and briefly when archving ends) and "off" (the status is not displayed) + +// Whether to display the archving widget. Possible values are: "auto" (the name is displayed +// when the status is first displayed and when the user mouses over the display), +// "off" (the name is not displayed), and "on" (the name is displayed). +// +// displays a name +// can be shown/hidden +// can be destroyed +OT.Chrome.Archiving = function(options) { + var _archiving = options.archiving, + _archivingStarted = options.archivingStarted || 'Archiving on', + _archivingEnded = options.archivingEnded || 'Archiving off', + _initialState = true, + _lightBox, + _light, + _text, + _textNode, + renderStageDelayedAction, + renderText, + renderStage; + + renderText = function(text) { + _textNode.nodeValue = text; + _lightBox.setAttribute('title', text); + }; + + renderStage = OT.$.bind(function() { + if(renderStageDelayedAction) { + clearTimeout(renderStageDelayedAction); + renderStageDelayedAction = null; + } + + if(_archiving) { + OT.$.addClass(_light, 'OT_active'); + } else { + OT.$.removeClass(_light, 'OT_active'); + } + + OT.$.removeClass(this.domElement, 'OT_archiving-' + (!_archiving ? 'on' : 'off')); + OT.$.addClass(this.domElement, 'OT_archiving-' + (_archiving ? 'on' : 'off')); + if(options.show && _archiving) { + renderText(_archivingStarted); + OT.$.addClass(_text, 'OT_mode-on'); + OT.$.removeClass(_text, 'OT_mode-auto'); + this.setDisplayMode('on'); + renderStageDelayedAction = setTimeout(function() { + OT.$.addClass(_text, 'OT_mode-auto'); + OT.$.removeClass(_text, 'OT_mode-on'); + }, 5000); + } else if(options.show && !_initialState) { + OT.$.addClass(_text, 'OT_mode-on'); + OT.$.removeClass(_text, 'OT_mode-auto'); + this.setDisplayMode('on'); + renderText(_archivingEnded); + renderStageDelayedAction = setTimeout(OT.$.bind(function() { + this.setDisplayMode('off'); + }, this), 5000); + } else { + this.setDisplayMode('off'); + } + }, this); + + // Mixin common widget behaviour + OT.Chrome.Behaviour.Widget(this, { + mode: _archiving && options.show && 'on' || 'off', + nodeName: 'h1', + htmlAttributes: {className: 'OT_archiving OT_edge-bar-item OT_edge-bottom'}, + onCreate: OT.$.bind(function() { + _lightBox = OT.$.createElement('div', { + className: 'OT_archiving-light-box' + }, ''); + _light = OT.$.createElement('div', { + className: 'OT_archiving-light' + }, ''); + _lightBox.appendChild(_light); + _text = OT.$.createElement('div', { + className: 'OT_archiving-status OT_mode-on OT_edge-bar-item OT_edge-bottom' + }, ''); + _textNode = document.createTextNode(''); + _text.appendChild(_textNode); + this.domElement.appendChild(_lightBox); + this.domElement.appendChild(_text); + renderStage(); + }, this) + }); + + this.setShowArchiveStatus = OT.$.bind(function(show) { + options.show = show; + if(this.domElement) { + renderStage.call(this); + } + }, this); + + this.setArchiving = OT.$.bind(function(status) { + _archiving = status; + _initialState = false; + if(this.domElement) { + renderStage.call(this); + } + }, this); + +}; + +// tb_require('../helpers.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +// Web OT Helpers +!(function(window) { + // guard for Node.js + if (window && typeof(navigator) !== 'undefined') { + 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 OTPlugin + // 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); + }; + } + } + + if (!window.URL && window.webkitURL) { + window.URL = window.webkitURL; + } + + OT.$.createPeerConnection = function (config, options, publishersWebRtcStream, completion) { + if (OTPlugin.isInstalled()) { + OTPlugin.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() { + return OT.$.env.name === 'Chrome' && OT.$.env.version < 25 ? 'SDES_SRTP' : 'DTLS_SRTP'; + }; + +})(window); + +// tb_require('../../helpers/helpers.js') +// tb_require('../../helpers/lib/web_rtc.js') + +/* exported subscribeProcessor */ +/* global SDPHelpers */ + +// 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 subscribeProcessor = 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 + ); +}; +// tb_require('../../helpers/helpers.js') +// tb_require('../../helpers/lib/web_rtc.js') + +/* exported offerProcessor */ +/* global SDPHelpers */ + +// 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') + ); + +}; +// tb_require('../../helpers/helpers.js') +// tb_require('../../helpers/lib/web_rtc.js') + +/* exported IceCandidateProcessor */ + +// Normalise these +var NativeRTCIceCandidate; + +if (!OTPlugin.isInstalled()) { + NativeRTCIceCandidate = (window.mozRTCIceCandidate || window.RTCIceCandidate); +} +else { + NativeRTCIceCandidate = OTPlugin.RTCIceCandidate; +} + +// 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()); + } + }; +}; +// tb_require('../../helpers/helpers.js') +// tb_require('../../helpers/lib/web_rtc.js') + +/* exported connectionStateLogger */ + +// @meta: ping Mike/Eric to let them know that this data is coming and what format it will be +// @meta: what reports would I like around this, what question am I trying to answer? +// +// log the sequence of iceconnectionstates, icegatheringstates, signalingstates +// log until we reach a terminal iceconnectionstate or signalingstate +// send a client event once we have a full sequence +// +// Format of the states: +// [ +// {delta: 1234, iceConnection: 'new', signalingstate: 'stable', iceGathering: 'new'}, +// {delta: 1234, iceConnection: 'new', signalingstate: 'stable', iceGathering: 'new'}, +// {delta: 1234, iceConnection: 'new', signalingstate: 'stable', iceGathering: 'new'}, +// ] +// +// Format of the logged event: +// { +// startTime: 1234, +// finishTime: 5678, +// states: [ +// {delta: 1234, iceConnection: 'new', signalingstate: 'stable', iceGathering: 'new'}, +// {delta: 1234, iceConnection: 'new', signalingstate: 'stable', iceGathering: 'new'}, +// {delta: 1234, iceConnection: 'new', signalingstate: 'stable', iceGathering: 'new'}, +// ] +// } +// +var connectionStateLogger = function(pc) { + var startTime = OT.$.now(), + finishTime, + suceeded, + states = []; + + var trackState = function() { + var now = OT.$.now(), + lastState = states[states.length-1], + state = {delta: finishTime ? now - finishTime : 0}; + + if (!lastState || lastState.iceConnection !== pc.iceConnectionState) { + state.iceConnectionState = pc.iceConnectionState; + } + + if (!lastState || lastState.signalingState !== pc.signalingState) { + state.signalingState = pc.signalingState; + } + + if (!lastState || lastState.iceGatheringState !== pc.iceGatheringState) { + state.iceGathering = pc.iceGatheringState; + } + OT.debug(state); + states.push(state); + finishTime = now; + }; + + pc.addEventListener('iceconnectionstatechange', trackState, false); + pc.addEventListener('signalingstatechange', trackState, false); + pc.addEventListener('icegatheringstatechange', trackState, false); + + return { + stop: function () { + pc.removeEventListener('iceconnectionstatechange', trackState, false); + pc.removeEventListener('signalingstatechange', trackState, false); + pc.removeEventListener('icegatheringstatechange', trackState, false); + + // @todo The client logging of these states is not currently used, so it's left todo. + + // @todo analyse final state and decide whether the connection was successful + suceeded = true; + + var payload = { + type: 'PeerConnectionWorkflow', + success: suceeded, + startTime: startTime, + finishTime: finishTime, + states: states + }; + + // @todo send client event + OT.debug(payload); + } + }; +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('../../helpers/lib/web_rtc.js') +// tb_require('./connection_state_logger.js') +// tb_require('./ice_candidate_processor.js') +// tb_require('./subscribe_processor.js') +// tb_require('./offer_processor.js') +// tb_require('./get_stats_adapter.js') + +/* global offerProcessor, subscribeProcessor, connectionStateLogger, IceCandidateProcessor */ + +// Normalise these +var NativeRTCSessionDescription; + +if (!OTPlugin.isInstalled()) { + // order is very important: 'RTCSessionDescription' defined in Firefox Nighly but useless + NativeRTCSessionDescription = (window.mozRTCSessionDescription || + window.RTCSessionDescription); +} +else { + NativeRTCSessionDescription = OTPlugin.RTCSessionDescription; +} + + +// 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.'); + } + }; +}; + +/* + * 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(), + _getStatsAdapter = OT.getStatsAdpater(), + _stateLogger, + _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, uri) { + 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, uri); + } + }, this), + + // Create and initialise the PeerConnection object. This deals with + // any differences between the various browser implementations and + // our own OTPlugin 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 OTPlugin, 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, + attachEventsToPeerConnection); + }, 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; + _stateLogger = connectionStateLogger(_peerConnection); + + _peerConnection.addEventListener('icecandidate', + iceCandidateForwarder(delegateMessage), false); + _peerConnection.addEventListener('addstream', onRemoteStreamAdded, false); + _peerConnection.addEventListener('removestream', onRemoteStreamRemoved, false); + _peerConnection.addEventListener('signalingstatechange', routeStateChanged, false); + + if (_peerConnection.oniceconnectionstatechange !== void 0) { + var failedStateTimer; + _peerConnection.addEventListener('iceconnectionstatechange', 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. + failedStateTimer = 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); + } + }, false); + } + + 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); + if (_stateLogger) _stateLogger.stop(); + + 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 = OT.$.bind(function() { + var newState = _peerConnection.signalingState; + + if (newState && newState !== _state) { + _state = newState; + OT.debug('PeerConnection.stateChange: ' + _state); + + switch(_state) { + case 'closed': + tearDownPeerConnection.call(this); + break; + } + } + }, this), + + 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 = OT.$.bind(function(event) { + this.trigger('streamAdded', event.stream); + }, this), + + onRemoteStreamRemoved = OT.$.bind(function(event) { + this.trigger('streamRemoved', event.stream); + }, this), + + // 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, uri) { + delegateMessage(messageType, sdp, uri); + }, + + + // 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(message) { + 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() { + subscribeProcessor( + _peerConnection, + + // Success: Relay Offer + function(offer) { + _offer = offer; + relaySDP(OT.Raptor.Actions.OFFER, _offer, message.uri); + }, + + // Failure + function(message, errorReason, prefix) { + triggerError('PeerConnection.subscribeProcessor ' + 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 && + _peerConnection.signalingState && + _peerConnection.signalingState.toLowerCase() !== 'closed') { + + _peerConnection.close(); + + if (OT.$.env.name === 'Firefox') { + // FF seems to never go into the closed signalingState when the close + // method is called on a PeerConnection. This means that we need to call + // our cleanup code manually. + // + // * https://bugzilla.mozilla.org/show_bug.cgi?id=989936 + // + OT.$.callAsync(OT.$.bind(tearDownPeerConnection, 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.getStats = function(callback) { + createPeerConnection(function() { + _getStatsAdapter(_peerConnection, callback); + }); + }; + + var qos = new OT.PeerConnection.QOS(qosCallback); +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('./peer_connection.js') + +// +// There are three implementations of stats parsing in this file. +// 1. For Chrome: Chrome is currently using an older version of the API +// 2. For OTPlugin: The plugin is using a newer version of the API that +// exists in the latest WebRTC codebase +// 3. For Firefox: FF is using a version that looks a lot closer to the +// current spec. +// +// I've attempted to keep the three implementations from sharing any code, +// accordingly you'll notice a bunch of duplication between the three. +// +// This is acceptable as the goal is to be able to remove each implementation +// as it's no longer needed without any risk of affecting the others. If there +// was shared code between them then each removal would require an audit of +// all the others. +// +// +!(function() { + + /// + // Get Stats using the older API. Used by all current versions + // of Chrome. + // + var parseStatsOldAPI = function parseStatsOldAPI (peerConnection, + prevStats, + currentStats, + completion) { + + /* this parses a result if there it contains the video bitrate */ + var parseVideoStats = function (result) { + if (result.stat('googFrameRateSent')) { + currentStats.videoSentBytes = Number(result.stat('bytesSent')); + currentStats.videoSentPackets = Number(result.stat('packetsSent')); + currentStats.videoSentPacketsLost = Number(result.stat('packetsLost')); + currentStats.videoRtt = Number(result.stat('googRtt')); + currentStats.videoFrameRate = Number(result.stat('googFrameRateInput')); + currentStats.videoWidth = Number(result.stat('googFrameWidthSent')); + currentStats.videoHeight = Number(result.stat('googFrameHeightSent')); + currentStats.videoCodec = result.stat('googCodecName'); + } else if (result.stat('googFrameRateReceived')) { + currentStats.videoRecvBytes = Number(result.stat('bytesReceived')); + currentStats.videoRecvPackets = Number(result.stat('packetsReceived')); + currentStats.videoRecvPacketsLost = Number(result.stat('packetsLost')); + currentStats.videoFrameRate = Number(result.stat('googFrameRateOutput')); + currentStats.videoWidth = Number(result.stat('googFrameWidthReceived')); + currentStats.videoHeight = Number(result.stat('googFrameHeightReceived')); + currentStats.videoCodec = result.stat('googCodecName'); + } + return null; + }, + + parseAudioStats = function (result) { + if (result.stat('audioInputLevel')) { + currentStats.audioSentPackets = Number(result.stat('packetsSent')); + currentStats.audioSentPacketsLost = Number(result.stat('packetsLost')); + currentStats.audioSentBytes = Number(result.stat('bytesSent')); + currentStats.audioCodec = result.stat('googCodecName'); + currentStats.audioRtt = Number(result.stat('googRtt')); + } else if (result.stat('audioOutputLevel')) { + currentStats.audioRecvPackets = Number(result.stat('packetsReceived')); + currentStats.audioRecvPacketsLost = Number(result.stat('packetsLost')); + currentStats.audioRecvBytes = Number(result.stat('bytesReceived')); + currentStats.audioCodec = result.stat('googCodecName'); + } + }, + + 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'); + } + + parseAudioStats(result); + parseVideoStats(result); + } + } + } + + completion(null, currentStats); + }; + + peerConnection.getStats(parseStatsReports); + }; + + /// + // Get Stats for the OT Plugin, newer than Chromes version, but + // still not in sync with the spec. + // + var parseStatsOTPlugin = function parseStatsOTPlugin (peerConnection, + prevStats, + currentStats, + completion) { + + var onStatsError = function onStatsError (error) { + completion(error); + }, + + /// + // From the Audio Tracks + // * avgAudioBitrate + // * audioBytesTransferred + // + parseAudioStats = function (statsReport) { + var lastBytesSent = prevStats.audioBytesTransferred || 0, + transferDelta; + + if (statsReport.audioInputLevel) { + currentStats.audioSentBytes = Number(statsReport.bytesSent); + currentStats.audioSentPackets = Number(statsReport.packetsSent); + currentStats.audioSentPacketsLost = Number(statsReport.packetsLost); + currentStats.audioRtt = Number(statsReport.googRtt); + currentStats.audioCodec = statsReport.googCodecName; + } + else if (statsReport.audioOutputLevel) { + currentStats.audioBytesTransferred = Number(statsReport.bytesReceived); + currentStats.audioCodec = statsReport.googCodecName; + } + + if (currentStats.audioBytesTransferred) { + transferDelta = currentStats.audioBytesTransferred - lastBytesSent; + currentStats.avgAudioBitrate = Math.round(transferDelta * 8 / currentStats.period); + } + }, + + /// + // From the Video Tracks + // * frameRate + // * avgVideoBitrate + // * videoBytesTransferred + // + parseVideoStats = function (statsReport) { + + var lastBytesSent = prevStats.videoBytesTransferred || 0, + transferDelta; + + if (statsReport.googFrameHeightSent) { + currentStats.videoSentBytes = Number(statsReport.bytesSent); + currentStats.videoSentPackets = Number(statsReport.packetsSent); + currentStats.videoSentPacketsLost = Number(statsReport.packetsLost); + currentStats.videoRtt = Number(statsReport.googRtt); + currentStats.videoCodec = statsReport.googCodecName; + currentStats.videoWidth = Number(statsReport.googFrameWidthSent); + currentStats.videoHeight = Number(statsReport.googFrameHeightSent); + } + else if (statsReport.googFrameHeightReceived) { + currentStats.videoRecvBytes = Number(statsReport.bytesReceived); + currentStats.videoRecvPackets = Number(statsReport.packetsReceived); + currentStats.videoRecvPacketsLost = Number(statsReport.packetsLost); + currentStats.videoRtt = Number(statsReport.googRtt); + currentStats.videoCodec = statsReport.googCodecName; + currentStats.videoWidth = Number(statsReport.googFrameWidthReceived); + currentStats.videoHeight = Number(statsReport.googFrameHeightReceived); + } + + if (currentStats.videoBytesTransferred) { + transferDelta = currentStats.videoBytesTransferred - lastBytesSent; + currentStats.avgVideoBitrate = Math.round(transferDelta * 8 / currentStats.period); + } + + if (statsReport.googFrameRateSent) { + currentStats.videoFrameRate = Number(statsReport.googFrameRateSent); + } else if (statsReport.googFrameRateReceived) { + currentStats.videoFrameRate = Number(statsReport.googFrameRateReceived); + } + }, + + isStatsForVideoTrack = function(statsReport) { + return statsReport.googFrameHeightSent !== void 0 || + statsReport.googFrameHeightReceived !== void 0 || + currentStats.videoBytesTransferred !== void 0 || + statsReport.googFrameRateSent !== void 0; + }, + + isStatsForIceCandidate = function(statsReport) { + return statsReport.googActiveConnection === 'true'; + }; + + peerConnection.getStats(null, function(statsReports) { + statsReports.forEach(function(statsReport) { + if (isStatsForIceCandidate(statsReport)) { + currentStats.localCandidateType = statsReport.googLocalCandidateType; + currentStats.remoteCandidateType = statsReport.googRemoteCandidateType; + currentStats.transportType = statsReport.googTransportType; + } + else if (isStatsForVideoTrack(statsReport)) { + parseVideoStats(statsReport); + } + else { + parseAudioStats(statsReport); + } + }); + + completion(null, currentStats); + }, onStatsError); + }; + + + /// + // Get Stats using the newer API. + // + var parseStatsNewAPI = function parseStatsNewAPI (peerConnection, + prevStats, + currentStats, + completion) { + + var onStatsError = function onStatsError (error) { + completion(error); + }, + + parseAudioStats = function (result) { + if (result.type==='outboundrtp') { + currentStats.audioSentPackets = result.packetsSent; + currentStats.audioSentPacketsLost = result.packetsLost; + currentStats.audioSentBytes = result.bytesSent; + } else if (result.type==='inboundrtp') { + currentStats.audioRecvPackets = result.packetsReceived; + currentStats.audioRecvPacketsLost = result.packetsLost; + currentStats.audioRecvBytes = result.bytesReceived; + } + }, + + parseVideoStats = function (result) { + if (result.type==='outboundrtp') { + currentStats.videoSentPackets = result.packetsSent; + currentStats.videoSentPacketsLost = result.packetsLost; + currentStats.videoSentBytes = result.bytesSent; + } else if (result.type==='inboundrtp') { + currentStats.videoRecvPackets = result.packetsReceived; + currentStats.videoRecvPacketsLost = result.packetsLost; + currentStats.videoRecvBytes = result.bytesReceived; + } + }; + + 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]; + + if (res.id.indexOf('audio') !== -1) { + parseAudioStats(res); + } else if (res.id.indexOf('video') !== -1) { + parseVideoStats(res); + } + } + } + + completion(null, currentStats); + }, onStatsError); + }; + + + var parseQOS = function (peerConnection, prevStats, currentStats, completion) { + if (OTPlugin.isInstalled()) { + parseQOS = parseStatsOTPlugin; + return parseStatsOTPlugin(peerConnection, prevStats, currentStats, completion); + } + else if (OT.$.env.name === 'Firefox' && OT.$.env.version >= 27) { + 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), + period: (now - prevStats.timeStamp) / 1000 + }; + + var onParsedStats = function (err, parsedStats) { + if (err) { + OT.error('Failed to Parse QOS Stats: ' + JSON.stringify(err)); + return; + } + + qosCallback(parsedStats, prevStats); + + // Recalculate the stats + setTimeout(OT.$.bind(calculateQOS, null, parsedStats), OT.PeerConnection.QOS.INTERVAL); + }; + + parseQOS(_peerConnection, prevStats, currentStats, onParsedStats); + }, this); + + + this.startCollecting = function (peerConnection) { + if (!peerConnection || !peerConnection.getStats) { + // It looks like this browser doesn't support getStats + // Bail. + return; + } + + _peerConnection = peerConnection; + + calculateQOS({ + timeStamp: OT.$.now() + }); + }; + + this.stopCollecting = function () { + _peerConnection = null; + }; + }; + + // Recalculate the stats in 30 sec + OT.PeerConnection.QOS.INTERVAL = 30000; +})(); + +// tb_require('../../helpers/helpers.js') +// tb_require('./peer_connection.js') + +OT.PeerConnections = (function() { + var _peerConnections = {}; + + return { + add: function(remoteConnection, streamId, config) { + var key = remoteConnection.id + '_' + streamId, + ref = _peerConnections[key]; + + if (!ref) { + ref = _peerConnections[key] = { + count: 0, + pc: new OT.PeerConnection(config) + }; + } + + // increase the PCs ref count by 1 + ref.count += 1; + + return ref.pc; + }, + + remove: function(remoteConnection, streamId) { + var key = remoteConnection.id + '_' + streamId, + ref = _peerConnections[key]; + + if (ref) { + ref.count -= 1; + + if (ref.count === 0) { + ref.pc.disconnect(); + delete _peerConnections[key]; + } + } + } + }; +})(); +// tb_require('../../helpers/helpers.js') +// tb_require('../messaging/raptor/raptor.js') +// tb_require('./peer_connections.js') + +/* + * 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', null, errorReason, this, prefix); + }; + + _relayMessageToPeer = OT.$.bind(function(type, payload) { + if (!_hasRelayCandidates){ + var extractCandidates = type === OT.Raptor.Actions.CANDIDATE || + type === OT.Raptor.Actions.OFFER || + type === OT.Raptor.Actions.ANSWER || + type === OT.Raptor.Actions.PRANSWER ; + + if (extractCandidates) { + var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp; + _hasRelayCandidates = message.indexOf('typ relay') !== -1; + } + } + + switch(type) { + case OT.Raptor.Actions.ANSWER: + case OT.Raptor.Actions.PRANSWER: + this.trigger('connected'); + + session._.jsepAnswerP2p(stream.id, subscriber.widgetId, payload.sdp); + break; + + case OT.Raptor.Actions.OFFER: + session._.jsepOfferP2p(stream.id, subscriber.widgetId, payload.sdp); + break; + + case OT.Raptor.Actions.CANDIDATE: + session._.jsepCandidateP2p(stream.id, subscriber.widgetId, payload); + break; + } + }, this); + + // Helper method used by subscribeToAudio/subscribeToVideo + _setEnabledOnStreamTracksCurry = function(isVideo) { + var method = 'get' + (isVideo ? 'Video' : 'Audio') + 'Tracks'; + + return function(enabled) { + var remoteStreams = _peerConnection.remoteStreams(), + tracks, + stream; + + if (remoteStreams.length === 0 || !remoteStreams[0][method]) { + // either there is no remote stream or we are in a browser that doesn't + // expose the media tracks (Firefox) + return; + } + + for (var i=0, num=remoteStreams.length; i 0) { + OT.$.forEach(_peerConnection.remoteStreams(), _onRemoteStreamAdded, this); + } else if (numDelegates === 1) { + // We only bother with the PeerConnection negotiation if we don't already + // have a remote stream. + + var channelsToSubscribeTo; + + if (properties.subscribeToVideo || properties.subscribeToAudio) { + var audio = stream.getChannelsOfType('audio'), + video = stream.getChannelsOfType('video'); + + channelsToSubscribeTo = OT.$.map(audio, function(channel) { + return { + id: channel.id, + type: channel.type, + active: properties.subscribeToAudio + }; + }).concat(OT.$.map(video, function(channel) { + return { + id: channel.id, + type: channel.type, + active: properties.subscribeToVideo, + restrictFrameRate: properties.restrictFrameRate !== void 0 ? + properties.restrictFrameRate : false + }; + })); + } + + session._.subscriberCreate(stream, subscriber, channelsToSubscribeTo, + OT.$.bind(function(err, message) { + if (err) { + this.trigger('error', err.message, this, 'Subscribe'); + } + if (_peerConnection) { + _peerConnection.setIceServers(OT.Raptor.parseIceServers(message)); + } + }, this)); + } + }; +}; + +// tb_require('../../helpers/helpers.js') +// tb_require('../messaging/raptor/raptor.js') +// tb_require('./peer_connections.js') + + +/* + * Abstracts PeerConnection related stuff away from OT.Publisher. + * + * Responsible for: + * * setting up the underlying PeerConnection (delegates to OT.PeerConnections) + * * triggering a connected event when the Peer connection is opened + * * triggering a disconnected event when the Peer connection is closed + * * providing a destroy method + * * providing a processMessage method + * + * Once the PeerConnection is connected and the video element playing it triggers + * the connected event + * + * Triggers the following events + * * connected + * * disconnected + */ +OT.PublisherPeerConnection = function(remoteConnection, session, streamId, webRTCStream) { + var _peerConnection, + _hasRelayCandidates = false, + _subscriberId = session._.subscriberMap[remoteConnection.id + '_' + streamId], + _onPeerClosed, + _onPeerError, + _relayMessageToPeer, + _onQOS; + + // Private + _onPeerClosed = function() { + this.destroy(); + this.trigger('disconnected', this); + }; + + // Note: All Peer errors are fatal right now. + _onPeerError = function(errorReason, prefix) { + this.trigger('error', null, errorReason, this, prefix); + this.destroy(); + }; + + _relayMessageToPeer = OT.$.bind(function(type, payload, uri) { + if (!_hasRelayCandidates){ + var extractCandidates = type === OT.Raptor.Actions.CANDIDATE || + type === OT.Raptor.Actions.OFFER || + type === OT.Raptor.Actions.ANSWER || + type === OT.Raptor.Actions.PRANSWER ; + + if (extractCandidates) { + var message = (type === OT.Raptor.Actions.CANDIDATE) ? payload.candidate : payload.sdp; + _hasRelayCandidates = message.indexOf('typ relay') !== -1; + } + } + + switch(type) { + case OT.Raptor.Actions.ANSWER: + case OT.Raptor.Actions.PRANSWER: + if (session.sessionInfo.p2pEnabled) { + session._.jsepAnswerP2p(streamId, _subscriberId, payload.sdp); + } else { + session._.jsepAnswer(streamId, payload.sdp); + } + + break; + + case OT.Raptor.Actions.OFFER: + this.trigger('connected'); + session._.jsepOffer(uri, 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; + }; + + }; +}; + + +// tb_require('../helpers.js') +// tb_require('./web_rtc.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT, OTPlugin */ + +var videoContentResizesMixin = function(self, domElement) { + + var width = domElement.videoWidth, + height = domElement.videoHeight, + stopped = true; + + function actor() { + if (stopped) { + return; + } + if (width !== domElement.videoWidth || height !== domElement.videoHeight) { + self.trigger('videoDimensionsChanged', + { width: width, height: height }, + { width: domElement.videoWidth, height: domElement.videoHeight } + ); + width = domElement.videoWidth; + height = domElement.videoHeight; + } + waiter(); + } + + function waiter() { + self.whenTimeIncrements(function() { + window.requestAnimationFrame(actor); + }); + } + + self.startObservingSize = function() { + stopped = false; + waiter(); + }; + + self.stopObservingSize = function() { + stopped = true; + }; + +}; + +(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 = OTPlugin.isInstalled() ? + new PluginVideoElement(_options, errorHandler, orientationHandler) : + new NativeDOMVideoElement(_options, errorHandler, orientationHandler), + _streamBound = false, + _stream, + _preInitialisedVolue; + + OT.$.eventing(this); + + _videoElement.on('videoDimensionsChanged', OT.$.bind(function(oldValue, newValue) { + this.trigger('videoDimensionsChanged', oldValue, newValue); + }, this)); + + _videoElement.on('mediaStopped', OT.$.bind(function() { + this.trigger('mediaStopped'); + }, 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; + } + + _videoElement.on('aspectRatioAvailable', + OT.$.bind(this.trigger, this, 'aspectRatioAvailable')); + + 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.onRatioAvailable = function(callabck) { + _videoElement.onRatioAvailable(callabck) ; + 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, + _ratioAvailable = false, + _ratioAvailableListeners = []; + + OT.$.eventing(this); + + 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(function(error) { + + if (!error) { + _ratioAvailable = true; + var listener; + while ((listener = _ratioAvailableListeners.shift())) { + listener(); + } + } + + completion(error); + }); + + return this; + }; + + // Unbind the currently bound stream from the video element. + this.unbindStream = function() { + // TODO: some way to tell OTPlugin 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.onRatioAvailable = function(callback) { + if(_ratioAvailable) { + callback(); + } else { + _ratioAvailableListeners.push(callback); + } + }; + + this.destroy = function() { + this.unbindStream(); + + return void 0; + }; + }; + + + var NativeDOMVideoElement = function NativeDOMVideoElement (options, + errorHandler, + orientationChangedHandler) { + var _domElement, + _videoElementMovedWarning = false; + + OT.$.eventing(this); + + /// 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); + + // dirty but it is the only way right now to get the aspect ratio in FF + // any other event is triggered too early + var ratioAvailable = false; + var ratioAvailableListeners = []; + _domElement.addEventListener('timeupdate', function timeupdateHandler() { + var aspectRatio = _domElement.videoWidth / _domElement.videoHeight; + if (!isNaN(aspectRatio)) { + _domElement.removeEventListener('timeupdate', timeupdateHandler); + ratioAvailable = true; + var listener; + while ((listener = ratioAvailableListeners.shift())) { + listener(); + } + } + }); + + _domElement.addEventListener('pause', _playVideoOnPause); + + videoContentResizesMixin(this, _domElement); + + 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) { + var _this = this; + bindStreamToNativeVideoElement(_domElement, webRtcStream, function(err) { + if (err) { + completion(err); + return; + } + + _this.startObservingSize(); + + webRtcStream.onended = function() { + _this.trigger('mediaStopped', this); + }; + + + _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); + } + + this.stopObservingSize(); + + 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 (_domElement) { + 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; + }; + + this.onRatioAvailable = function(callback) { + if(ratioAvailable) { + callback(); + } else { + ratioAvailableListeners.push(callback); + } + }; + }; + +/// 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.$.env.name) { + case 'Chrome': + case 'Safari': + getDomElementCallback().style.webkitTransform = transform; + break; + + case 'IE': + if (OT.$.env.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 timeout, + minVideoTracksForTimeUpdate = OT.$.env.name === 'Chrome' ? 1 : 0, + loadedEvent = webRtcStream.getVideoTracks().length > minVideoTracksForTimeUpdate ? + 'timeupdate' : 'loadedmetadata'; + + var cleanup = function cleanup () { + clearTimeout(timeout); + videoElement.removeEventListener(loadedEvent, 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.'); + }; + + videoElement.addEventListener(loadedEvent, onLoad, false); + videoElement.addEventListener('error', onError, false); + webRtcStream.onended = onStoppedLoading; + + // 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); + } + } + + + 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.js') +// tb_require('./video_element.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +!(function() { +/*global OT:true */ + + var defaultAspectRatio = 4.0/3.0, + miniWidth = 128, + miniHeight = 128, + microWidth = 64, + microHeight = 64; + + /** + * Sets the video element size so by preserving the intrinsic aspect ratio of the element content + * but altering the width and height so that the video completely covers the container. + * + * @param {Element} element the container of the video element + * @param {number} containerWidth + * @param {number} containerHeight + * @param {number} intrinsicRatio the aspect ratio of the video media + * @param {boolean} rotated + */ + function fixFitModeCover(element, containerWidth, containerHeight, intrinsicRatio, rotated) { + + var $video = OT.$('.OT_video-element', element); + + if ($video.length > 0) { + + var cssProps = {left: '', top: ''}; + + if (OTPlugin.isInstalled()) { + cssProps.width = '100%'; + cssProps.height = '100%'; + } else { + intrinsicRatio = intrinsicRatio || defaultAspectRatio; + intrinsicRatio = rotated ? 1 / intrinsicRatio : intrinsicRatio; + + var containerRatio = containerWidth / containerHeight; + + var enforcedVideoWidth, + enforcedVideoHeight; + + if (rotated) { + // in case of rotation same code works for both kind of ration + enforcedVideoHeight = containerWidth; + enforcedVideoWidth = enforcedVideoHeight * intrinsicRatio; + + cssProps.width = enforcedVideoWidth + 'px'; + cssProps.height = enforcedVideoHeight + 'px'; + cssProps.top = (enforcedVideoWidth + containerHeight) / 2 + 'px'; + } else { + if (intrinsicRatio < containerRatio) { + // the container is wider than the video -> we will crop the height of the video + enforcedVideoWidth = containerWidth; + enforcedVideoHeight = enforcedVideoWidth / intrinsicRatio; + + cssProps.width = enforcedVideoWidth + 'px'; + cssProps.height = enforcedVideoHeight + 'px'; + cssProps.top = (-enforcedVideoHeight + containerHeight) / 2 + 'px'; + } else { + enforcedVideoHeight = containerHeight; + enforcedVideoWidth = enforcedVideoHeight * intrinsicRatio; + + cssProps.width = enforcedVideoWidth + 'px'; + cssProps.height = enforcedVideoHeight + 'px'; + cssProps.left = (-enforcedVideoWidth + containerWidth) / 2 + 'px'; + } + } + } + + $video.css(cssProps); + } + } + + /** + * Sets the video element size so that the video is entirely visible inside the container. + * + * @param {Element} element the container of the video element + * @param {number} containerWidth + * @param {number} containerHeight + * @param {number} intrinsicRatio the aspect ratio of the video media + * @param {boolean} rotated + */ + function fixFitModeContain(element, containerWidth, containerHeight, intrinsicRatio, rotated) { + + var $video = OT.$('.OT_video-element', element); + + if ($video.length > 0) { + + var cssProps = {left: '', top: ''}; + + + if (OTPlugin.isInstalled()) { + intrinsicRatio = intrinsicRatio || defaultAspectRatio; + + var containerRatio = containerWidth / containerHeight; + + var enforcedVideoWidth, + enforcedVideoHeight; + + if (intrinsicRatio < containerRatio) { + enforcedVideoHeight = containerHeight; + enforcedVideoWidth = containerHeight * intrinsicRatio; + + cssProps.width = enforcedVideoWidth + 'px'; + cssProps.height = enforcedVideoHeight + 'px'; + cssProps.left = (containerWidth - enforcedVideoWidth) / 2 + 'px'; + } else { + enforcedVideoWidth = containerWidth; + enforcedVideoHeight = enforcedVideoWidth / intrinsicRatio; + + cssProps.width = enforcedVideoWidth + 'px'; + cssProps.height = enforcedVideoHeight + 'px'; + cssProps.top = (containerHeight - enforcedVideoHeight) / 2 + 'px'; + } + } else { + if (rotated) { + cssProps.width = containerHeight + 'px'; + cssProps.height = containerWidth + 'px'; + cssProps.top = containerHeight + 'px'; + } else { + cssProps.width = '100%'; + cssProps.height = '100%'; + } + } + + $video.css(cssProps); + } + } + + function fixMini(container, width, height) { + var w = parseInt(width, 10), + h = parseInt(height, 10); + + if(w < microWidth || h < microHeight) { + OT.$.addClass(container, 'OT_micro'); + } else { + OT.$.removeClass(container, 'OT_micro'); + } + if(w < miniWidth || h < miniHeight) { + OT.$.addClass(container, 'OT_mini'); + } else { + OT.$.removeClass(container, 'OT_mini'); + } + } + + var getOrCreateContainer = function getOrCreateContainer(elementOrDomId, insertMode) { + var container, + domId; + + if (elementOrDomId && elementOrDomId.nodeName) { + // It looks like we were given a DOM element. Grab the id or generate + // one if it doesn't have one. + container = elementOrDomId; + if (!container.getAttribute('id') || container.getAttribute('id').length === 0) { + container.setAttribute('id', 'OT_' + OT.$.uuid()); + } + + domId = container.getAttribute('id'); + } else { + // We may have got an id, try and get it's DOM element. + container = OT.$('#' + elementOrDomId).get(0); + domId = elementOrDomId || ('OT_' + OT.$.uuid()); + } + + if (!container) { + container = OT.$.createElement('div', {id: domId}); + container.style.backgroundColor = '#000000'; + document.body.appendChild(container); + } else { + if(!(insertMode == null || insertMode === 'replace')) { + var placeholder = document.createElement('div'); + placeholder.id = ('OT_' + OT.$.uuid()); + if(insertMode === 'append') { + container.appendChild(placeholder); + container = placeholder; + } else if(insertMode === 'before') { + container.parentNode.insertBefore(placeholder, container); + container = placeholder; + } else if(insertMode === 'after') { + container.parentNode.insertBefore(placeholder, container.nextSibling); + container = placeholder; + } + } else { + OT.$.emptyElement(container); + } + } + + return container; + }; + + // Creates the standard container that the Subscriber and Publisher use to hold + // their video element and other chrome. + OT.WidgetView = function(targetElement, properties) { + + var widgetView = {}; + + var container = getOrCreateContainer(targetElement, properties && properties.insertMode), + widgetContainer = document.createElement('div'), + oldContainerStyles = {}, + dimensionsObserver, + videoElement, + videoObserver, + posterContainer, + loadingContainer, + width, + height, + loading = true, + audioOnly = false, + fitMode = 'cover', + fixFitMode = fixFitModeCover; + + OT.$.eventing(widgetView); + + if (properties) { + width = properties.width; + height = properties.height; + + if (width) { + if (typeof(width) === 'number') { + width = width + 'px'; + } + } + + if (height) { + if (typeof(height) === 'number') { + height = height + 'px'; + } + } + + container.style.width = width ? width : '264px'; + container.style.height = height ? height : '198px'; + container.style.overflow = 'hidden'; + fixMini(container, width || '264px', height || '198px'); + + if (properties.mirror === undefined || properties.mirror) { + OT.$.addClass(container, 'OT_mirrored'); + } + + if (properties.fitMode === 'contain') { + fitMode = 'contain'; + fixFitMode = fixFitModeContain; + } else if (properties.fitMode !== 'cover') { + OT.warn('Invalid fit value "' + properties.fitMode + '" passed. ' + + 'Only "contain" and "cover" can be used.'); + } + } + + if (properties.classNames) OT.$.addClass(container, properties.classNames); + + OT.$(container).addClass('OT_loading OT_fit-mode-' + fitMode); + + OT.$.addClass(widgetContainer, 'OT_widget-container'); + widgetContainer.style.width = '100%'; //container.style.width; + widgetContainer.style.height = '100%'; // container.style.height; + container.appendChild(widgetContainer); + + loadingContainer = document.createElement('div'); + OT.$.addClass(loadingContainer, 'OT_video-loading'); + widgetContainer.appendChild(loadingContainer); + + posterContainer = document.createElement('div'); + OT.$.addClass(posterContainer, 'OT_video-poster'); + widgetContainer.appendChild(posterContainer); + + oldContainerStyles.width = container.offsetWidth; + oldContainerStyles.height = container.offsetHeight; + + if (!OTPlugin.isInstalled()) { + // Observe changes to the width and height and update the aspect ratio + dimensionsObserver = OT.$.observeStyleChanges(container, ['width', 'height'], + function(changeSet) { + var width = changeSet.width ? changeSet.width[1] : container.offsetWidth, + height = changeSet.height ? changeSet.height[1] : container.offsetHeight; + + fixMini(container, width, height); + + if (videoElement) { + fixFitMode(widgetContainer, width, height, videoElement.aspectRatio(), + videoElement.isRotated()); + } + }); + + + // @todo observe if the video container or the video element get removed + // if they do we should do some cleanup + videoObserver = OT.$.observeNodeOrChildNodeRemoval(container, function(removedNodes) { + if (!videoElement) return; + + // This assumes a video element being removed is the main video element. This may + // not be the case. + var videoRemoved = OT.$.some(removedNodes, function(node) { + return node === widgetContainer || node.nodeName === 'VIDEO'; + }); + + if (videoRemoved) { + videoElement.destroy(); + videoElement = null; + } + + if (widgetContainer) { + OT.$.removeElement(widgetContainer); + widgetContainer = null; + } + + if (dimensionsObserver) { + dimensionsObserver.disconnect(); + dimensionsObserver = null; + } + + if (videoObserver) { + videoObserver.disconnect(); + videoObserver = null; + } + }); + } + + widgetView.destroy = function() { + if (dimensionsObserver) { + dimensionsObserver.disconnect(); + dimensionsObserver = null; + } + + if (videoObserver) { + videoObserver.disconnect(); + videoObserver = null; + } + + if (videoElement) { + videoElement.destroy(); + videoElement = null; + } + + if (container) { + OT.$.removeElement(container); + container = null; + } + }; + + widgetView.setBackgroundImageURI = function(bgImgURI) { + if (bgImgURI.substr(0, 5) !== 'http:' && bgImgURI.substr(0, 6) !== 'https:') { + if (bgImgURI.substr(0, 22) !== 'data:image/png;base64,') { + bgImgURI = 'data:image/png;base64,' + bgImgURI; + } + } + OT.$.css(posterContainer, 'backgroundImage', 'url(' + bgImgURI + ')'); + OT.$.css(posterContainer, 'backgroundSize', 'contain'); + OT.$.css(posterContainer, 'opacity', '1.0'); + }; + + if (properties && properties.style && properties.style.backgroundImageURI) { + widgetView.setBackgroundImageURI(properties.style.backgroundImageURI); + } + + widgetView.bindVideo = function(webRTCStream, options, completion) { + // remove the old video element if it exists + // @todo this might not be safe, publishers/subscribers use this as well... + if (videoElement) { + videoElement.destroy(); + videoElement = null; + } + + var onError = options && options.error ? options.error : void 0; + delete options.error; + + var video = new OT.VideoElement({attributes: options}, onError); + + // Initialize the audio volume + if (options.audioVolume) video.setAudioVolume(options.audioVolume); + + // makes the incoming audio streams take priority (will impact only FF OS for now) + video.audioChannelType('telephony'); + + video.appendTo(widgetContainer).bindToStream(webRTCStream, function(err) { + if (err) { + video.destroy(); + completion(err); + return; + } + + videoElement = video; + + // clear inline height value we used to init plugin rendering + OT.$.css(video.domElement(), 'height', ''); + + var fixFitModePartial = function() { + fixFitMode(widgetContainer, container.offsetWidth, container.offsetHeight, + video.aspectRatio(), video.isRotated()); + }; + + video.on({ + orientationChanged: fixFitModePartial, + videoDimensionsChanged: function(oldValue, newValue) { + fixFitModePartial(); + widgetView.trigger('videoDimensionsChanged', oldValue, newValue); + }, + mediaStopped: function() { + widgetView.trigger('mediaStopped'); + } + }); + + video.onRatioAvailable(fixFitModePartial); + + completion(null, video); + }); + + OT.$.addClass(video.domElement(), 'OT_video-element'); + + // plugin needs a minimum of height to be rendered and kicked off + // we will reset that once the video is bound to the stream + OT.$.css(video.domElement(), 'height', '1px'); + + return video; + }; + + OT.$.defineProperties(widgetView, { + + video: { + get: function() { + return videoElement; + } + }, + + showPoster: { + get: function() { + return !OT.$.isDisplayNone(posterContainer); + }, + set: function(newValue) { + if(newValue) { + OT.$.show(posterContainer); + } else { + OT.$.hide(posterContainer); + } + } + }, + + poster: { + get: function() { + return OT.$.css(posterContainer, 'backgroundImage'); + }, + set: function(src) { + OT.$.css(posterContainer, 'backgroundImage', 'url(' + src + ')'); + } + }, + + loading: { + get: function() { return loading; }, + set: function(l) { + loading = l; + + if (loading) { + OT.$.addClass(container, 'OT_loading'); + } else { + OT.$.removeClass(container, 'OT_loading'); + } + } + }, + + audioOnly: { + get: function() { return audioOnly; }, + set: function(a) { + audioOnly = a; + + if (audioOnly) { + OT.$.addClass(container, 'OT_audio-only'); + } else { + OT.$.removeClass(container, 'OT_audio-only'); + } + } + }, + + domId: { + get: function() { return container.getAttribute('id'); } + } + + }); + + widgetView.domElement = container; + + widgetView.addError = function(errorMsg, helpMsg, classNames) { + container.innerHTML = '

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

'; + OT.$.addClass(container, classNames || 'OT_subscriber_error'); + if(container.querySelector('p').offsetHeight > container.offsetHeight) { + container.querySelector('span').style.display = 'none'; + } + }; + + 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.

+ * + * @param {Object} style Either an object containing properties that define the style, or a + * String defining this single style property to set. + * @param {String} value The value to set for the style passed in. Pass a value + * for this parameter only if the value of the style parameter is a String.

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

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

+ * + *

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

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

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

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

+ * + *

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

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

If you pass two parameters, style and value, they are key-value + * pair that define one property of the display style. For example, the following code passes + * two parameter values to the method:

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

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

+ * + *

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

+ * + * @param {Object} style Either an object containing properties that define the style, or a + * String defining this single style property to set. + * @param {String} value The value to set for the style passed in. Pass a value + * for this parameter only if the value of the style parameter is a String.

+ * + * @returns {Subscriber} The Subscriber object. + * + * @see getStyle() + * @see setStyle() + * + * @see Session.subscribe() + * @method #setStyle + * @memberOf Subscriber + */ + + 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; iwindow.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; + }; +}; + +// tb_require('../helpers/helpers.js') + +!(function() { + /* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ + /* global OT */ + + /** + * The Event object defines the basic OpenTok event object that is passed to + * event listeners. Other OpenTok event classes implement the properties and methods of + * the Event object.

+ * + *

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

+ * + *
+   * stream.on("streamPropertyChanged", function (event) {
+   *     alert("Properties changed for stream " + event.target.streamId);
+   * });
+ * + * @class Event + * @property {Boolean} cancelable Whether the event has a default behavior that is cancelable + * (true) or not (false). You can cancel the default behavior by + * calling the preventDefault() method of the Event object in the callback + * function. (See preventDefault().) + * + * @property {Object} target The object that dispatched the event. + * + * @property {String} type The type of event. + */ + OT.Event = OT.$.eventing.Event(); + /** + * Prevents the default behavior associated with the event from taking place. + * + *

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

+ * + *

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

+ * + *

The following events have default behaviors:

+ * + * + * + * @method #preventDefault + * @memberof Event + */ + /** + * Whether the default event behavior has been prevented via a call to + * preventDefault() (true) or not (false). + * See preventDefault(). + * @method #isDefaultPrevented + * @return {Boolean} + * @memberof Event + */ + + // Event names lookup + OT.Event.names = { + // Activity Status for cams/mics + ACTIVE: 'active', + INACTIVE: 'inactive', + UNKNOWN: 'unknown', + + // Archive types + PER_SESSION: 'perSession', + PER_STREAM: 'perStream', + + // OT Events + EXCEPTION: 'exception', + ISSUE_REPORTED: 'issueReported', + + // Session Events + SESSION_CONNECTED: 'sessionConnected', + SESSION_DISCONNECTED: 'sessionDisconnected', + STREAM_CREATED: 'streamCreated', + STREAM_DESTROYED: 'streamDestroyed', + CONNECTION_CREATED: 'connectionCreated', + CONNECTION_DESTROYED: 'connectionDestroyed', + SIGNAL: 'signal', + STREAM_PROPERTY_CHANGED: 'streamPropertyChanged', + MICROPHONE_LEVEL_CHANGED: 'microphoneLevelChanged', + + + // Publisher Events + RESIZE: 'resize', + SETTINGS_BUTTON_CLICK: 'settingsButtonClick', + DEVICE_INACTIVE: 'deviceInactive', + INVALID_DEVICE_NAME: 'invalidDeviceName', + ACCESS_ALLOWED: 'accessAllowed', + ACCESS_DENIED: 'accessDenied', + ACCESS_DIALOG_OPENED: 'accessDialogOpened', + ACCESS_DIALOG_CLOSED: 'accessDialogClosed', + ECHO_CANCELLATION_MODE_CHANGED: 'echoCancellationModeChanged', + 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.

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

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * code + * + * + * title + *
+ * 1004 + * + * + * Authentication error + *
+ * 1005 + * + * + * Invalid Session ID + *
+ * 1006 + * + * + * Connect Failed + *
+ * 1007 + * + * + * Connect Rejected + *
+ * 1008 + * + * + * Connect Time-out + *
+ * 1009 + * + * + * Security Error + *
+ * 1010 + * + * + * Not Connected + *
+ * 1011 + * + * + * Invalid Parameter + *
+ * 1013 + * + * Connection Failed + *
+ * 1014 + * + * API Response Failure + *
+ * 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.

+ * + * @property {String} message The error message. + * + * @property {Object} target The object that the event pertains to. For an + * exception event, this will be an object other than the OT object + * (such as a Session object or a Publisher object). + * + * @property {String} title The error title. + * @augments Event + */ + OT.ExceptionEvent = function (type, message, title, code, component, target) { + OT.Event.call(this, type); + + this.message = message; + this.title = title; + this.code = code; + this.component = component; + this.target = target; + }; + + + OT.IssueReportedEvent = function (type, issueId) { + OT.Event.call(this, type); + this.issueId = issueId; + }; + + // Triggered when the JS dynamic config and the DOM have loaded. + OT.EnvLoadedEvent = function (type) { + OT.Event.call(this, type); + }; + + +/** + * 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.) + * + *

Example
+ * + *

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

+ * + *
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
+ * var sessionID = ""; // Replace with your own session ID.
+ *                     // See https://dashboard.tokbox.com/projects
+ * var token = ""; // Replace with a generated token that has been assigned the moderator role.
+ *                 // See https://dashboard.tokbox.com/projects
+ * var connectionCount = 0;
+ *
+ * var session = OT.initSession(apiKey, sessionID);
+ * session.on("connectionCreated", function(event) {
+ *    connectionCount++;
+ *    displayConnectionCount();
+ * });
+ * session.on("connectionDestroyed", function(event) {
+ *    connectionCount--;
+ *    displayConnectionCount();
+ * });
+ * session.connect(token);
+ *
+ * function displayConnectionCount() {
+ *     document.getElementById("connectionCountField").value = connectionCount.toString();
+ * }
+ * + *

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

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

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

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

+ * + *

For a connectionCreated event, this string is undefined.

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

Example — streamCreated event dispatched + * by the Session object

+ *

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

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

Example — streamDestroyed event dispatched + * by the Session object

+ * + *

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

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

Example — streamCreated event dispatched + * by a Publisher object

+ *

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

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

Example — streamDestroyed event + * dispatched by a Publisher object

+ * + *

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

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

+ *
    + *
  • "clientDisconnected" — A client disconnected from the session by calling + * the disconnect() method of the Session object or by closing the browser. + * (See Session.disconnect().)
  • + * + *
  • "forceDisconnected" — A moderator has disconnected the publisher of the + * stream from the session, by calling the forceDisconnect() method of the Session +* object. (See Session.forceDisconnect().)
  • + * + *
  • "forceUnpublished" — A moderator has forced the publisher of the stream + * to stop publishing the stream, by calling the forceUnpublish() method of the + * Session object. (See Session.forceUnpublish().)
  • + * + *
  • "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.

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

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

+*

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

+*

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

+* +*

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

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

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

+ * @augments Event + */ + + var sessionConnectedConnectionsDeprecationWarningShown = false; + var sessionConnectedStreamsDeprecationWarningShown = false; + var sessionConnectedArchivesDeprecationWarningShown = false; + + OT.SessionConnectEvent = function (type) { + OT.Event.call(this, type, false); + if (OT.$.canDefineProperty) { + Object.defineProperties(this, { + connections: { + get: function() { + if(!sessionConnectedConnectionsDeprecationWarningShown) { + OT.warn('OT.SessionConnectedEvent no longer includes connections. Listen ' + + 'for connectionCreated events instead.'); + sessionConnectedConnectionsDeprecationWarningShown = true; + } + return []; + } + }, + streams: { + get: function() { + if(!sessionConnectedStreamsDeprecationWarningShown) { + OT.warn('OT.SessionConnectedEvent no longer includes streams. Listen for ' + + 'streamCreated events instead.'); + sessionConnectedConnectionsDeprecationWarningShown = true; + } + return []; + } + }, + archives: { + get: function() { + if(!sessionConnectedArchivesDeprecationWarningShown) { + OT.warn('OT.SessionConnectedEvent no longer includes archives. Listen for ' + + 'archiveStarted events instead.'); + sessionConnectedArchivesDeprecationWarningShown = true; + } + return []; + } + } + }); + } else { + this.connections = []; + this.streams = []; + this.archives = []; + } + }; + +/** + * The Session object dispatches SessionDisconnectEvent object when a session has disconnected. + * This event may be dispatched asynchronously in response to a successful call to the + * disconnect() method of the session object. + * + *

+ * Example + *

+ *

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

+ *
var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
+ *  var sessionID = ""; // Replace with your own session ID.
+ *                      // See https://dashboard.tokbox.com/projects
+ *  var token = ""; // Replace with a generated token that has been assigned the moderator role.
+ *                  // See https://dashboard.tokbox.com/projects
+ *
+ *  var session = OT.initSession(apiKey, sessionID);
+ *  session.on("sessionDisconnected", function(event) {
+ *      alert("The session disconnected. " + event.reason);
+ *  });
+ *  session.connect(token);
+ *  
+ * + * @property {String} reason A description of why the session disconnected. + * This property can have two values: + *

+ *
    + *
  • "clientDisconnected" — A client disconnected from the session by calling + * the disconnect() method of the Session object or by closing the browser. + * ( See Session.disconnect().)
  • + *
  • "forceDisconnected" — A moderator has disconnected you from the session + * by calling the forceDisconnect() method of the Session object. (See + * Session.forceDisconnect().)
  • + *
  • "networkDisconnected" — The network connection terminated abruptly + * (for example, the client lost their internet connection).
  • + *
+ * + * @class SessionDisconnectEvent + * @augments Event + */ + OT.SessionDisconnectEvent = function (type, reason, cancelable) { + OT.Event.call(this, type, cancelable); + this.reason = reason; + }; + +/** +* Prevents the default behavior associated with the event from taking place. +* +*

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

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

+* +*

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

+* +* @method #preventDefault +* @memberof SessionDisconnectEvent +*/ + +/** + * The Session object dispatches a streamPropertyChanged event in the + * following circumstances: + * + *
    + *
  • A stream has started or stopped publishing audio or video (see + * Publisher.publishAudio() and + * Publisher.publishVideo()). + * This change results from a call to the 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. + *
  • + *
  • The videoDimensions property of the Stream object has + * changed (see Stream.videoDimensions). + *
  • + *
  • The videoType property of the Stream object has changed. + * This can happen in a stream published by a mobile device. (See + * Stream.videoType.) + *
  • + *
+ * + * @class StreamPropertyChangedEvent + * @property {String} changedProperty The property of the stream that changed. This value + * is either "hasAudio", "hasVideo", or "videoDimensions". + * @property {Object} newValue The new value of the property (after the change). + * @property {Object} oldValue The old value of the property (before the change). + * @property {Stream} stream The Stream object for which a property has changed. + * + * @see Publisher.publishAudio()

+ * @see Publisher.publishVideo()

+ * @see Stream.videoDimensions

+ * @augments Event + */ + OT.StreamPropertyChangedEvent = function (type, stream, changedProperty, oldValue, newValue) { + OT.Event.call(this, type, false); + this.type = type; + this.stream = stream; + this.changedProperty = changedProperty; + this.oldValue = oldValue; + this.newValue = newValue; + }; + + 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). + *
  • + * + *
+ * + * @property {Object} target The object that dispatched the event. + * + * @property {String} type The type of event: "videoDisabled" or + * "videoEnabled". + * + * @see Subscriber videoDisabled event

+ * @see Subscriber videoEnabled event

+ * @augments Event + */ + OT.VideoEnabledChangedEvent = function(type, properties) { + OT.Event.call(this, type, false); + this.reason = properties.reason; + }; + + OT.VideoDisableWarningEvent = function(type/*, properties*/) { + OT.Event.call(this, type, false); + }; + +/** + * Dispatched periodically by a Subscriber or Publisher object to indicate the audio + * level. This event is dispatched up to 60 times per second, depending on the browser. + * + * @property {String} audioLevel The audio level, from 0 to 1.0. Adjust this value logarithmically + * for use in adjusting a user interface element, such as a volume meter. Use a moving average + * to smooth the data. + * + * @class AudioLevelUpdatedEvent + * @augments Event + */ + OT.AudioLevelUpdatedEvent = function(audioLevel) { + OT.Event.call(this, OT.Event.names.AUDIO_LEVEL_UPDATED, false); + this.audioLevel = audioLevel; + }; + + 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. + *
  • + *
+ * + * @see OT.initPublisher() + * @see OT.registerScreenSharingExtension() + * @method OT.checkScreenSharingCapability + * @memberof OT + */ +OT.checkScreenSharingCapability = function(callback) { + + var response = { + supported: false, + extensionRequired: void 0, + extensionRegistered: void 0, + extensionInstalled: void 0, + supportedSources: {} + }; + + // find a supported browser + + var helper = screenSharingPickHelper(); + + if (helper.name === void 0) { + setTimeout(callback.bind(null, response)); + return; + } + + response.supported = true; + response.extensionRequired = helper.proto.extensionRequired ? helper.name : void 0; + + response.supportedSources = { + screen: helper.proto.sources.screen, + application: helper.proto.sources.application, + window: helper.proto.sources.window + }; + + if (!helper.instance) { + response.extensionRegistered = false; + if (response.extensionRequired) { + response.extensionInstalled = false; + } + setTimeout(callback.bind(null, response)); + return; + } + + response.extensionRegistered = response.extensionRequired ? true : void 0; + helper.instance.isInstalled(function(installed) { + response.extensionInstalled = response.extensionRequired ? installed : void 0; + callback(response); + }); + +}; + +// tb_require('./register.js') + +OT.registerScreenSharingExtensionHelper('firefox', { + isSupportedInThisBrowser: OT.$.env.name === 'Firefox', + autoRegisters: true, + extensionRequired: false, + getConstraintsShowsPermissionUI: false, + sources: { + screen: true, + application: OT.$.env.name === 'Firefox' && OT.$.env.version >= 34, + window: OT.$.env.name === 'Firefox' && OT.$.env.version >= 34 + }, + register: function() { + return { + isInstalled: function(callback) { + callback(true); + }, + getConstraints: function(source, constraints, callback) { + constraints.video = { + mediaSource: source + }; + callback(void 0, constraints); + } + }; + } +}); + +OT.registerScreenSharingExtensionHelper('chrome', { + isSupportedInThisBrowser: !!navigator.webkitGetUserMedia && typeof chrome !== 'undefined', + autoRegisters: false, + extensionRequired: true, + getConstraintsShowsPermissionUI: true, + sources: { + screen: true, + application: false, + window: false + }, + register: function (extensionID) { + if(!extensionID) { + throw new Error('initChromeScreenSharingExtensionHelper: extensionID is required.'); + } + + var isChrome = !!navigator.webkitGetUserMedia && typeof chrome !== 'undefined', + callbackRegistry = {}, + isInstalled = void 0; + + var prefix = 'com.tokbox.screenSharing.' + extensionID; + var request = function(method, payload) { + var res = { payload: payload, from: 'jsapi' }; + res[prefix] = method; + return res; + }; + + var addCallback = function(fn, timeToWait) { + var requestId = OT.$.uuid(), + timeout; + callbackRegistry[requestId] = function() { + clearTimeout(timeout); + timeout = null; + fn.apply(null, arguments); + }; + if(timeToWait) { + timeout = setTimeout(function() { + delete callbackRegistry[requestId]; + fn(new Error('Timeout waiting for response to request.')); + }, timeToWait); + } + return requestId; + }; + + var isAvailable = function(callback) { + if(!callback) { + throw new Error('isAvailable: callback is required.'); + } + + if(!isChrome) { + setTimeout(callback.bind(null, false)); + } + + if(isInstalled !== void 0) { + setTimeout(callback.bind(null, isInstalled)); + } else { + var requestId = addCallback(function(error, event) { + if(isInstalled !== true) { + isInstalled = (event === 'extensionLoaded'); + } + callback(isInstalled); + }, 2000); + var post = request('isExtensionInstalled', { requestId: requestId }); + window.postMessage(post, '*'); + } + }; + + var getConstraints = function(source, constraints, callback) { + if(!callback) { + throw new Error('getSourceId: callback is required'); + } + isAvailable(function(isInstalled) { + if(isInstalled) { + var requestId = addCallback(function(error, event, payload) { + if(event === 'permissionDenied') { + callback(new Error('PermissionDeniedError')); + } else { + if (!constraints.video) { + constraints.video = {}; + } + if (!constraints.video.mandatory) { + constraints.video.mandatory = {}; + } + constraints.video.mandatory.chromeMediaSource = 'desktop'; + constraints.video.mandatory.chromeMediaSourceId = payload.sourceId; + callback(void 0, constraints); + } + }); + window.postMessage(request('getSourceId', { requestId: requestId, source: source }), '*'); + } else { + callback(new Error('Extension is not installed')); + } + }); + }; + + window.addEventListener('message', function(event) { + + if (event.origin !== window.location.origin) { + return; + } + + if(!(event.data != null && typeof event.data === 'object')) { + return; + } + + if(event.data.from !== 'extension') { + return; + } + + var method = event.data[prefix], + payload = event.data.payload; + + if(payload && payload.requestId) { + var callback = callbackRegistry[payload.requestId]; + delete callbackRegistry[payload.requestId]; + if(callback) { + callback(null, method, payload); + } + } + + if(method === 'extensionLoaded') { + isInstalled = true; + } + }); + + return { + isInstalled: isAvailable, + getConstraints: getConstraints + }; + } +}); + +// tb_require('../helpers/helpers.js') +// tb_require('../helpers/lib/properties.js') +// tb_require('../helpers/lib/video_element.js') +// tb_require('./events.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + + +// 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); + + // The defaults are used for incoming streams from pre 2015Q1 release clients. + this.source = options.source || 'camera'; + this.fitMode = options.fitMode || 'cover'; + + 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; + + case 'fitMode': + this[key] = attributes[key]; + break; + + case 'source': + this[key] = attributes[key]; + 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; + }; +}; + +// tb_require('../helpers/helpers.js') +// tb_require('../helpers/lib/properties.js') +// tb_require('./events.js') +// tb_require('./stream_channel.js') + +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ + +!(function() { + + var validPropertyNames = ['name', 'archiving']; + +/** + * Specifies a stream. A stream is a representation of a published stream in a session. When a + * client calls the Session.publish() method, a new stream is + * created. Properties of the Stream object provide information about the stream. + * + *

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

+ * + *

When a connection to a session is made, the Session object dispatches a + * sessionConnected event, defined by the SessionConnectEvent object. The + * SessionConnectEvent object has a streams property, which is an array of Stream + * objects pertaining to the streams in the session at that time. For details and a code example, + * see {@link SessionConnectEvent}.

+ * + * @class Stream + * @property {Connection} connection The Connection object corresponding + * to the connection that is publishing the stream. You can compare this to to the + * connection property of the Session object to see if the stream is being published + * by the local web page. + * + * @property {Number} creationTime The timestamp for the creation + * of the stream. This value is calculated in milliseconds. You can convert this value to a + * Date object by calling new Date(creationTime), where creationTime is + * the creationTime property of the Stream object. + * + * @property {Number} frameRate The frame rate of the video stream. This property is only set if the + * publisher of the stream specifies a frame rate when calling the OT.initPublisher() + * method; otherwise, this property is undefined. + * + * @property {Boolean} hasAudio Whether the stream has audio. This property can change if the + * publisher turns on or off audio (by calling + * Publisher.publishAudio()). When this occurs, the + * {@link Session} object dispatches a streamPropertyChanged event (see + * {@link StreamPropertyChangedEvent}). + * + * @property {Boolean} hasVideo Whether the stream has video. This property can change if the + * publisher turns on or off video (by calling + * Publisher.publishVideo()). When this occurs, the + * {@link Session} object dispatches a streamPropertyChanged event (see + * {@link StreamPropertyChangedEvent}). + * + * @property {String} name The name of the stream. Publishers can specify a name when publishing + * a stream (using the publish() method of the publisher's Session object). + * + * @property {String} streamId The unique ID of the stream. + * + * @property {Object} videoDimensions This object has two properties: width and + * height. Both are numbers. The width property is the width of the + * encoded stream; the height property is the height of the encoded stream. (These + * are independent of the actual width of Publisher and Subscriber objects corresponding to the + * stream.) This property can change if a stream published from 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; i * * - * Errors when calling Session.subscribe(): - * - * - * - * * code * * Description @@ -14005,7 +17324,7 @@ waitForDomReady(); * * * - *

Errors when calling TB.initPublisher():

+ *

Errors when calling OT.initPublisher():

* * * @@ -14020,6 +17339,32 @@ waitForDomReady(); * the current version of one of the * OpenTok server SDKs. * + * + * + * + * + * + * + * + * + * + * + * + * *
1550Screen 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.
1551A 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.
1552A 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}.

- * - * @class Stream - * @property {Connection} connection The Connection object corresponding - * to the connection that is publishing the stream. You can compare this to to the - * connection property of the Session object to see if the stream is being published - * by the local web page. - * - * @property {Number} creationTime The timestamp for the creation - * of the stream. This value is calculated in milliseconds. You can convert this value to a - * Date object by calling new Date(creationTime), where creationTime is - * the creationTime property of the Stream object. - * - * @property {Number} frameRate The frame rate of the video stream. This property is only set if the - * publisher of the stream specifies a frame rate when calling the OT.initPublisher() - * method; otherwise, this property is undefined. - * - * @property {Boolean} hasAudio Whether the stream has audio. This property can change if the - * publisher turns on or off audio (by calling - * Publisher.publishAudio()). When this occurs, the - * {@link Session} object dispatches a streamPropertyChanged event (see - * {@link StreamPropertyChangedEvent}.) - * - * @property {Boolean} hasVideo Whether the stream has video. This property can change if the - * publisher turns on or off video (by calling - * Publisher.publishVideo()). When this occurs, the - * {@link Session} object dispatches a streamPropertyChanged event (see - * {@link StreamPropertyChangedEvent}.) - * - * @property {String} name The name of the stream. Publishers can specify a name when publishing - * a stream (using the publish() method of the publisher's Session object). - * - * @property {String} streamId The unique ID of the stream. - * - * @property {Object} videoDimensions This object has two properties: width and - * height. Both are numbers. The width property is the width of the - * encoded stream; the height property is the height of the encoded stream. (These - * are independent of the actual width of Publisher and Subscriber objects corresponding to the - * stream.) This property can change if a stream - * published from an iOS device resizes, based on a change in the device orientation. When this - * occurs, the {@link Session} object dispatches a streamPropertyChanged event (see - * {@link StreamPropertyChangedEvent}.) - */ - - - OT.Stream = function(id, name, creationTime, connection, session, channel) { - var destroyedReason; - - this.id = this.streamId = id; - this.name = name; - this.creationTime = Number(creationTime); - - this.connection = connection; - this.channel = channel; - this.publisher = OT.publishers.find({streamId: this.id}); - - OT.$.eventing(this); - - var onChannelUpdate = OT.$.bind(function(channel, key, oldValue, newValue) { - var _key = key; - - switch(_key) { - case 'active': - _key = channel.type === 'audio' ? 'hasAudio' : 'hasVideo'; - this[_key] = newValue; - break; - - case 'disableWarning': - _key = channel.type === 'audio' ? 'audioDisableWarning': 'videoDisableWarning'; - this[_key] = newValue; - if (!this[channel.type === 'audio' ? 'hasAudio' : 'hasVideo']) { - return; // Do NOT event in this case. + 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; iOT.upgradeSystemRequirements() + * @method OT.checkSystemRequirements + * @memberof OT + */ +OT.checkSystemRequirements = function() { + OT.debug('OT.checkSystemRequirements()'); + + // Try native support first, then OTPlugin... + var systemRequirementsMet = OT.$.hasCapabilities('websockets', 'webrtc') || + OTPlugin.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) { + OT.analytics.logEvent({ + action: 'checkSystemRequirements', + variation: 'notHasRequirements', + partnerId: OT.APIKEY, + payload: {userAgent: OT.$.env.userAgent} + }); + } + + return systemRequirementsMet; +}; - /* - * A RTCPeerConnection.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. + *

+ * @see OT.checkSystemRequirements() + * @method OT.upgradeSystemRequirements + * @memberof OT + */ +OT.upgradeSystemRequirements = function(){ + // trigger after the OT environment has loaded + OT.onLoad( function() { - if (!OT.$.hasCapabilities('audioOutputLevelStat', 'getStatsWithSingleParameter')) { - throw new Error('The current platform does not provide the required capabilities'); + if(OTPlugin.isSupported()) { + OT.Dialogs.Plugin.promptToInstall().on({ + download: function() { + window.location = OTPlugin.pathToInstaller(); + }, + refresh: function() { + location.reload(); + }, + closed: function() {} + }); + return; } - var _peerConnection = peerConnection, - _statsProperty = 'audioOutputLevel'; + var id = '_upgradeFlash'; - /* - * Acquires the audio level. - * - * @param {function(?number)} done a callback to be called with the acquired value in the - * [0, 1] range when available or null if no value could be acquired - */ - this.sample = function(done) { - _peerConnection.getStatsWithSingleParameter(function(statsReport) { - var results = statsReport.result(); + // 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: / [/] -// * m= / -// -// References: -// * https://tools.ietf.org/html/rfc4566 -// * http://en.wikipedia.org/wiki/Session_Description_Protocol -// -var SDPHelpers = { - // Search through sdpLines to find the Media Line of type +mediaType+. - getMLineIndex: function getMLineIndex(sdpLines, mediaType) { - var targetMLine = 'm=' + mediaType; +// tb_require('../helpers/helpers.js') +// tb_require('../helpers/lib/properties.js') - // Find the index of the media line for +type+ - return findIndex(sdpLines, function(line) { - if (line.indexOf(targetMLine) !== -1) { - return true; - } +/* jshint globalstrict: true, strict: false, undef: true, unused: true, + trailing: true, browser: true, smarttabs:true */ +/* global OT */ - return false; - }); - }, - // Extract the payload types for a give Media Line. - // - getMLinePayloadTypes: function getMLinePayloadTypes (mediaLine, mediaType) { - var mLineSelector = new RegExp('^m=' + mediaType + - ' \\d+(/\\d+)? [a-zA-Z0-9/]+(( [a-zA-Z0-9/]+)+)$', 'i'); +/** + * 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; - // 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 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; i 0) { - OT.$.forEach(_peerConnection.remoteStreams(), _onRemoteStreamAdded, this); - } else if (numDelegates === 1) { - // We only bother with the PeerConnection negotiation if we don't already - // have a remote stream. - - var channelsToSubscribeTo; - - if (properties.subscribeToVideo || properties.subscribeToAudio) { - var audio = stream.getChannelsOfType('audio'), - video = stream.getChannelsOfType('video'); - - channelsToSubscribeTo = OT.$.map(audio, function(channel) { - return { - id: channel.id, - type: channel.type, - active: properties.subscribeToAudio - }; - }).concat(OT.$.map(video, function(channel) { - return { - id: channel.id, - type: channel.type, - active: properties.subscribeToVideo, - restrictFrameRate: properties.restrictFrameRate !== void 0 ? - properties.restrictFrameRate : false - }; - })); + if(err && err.code === 4001) { + reason = 'networkTimedout'; } - session._.subscriberCreate(stream, subscriber, channelsToSubscribeTo, - OT.$.bind(function(err, message) { - if (err) { - this.trigger('error', err.message, this, 'Subscribe'); - } - _peerConnection.setIceServers(OT.Raptor.parseIceServers(message)); - }, this)); - } - }; + setState('disconnected'); - this.getStatsWithSingleParameter = function(callback) { - if(_peerConnection) { - _peerConnection.getStatsWithSingleParameter(callback); - } - }; - }; + _dispatcher.onClose(reason); + }, this), -})(window); -!(function() { + onError = function onError () {}; + // @todo what does having an error mean? Are they always fatal? Are we disconnected now? -// Manages N Chrome elements - OT.Chrome = function(properties) { - var _visible = false, - _widgets = {}, - // Private helper function - _set = function(name, widget) { - widget.parent = this; - widget.appendTo(properties.parent); + //// Public API - _widgets[name] = widget; - - this[name] = widget; - }; - - if (!properties.parent) { - // @todo raise an exception + this.connect = function (token, sessionInfo, completion) { + if (!this.is('disconnected', 'error')) { + OT.warn('Cannot connect the Raptor Socket as it is currently connected. You should ' + + 'disconnect first.'); return; } - OT.$.eventing(this); + setState('connecting'); + _sessionId = sessionInfo.sessionId; + _token = token; + _completion = completion; - this.destroy = function() { - this.off(); - this.hide(); + var rumorChannel = '/v2/partner/' + OT.APIKEY + '/session/' + _sessionId; - for (var name in _widgets) { - _widgets[name].destroy(); + _rumor = new OT.Rumor.Socket(messagingSocketUrl, symphonyUrl); + _rumor.onClose(onClose); + _rumor.onMessage(OT.$.bind(_dispatcher.dispatch, _dispatcher)); + + _rumor.connect(connectionId, OT.$.bind(function(error) { + if (error) { + onConnectComplete({ + reason: 'WebSocketConnection', + code: error.code, + message: error.message + }); + return; } - }; - this.show = function() { - _visible = true; + // we do this here to avoid getting connect errors twice + _rumor.onError(onError); - for (var name in _widgets) { - _widgets[name].show(); - } - }; + OT.debug('Raptor Socket connected. Subscribing to ' + + rumorChannel + ' on ' + messagingSocketUrl); - this.hide = function() { - _visible = false; + _rumor.subscribe([rumorChannel]); - for (var name in _widgets) { - _widgets[name].hide(); - } - }; + //connect to session + var connectMessage = OT.Raptor.Message.connections.create(OT.APIKEY, + _sessionId, _rumor.id()); + this.publish(connectMessage, {'X-TB-TOKEN-AUTH': _token}, OT.$.bind(function(error) { + if (error) { + error.message = 'ConnectToSession:' + error.code + + ':Received error response to connection create message.'; + var payload = { + reason: 'ConnectToSession', + code: error.code, + message: 'Received error response to connection create message.' + }; + var event = { + action: 'Connect', + variation: 'Failure', + payload: payload, + sessionId: _sessionId, + partnerId: OT.APIKEY, + connectionId: connectionId + }; + OT.analytics.logEvent(event); + onConnectComplete(payload); + return; + } - - // Adds the widget to the chrome and to the DOM. Also creates a accessor - // property for it on the chrome. - // - // @example - // chrome.set('foo', new FooWidget()); - // chrome.foo.setDisplayMode('on'); - // - // @example - // chrome.set({ - // foo: new FooWidget(), - // bar: new BarWidget() - // }); - // chrome.foo.setDisplayMode('on'); - // - this.set = function(widgetName, widget) { - if (typeof(widgetName) === 'string' && widget) { - _set.call(this, widgetName, widget); - - } else { - for (var name in widgetName) { - if (widgetName.hasOwnProperty(name)) { - _set.call(this, name, widgetName[name]); + this.publish( OT.Raptor.Message.sessions.get(OT.APIKEY, _sessionId), + function (error) { + if (error) { + var payload = { + reason: 'GetSessionState', + code: error.code, + message: 'Received error response to session read' + }; + var event = { + action: 'Connect', + variation: 'Failure', + payload: payload, + sessionId: _sessionId, + partnerId: OT.APIKEY, + connectionId: connectionId + }; + OT.analytics.logEvent(event); + onConnectComplete(payload); + } else { + onConnectComplete.apply(null, arguments); } - } - } - return this; - }; - + }); + }, this)); + }, this)); }; -})(window); -!(function() { - if (!OT.Chrome.Behaviour) OT.Chrome.Behaviour = {}; + this.disconnect = function (drainSocketBuffer) { + if (this.is('disconnected')) return; - // A mixin to encapsulate the basic widget behaviour. This needs a better name, - // it's not actually a widget. It's actually "Behaviour that can be applied to - // an object to make it support the basic Chrome widget workflow"...but that would - // probably been too long a name. - OT.Chrome.Behaviour.Widget = function(widget, options) { - var _options = options || {}, - _mode, - _previousMode; - - // - // @param [String] mode - // 'on', 'off', or 'auto' - // - widget.setDisplayMode = function(mode) { - var newMode = mode || 'auto'; - if (_mode === newMode) return; - - OT.$.removeClass(this.domElement, 'OT_mode-' + _mode); - OT.$.addClass(this.domElement, 'OT_mode-' + newMode); - - _previousMode = _mode; - _mode = newMode; - }; - - widget.show = function() { - this.setDisplayMode(_previousMode); - if (_options.onShow) _options.onShow(); - - return this; - }; - - widget.hide = function() { - this.setDisplayMode('off'); - if (_options.onHide) _options.onHide(); - - return this; - }; - - widget.destroy = function() { - if (_options.onDestroy) _options.onDestroy(this.domElement); - if (this.domElement) OT.$.removeElement(this.domElement); - - return widget; - }; - - widget.appendTo = function(parent) { - // create the element under parent - this.domElement = OT.$.createElement(_options.nodeName || 'div', - _options.htmlAttributes, - _options.htmlContent); - - if (_options.onCreate) _options.onCreate(this.domElement); - - widget.setDisplayMode(_options.mode); - - if (_options.mode === 'auto') { - // if the mode is auto we hold the "on mode" for 2 seconds - // this will let the proper widgets nicely fade away and help discoverability - OT.$.addClass(widget.domElement, 'OT_mode-on-hold'); - setTimeout(function() { - OT.$.removeClass(widget.domElement, 'OT_mode-on-hold'); - }, 2000); - } - - - // add the widget to the parent - parent.appendChild(this.domElement); - - return widget; - }; + setState('disconnecting'); + _rumor.disconnect(drainSocketBuffer); }; -})(window); -!(function() { - - // BackingBar Chrome Widget + // Publishs +message+ to the Symphony app server. // - // nameMode (String) - // Whether or not the name panel is being displayed - // Possible values are: "auto" (the name is displayed - // when the stream is first displayed and when the user mouses over the display), - // "off" (the name is not displayed), and "on" (the name is displayed). + // The completion handler is optional, as is the headers + // dict, but if you provide the completion handler it must + // be the last argument. // - // muteMode (String) - // Whether or not the mute button is being displayed - // Possible values are: "auto" (the mute button is displayed - // when the stream is first displayed and when the user mouses over the display), - // "off" (the mute button is not displayed), and "on" (the mute button is displayed). - // - // displays a backing bar - // can be shown/hidden - // can be destroyed - OT.Chrome.BackingBar = function(options) { - var _nameMode = options.nameMode, - _muteMode = options.muteMode; - - function getDisplayMode() { - if(_nameMode === 'on' || _muteMode === 'on') { - return 'on'; - } else if(_nameMode === 'mini' || _muteMode === 'mini') { - return 'mini'; - } else if(_nameMode === 'mini-auto' || _muteMode === 'mini-auto') { - return 'mini-auto'; - } else if(_nameMode === 'auto' || _muteMode === 'auto') { - return 'auto'; - } else { - return 'off'; - } + this.publish = function (message, headers, completion) { + if (_rumor.isNot('connected')) { + OT.error('OT.Raptor.Socket: cannot publish until the socket is connected.' + message); + return; } - // Mixin common widget behaviour - OT.Chrome.Behaviour.Widget(this, { - mode: getDisplayMode(), - nodeName: 'div', - htmlContent: '', - htmlAttributes: { - className: 'OT_bar OT_edge-bar-item' + var transactionId = OT.$.uuid(), + _headers = {}, + _completion; + + // Work out if which of the optional arguments (headers, completion) + // have been provided. + if (headers) { + if (OT.$.isFunction(headers)) { + _headers = {}; + _completion = headers; } - }); + else { + _headers = headers; + } + } + if (!_completion && completion && OT.$.isFunction(completion)) _completion = completion; - this.setNameMode = function(nameMode) { - _nameMode = nameMode; - this.setDisplayMode(getDisplayMode()); - }; - this.setMuteMode = function(muteMode) { - _muteMode = muteMode; - this.setDisplayMode(getDisplayMode()); - }; + if (_completion) _dispatcher.registerCallback(transactionId, _completion); + OT.debug('OT.Raptor.Socket Publish (ID:' + transactionId + ') '); + OT.debug(message); + + _rumor.publish([symphonyUrl], message, OT.$.extend(_headers, { + 'Content-Type': 'application/x-raptor+v2', + 'TRANSACTION-ID': transactionId, + 'X-TB-FROM-ADDRESS': _rumor.id() + })); + + return transactionId; }; -})(window); -!(function() { + // Register a new stream against _sessionId + this.streamCreate = function(name, audioFallbackEnabled, channels, minBitrate, maxBitrate, + completion) { + var streamId = OT.$.uuid(), + message = OT.Raptor.Message.streams.create( OT.APIKEY, + _sessionId, + streamId, + name, + audioFallbackEnabled, + channels, + minBitrate, + maxBitrate); - // NamePanel Chrome Widget - // - // mode (String) - // Whether to display the name. Possible values are: "auto" (the name is displayed - // when the stream is first displayed and when the user mouses over the display), - // "off" (the name is not displayed), and "on" (the name is displayed). - // - // displays a name - // can be shown/hidden - // can be destroyed - OT.Chrome.NamePanel = function(options) { - var _name = options.name; + this.publish(message, function(error, message) { + completion(error, streamId, message); + }); + }; - if (!_name || OT.$.trim(_name).length === '') { - _name = null; + this.streamDestroy = function(streamId) { + this.publish( OT.Raptor.Message.streams.destroy(OT.APIKEY, _sessionId, streamId) ); + }; - // THere's no name, just flip the mode off - options.mode = 'off'; + this.streamChannelUpdate = function(streamId, channelId, attributes) { + this.publish( OT.Raptor.Message.streamChannels.update(OT.APIKEY, _sessionId, + streamId, channelId, attributes) ); + }; + + this.subscriberCreate = function(streamId, subscriberId, channelsToSubscribeTo, completion) { + this.publish( OT.Raptor.Message.subscribers.create(OT.APIKEY, _sessionId, + streamId, subscriberId, _rumor.id(), channelsToSubscribeTo), completion ); + }; + + this.subscriberDestroy = function(streamId, subscriberId) { + this.publish( OT.Raptor.Message.subscribers.destroy(OT.APIKEY, _sessionId, + streamId, subscriberId) ); + }; + + this.subscriberUpdate = function(streamId, subscriberId, attributes) { + this.publish( OT.Raptor.Message.subscribers.update(OT.APIKEY, _sessionId, + streamId, subscriberId, attributes) ); + }; + + this.subscriberChannelUpdate = function(streamId, subscriberId, channelId, attributes) { + this.publish( OT.Raptor.Message.subscriberChannels.update(OT.APIKEY, _sessionId, + streamId, subscriberId, channelId, attributes) ); + }; + + this.forceDisconnect = function(connectionIdToDisconnect, completion) { + this.publish( OT.Raptor.Message.connections.destroy(OT.APIKEY, _sessionId, + connectionIdToDisconnect), completion ); + }; + + this.forceUnpublish = function(streamIdToUnpublish, completion) { + this.publish( OT.Raptor.Message.streams.destroy(OT.APIKEY, _sessionId, + streamIdToUnpublish), completion ); + }; + + this.jsepCandidate = function(streamId, candidate) { + this.publish( + OT.Raptor.Message.streams.candidate(OT.APIKEY, _sessionId, streamId, candidate) + ); + }; + + this.jsepCandidateP2p = function(streamId, subscriberId, candidate) { + this.publish( + OT.Raptor.Message.subscribers.candidate(OT.APIKEY, _sessionId, streamId, + subscriberId, candidate) + ); + }; + + this.jsepOffer = function(uri, offerSdp) { + this.publish( OT.Raptor.Message.offer(uri, offerSdp) ); + }; + + this.jsepAnswer = function(streamId, answerSdp) { + this.publish( OT.Raptor.Message.streams.answer(OT.APIKEY, _sessionId, streamId, answerSdp) ); + }; + + this.jsepAnswerP2p = function(streamId, subscriberId, answerSdp) { + this.publish( OT.Raptor.Message.subscribers.answer(OT.APIKEY, _sessionId, streamId, + subscriberId, answerSdp) ); + }; + + this.signal = function(options, completion, logEventFn) { + var signal = new OT.Signal(_sessionId, _rumor.id(), options || {}); + + if (!signal.valid) { + if (completion && OT.$.isFunction(completion)) { + completion( new SignalError(signal.error.code, signal.error.reason), signal.toHash() ); + } + + return; } - this.setName = OT.$.bind(function(name) { - if (!_name) this.setDisplayMode('auto'); - _name = name; - this.domElement.innerHTML = _name; - }); - - // Mixin common widget behaviour - OT.Chrome.Behaviour.Widget(this, { - mode: options.mode, - nodeName: 'h1', - htmlContent: _name, - htmlAttributes: { - className: 'OT_name OT_edge-bar-item' - } - }); - - }; - -})(window); -!(function() { - - OT.Chrome.MuteButton = function(options) { - var _onClickCb, - _muted = options.muted || false, - updateClasses, - attachEvents, - detachEvents, - onClick; - - updateClasses = OT.$.bind(function() { - if (_muted) { - OT.$.addClass(this.domElement, 'OT_active'); + this.publish( signal.toRaptorMessage(), function(err) { + var error; + if (err) { + error = new SignalError(err.code, err.message); } else { - OT.$.removeClass(this.domElement, 'OT_active '); - } - }, this); - - // Private Event Callbacks - attachEvents = function(elem) { - _onClickCb = OT.$.bind(onClick, this); - OT.$.on(elem, 'click', _onClickCb); - }; - - detachEvents = function(elem) { - _onClickCb = null; - OT.$.off(elem, 'click', _onClickCb); - }; - - onClick = function() { - _muted = !_muted; - - updateClasses(); - - if (_muted) { - this.parent.trigger('muted', this); - } else { - this.parent.trigger('unmuted', this); + var typeStr = signal.data? typeof(signal.data) : null; + logEventFn('signal', 'send', {type: typeStr}); } - return false; - }; - - OT.$.defineProperties(this, { - muted: { - get: function() { return _muted; }, - set: function(muted) { - _muted = muted; - updateClasses(); - } - } - }); - - // Mixin common widget behaviour - var classNames = _muted ? 'OT_edge-bar-item OT_mute OT_active' : 'OT_edge-bar-item OT_mute'; - OT.Chrome.Behaviour.Widget(this, { - mode: options.mode, - nodeName: 'button', - htmlContent: 'Mute', - htmlAttributes: { - className: classNames - }, - onCreate: OT.$.bind(attachEvents, this), - onDestroy: OT.$.bind(detachEvents, this) + if (completion && OT.$.isFunction(completion)) completion(error, signal.toHash()); }); }; - -})(window); -!(function() { - - // Archving Chrome Widget - // - // mode (String) - // Whether to display the archving widget. Possible values are: "on" (the status is displayed - // when archiving and briefly when archving ends) and "off" (the status is not displayed) - - // Whether to display the archving widget. Possible values are: "auto" (the name is displayed - // when the status is first displayed and when the user mouses over the display), - // "off" (the name is not displayed), and "on" (the name is displayed). - // - // displays a name - // can be shown/hidden - // can be destroyed - OT.Chrome.Archiving = function(options) { - var _archiving = options.archiving, - _archivingStarted = options.archivingStarted || 'Archiving on', - _archivingEnded = options.archivingEnded || 'Archiving off', - _initialState = true, - _lightBox, - _light, - _text, - _textNode, - renderStageDelayedAction, - renderText, - renderStage; - - renderText = function(text) { - _textNode.nodeValue = text; - _lightBox.setAttribute('title', text); - }; - - renderStage = OT.$.bind(function() { - if(renderStageDelayedAction) { - clearTimeout(renderStageDelayedAction); - renderStageDelayedAction = null; - } - - if(_archiving) { - OT.$.addClass(_light, 'OT_active'); - } else { - OT.$.removeClass(_light, 'OT_active'); - } - - OT.$.removeClass(this.domElement, 'OT_archiving-' + (!_archiving ? 'on' : 'off')); - OT.$.addClass(this.domElement, 'OT_archiving-' + (_archiving ? 'on' : 'off')); - if(options.show && _archiving) { - renderText(_archivingStarted); - OT.$.addClass(_text, 'OT_mode-on'); - OT.$.removeClass(_text, 'OT_mode-auto'); - this.setDisplayMode('on'); - renderStageDelayedAction = setTimeout(function() { - OT.$.addClass(_text, 'OT_mode-auto'); - OT.$.removeClass(_text, 'OT_mode-on'); - }, 5000); - } else if(options.show && !_initialState) { - OT.$.addClass(_text, 'OT_mode-on'); - OT.$.removeClass(_text, 'OT_mode-auto'); - this.setDisplayMode('on'); - renderText(_archivingEnded); - renderStageDelayedAction = setTimeout(OT.$.bind(function() { - this.setDisplayMode('off'); - }, this), 5000); - } else { - this.setDisplayMode('off'); - } - }, this); - - // Mixin common widget behaviour - OT.Chrome.Behaviour.Widget(this, { - mode: _archiving && options.show && 'on' || 'off', - nodeName: 'h1', - htmlAttributes: {className: 'OT_archiving OT_edge-bar-item OT_edge-bottom'}, - onCreate: OT.$.bind(function() { - _lightBox = OT.$.createElement('div', { - className: 'OT_archiving-light-box' - }, ''); - _light = OT.$.createElement('div', { - className: 'OT_archiving-light' - }, ''); - _lightBox.appendChild(_light); - _text = OT.$.createElement('div', { - className: 'OT_archiving-status OT_mode-on OT_edge-bar-item OT_edge-bottom' - }, ''); - _textNode = document.createTextNode(''); - _text.appendChild(_textNode); - this.domElement.appendChild(_lightBox); - this.domElement.appendChild(_text); - renderStage(); - }, this) - }); - - this.setShowArchiveStatus = OT.$.bind(function(show) { - options.show = show; - if(this.domElement) { - renderStage.call(this); - } - }, this); - - this.setArchiving = OT.$.bind(function(status) { - _archiving = status; - _initialState = false; - if(this.domElement) { - renderStage.call(this); - } - }, this); - + this.id = function() { + return _rumor && _rumor.id(); }; -})(window); -!(function() { + if(dispatcher == null) { + dispatcher = new OT.Raptor.Dispatcher(); + } + _dispatcher = dispatcher; +}; - OT.Chrome.AudioLevelMeter = function(options) { +// tb_require('../helpers/helpers.js') +// tb_require('../helpers/lib/capabilities.js') +// tb_require('./peer_connection/publisher_peer_connection.js') +// tb_require('./peer_connection/subscriber_peer_connection.js') - var widget = this, - _meterBarElement, - _voiceOnlyIconElement, - _meterValueElement, - _value, - _maxValue = options.maxValue || 1, - _minValue = options.minValue || 0; - - function onCreate() { - _meterBarElement = OT.$.createElement('div', { - className: 'OT_audio-level-meter__bar' - }, ''); - _meterValueElement = OT.$.createElement('div', { - className: 'OT_audio-level-meter__value' - }, ''); - _voiceOnlyIconElement = OT.$.createElement('div', { - className: 'OT_audio-level-meter__audio-only-img' - }, ''); - - widget.domElement.appendChild(_meterBarElement); - widget.domElement.appendChild(_voiceOnlyIconElement); - widget.domElement.appendChild(_meterValueElement); - } - - function updateView() { - var percentSize = _value * 100 / (_maxValue - _minValue); - _meterValueElement.style.width = _meterValueElement.style.height = 2 * percentSize + '%'; - _meterValueElement.style.top = _meterValueElement.style.right = -percentSize + '%'; - } - - // Mixin common widget behaviour - var widgetOptions = { - mode: options ? options.mode : 'auto', - nodeName: 'div', - htmlAttributes: { - className: 'OT_audio-level-meter' - }, - onCreate: onCreate - }; - - OT.Chrome.Behaviour.Widget(this, widgetOptions); - - // override - var _setDisplayMode = OT.$.bind(widget.setDisplayMode, widget); - widget.setDisplayMode = function(mode) { - _setDisplayMode(mode); - if (mode === 'off') { - if (options.onPassivate) options.onPassivate(); - } else { - if (options.onActivate) options.onActivate(); - } - }; - - widget.setValue = function(value) { - _value = value; - updateView(); - }; - }; - -})(window); -!(function() { - OT.Chrome.VideoDisabledIndicator = function(options) { - var _mode, - _videoDisabled = false, - _warning = false, - updateClasses; - - _mode = options.mode || 'auto'; - updateClasses = function(domElement) { - if (_videoDisabled) { - OT.$.addClass(domElement, 'OT_video-disabled'); - } else { - OT.$.removeClass(domElement, 'OT_video-disabled'); - } - if(_warning) { - OT.$.addClass(domElement, 'OT_video-disabled-warning'); - } else { - OT.$.removeClass(domElement, 'OT_video-disabled-warning'); - } - if ((_videoDisabled || _warning) && (_mode === 'auto' || _mode === 'on')) { - OT.$.addClass(domElement, 'OT_active'); - } else { - OT.$.removeClass(domElement, 'OT_active'); - } - }; - - this.disableVideo = function(value) { - _videoDisabled = value; - if(value === true) { - _warning = false; - } - updateClasses(this.domElement); - }; - - this.setWarning = function(value) { - _warning = value; - updateClasses(this.domElement); - }; - - // Mixin common widget behaviour - OT.Chrome.Behaviour.Widget(this, { - mode: _mode, - nodeName: 'div', - htmlAttributes: { - className: 'OT_video-disabled-indicator' - } - }); - - this.setDisplayMode = function(mode) { - _mode = mode; - updateClasses(this.domElement); - }; - }; -})(window); -(function() { -/* Stylable Notes - * RTC doesn't need to wait until anything is loaded - * Some bits are controlled by multiple flags, i.e. buttonDisplayMode and nameDisplayMode. - * When there are multiple flags how is the final setting chosen? - * When some style bits are set updates will need to be pushed through to the Chrome - */ - - // Mixes the StylableComponent behaviour into the +self+ object. It will - // also set the default styles to +initialStyles+. - // - // @note This Mixin is dependent on OT.Eventing. - // - // - // @example - // - // function SomeObject { - // OT.StylableComponent(this, { - // name: 'SomeObject', - // foo: 'bar' - // }); - // } - // - // var obj = new SomeObject(); - // obj.getStyle('foo'); // => 'bar' - // obj.setStyle('foo', 'baz') - // obj.getStyle('foo'); // => 'baz' - // obj.getStyle(); // => {name: 'SomeObject', foo: 'baz'} - // - OT.StylableComponent = function(self, initalStyles) { - if (!self.trigger) { - throw new Error('OT.StylableComponent is dependent on the mixin OT.$.eventing. ' + - 'Ensure that this is included in the object before StylableComponent.'); - } - - // Broadcast style changes as the styleValueChanged event - var onStyleChange = function(key, value, oldValue) { - if (oldValue) { - self.trigger('styleValueChanged', key, value, oldValue); - } else { - self.trigger('styleValueChanged', key, value); - } - }; - - var _style = new Style(initalStyles, onStyleChange); - - /** - * Returns an object that has the properties that define the current user interface controls of - * the Publisher. You can modify the properties of this object and pass the object to the - * setStyle() method of thePublisher object. (See the documentation for - * setStyle() to see the styles that define this object.) - * @return {Object} The object that defines the styles of the Publisher. - * @see setStyle() - * @method #getStyle - * @memberOf Publisher - */ - - /** - * Returns an object that has the properties that define the current user interface controls of - * the Subscriber. You can modify the properties of this object and pass the object to the - * setStyle() method of the Subscriber object. (See the documentation for - * setStyle() to see the styles that define this object.) - * @return {Object} The object that defines the styles of the Subscriber. - * @see setStyle() - * @method #getStyle - * @memberOf Subscriber - */ - // If +key+ is falsly then all styles will be returned. - self.getStyle = function(key) { - return _style.get(key); - }; - - /** - * Sets properties that define the appearance of some user interface controls of the Publisher. - * - *

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

- * - *

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

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

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

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

- * - *

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

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

If you pass two parameters, style and value, they are - * key-value pair that define one property of the display style. For example, the following - * code passes two parameter values to the method:

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

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

- * - *

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

- * - * @param {Object} style Either an object containing properties that define the style, or a - * String defining this single style property to set. - * @param {String} value The value to set for the style passed in. Pass a value - * for this parameter only if the value of the style parameter is a String.

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

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

- * - *

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

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

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

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

- * - *

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

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

If you pass two parameters, style and value, they are key-value - * pair that define one property of the display style. For example, the following code passes - * two parameter values to the method:

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

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

- * - *

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

- * - * @param {Object} style Either an object containing properties that define the style, or a - * String defining this single style property to set. - * @param {String} value The value to set for the style passed in. Pass a value - * for this parameter only if the value of the style parameter is a String.

- * - * @returns {Subscriber} The Subscriber object. - * - * @see getStyle() - * @see setStyle() - * - * @see Session.subscribe() - * @method #setStyle - * @memberOf Subscriber - */ - self.setStyle = function(keyOrStyleHash, value, silent) { - if (typeof(keyOrStyleHash) !== 'string') { - _style.setAll(keyOrStyleHash, silent); - } else { - _style.set(keyOrStyleHash, value); - } - return this; - }; - }; - - var Style = function(initalStyles, onStyleChange) { - var _style = {}, - _COMPONENT_STYLES, - _validStyleValues, - isValidStyle, - castValue; - - _COMPONENT_STYLES = [ - 'showMicButton', - 'showSpeakerButton', - 'nameDisplayMode', - 'buttonDisplayMode', - 'backgroundImageURI' - ]; - - _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; inull if no value could be acquired + */ + this.sample = function(done) { + _peerConnection.getStats(function(error, stats) { + if (!error) { + for (var idx = 0; idx < stats.length; idx++) { + var stat = stats[idx]; + var audioOutputLevel = parseFloat(stat[_statsProperty]); + if (!isNaN(audioOutputLevel)) { + // the mex value delivered by getStats for audio levels is 2^15 + done(audioOutputLevel / 32768); + return; } } } + + done(null); }); + }; +}; - // Set the initial value - if (muted !== undefined) { - this.muted(muted === true); - } else if (webRTCStream.getAudioTracks().length) { - this.muted(!webRTCStream.getAudioTracks()[0].enabled); +/* + * 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) { - } 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.

+ * + * @param {Number} value The audio volume, between 0 and 100. + * + * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the + * following: + * + *
mySubscriber.setAudioVolume(50).setStyle(newStyle);
+ * + * @see getAudioVolume() + * @see Session.subscribe() + * @method #setAudioVolume + * @memberOf Subscriber + */ + this.setAudioVolume = function(value) { + value = parseInt(value, 10); + if (isNaN(value)) { + OT.error('OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100'); + return this; + } + _audioVolume = Math.max(0, Math.min(100, value)); + if (_audioVolume !== value) { + OT.warn('OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100'); + } + if(_properties.muted && _audioVolume > 0) { + _properties.premuteVolume = value; + muteAudio.call(this, false); + } + if (_streamContainer) { + _streamContainer.setAudioVolume(_audioVolume); + } + return this; + }; -// 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.

+ * + * @return {Number} The audio volume, between 0 and 100, of the Subscriber. + * @see setAudioVolume() + * @method #getAudioVolume + * @memberOf Subscriber + */ + this.getAudioVolume = function() { + if(_properties.muted) { + return 0; + } + if (_streamContainer) return _streamContainer.getAudioVolume(); + else return _audioVolume; + }; - var validStates = [ - 'NotPublishing', 'GetUserMedia', 'BindingMedia', 'MediaBound', - 'PublishingToSession', 'Publishing', 'Failed', - 'Destroyed' - ], + /** + * Toggles audio on and off. Starts subscribing to audio (if it is available and currently + * not being subscribed to) when the value is true; stops + * subscribing to audio (if it is currently being subscribed to) when the value + * is false. + *

+ * Note: This method only affects the local playback of audio. It has no impact on the + * audio for other connections subscribing to the same stream. If the Publsher is not + * publishing audio, enabling the Subscriber audio will have no practical effect. + *

+ * + * @param {Boolean} value Whether to start subscribing to audio (true) or not + * (false). + * + * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the + * following: + * + *
mySubscriber.subscribeToAudio(true).subscribeToVideo(false);
+ * + * @see subscribeToVideo() + * @see Session.subscribe() + * @see StreamPropertyChangedEvent + * + * @method #subscribeToAudio + * @memberOf Subscriber + */ + this.subscribeToAudio = function(pValue) { + var value = OT.$.castToBoolean(pValue, true); - 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: + *

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

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

mySubscriber.restrictFrameRate(false).subscribeToAudio(true);
+ * + * @method #restrictFrameRate + * @memberOf Subscriber + */ + this.restrictFrameRate = function(val) { + OT.debug('OT.Subscriber.restrictFrameRate(' + val + ')'); + + logAnalyticsEvent('restrictFrameRate', val.toString(), {streamId: _stream.id}); + + if (_session.sessionInfo.p2pEnabled) { + OT.warn('OT.Subscriber.restrictFrameRate: Cannot restrictFrameRate on a P2P session'); + } + + if (typeof val !== 'boolean') { + OT.error('OT.Subscriber.restrictFrameRate: expected a boolean value got a ' + typeof val); + } else { + _frameRateRestricted = val; + _stream.setRestrictFrameRate(val); + } + return this; + }; + + this.on('styleValueChanged', updateChromeForStyleChange, this); + + this._ = { + archivingStatus: function(status) { + if(_chrome) { + _chrome.archive.setArchiving(status); + } + } + }; + + _state = new OT.SubscribingState(stateChangeFailed); + + 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). +*
  • +* +*
+* +* @see VideoEnabledChangedEvent +* @see event:videoDisableWarning +* @see event:videoEnabled +* @name videoDisabled +* @event +* @memberof Subscriber +*/ + +/** +* Dispatched when the OpenTok Media Router determines that the stream quality has degraded +* and the video will be disabled if the quality degrades more. If the quality degrades further, +* the Subscriber disables the video and dispatches a videoDisabled event. +*

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

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

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

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

+* +*

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

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

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

+* To prevent video from resuming, in the videoEnabled event listener, +* call subscribeToVideo(false) on the Subscriber object. +* +* @see VideoEnabledChangedEvent +* @see event:videoDisabled +* @name videoEnabled +* @event +* @memberof Subscriber +*/ + +/** +* Dispatched when the Subscriber element is removed from the HTML DOM. When this event is +* dispatched, you may choose to adjust or remove HTML DOM elements related to the subscriber. +* @see Event +* @name destroyed +* @event +* @memberof Subscriber +*/ + +/** +* 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. +*

+* +*
+* Example +*
+*

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

+*
+*  var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
+*  var sessionID = ""; // Replace with your own session ID.
+*                      // See https://dashboard.tokbox.com/projects
+*  var token = ""; // Replace with a generated token that has been assigned the moderator role.
+*                  // See https://dashboard.tokbox.com/projects
+*
+*  var session = OT.initSession(apiKey, sessionID);
+*  session.on("sessionConnected", function(sessionConnectEvent) {
+*      //
+*  });
+*  session.connect(token);
+*  
+*

+*

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

+* +*
+* Events dispatched: +*
+* +*

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

+*

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

+*

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

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

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

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

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

+*

+* Session objects on remote clients dispatch streamDestroyed events for any +* stream you were publishing. The Session object dispatches a sessionDisconnected +* event locally. The Session objects on remote clients dispatch connectionDestroyed +* events, letting other connections know you have left the session. The +* {@link SessionDisconnectEvent} and {@link StreamEvent} objects that define the +* sessionDisconnect and connectionDestroyed events each have a +* reason property. The reason property lets the developer determine +* whether the connection is being terminated voluntarily and whether any streams are being +* destroyed as a byproduct of the underlying connection's voluntary destruction. +*

+*

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

+* +*

+* Note: If you intend to reuse a Publisher object created using +* OT.initPublisher() to publish to different sessions sequentially, call either +* Session.disconnect() or Session.unpublish(). Do not call both. +* Then call the preventDefault() method of the streamDestroyed or +* sessionDisconnected event object to prevent the Publisher object from being +* removed from the page. Be sure to call preventDefault() only if the +* connection.connectionId property of the Stream object in the event matches the +* connection.connectionId property of your Session object (to ensure that you are +* preventing the default behavior for your published streams, not for other streams that you +* subscribe to). +*

+* +*
+* Events dispatched: +*
+*

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

+*

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

+* +*

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

+* +* @method #disconnect +* @memberOf Session +*/ + var disconnect = OT.$.bind(function disconnect(drainSocketBuffer) { + if (_socket && _socket.isNot('disconnected')) { + if (_socket.isNot('disconnecting')) { + setState('disconnecting'); + _socket.disconnect(drainSocketBuffer); + } + } + else { + reset(); + } + }, this); + + this.disconnect = function(drainSocketBuffer) { + disconnect(drainSocketBuffer !== void 0 ? drainSocketBuffer : true); + }; + + this.destroy = function(reason) { + this.streams.destroy(); + this.connections.destroy(); + this.archives.destroy(); + disconnect(reason !== 'unloaded'); + }; + +/** +* The publish() method starts publishing an audio-video stream to the session. +* The audio-video stream is captured from a local microphone and webcam. Upon successful +* publishing, the Session objects on all connected clients dispatch the +* streamCreated event. +*

+* +* +*

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

+* +*

This method takes an alternate form: publish([targetElement:String, +* properties:Object]):Publisher — In this form, you do not pass a Publisher +* object into the function. Instead, you pass in a targetElement (the ID of the +* DOM element that the Publisher will replace) and a properties object that +* defines options for the Publisher (see OT.initPublisher().) +* The method returns a new Publisher object, which starts sending an audio-video stream to the +* session. The remainder of this documentation describes the form that takes a single Publisher +* object as a parameter. +* +*

+* A local display of the published stream is created on the web page by replacing +* the specified element in the DOM with a streaming video display. The video stream +* is automatically mirrored horizontally so that users see themselves and movement +* in their stream in a natural way. If the width and height of the display do not match +* the 4:3 aspect ratio of the video signal, the video stream is cropped to fit the +* display. +*

+* +*

+* If calling this method creates a new Publisher object and the OpenTok library does not +* have access to the camera or microphone, the web page alerts the user to grant access +* to the camera and microphone. +*

+* +*

+* The OT object dispatches an exception event if the user's role does not +* include permissions required to publish. For example, if the user's role is set to subscriber, +* then they cannot publish. You define a user's role when you create the user token using the +* generate_token() method of the +* OpenTok server-side +* libraries or the Dashboard page. +* You pass the token string as a parameter of the connect() method of the Session +* object. See ExceptionEvent and +* OT.on(). +*

+*

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

+* +*
Events dispatched:
+*

+* exception (ExceptionEvent) — Dispatched +* by the OT object. This can occur when user's role does not allow publishing (the +* code property of event object is set to 1500); it can also occur if the c +* onnection fails to connect (the code property of event object is set to 1013). +* WebRTC is a peer-to-peer protocol, and it is possible that connections will fail to connect. +* The most common cause for failure is a firewall that the protocol cannot traverse. +*

+*

+* streamCreated (StreamEvent) — +* The stream has been published. The Session object dispatches this on all clients +* subscribed to the stream, as well as on the publisher's client. +*

+* +*
Example
+* +*

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

+*
+* var sessionId = ""; // Replace with your own session ID.
+*                     // See https://dashboard.tokbox.com/projects
+* var token = ""; // Replace with a generated token that has been assigned the moderator role.
+*                 // See https://dashboard.tokbox.com/projects
+* var session = OT.initSession(apiKey, sessionID);
+* session.on("sessionConnected", function (event) {
+*     var publisherOptions = {width: 400, height:300, name:"Bob's stream"};
+*     // This assumes that there is a DOM element with the ID 'publisher':
+*     publisher = OT.initPublisher('publisher', publisherOptions);
+*     session.publish(publisher);
+* });
+* session.connect(token);
+* 
+* +* @param {Publisher} publisher A Publisher object, which you initialize by calling the +* OT.initPublisher() method. +* +* @param {Function} completionHandler (Optional) A function to be called when the call to the +* publish() method succeeds or fails. This function takes one parameter — +* error. On success, the completionHandler function is not passed any +* arguments. On error, the function is passed an error object parameter +* (see the Error object). The +* error object has two properties: code (an integer) and +* message (a string), which identify the cause of the failure. Calling +* publish() fails if the role assigned to your token is not "publisher" or +* "moderator"; in this case error.code is set to 1500. Calling +* publish() also fails the client fails to connect; in this case +* error.code is set to 1013. The following code adds a +* completionHandler when calling the publish() method: +*
+* session.publish(publisher, null, function (error) {
+*   if (error) {
+*     console.log(error.message);
+*   } else {
+*     console.log("Publishing a stream.");
+*   }
+* });
+* 
+* +* @returns The Publisher object for this stream. +* +* @method #publish +* @memberOf Session +*/ + this.publish = function(publisher, properties, completionHandler) { + if(typeof publisher === 'function') { + completionHandler = publisher; + publisher = undefined; + } + if(typeof properties === 'function') { + completionHandler = properties; + properties = undefined; + } + if (this.isNot('connected')) { + 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). +*

+* +*
Events dispatched:
+* +*

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

+* +*
Example
+* +* The following example publishes a stream to a session and adds a Disconnect link to the +* web page. Clicking this link causes the stream to stop being published. +* +*
+* <script>
+*     var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
+*     var sessionID = ""; // Replace with your own session ID.
+*                      // See https://dashboard.tokbox.com/projects
+*     var token = "Replace with the TokBox token string provided to you."
+*     var session = OT.initSession(apiKey, sessionID);
+*     session.on("sessionConnected", function sessionConnectHandler(event) {
+*         // This assumes that there is a DOM element with the ID 'publisher':
+*         publisher = OT.initPublisher('publisher');
+*         session.publish(publisher);
+*     });
+*     session.connect(token);
+*     var publisher;
+*
+*     function unpublish() {
+*         session.unpublish(publisher);
+*     }
+* </script>
+*
+* <body>
+*
+*     <div id="publisherContainer/>
+*     <br/>
+*
+*     <a href="javascript:unpublish()">Stop Publishing</a>
+*
+* </body>
+*
+* 
+* +* @see publish() +* +* @see streamDestroyed event +* +* @param {Publisher} publisher The Publisher object to stop streaming. +* +* @method #unpublish +* @memberOf Session +*/ + this.unpublish = function(publisher) { + if (!publisher) { + OT.error('OT.Session.unpublish: publisher parameter missing.'); + return; + } + + // Unpublish the localMedia publisher + publisher._.unpublishFromSession(this, 'unpublished'); + }; + + +/** +* Subscribes to a stream that is available to the session. You can get an array of +* available streams from the streams property of the sessionConnected +* and streamCreated events (see +* SessionConnectEvent and +* StreamEvent). +*

+*

+* The subscribed stream is displayed on the local web page by replacing the specified element +* in the DOM with a streaming video display. If the width and height of the display do not +* match the 4:3 aspect ratio of the video signal, the video stream is cropped to fit +* the display. If the stream lacks a video component, a blank screen with an audio indicator +* is displayed in place of the video stream. +*

+* +*

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

+* +*
Example
+* +* The following code subscribes to other clients' streams: +* +*
+* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
+* var sessionID = ""; // Replace with your own session ID.
+*                     // See https://dashboard.tokbox.com/projects
+*
+* var session = OT.initSession(apiKey, sessionID);
+* session.on("streamCreated", function(event) {
+*   subscriber = session.subscribe(event.stream, targetElement);
+* });
+* session.connect(token);
+* 
+* +* @param {Stream} stream The Stream object representing the stream to which we are trying to +* subscribe. +* +* @param {Object} targetElement (Optional) The DOM element or the id attribute of +* the existing DOM element used to determine the location of the Subscriber video in the HTML +* DOM. See the insertMode property of the properties parameter. If +* you do not specify a targetElement, the application appends a new DOM element +* to the HTML body. +* +* @param {Object} properties This is an object that contains the following properties: +*
    +*
  • audioVolume (Number) — The desired audio volume, between 0 and +* 100, when the Subscriber is first opened (default: 50). After you subscribe to the +* stream, you can adjust the volume by calling the +* setAudioVolume() method of the +* Subscriber object. This volume setting affects local playback only; it does not affect +* the stream's volume on other clients.
  • +* +*
  • +* 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).
  • +* +*
+* +* @param {Function} completionHandler (Optional) A function to be called when the call to the +* subscribe() method succeeds or fails. This function takes one parameter — +* error. On success, the completionHandler function is not passed any +* arguments. On error, the function is passed an error object, defined by the +* Error class, has two properties: code (an integer) and +* message (a string), which identify the cause of the failure. The following +* code adds a completionHandler when calling the subscribe() method: +*
+* session.subscribe(stream, "subscriber", null, function (error) {
+*   if (error) {
+*     console.log(error.message);
+*   } else {
+*     console.log("Subscribed to stream: " + stream.id);
+*   }
+* });
+* 
+* +* @signature subscribe(stream, targetElement, properties, completionHandler) +* @returns {Subscriber} The Subscriber object for this stream. Stream control functions +* are exposed through the Subscriber object. +* @method #subscribe +* @memberOf Session +*/ + this.subscribe = function(stream, targetElement, properties, completionHandler) { + + if (!this.connection || !this.connection.connectionId) { + dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, + 'Session.subscribe :: Connection required to subscribe', + completionHandler); + return; + } + + if (!stream) { + dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, + 'Session.subscribe :: stream cannot be null', + completionHandler); + return; + } + + if (!stream.hasOwnProperty('streamId')) { + dispatchError(OT.ExceptionCodes.UNABLE_TO_SUBSCRIBE, + 'Session.subscribe :: invalid stream object', + completionHandler); + return; + } + + if(typeof targetElement === 'function') { + completionHandler = targetElement; + targetElement = undefined; + 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. +* +*
Example
+*

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

+*
+* var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
+* var sessionID = ""; // Replace with your own session ID.
+*                     // See https://dashboard.tokbox.com/projects
+* var streams = [];
+*
+* var session = OT.initSession(apiKey, sessionID);
+* session.on("streamCreated", function(event) {
+*     var stream = event.stream;
+*     displayStream(stream);
+* });
+* session.connect(token);
+*
+* function displayStream(stream) {
+*     var div = document.createElement('div');
+*     div.setAttribute('id', 'stream' + stream.streamId);
+*
+*     var subscriber = session.subscribe(stream, div);
+*     subscribers.push(subscriber);
+*
+*     var aLink = document.createElement('a');
+*     aLink.setAttribute('href', 'javascript: unsubscribe("' + subscriber.id + '")');
+*     aLink.innerHTML = "Unsubscribe";
+*
+*     var streamsContainer = document.getElementById('streamsContainer');
+*     streamsContainer.appendChild(div);
+*     streamsContainer.appendChild(aLink);
+*
+*     streams = event.streams;
+* }
+*
+* function unsubscribe(subscriberId) {
+*     console.log("unsubscribe called");
+*     for (var i = 0; i < subscribers.length; i++) {
+*         var subscriber = subscribers[i];
+*         if (subscriber.id == subscriberId) {
+*             session.unsubscribe(subscriber);
+*         }
+*     }
+* }
+* 
+* +* @param {Subscriber} subscriber The Subscriber object to unsubcribe. +* +* @see subscribe() +* +* @method #unsubscribe +* @memberOf Session +*/ + this.unsubscribe = function(subscriber) { + if (!subscriber) { + var errorMsg = 'OT.Session.unsubscribe: subscriber cannot be null'; + OT.error(errorMsg); + throw new Error(errorMsg); + } + + if (!subscriber.stream) { + OT.warn('OT.Session.unsubscribe:: tried to unsubscribe a subscriber that had no stream'); + return false; + } + + OT.debug('OT.Session.unsubscribe: subscriber ' + subscriber.id); + + subscriber.destroy(); + + return true; + }; + +/** +* Returns an array of local Subscriber objects for a given stream. +* +* @param {Stream} stream The stream for which you want to find subscribers. +* +* @returns {Array} An array of {@link Subscriber} objects for the specified stream. +* +* @see unsubscribe() +* @see Subscriber +* @see StreamEvent +* @method #getSubscribersForStream +* @memberOf Session +*/ + this.getSubscribersForStream = function(stream) { + return OT.subscribers.where({streamId: stream.id}); + }; + +/** +* Returns the local Publisher object for a given stream. +* +* @param {Stream} stream The stream for which you want to find the Publisher. +* +* @returns {Publisher} A Publisher object for the specified stream. Returns +* null if there is no local Publisher object +* for the specified stream. +* +* @see forceUnpublish() +* @see Subscriber +* @see StreamEvent +* +* @method #getPublisherForStream +* @memberOf Session +*/ + this.getPublisherForStream = function(stream) { + var streamId, + errorMsg; + + if (typeof stream === 'string') { + streamId = stream; + } else if (typeof stream === 'object' && stream && stream.hasOwnProperty('id')) { + streamId = stream.id; + } else { + errorMsg = 'Session.getPublisherForStream :: Invalid stream type'; + OT.error(errorMsg); + throw new Error(errorMsg); + } + + return OT.publishers.where({streamId: streamId})[0]; + }; + + // Private Session API: for internal OT use only + this._ = { + jsepCandidateP2p: function(streamId, subscriberId, candidate) { + return _socket.jsepCandidateP2p(streamId, subscriberId, candidate); }, - 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. +*

+* +*
Events dispatched:
+* +*

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

+*

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

+*

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

+*

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

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

+* +*
Events dispatched:
+* +*

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

+*

+* streamDestroyed (StreamEvent) — +* The stream has been unpublished. The Session object dispatches this on all clients +* subscribed to the stream, as well as on the publisher's client. +*

+* +* @param {Stream} stream The stream to be unpublished. +* +* @param {Function} completionHandler (Optional) A function to be called when the call to the +* forceUnpublish() method succeeds or fails. This function takes one parameter +* — error. On success, the completionHandler function is +* not passed any arguments. On error, the function is passed an error object +* parameter. The error object, defined by the Error +* class, has two properties: code (an integer) +* and message (a string), which identify the cause of the failure. Calling +* forceUnpublish() fails if the role assigned to your token is not "moderator"; +* in this case error.code is set to 1530. The following code adds a +* completionHandler when calling the forceUnpublish() method: +*
+* session.forceUnpublish(stream, function (error) {
+*   if (error) {
+*       console.log(error);
+*     } else {
+*       console.log("Connection forced to disconnect: " + connection.id);
+*     }
+*   });
+* 
+* +* @method #forceUnpublish +* @memberOf Session +*/ + this.forceUnpublish = function(streamOrStreamId, completionHandler) { + if (this.isNot('connected')) { + var notConnectedErrorMsg = 'Cannot call forceUnpublish(). You are not ' + + 'connected to the session.'; + dispatchError(OT.ExceptionCodes.NOT_CONNECTED, notConnectedErrorMsg, completionHandler); + return; + } - // 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: + *

+ *

    + *
  • A stream has started or stopped publishing audio or video (see + * Publisher.publishAudio() and + * Publisher.publishVideo()). 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. + *
  • + *
  • The videoDimensions property of the Stream object has + * changed (see Stream.videoDimensions). + *
  • + *
  • The videoType property of the Stream object has changed. + * This can happen in a stream published by a mobile device. (See + * Stream.videoType.) + *
  • + *
+ * + * @name streamPropertyChanged + * @event + * @memberof Session + * @see StreamPropertyChangedEvent + * @see Publisher.publishAudio() + * @see Publisher.publishVideo() + * @see Stream.hasAudio + * @see Stream.hasVideo + * @see Stream.videoDimensions + * @see Subscriber videoDisabled event + * @see Subscriber videoEnabled event + */ + +/** + * A signal was received from the session. The SignalEvent + * class defines this event object. It includes the following properties: + *
    + *
  • data — (String) The data string sent with the signal (if there + * is one).
  • + *
  • from — (Connection) The Connection + * corresponding to the client that sent with the signal.
  • + *
  • type — (String) The type assigned to the signal (if there is + * one).
  • + *
+ *

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

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

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

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

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

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

+ * You can register to receive all signals sent in the session, by adding an event + * handler for the signal event. + * + * @name signal:type + * @event + * @memberof Session + * @see Session.signal() + * @see SignalEvent + * @see signal event + */ +}; + +// 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; i - * The Publisher object dispatches a destroyed event when the DOM - * element is removed. - *

- * @method #destroy - * @memberOf Publisher - * @return {Publisher} The Publisher. - */ - this.destroy = function(/* unused */ reason, quiet) { - if (_state.isDestroyed()) return; - _state.set('Destroyed'); - - reset(); - - if (quiet !== true) { - this.dispatchEvent( - new OT.DestroyedEvent( - OT.Event.names.PUBLISHER_DESTROYED, - this, - reason - ), - OT.$.bind(this.off,this) - ); - } - - return this; - }; - - /** - * @methodOf Publisher - * @private - */ - this.disconnect = function() { - // Close the connection to each of our subscribers - for (var fromConnectionId in _peerConnections) { - this.cleanupSubscriber(fromConnectionId); - } - }; - - this.cleanupSubscriber = function(fromConnectionId) { - var pc = _peerConnections[fromConnectionId]; - - if (pc) { - pc.destroy(); - delete _peerConnections[fromConnectionId]; - - logAnalyticsEvent('disconnect', 'PeerConnection', - 'subscriberConnection', fromConnectionId); - } - }; - - - this.processMessage = function(type, fromConnection, message) { - OT.debug('OT.Publisher.processMessage: Received ' + type + ' from ' + fromConnection.id); - OT.debug(message); - - switch (type) { - case 'unsubscribe': - this.cleanupSubscriber(message.content.connection.id); - break; - - default: - var peerConnection = createPeerConnectionForRemote.call(this, fromConnection); - peerConnection.processMessage(type, message); - } - }; - - /** - * Returns the base-64-encoded string of PNG data representing the Publisher video. - * - *

You can use the string as the value for a data URL scheme passed to the src parameter of - * an image file, as in the following:

- * - *
-    *  var imgData = publisher.getImgData();
-    *
-    *  var img = document.createElement("img");
-    *  img.setAttribute("src", "data:image/png;base64," + imgData);
-    *  var imgWin = window.open("about:blank", "Screenshot");
-    *  imgWin.document.write("<body></body>");
-    *  imgWin.document.body.appendChild(img);
-    * 
- * - * @method #getImgData - * @memberOf Publisher - * @return {String} The base-64 encoded string. Returns an empty string if there is no video. - */ - - this.getImgData = function() { - if (!_loaded) { - OT.error('OT.Publisher.getImgData: Cannot getImgData before the Publisher is publishing.'); - - return null; - } - - return _targetElement.imgData(); - }; - - - // API Compatibility layer for Flash Publisher, this could do with some tidyup. - this._ = { - publishToSession: OT.$.bind(function(session) { - // Add session property to Publisher - this.session = _session = session; - - var createStream = function() { - - var streamWidth, - streamHeight; - - // Bail if this.session is gone, it means we were unpublished - // before createStream could finish. - if (!_session) return; - - _state.set('PublishingToSession'); - - var onStreamRegistered = OT.$.bind(function(err, streamId, message) { - if (err) { - // @todo we should respect err.code here and translate it to the local - // client equivalent. - logAnalyticsEvent('publish', 'Failure', 'reason', - 'Publish:' + OT.ExceptionCodes.UNABLE_TO_PUBLISH + ':' + err.message); - if (_state.isAttemptingToPublish()) { - this.trigger('publishComplete', new OT.Error(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - err.message)); - } - return; - } - - this.streamId = _streamId = streamId; - _iceServers = OT.Raptor.parseIceServers(message); - }, this); - - // We set the streamWidth and streamHeight to be the minimum of the requested - // resolution and the actual resolution. - if (_publishProperties.videoDimensions) { - streamWidth = Math.min(_publishProperties.videoDimensions.width, - _targetElement.videoWidth() || 640); - streamHeight = Math.min(_publishProperties.videoDimensions.height, - _targetElement.videoHeight() || 480); - } else { - streamWidth = _targetElement.videoWidth() || 640; - streamHeight = _targetElement.videoHeight() || 480; - } - - session._.streamCreate( - _publishProperties && _publishProperties.name ? _publishProperties.name : '', - OT.VideoOrientation.ROTATED_NORMAL, - streamWidth, - streamHeight, - _publishProperties.publishAudio, - _publishProperties.publishVideo, - _publishProperties.frameRate, - onStreamRegistered - ); - }; - - if (_loaded) createStream.call(this); - else this.on('initSuccess', createStream, this); - - logAnalyticsEvent('publish', 'Attempt', 'streamType', 'WebRTC'); - - return this; - }, this), - - unpublishFromSession: OT.$.bind(function(session, reason) { - if (!_session || session.id !== _session.id) { - OT.warn('The publisher ' + _guid + ' is trying to unpublish from a session ' + - session.id + ' it is not attached to (it is attached to ' + - (_session && _session.id || 'no session') + ')'); - return this; - } - - if (session.isConnected() && this.stream) { - session._.streamDestroy(this.stream.id); - } - - // Disconnect immediately, rather than wait for the WebSocket to - // reply to our destroyStream message. - this.disconnect(); - this.session = _session = null; - - // We're back to being a stand-alone publisher again. - if (!_state.isDestroyed()) _state.set('MediaBound'); - - logAnalyticsEvent('unpublish', 'Success', 'sessionId', session.id); - - this._.streamDestroyed(reason); - - return this; - }, this), - - streamDestroyed: OT.$.bind(function(reason) { - if(OT.$.arrayIndexOf(['reset'], reason) < 0) { - var event = new OT.StreamEvent('streamDestroyed', _stream, reason, true); - var defaultAction = OT.$.bind(function() { - if(!event.isDefaultPrevented()) { - this.destroy(); - } - }, this); - this.dispatchEvent(event, defaultAction); - } - }, this), - - - archivingStatus: OT.$.bind(function(status) { - if(_chrome) { - _chrome.archive.setArchiving(status); - } - - return status; - }, this), - - webRtcStream: function() { - return _webRTCStream; - } - }; - - this.detectDevices = function() { - OT.warn('Fixme: Haven\'t implemented detectDevices'); - }; - - this.detectMicActivity = function() { - OT.warn('Fixme: Haven\'t implemented detectMicActivity'); - }; - - this.getEchoCancellationMode = function() { - OT.warn('Fixme: Haven\'t implemented getEchoCancellationMode'); - return 'fullDuplex'; - }; - - this.setMicrophoneGain = function() { - OT.warn('Fixme: Haven\'t implemented setMicrophoneGain'); - }; - - this.getMicrophoneGain = function() { - OT.warn('Fixme: Haven\'t implemented getMicrophoneGain'); - return 0.5; - }; - - this.setCamera = function() { - OT.warn('Fixme: Haven\'t implemented setCamera'); - }; - - this.setMicrophone = function() { - OT.warn('Fixme: Haven\'t implemented setMicrophone'); - }; - - - // Platform methods: - - this.guid = function() { - return _guid; - }; - - this.videoElement = function() { - return _targetElement.domElement(); - }; - - this.setStream = assignStream; - - this.isWebRTC = true; - - this.isLoading = function() { - return _container && _container.loading(); - }; - - this.videoWidth = function() { - return _targetElement.videoWidth(); - }; - - this.videoHeight = function() { - return _targetElement.videoHeight(); - }; - - // Make read-only: element, guid, _.webRtcStream - - this.on('styleValueChanged', updateChromeForStyleChange, this); - _state = new OT.PublishingState(stateChangeFailed); - - this.accessAllowed = false; - - /** - * Dispatched when the user has clicked the Allow button, granting the - * app access to the camera and microphone. The Publisher object has an - * accessAllowed property which indicates whether the user - * has granted access to the camera and microphone. - * @see Event - * @name accessAllowed - * @event - * @memberof Publisher - */ - - /** - * Dispatched when the user has clicked the Deny button, preventing the - * app from having access to the camera and microphone. - *

- * The default behavior of this event is to display a user interface element - * in the Publisher object, indicating that that user has denied access to - * the camera and microphone. Call the preventDefault() method - * method of the event object in the event listener to prevent this message - * from being displayed. - * @see Event - * @name accessDenied - * @event - * @memberof Publisher - */ - - /** - * Dispatched when the Allow/Deny dialog box is opened. (This is the dialog box in which - * the user can grant the app access to the camera and microphone.) - *

- * The default behavior of this event is to display a message in the browser that instructs - * the user how to enable the camera and microphone. Call the preventDefault() - * method of the event object in the event listener to prevent this message from being displayed. - * @see Event - * @name accessDialogOpened - * @event - * @memberof Publisher - */ - - /** - * Dispatched when the Allow/Deny box is closed. (This is the dialog box in which the - * user can grant the app access to the camera and microphone.) - * @see Event - * @name accessDialogClosed - * @event - * @memberof Publisher - */ - - /** - * Dispatched periodically to indicate the publisher's audio level. The event is dispatched - * up to 60 times per second, depending on the browser. The audioLevel property - * of the event is audio level, from 0 to 1.0. See {@link AudioLevelUpdatedEvent} for more - * information. - *

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

- *

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

This example shows the algorithm used by the default audio level indicator displayed - * in an audio-only Publisher. - * - * @name audioLevelUpdated - * @event - * @memberof Publisher - * @see AudioLevelUpdatedEvent - */ - - /** - * The publisher has started streaming to the session. - * @name streamCreated - * @event - * @memberof Publisher - * @see StreamEvent - * @see Session.publish() - */ - - /** - * The publisher has stopped streaming to the session. The default behavior is that - * the Publisher object is removed from the HTML DOM). The Publisher object dispatches a - * destroyed event when the element is removed from the HTML DOM. If you call the - * preventDefault() method of the event object in the event listener, the default - * behavior is prevented, and you can, optionally, retain the Publisher for reuse or clean it up - * using your own code. - * @name streamDestroyed - * @event - * @memberof Publisher - * @see StreamEvent - */ - - /** - * Dispatched when the Publisher element is removed from the HTML DOM. When this event - * is dispatched, you may choose to adjust or remove HTML DOM elements related to the publisher. - * @name destroyed - * @event - * @memberof Publisher - */ + _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.

- * - * @param {Number} value The audio volume, between 0 and 100. - * - * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the - * following: - * - *
mySubscriber.setAudioVolume(50).setStyle(newStyle);
- * - * @see getAudioVolume() - * @see Session.subscribe() - * @method #setAudioVolume - * @memberOf Subscriber - */ - this.setAudioVolume = function(value) { - value = parseInt(value, 10); - if (isNaN(value)) { - OT.error('OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100'); - return this; - } - _audioVolume = Math.max(0, Math.min(100, value)); - if (_audioVolume !== value) { - OT.warn('OT.Subscriber.setAudioVolume: value should be an integer between 0 and 100'); - } - if(_properties.muted && _audioVolume > 0) { - _properties.premuteVolume = value; - muteAudio.call(this, false); - } - if (_streamContainer) { - _streamContainer.setAudioVolume(_audioVolume); - } - return this; - }; - - /** - * Returns the audio volume, between 0 and 100, of the Subscriber. - * - *

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

- * - * @return {Number} The audio volume, between 0 and 100, of the Subscriber. - * @see setAudioVolume() - * @method #getAudioVolume - * @memberOf Subscriber - */ - this.getAudioVolume = function() { - if(_properties.muted) { - return 0; - } - if (_streamContainer) return _streamContainer.getAudioVolume(); - else return _audioVolume; - }; - - /** - * Toggles audio on and off. Starts subscribing to audio (if it is available and currently - * not being subscribed to) when the value is true; stops - * subscribing to audio (if it is currently being subscribed to) when the value - * is false. - *

- * Note: This method only affects the local playback of audio. It has no impact on the - * audio for other connections subscribing to the same stream. If the Publsher is not - * publishing audio, enabling the Subscriber audio will have no practical effect. - *

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

- * Note: This method only affects the local playback of video. It has no impact on - * the video for other connections subscribing to the same stream. If the Publsher is not - * publishing video, enabling the Subscriber video will have no practical video. - *

- * - * @param {Boolean} value Whether to start subscribing to video (true) or not - * (false). - * - * @return {Subscriber} The Subscriber object. This lets you chain method calls, as in the - * following: - * - *
mySubscriber.subscribeToVideo(true).subscribeToAudio(false);
- * - * @see subscribeToAudio() - * @see Session.subscribe() - * @see StreamPropertyChangedEvent - * - * @method #subscribeToVideo - * @memberOf Subscriber - */ - this.subscribeToVideo = function(pValue, reason) { - var value = OT.$.castToBoolean(pValue, true); - - 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: - *

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

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

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

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

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

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

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

- * - *

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

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

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

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

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

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

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

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

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

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

- * - *

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

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

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

- * To prevent video from resuming, in the videoEnabled event listener, - * call subscribeToVideo(false) on the Subscriber object. - * - * @see VideoEnabledChangedEvent - * @see event:videoDisabled - * @name videoEnabled - * @event - * @memberof Subscriber - */ - - /** - * Dispatched when the Subscriber element is removed from the HTML DOM. When this event is - * dispatched, you may choose to adjust or remove HTML DOM elements related to the subscriber. - * @see Event - * @name destroyed - * @event - * @memberof Subscriber - */ - }; - -})(window); -!(function() { - - var parseErrorFromJSONDocument, - onGetResponseCallback, - onGetErrorCallback; - - OT.SessionInfo = function(jsonDocument) { - var sessionJSON = jsonDocument[0]; - - OT.log('SessionInfo Response:'); - OT.log(jsonDocument); - - /*jshint camelcase:false*/ - - this.sessionId = sessionJSON.session_id; - this.partnerId = sessionJSON.partner_id; - this.sessionStatus = sessionJSON.session_status; - - this.messagingServer = sessionJSON.messaging_server_url; - - this.messagingURL = sessionJSON.messaging_url; - this.symphonyAddress = sessionJSON.symphony_address; - - this.p2pEnabled = !!(sessionJSON.properties && - sessionJSON.properties.p2p && - sessionJSON.properties.p2p.preference && - sessionJSON.properties.p2p.preference.value === 'enabled'); - }; - - // Retrieves Session Info for +session+. The SessionInfo object will be passed - // to the +onSuccess+ callback. The +onFailure+ callback will be passed an error - // object and the DOMEvent that relates to the error. - OT.SessionInfo.get = function(session, onSuccess, onFailure) { - var sessionInfoURL = OT.properties.apiURL + '/session/' + session.id + '?extended=true', - - browser = OT.$.browserVersion(), - - startTime = OT.$.now(), - - options, - - validateRawSessionInfo = function(sessionInfo) { - session.logEvent('Instrumentation', null, 'gsi', OT.$.now() - startTime); - var error = parseErrorFromJSONDocument(sessionInfo); - if (error === false) { - onGetResponseCallback(session, onSuccess, sessionInfo); } else { - onGetErrorCallback(session, onFailure, error, JSON.stringify(sessionInfo)); + 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. - *

- * - *
- * Example - *
- *

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

- *
- *  var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
- *  var sessionID = ""; // Replace with your own session ID.
- *                      // See https://dashboard.tokbox.com/projects
- *  var token = ""; // Replace with a generated token that has been assigned the moderator role.
- *                  // See https://dashboard.tokbox.com/projects
- *
- *  var session = OT.initSession(apiKey, sessionID);
- *  session.on("sessionConnected", function(sessionConnectEvent) {
- *      //
- *  });
- *  session.connect(token);
- *  
- *

- *

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

- * - *
- * Events dispatched: - *
- * - *

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

- *

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

- *

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

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

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

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

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

- *

- * Session objects on remote clients dispatch streamDestroyed events for any - * stream you were publishing. The Session object dispatches a sessionDisconnected - * event locally. The Session objects on remote clients dispatch connectionDestroyed - * events, letting other connections know you have left the session. The - * {@link SessionDisconnectEvent} and {@link StreamEvent} objects that define the - * sessionDisconnect and connectionDestroyed events each have a - * reason property. The reason property lets the developer determine - * whether the connection is being terminated voluntarily and whether any streams are being - * destroyed as a byproduct of the underlying connection's voluntary destruction. - *

- *

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

- * - *

- * Note: If you intend to reuse a Publisher object created using - * OT.initPublisher() to publish to different sessions sequentially, call either - * Session.disconnect() or Session.unpublish(). Do not call both. - * Then call the preventDefault() method of the streamDestroyed or - * sessionDisconnected event object to prevent the Publisher object from being - * removed from the page. Be sure to call preventDefault() only if the - * connection.connectionId property of the Stream object in the event matches the - * connection.connectionId property of your Session object (to ensure that you are - * preventing the default behavior for your published streams, not for other streams that you - * subscribe to). - *

- * - *
- * Events dispatched: - *
- *

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

- *

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

- * - *

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

- * - * @method #disconnect - * @memberOf Session - */ - var disconnect = OT.$.bind(function disconnect(drainSocketBuffer) { - if (_socket && _socket.isNot('disconnected')) { - if (_socket.isNot('disconnecting')) { - setState('disconnecting'); - _socket.disconnect(drainSocketBuffer); - } - } - else { - reset.call(this); - } - }, this); - - this.disconnect = function(drainSocketBuffer) { - disconnect(drainSocketBuffer !== void 0 ? drainSocketBuffer : true); - }; - - this.destroy = function(reason) { - this.streams.destroy(); - this.connections.destroy(); - this.archives.destroy(); - disconnect(reason !== 'unloaded'); - }; - - /** - * The publish() method starts publishing an audio-video stream to the session. - * The audio-video stream is captured from a local microphone and webcam. Upon successful - * publishing, the Session objects on all connected clients dispatch the - * streamCreated event. - *

- * - * - *

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

- * - *

This method takes an alternate form: publish([targetElement:String, - * properties:Object]):Publisher — In this form, you do not pass a Publisher - * object into the function. Instead, you pass in a targetElement (the ID of the - * DOM element that the Publisher will replace) and a properties object that - * defines options for the Publisher (see OT.initPublisher().) - * The method returns a new Publisher object, which starts sending an audio-video stream to the - * session. The remainder of this documentation describes the form that takes a single Publisher - * object as a parameter. - * - *

- * A local display of the published stream is created on the web page by replacing - * the specified element in the DOM with a streaming video display. The video stream - * is automatically mirrored horizontally so that users see themselves and movement - * in their stream in a natural way. If the width and height of the display do not match - * the 4:3 aspect ratio of the video signal, the video stream is cropped to fit the - * display. - *

- * - *

- * If calling this method creates a new Publisher object and the OpenTok library does not - * have access to the camera or microphone, the web page alerts the user to grant access - * to the camera and microphone. - *

- * - *

- * The OT object dispatches an exception event if the user's role does not - * include permissions required to publish. For example, if the user's role is set to subscriber, - * then they cannot publish. You define a user's role when you create the user token using the - * generate_token() method of the - * OpenTok server-side - * libraries or the Dashboard page. - * You pass the token string as a parameter of the connect() method of the Session - * object. See ExceptionEvent and - * OT.on(). - *

- *

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

- * - *
Events dispatched:
- *

- * exception (ExceptionEvent) — Dispatched - * by the OT object. This can occur when user's role does not allow publishing (the - * code property of event object is set to 1500); it can also occur if the c - * onnection fails to connect (the code property of event object is set to 1013). - * WebRTC is a peer-to-peer protocol, and it is possible that connections will fail to connect. - * The most common cause for failure is a firewall that the protocol cannot traverse. - *

- *

- * streamCreated (StreamEvent) — - * The stream has been published. The Session object dispatches this on all clients - * subscribed to the stream, as well as on the publisher's client. - *

- * - *
Example
- * - *

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

- *
-  * var sessionId = ""; // Replace with your own session ID.
-  *                     // See https://dashboard.tokbox.com/projects
-  * var token = ""; // Replace with a generated token that has been assigned the moderator role.
-  *                 // See https://dashboard.tokbox.com/projects
-  * var session = OT.initSession(apiKey, sessionID);
-  * session.on("sessionConnected", function (event) {
-  *     var publisherOptions = {width: 400, height:300, name:"Bob's stream"};
-  *     // This assumes that there is a DOM element with the ID 'publisher':
-  *     publisher = OT.initPublisher('publisher', publisherOptions);
-  *     session.publish(publisher);
-  * });
-  * session.connect(token);
-  * 
- * - * @param {Publisher} publisher A Publisher object, which you initialize by calling the - * OT.initPublisher() method. - * - * @param {Function} completionHandler (Optional) A function to be called when the call to the - * publish() method succeeds or fails. This function takes one parameter — - * error. On success, the completionHandler function is not passed any - * arguments. On error, the function is passed an error object parameter - * (see the Error object). The - * error object has two properties: code (an integer) and - * message (a string), which identify the cause of the failure. Calling - * publish() fails if the role assigned to your token is not "publisher" or - * "moderator"; in this case error.code is set to 1500. Calling - * publish() also fails the client fails to connect; in this case - * error.code is set to 1013. The following code adds a - * completionHandler when calling the publish() method: - *
-  * session.publish(publisher, null, function (error) {
-  *   if (error) {
-  *     console.log(error.message);
-  *   } else {
-  *     console.log("Publishing a stream.");
-  *   }
-  * });
-  * 
- * - * @returns The Publisher object for this stream. - * - * @method #publish - * @memberOf Session - */ - this.publish = function(publisher, properties, completionHandler) { - if(typeof publisher === 'function') { - completionHandler = publisher; - publisher = undefined; - } - if(typeof properties === 'function') { - completionHandler = properties; - properties = undefined; - } - if (this.isNot('connected')) { - /*jshint camelcase:false*/ - _analytics.logError(1010, 'OT.exception', - 'We need to be connected before you can publish', null, { - action: 'publish', - variation: 'Failure', - payload_type: 'reason', - payload: 'We need to be connected before you can publish', - session_id: _sessionId, - partner_id: _apiKey, - widgetId: _widgetId, - widget_type: 'Controller' - }); - - if (completionHandler && OT.$.isFunction(completionHandler)) { - dispatchError(OT.ExceptionCodes.NOT_CONNECTED, - 'We need to be connected before you can publish', completionHandler); - } - - return null; - } - - if (!permittedTo('publish')) { - this.logEvent('publish', 'Failure', 'reason', - 'This token does not allow publishing. The role must be at least `publisher` ' + - 'to enable this functionality'); - dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - 'This token does not allow publishing. The role must be at least `publisher` ' + - 'to enable this functionality', completionHandler); - return null; - } - - // If the user has passed in an ID of a element then we create a new publisher. - if (!publisher || typeof(publisher)==='string' || OT.$.isElementNode(publisher)) { - // Initiate a new Publisher with the new session credentials - publisher = OT.initPublisher(publisher, properties); - - } else if (publisher instanceof OT.Publisher){ - - // If the publisher already has a session attached to it we can - if ('session' in publisher && publisher.session && 'sessionId' in publisher.session) { - // send a warning message that we can't publish again. - if( publisher.session.sessionId === this.sessionId){ - OT.warn('Cannot publish ' + publisher.guid() + ' again to ' + - this.sessionId + '. Please call session.unpublish(publisher) first.'); - } else { - OT.warn('Cannot publish ' + publisher.guid() + ' publisher already attached to ' + - publisher.session.sessionId+ '. Please call session.unpublish(publisher) first.'); - } - } - - } else { - dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - 'Session.publish :: First parameter passed in is neither a ' + - 'string nor an instance of the Publisher', - completionHandler); - return; - } - - publisher.once('publishComplete', function(err) { - if (err) { - dispatchError(OT.ExceptionCodes.UNABLE_TO_PUBLISH, - 'Session.publish :: ' + err.message, - completionHandler); - return; - } - - if (completionHandler && OT.$.isFunction(completionHandler)) { - completionHandler.apply(null, arguments); - } - }); - - // Add publisher reference to the session - publisher._.publishToSession(this); - - // return the embed publisher - return publisher; - }; - - /** - * Ceases publishing the specified publisher's audio-video stream - * to the session. By default, the local representation of the audio-video stream is - * removed from the web page. Upon successful termination, the Session object on every - * connected web page dispatches - * a streamDestroyed event. - *

- * - *

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

- * - *

- * Note: If you intend to reuse a Publisher object created using - * OT.initPublisher() to publish to different sessions sequentially, call - * either Session.disconnect() or Session.unpublish(). Do not call - * both. Then call the preventDefault() method of the streamDestroyed - * or sessionDisconnected event object to prevent the Publisher object from being - * removed from the page. Be sure to call preventDefault() only if the - * connection.connectionId property of the Stream object in the event matches the - * connection.connectionId property of your Session object (to ensure that you are - * preventing the default behavior for your published streams, not for other streams that you - * subscribe to). - *

- * - *
Events dispatched:
- * - *

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

- * - *
Example
- * - * The following example publishes a stream to a session and adds a Disconnect link to the - * web page. Clicking this link causes the stream to stop being published. - * - *
-  * <script>
-  *     var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
-  *     var sessionID = ""; // Replace with your own session ID.
-  *                      // See https://dashboard.tokbox.com/projects
-  *     var token = "Replace with the TokBox token string provided to you."
-  *     var session = OT.initSession(apiKey, sessionID);
-  *     session.on("sessionConnected", function sessionConnectHandler(event) {
-  *         // This assumes that there is a DOM element with the ID 'publisher':
-  *         publisher = OT.initPublisher('publisher');
-  *         session.publish(publisher);
-  *     });
-  *     session.connect(token);
-  *     var publisher;
-  *
-  *     function unpublish() {
-  *         session.unpublish(publisher);
-  *     }
-  * </script>
-  *
-  * <body>
-  *
-  *     <div id="publisherContainer/>
-  *     <br/>
-  *
-  *     <a href="javascript:unpublish()">Stop Publishing</a>
-  *
-  * </body>
-  *
-  * 
- * - * @see publish() - * - * @see streamDestroyed event - * - * @param {Publisher} publisher The Publisher object to stop streaming. - * - * @method #unpublish - * @memberOf Session - */ - this.unpublish = function(publisher) { - if (!publisher) { - OT.error('OT.Session.unpublish: publisher parameter missing.'); - return; - } - - // Unpublish the localMedia publisher - publisher._.unpublishFromSession(this, 'unpublished'); - }; - - - /** - * Subscribes to a stream that is available to the session. You can get an array of - * available streams from the streams property of the sessionConnected - * and streamCreated events (see - * SessionConnectEvent and - * StreamEvent). - *

- *

- * The subscribed stream is displayed on the local web page by replacing the specified element - * in the DOM with a streaming video display. If the width and height of the display do not - * match the 4:3 aspect ratio of the video signal, the video stream is cropped to fit - * the display. If the stream lacks a video component, a blank screen with an audio indicator - * is displayed in place of the video stream. - *

- * - *

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

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

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

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

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

- *
-  * var apiKey = ""; // Replace with your API key. See https://dashboard.tokbox.com/projects
-  * var sessionID = ""; // Replace with your own session ID.
-  *                     // See https://dashboard.tokbox.com/projects
-  * var streams = [];
-  *
-  * var session = OT.initSession(apiKey, sessionID);
-  * session.on("streamCreated", function(event) {
-  *     var stream = event.stream;
-  *     displayStream(stream);
-  * });
-  * session.connect(token);
-  *
-  * function displayStream(stream) {
-  *     var div = document.createElement('div');
-  *     div.setAttribute('id', 'stream' + stream.streamId);
-  *
-  *     var subscriber = session.subscribe(stream, div);
-  *     subscribers.push(subscriber);
-  *
-  *     var aLink = document.createElement('a');
-  *     aLink.setAttribute('href', 'javascript: unsubscribe("' + subscriber.id + '")');
-  *     aLink.innerHTML = "Unsubscribe";
-  *
-  *     var streamsContainer = document.getElementById('streamsContainer');
-  *     streamsContainer.appendChild(div);
-  *     streamsContainer.appendChild(aLink);
-  *
-  *     streams = event.streams;
-  * }
-  *
-  * function unsubscribe(subscriberId) {
-  *     console.log("unsubscribe called");
-  *     for (var i = 0; i < subscribers.length; i++) {
-  *         var subscriber = subscribers[i];
-  *         if (subscriber.id == subscriberId) {
-  *             session.unsubscribe(subscriber);
-  *         }
-  *     }
-  * }
-  * 
- * - * @param {Subscriber} subscriber The Subscriber object to unsubcribe. - * - * @see subscribe() - * - * @method #unsubscribe - * @memberOf Session - */ - this.unsubscribe = function(subscriber) { - if (!subscriber) { - var errorMsg = 'OT.Session.unsubscribe: subscriber cannot be null'; - OT.error(errorMsg); - throw new Error(errorMsg); - } - - if (!subscriber.stream) { - OT.warn('OT.Session.unsubscribe:: tried to unsubscribe a subscriber that had no stream'); - return false; - } - - OT.debug('OT.Session.unsubscribe: subscriber ' + subscriber.id); - - subscriber.destroy(); - - return true; - }; - - /** - * Returns an array of local Subscriber objects for a given stream. - * - * @param {Stream} stream The stream for which you want to find subscribers. - * - * @returns {Array} An array of {@link Subscriber} objects for the specified stream. - * - * @see unsubscribe() - * @see Subscriber - * @see StreamEvent - * @method #getSubscribersForStream - * @memberOf Session - */ - this.getSubscribersForStream = function(stream) { - return OT.subscribers.where({streamId: stream.id}); - }; - - /** - * Returns the local Publisher object for a given stream. - * - * @param {Stream} stream The stream for which you want to find the Publisher. - * - * @returns {Publisher} A Publisher object for the specified stream. Returns - * null if there is no local Publisher object - * for the specified stream. - * - * @see forceUnpublish() - * @see Subscriber - * @see StreamEvent - * - * @method #getPublisherForStream - * @memberOf Session - */ - this.getPublisherForStream = function(stream) { - var streamId, - errorMsg; - - if (typeof stream === 'string') { - streamId = stream; - } else if (typeof stream === 'object' && stream && stream.hasOwnProperty('id')) { - streamId = stream.id; - } else { - errorMsg = 'Session.getPublisherForStream :: Invalid stream type'; - OT.error(errorMsg); - throw new Error(errorMsg); - } - - return OT.publishers.where({streamId: streamId})[0]; - }; - - // Private Session API: for internal OT use only - this._ = { - jsepCandidateP2p: function(streamId, subscriberId, candidate) { - return _socket.jsepCandidateP2p(streamId, subscriberId, candidate); - }, - - jsepCandidate: function(streamId, candidate) { - return _socket.jsepCandidate(streamId, candidate); - }, - - jsepOffer: function(streamId, offerSdp) { - return _socket.jsepOffer(streamId, offerSdp); - }, - - jsepOfferP2p: function(streamId, subscriberId, offerSdp) { - return _socket.jsepOfferP2p(streamId, subscriberId, offerSdp); - }, - - jsepAnswer: function(streamId, answerSdp) { - return _socket.jsepAnswer(streamId, answerSdp); - }, - - jsepAnswerP2p: function(streamId, subscriberId, answerSdp) { - return _socket.jsepAnswerP2p(streamId, subscriberId, answerSdp); - }, - - // session.on("signal", function(SignalEvent)) - // session.on("signal:{type}", function(SignalEvent)) - dispatchSignal: OT.$.bind(function(fromConnection, type, data) { - var event = new OT.SignalEvent(type, data, fromConnection); - event.target = this; - - // signal a "signal" event - // NOTE: trigger doesn't support defaultAction, and therefore preventDefault. - this.trigger(OT.Event.names.SIGNAL, event); - - // signal an "signal:{type}" event" if there was a custom type - if (type) this.dispatchEvent(event); - }, this), - - subscriberCreate: function(stream, subscriber, channelsToSubscribeTo, completion) { - return _socket.subscriberCreate(stream.id, subscriber.widgetId, - channelsToSubscribeTo, completion); - }, - - subscriberDestroy: function(stream, subscriber) { - return _socket.subscriberDestroy(stream.id, subscriber.widgetId); - }, - - subscriberUpdate: function(stream, subscriber, attributes) { - return _socket.subscriberUpdate(stream.id, subscriber.widgetId, attributes); - }, - - subscriberChannelUpdate: function(stream, subscriber, channel, attributes) { - return _socket.subscriberChannelUpdate(stream.id, subscriber.widgetId, channel.id, - attributes); - }, - - streamCreate: OT.$.bind(function(name, orientation, encodedWidth, encodedHeight, - hasAudio, hasVideo, - frameRate, completion) { - - _socket.streamCreate( - name, - orientation, - encodedWidth, - encodedHeight, - hasAudio, - hasVideo, - frameRate, - OT.Config.get('bitrates', 'min', OT.APIKEY), - OT.Config.get('bitrates', 'max', OT.APIKEY), - completion - ); - }, this), - - streamDestroy: function(streamId) { - _socket.streamDestroy(streamId); - }, - - streamChannelUpdate: function(stream, channel, attributes) { - _socket.streamChannelUpdate(stream.id, channel.id, attributes); - } - }; - - - /** - * Sends a signal to each client or a specified client in the session. Specify a - * to property of the signal parameter to limit the signal to - * be sent to a specific client; otherwise the signal is sent to each client connected to - * the session. - *

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

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

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

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

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

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

    - *
  • data — (String) The data to send. The limit to the length of data - * string is 8kB. Do not set the data string to null or - * undefined.
  • - *
  • to — (Connection) A Connection - * object corresponding to the client that the message is to be sent to. If you do not - * specify this property, the signal is sent to all clients connected to the session.
  • - *
  • type — (String) The type of the signal. You can use the type to - * filter signals when setting an event handler for the signal:type event - * (where you replace type with the type string). The maximum length of the - * type string is 128 characters, and it must contain only letters (A-Z and a-z), - * numbers (0-9), '-', '_', and '~'.
  • - * - *
- * - *

Each property is optional. If you set none of the properties, you will send a signal - * with no data or type to each client connected to the session.

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

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

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

- *

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

- *

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

- *

- * The OT object dispatches an exception event if the user's role - * does not include permissions required to force other users to disconnect. - * You define a user's role when you create the user token using the - * generate_token() method using - * OpenTok - * server-side libraries or the - * Dashboard page. - * See ExceptionEvent and OT.on(). - *

- *

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

- * - *
Events dispatched:
- * - *

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

- *

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

- *

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

- *

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

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

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

- *

- * The OT object dispatches an exception event if the user's role - * does not include permissions required to force other users to unpublish. - * You define a user's role when you create the user token using the generate_token() - * method using the - * OpenTok - * server-side libraries or the Dashboard - * page. - * You pass the token string as a parameter of the connect() method of the Session - * object. See ExceptionEvent and - * OT.on(). - *

- * - *
Events dispatched:
- * - *

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

- *

- * streamDestroyed (StreamEvent) — - * The stream has been unpublished. The Session object dispatches this on all clients - * subscribed to the stream, as well as on the publisher's client. - *

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

- * The default behavior is that all Subscriber objects are unsubscribed and removed from the - * HTML DOM. Each Subscriber object dispatches a destroyed event when the element is - * removed from the HTML DOM. If you call the preventDefault() method in the event - * listener for the sessionDisconnect event, the default behavior is prevented, and - * you can, optionally, clean up Subscriber objects using your own code. +* 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; istreamCreated - * event. For a code example and more details, see {@link StreamEvent}. - * @name streamCreated - * @event - * @memberof Session - * @see StreamEvent - * @see Session.publish() - */ + * Deletes the Publisher object and removes it from the HTML DOM. + *

+ * The Publisher object dispatches a destroyed event when the DOM + * element is removed. + *

+ * @method #destroy + * @memberOf Publisher + * @return {Publisher} The Publisher. + */ + this.destroy = function(/* unused */ reason, quiet) { + if (_state.isDestroyed()) return; + _state.set('Destroyed'); - /** - * 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 - */ + reset(); - /** - * A stream has started or stopped publishing audio or video (see - * Publisher.publishAudio() and - * Publisher.publishVideo()); or the - * videoDimensions property of the Stream - * object has changed (see Stream.videoDimensions). - *

- * Note that a subscriber's video can be disabled or enabled for reasons other than the - * publisher disabling or enabling it. A Subscriber object dispatches videoDisabled - * and videoEnabled events in all conditions that cause the subscriber's stream - * to be disabled or enabled. - * - * @name streamPropertyChanged - * @event - * @memberof Session - * @see StreamPropertyChangedEvent - * @see Publisher.publishAudio() - * @see Publisher.publishVideo() - * @see Stream.hasAudio - * @see Stream.hasVideo - * @see Stream.videoDimensions - * @see Subscriber videoDisabled event - * @see Subscriber videoEnabled event - */ + 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).
  • - *
- *

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

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

+ * 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 +*/ + +/** +* 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 +*/ + +/** +*

+* 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; } - OT.HardwareSetup = function(targetElement, options, callback) { + var session = OT.sessions.get(sessionId); - var camera, - microphone, - setupDOM, - setState; + if (!session) { + session = new OT.Session(apiKey, sessionId); + OT.sessions.add(session); + } - setState = OT.$.statable(this, ['getDevices', 'chooseDevices', 'destroyed'], 'getDevices'); + return session; +}; - this.audioSource = function() { - return microphone && microphone.pickedDevice; - }; +/** +*

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

+*

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

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

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

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

+*
    +*
  • +* audioFallbackEnabled (String) — Whether the stream will use the +* audio-fallback feature (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. +*
  • +*
  • +* audioSource (String) — The ID of the audio input device (such as a +* microphone) to be used by the publisher. You can obtain a list of available devices, including +* audio input devices, by calling the OT.getDevices() method. Each +* device listed by the method has a unique device ID. If you pass in a device ID that does not +* match an existing audio input device, the call to OT.initPublisher() fails with an +* error (error code 1500, "Unable to Publish") passed to the completion handler function. +*

    +* If you set this property to null or false, the browser does not +* request access to the microphone, and no audio is published. +*

    +*
  • +*
  • +* 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. +*
    • +*
    • +* "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. +*
    • +*
    +*
  • +*
  • +* frameRate (Number) — The desired frame rate, in frames per second, +* of the video. Valid values are 30, 15, 7, and 1. The published stream will use the closest +* value supported on the publishing client. The frame rate can differ slightly from the value +* you set, depending on the browser of the client. And the video will only use the desired +* frame rate if the client configuration supports it. +*

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

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

    +*

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

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

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

    +*

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

    +*

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

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

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

    • +* +*
    • buttonDisplayMode (String) — How to display the microphone controls +* Possible values are: "auto" (controls are displayed when the stream is first +* displayed and when the user mouses over the display), "off" (controls are not +* displayed), and "on" (controls are always displayed).
    • +* +*
    • nameDisplayMode (String) — Whether to display the stream name. +* Possible values are: "auto" (the name is displayed when the stream is first +* displayed and when the user mouses over the display), "off" (the name is not +* displayed), and "on" (the name is always displayed).
    • +*
    +*
  • +*
  • +* videoSource (String) — The ID of the video input device (such as a +* camera) to be used by the publisher. You can obtain a list of available devices, including +* video input devices, by calling the OT.getDevices() method. Each +* device listed by the method has a unique device ID. If you pass in a device ID that does not +* match an existing video input device, the call to OT.initPublisher() fails with an +* error (error code 1500, "Unable to Publish") passed to the completion handler function. +*

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

  • +*
  • +* width (Number) — The desired width, in pixels, of the +* displayed Publisher video stream (default: 264). Note: Use the +* height and width properties to set the dimensions +* of the publisher video; do not set the height and width of the DOM element +* (using CSS). +*
  • +*
+* @param {Function} completionHandler (Optional) A function to be called when the method succeeds +* or fails in initializing a Publisher object. This function takes one parameter — +* error. On success, the error object is set to null. On +* failure, the error object has two properties: code (an integer) and +* message (a string), which identify the cause of the failure. The method succeeds +* when the user grants access to the camera and microphone. The method fails if the user denies +* access to the camera and microphone. The completionHandler function is called +* before the Publisher dispatches an accessAllowed (success) event or an +* accessDenied (failure) event. +*

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

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

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

+ * The label property identifies the device. The label + * property is set to an empty string if the user has not previously granted access to + * a camera and microphone. In HTTP, the user must have granted access to a camera and + * microphone in the current page (for example, in response to a call to + * OT.initPublisher()). In HTTPS, the user must have previously granted access + * to the camera and microphone in the current page or in a page previously loaded from the + * domain. + * + * + * @see OT.initPublisher() + * @method OT.getDevices + * @memberof OT + */ +OT.getDevices = function(callback) { + OT.$.getMediaDevices(callback); +}; + + + +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. + *

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

+ * Removes an event listener for a specific event. + *

+ * + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ *
+ * OT.off(
+ *   {
+ *     exceptionEvent: exceptionEventHandler
+ *   },
+ *   this
+ * );
+ * 
+ * + * @param {String} type (Optional) The string identifying the type of event. You can use a space to + * specify multiple events, as in "eventName1 eventName2 eventName3". If you pass in no + * type value (or other arguments), all event handlers are removed for the object. + * @param {Function} handler (Optional) The event handler function to remove. If you pass in no + * handler, all event handlers are removed for the specified event type. + * @param {Object} context (Optional) If you specify a context, the event handler is + * removed for all specified events and handlers that use the specified context. + * + * @memberof OT + * @method off + * @see on() + * @see once() + * @see Events + */ + +/** + * Dispatched by the OT class when the app encounters an exception. + * Note that you set up an event handler for the exception event by calling the + * OT.on() method. + * + * @name exception + * @event + * @borrows ExceptionEvent#message as this.message + * @memberof OT + * @see ExceptionEvent + */ + + +// 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 has experienced * a crash. Also does the job of cleaning up the minidump of the crash. @@ -58,6 +67,7 @@ function crashBrowser(browser) { } Services.obs.removeObserver(observer, 'ipc:content-shutdown'); + info("Crash cleaned up"); resolve(); }; @@ -67,6 +77,7 @@ function crashBrowser(browser) { let aboutTabCrashedLoadPromise = new Promise((resolve, reject) => { browser.addEventListener("AboutTabCrashedLoad", function onCrash() { browser.removeEventListener("AboutTabCrashedLoad", onCrash, false); + info("about:tabcrashed loaded"); resolve(); }, false, true); }); @@ -75,7 +86,22 @@ function crashBrowser(browser) { // evaluated. let mm = browser.messageManager; mm.loadFrameScript("data:,(" + frame_script.toString() + ")();", false); - return Promise.all([crashCleanupPromise, aboutTabCrashedLoadPromise]); + return Promise.all([crashCleanupPromise, aboutTabCrashedLoadPromise]).then(() => { + let tab = gBrowser.getTabForBrowser(browser); + is(tab.getAttribute("crashed"), "true", "Tab should be marked as crashed"); + }); +} + +function clickButton(browser, id) { + info("Clicking " + id); + + let frame_script = (id) => { + let button = content.document.getElementById(id); + button.click(); + }; + + let mm = browser.messageManager; + mm.loadFrameScript("data:,(" + frame_script.toString() + ")('" + id + "');", false); } /** @@ -194,8 +220,6 @@ add_task(function test_crash_page_not_in_history() { // Crash the tab yield crashBrowser(browser); - // Flush out any notifications from the crashed browser. - TabState.flush(browser); // Check the tab state and make sure the tab crashed page isn't // mentioned. @@ -225,13 +249,12 @@ add_task(function test_revived_history_from_remote() { // Crash the tab yield crashBrowser(browser); - // Flush out any notifications from the crashed browser. - TabState.flush(browser); // Browse to a new site that will cause the browser to // become remote again. browser.loadURI(PAGE_2); yield promiseTabRestored(newTab); + ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore."); ok(browser.isRemoteBrowser, "Should be a remote browser"); TabState.flush(browser); @@ -265,13 +288,12 @@ add_task(function test_revived_history_from_non_remote() { // Crash the tab yield crashBrowser(browser); - // Flush out any notifications from the crashed browser. - TabState.flush(browser); // Browse to a new site that will not cause the browser to // become remote again. browser.loadURI("about:mozilla"); yield promiseBrowserLoaded(browser); + ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore."); ok(!browser.isRemoteBrowser, "Should not be a remote browser"); TabState.flush(browser); @@ -301,6 +323,14 @@ add_task(function test_revive_tab_from_session_store() { browser.loadURI(PAGE_1); yield promiseBrowserLoaded(browser); + let newTab2 = gBrowser.addTab(); + let browser2 = newTab2.linkedBrowser; + ok(browser2.isRemoteBrowser, "Should be a remote browser"); + yield promiseBrowserLoaded(browser2); + + browser.loadURI(PAGE_1); + yield promiseBrowserLoaded(browser); + browser.loadURI(PAGE_2); yield promiseBrowserLoaded(browser); @@ -308,12 +338,13 @@ add_task(function test_revive_tab_from_session_store() { // Crash the tab yield crashBrowser(browser); - // Flush out any notifications from the crashed browser. - TabState.flush(browser); + is(newTab2.getAttribute("crashed"), "true", "Second tab should be crashed too."); // Use SessionStore to revive the tab - SessionStore.reviveCrashedTab(newTab); - yield promiseBrowserLoaded(browser); + clickButton(browser, "restoreTab"); + yield promiseTabRestored(newTab); + ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore."); + is(newTab2.getAttribute("crashed"), "true", "Second tab should still be crashed though."); // We can't just check browser.currentURI.spec, because from // the outside, a crashed tab has the same URI as the page @@ -325,4 +356,90 @@ add_task(function test_revive_tab_from_session_store() { yield promiseHistoryLength(browser, 2); gBrowser.removeTab(newTab); -}); \ No newline at end of file + gBrowser.removeTab(newTab2); +}); + +/** + * Checks that we can revive a crashed tab back to the page that + * it was on when it crashed. + */ +add_task(function test_revive_all_tabs_from_session_store() { + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + let browser = newTab.linkedBrowser; + ok(browser.isRemoteBrowser, "Should be a remote browser"); + yield promiseBrowserLoaded(browser); + + browser.loadURI(PAGE_1); + yield promiseBrowserLoaded(browser); + + let newTab2 = gBrowser.addTab(PAGE_1); + let browser2 = newTab2.linkedBrowser; + ok(browser2.isRemoteBrowser, "Should be a remote browser"); + yield promiseBrowserLoaded(browser2); + + browser.loadURI(PAGE_1); + yield promiseBrowserLoaded(browser); + + browser.loadURI(PAGE_2); + yield promiseBrowserLoaded(browser); + + TabState.flush(browser); + TabState.flush(browser2); + + // Crash the tab + yield crashBrowser(browser); + is(newTab2.getAttribute("crashed"), "true", "Second tab should be crashed too."); + + // Use SessionStore to revive all the tabs + clickButton(browser, "restoreAll"); + yield promiseTabRestored(newTab); + ok(!newTab.hasAttribute("crashed"), "Tab shouldn't be marked as crashed anymore."); + ok(!newTab.hasAttribute("pending"), "Tab shouldn't be pending."); + ok(!newTab2.hasAttribute("crashed"), "Second tab shouldn't be marked as crashed anymore."); + ok(newTab2.hasAttribute("pending"), "Second tab should be pending."); + + gBrowser.selectedTab = newTab2; + yield promiseTabRestored(newTab2); + ok(!newTab2.hasAttribute("pending"), "Second tab shouldn't be pending."); + + // We can't just check browser.currentURI.spec, because from + // the outside, a crashed tab has the same URI as the page + // it crashed on (much like an about:neterror page). Instead, + // we have to use the documentURI on the content. + yield promiseContentDocumentURIEquals(browser, PAGE_2); + yield promiseContentDocumentURIEquals(browser2, PAGE_1); + + // We should also have two entries in the browser history. + yield promiseHistoryLength(browser, 2); + + gBrowser.removeTab(newTab); + gBrowser.removeTab(newTab2); +}); + +/** + * Checks that about:tabcrashed can close the current tab + */ +add_task(function test_close_tab_after_crash() { + let newTab = gBrowser.addTab(); + gBrowser.selectedTab = newTab; + let browser = newTab.linkedBrowser; + ok(browser.isRemoteBrowser, "Should be a remote browser"); + yield promiseBrowserLoaded(browser); + + browser.loadURI(PAGE_1); + yield promiseBrowserLoaded(browser); + + TabState.flush(browser); + + // Crash the tab + yield crashBrowser(browser); + + let promise = promiseEvent(gBrowser.tabContainer, "TabClose"); + + // Click the close tab button + clickButton(browser, "closeTab"); + yield promise; + + is(gBrowser.tabs.length, 1, "Should have closed the tab"); +}); diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js index e71e6004baee..bd5f27a63dd0 100644 --- a/browser/components/sessionstore/test/head.js +++ b/browser/components/sessionstore/test/head.js @@ -488,15 +488,19 @@ function promiseDelayedStartupFinished(aWindow) { return new Promise(resolve => whenDelayedStartupFinished(aWindow, resolve)); } -function promiseTabRestored(tab) { +function promiseEvent(element, eventType, isCapturing = false) { return new Promise(resolve => { - tab.addEventListener("SSTabRestored", function onRestored() { - tab.removeEventListener("SSTabRestored", onRestored); - resolve(); - }); + element.addEventListener(eventType, function listener(event) { + element.removeEventListener(eventType, listener, isCapturing); + resolve(event); + }, isCapturing); }); } +function promiseTabRestored(tab) { + return promiseEvent(tab, "SSTabRestored"); +} + function sendMessage(browser, name, data = {}) { browser.messageManager.sendAsyncMessage(name, data); return promiseContentMessage(browser, name); diff --git a/browser/components/uitour/test/browser_UITour_loop.js b/browser/components/uitour/test/browser_UITour_loop.js index d2372632279f..d3ae6e034936 100644 --- a/browser/components/uitour/test/browser_UITour_loop.js +++ b/browser/components/uitour/test/browser_UITour_loop.js @@ -16,6 +16,16 @@ function test() { UITourTest(); } +function runOffline(fun) { + return (done) => { + Services.io.offline = true; + fun(function onComplete() { + Services.io.offline = false; + done(); + }); + } +} + let tests = [ taskify(function* test_menu_show_hide() { ise(loopButton.open, false, "Menu should initially be closed"); @@ -94,7 +104,7 @@ let tests = [ }); }); }, - function test_notifyLoopChatWindowOpenedClosed(done) { + runOffline(function test_notifyLoopChatWindowOpenedClosed(done) { gContentAPI.observe((event, params) => { is(event, "Loop:ChatWindowOpened", "Check Loop:ChatWindowOpened notification"); gContentAPI.observe((event, params) => { @@ -110,8 +120,8 @@ let tests = [ document.querySelector("#pinnedchats > chatbox").close(); }); LoopRooms.open("fakeTourRoom"); - }, - function test_notifyLoopRoomURLCopied(done) { + }), + runOffline(function test_notifyLoopRoomURLCopied(done) { gContentAPI.observe((event, params) => { is(event, "Loop:ChatWindowOpened", "Loop chat window should've opened"); gContentAPI.observe((event, params) => { @@ -131,8 +141,8 @@ let tests = [ }); setupFakeRoom(); LoopRooms.open("fakeTourRoom"); - }, - function test_notifyLoopRoomURLEmailed(done) { + }), + runOffline(function test_notifyLoopRoomURLEmailed(done) { gContentAPI.observe((event, params) => { is(event, "Loop:ChatWindowOpened", "Loop chat window should've opened"); gContentAPI.observe((event, params) => { @@ -162,7 +172,7 @@ let tests = [ }); }); LoopRooms.open("fakeTourRoom"); - }, + }), taskify(function* test_arrow_panel_position() { ise(loopButton.open, false, "Menu should initially be closed"); let popup = document.getElementById("UITourTooltip"); @@ -274,6 +284,7 @@ if (Services.prefs.getBoolPref("loop.enabled")) { Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin"); Services.prefs.clearUserPref("loop.gettingStarted.seen"); Services.prefs.clearUserPref("loop.gettingStarted.url"); + Services.io.offline = false; // Copied from browser/components/loop/test/mochitest/head.js // Remove the iframe after each test. This also avoids mochitest complaining diff --git a/browser/extensions/pdfjs/README.mozilla b/browser/extensions/pdfjs/README.mozilla index 2c5a980c0dc2..cb411f6aad93 100644 --- a/browser/extensions/pdfjs/README.mozilla +++ b/browser/extensions/pdfjs/README.mozilla @@ -1,4 +1,4 @@ This is the pdf.js project output, https://github.com/mozilla/pdf.js -Current extension version is: 1.0.1040 +Current extension version is: 1.0.1130 diff --git a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm index 345076738960..11db211a1f53 100644 --- a/browser/extensions/pdfjs/content/PdfStreamConverter.jsm +++ b/browser/extensions/pdfjs/content/PdfStreamConverter.jsm @@ -349,7 +349,11 @@ ChromeActions.prototype = { return (!!prefBrowser && prefGfx); }, supportsDocumentColors: function() { - return getBoolPref('browser.display.use_document_colors', true); + if (getIntPref('browser.display.document_color_use', 0) === 2 || + !getBoolPref('browser.display.use_document_colors', true)) { + return false; + } + return true; }, reportTelemetry: function (data) { var probeInfo = JSON.parse(data); diff --git a/browser/extensions/pdfjs/content/build/pdf.js b/browser/extensions/pdfjs/content/build/pdf.js index 8ef617112f39..e1660d96e073 100644 --- a/browser/extensions/pdfjs/content/build/pdf.js +++ b/browser/extensions/pdfjs/content/build/pdf.js @@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.1040'; -PDFJS.build = '997096f'; +PDFJS.version = '1.0.1130'; +PDFJS.build = 'e4f0ae2'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it @@ -341,6 +341,7 @@ function isValidUrl(url, allowRelative) { case 'https': case 'ftp': case 'mailto': + case 'tel': return true; default: return false; @@ -470,6 +471,8 @@ var XRefParseException = (function XRefParseExceptionClosure() { function bytesToString(bytes) { + assert(bytes !== null && typeof bytes === 'object' && + bytes.length !== undefined, 'Invalid argument for bytesToString'); var length = bytes.length; var MAX_ARGUMENT_COUNT = 8192; if (length < MAX_ARGUMENT_COUNT) { @@ -485,6 +488,7 @@ function bytesToString(bytes) { } function stringToBytes(str) { + assert(typeof str === 'string', 'Invalid argument for stringToBytes'); var length = str.length; var bytes = new Uint8Array(length); for (var i = 0; i < length; ++i) { @@ -1374,6 +1378,9 @@ PDFJS.disableStream = (PDFJS.disableStream === undefined ? * Disable pre-fetching of PDF file data. When range requests are enabled PDF.js * will automatically keep fetching more data even if it isn't needed to display * the current page. This default behavior can be disabled. + * + * NOTE: It is also necessary to disable streaming, see above, + * in order for disabling of pre-fetching to work correctly. * @var {boolean} */ PDFJS.disableAutoFetch = (PDFJS.disableAutoFetch === undefined ? @@ -1437,7 +1444,9 @@ PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? * * @typedef {Object} DocumentInitParameters * @property {string} url - The URL of the PDF. - * @property {TypedArray} data - A typed array with PDF data. + * @property {TypedArray|Array|string} data - Binary PDF data. Use typed arrays + * (Uint8Array) to improve the memory usage. If PDF data is BASE64-encoded, + * use atob() to convert it to a binary string first. * @property {Object} httpHeaders - Basic authentication headers. * @property {boolean} withCredentials - Indicates whether or not cross-site * Access-Control requests should be made using credentials such as cookies @@ -1446,6 +1455,9 @@ PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? * @property {TypedArray} initialData - A typed array with the first portion or * all of the pdf data. Used by the extension since some data is already * loaded before the switch to range requests. + * @property {number} length - The PDF file length. It's used for progress + * reports and range requests operations. + * @property {PDFDataRangeTransport} range */ /** @@ -1462,68 +1474,226 @@ PDFJS.maxCanvasPixels = (PDFJS.maxCanvasPixels === undefined ? * is used, which means it must follow the same origin rules that any XHR does * e.g. No cross domain requests without CORS. * - * @param {string|TypedArray|DocumentInitParameters} source Can be a url to - * where a PDF is located, a typed array (Uint8Array) already populated with - * data or parameter object. + * @param {string|TypedArray|DocumentInitParameters|PDFDataRangeTransport} src + * Can be a url to where a PDF is located, a typed array (Uint8Array) + * already populated with data or parameter object. * - * @param {Object} pdfDataRangeTransport is optional. It is used if you want - * to manually serve range requests for data in the PDF. See viewer.js for - * an example of pdfDataRangeTransport's interface. + * @param {PDFDataRangeTransport} pdfDataRangeTransport (deprecated) It is used + * if you want to manually serve range requests for data in the PDF. * - * @param {function} passwordCallback is optional. It is used to request a + * @param {function} passwordCallback (deprecated) It is used to request a * password if wrong or no password was provided. The callback receives two * parameters: function that needs to be called with new password and reason * (see {PasswordResponses}). * - * @param {function} progressCallback is optional. It is used to be able to + * @param {function} progressCallback (deprecated) It is used to be able to * monitor the loading progress of the PDF file (necessary to implement e.g. * a loading bar). The callback receives an {Object} with the properties: * {number} loaded and {number} total. * - * @return {Promise} A promise that is resolved with {@link PDFDocumentProxy} - * object. + * @return {PDFDocumentLoadingTask} */ -PDFJS.getDocument = function getDocument(source, +PDFJS.getDocument = function getDocument(src, pdfDataRangeTransport, passwordCallback, progressCallback) { - var workerInitializedCapability, workerReadyCapability, transport; + var task = new PDFDocumentLoadingTask(); - if (typeof source === 'string') { - source = { url: source }; - } else if (isArrayBuffer(source)) { - source = { data: source }; - } else if (typeof source !== 'object') { - error('Invalid parameter in getDocument, need either Uint8Array, ' + - 'string or a parameter object'); + // Support of the obsolete arguments (for compatibility with API v1.0) + if (pdfDataRangeTransport) { + if (!(pdfDataRangeTransport instanceof PDFDataRangeTransport)) { + // Not a PDFDataRangeTransport instance, trying to add missing properties. + pdfDataRangeTransport = Object.create(pdfDataRangeTransport); + pdfDataRangeTransport.length = src.length; + pdfDataRangeTransport.initialData = src.initialData; + } + src = Object.create(src); + src.range = pdfDataRangeTransport; + } + task.onPassword = passwordCallback || null; + task.onProgress = progressCallback || null; + + var workerInitializedCapability, transport; + var source; + if (typeof src === 'string') { + source = { url: src }; + } else if (isArrayBuffer(src)) { + source = { data: src }; + } else if (src instanceof PDFDataRangeTransport) { + source = { range: src }; + } else { + if (typeof src !== 'object') { + error('Invalid parameter in getDocument, need either Uint8Array, ' + + 'string or a parameter object'); + } + if (!src.url && !src.data && !src.range) { + error('Invalid parameter object: need either .data, .range or .url'); + } + + source = src; } - if (!source.url && !source.data) { - error('Invalid parameter array, need either .data or .url'); - } - - // copy/use all keys as is except 'url' -- full path is required var params = {}; for (var key in source) { if (key === 'url' && typeof window !== 'undefined') { + // The full path is required in the 'url' field. params[key] = combineUrl(window.location.href, source[key]); continue; + } else if (key === 'range') { + continue; + } else if (key === 'data' && !(source[key] instanceof Uint8Array)) { + // Converting string or array-like data to Uint8Array. + var pdfBytes = source[key]; + if (typeof pdfBytes === 'string') { + params[key] = stringToBytes(pdfBytes); + } else if (typeof pdfBytes === 'object' && pdfBytes !== null && + !isNaN(pdfBytes.length)) { + params[key] = new Uint8Array(pdfBytes); + } else { + error('Invalid PDF binary data: either typed array, string or ' + + 'array-like object is expected in the data property.'); + } + continue; } params[key] = source[key]; } workerInitializedCapability = createPromiseCapability(); - workerReadyCapability = createPromiseCapability(); - transport = new WorkerTransport(workerInitializedCapability, - workerReadyCapability, pdfDataRangeTransport, - progressCallback); + transport = new WorkerTransport(workerInitializedCapability, source.range); workerInitializedCapability.promise.then(function transportInitialized() { - transport.passwordCallback = passwordCallback; - transport.fetchDocument(params); + transport.fetchDocument(task, params); }); - return workerReadyCapability.promise; + + return task; }; +/** + * PDF document loading operation. + * @class + */ +var PDFDocumentLoadingTask = (function PDFDocumentLoadingTaskClosure() { + /** @constructs PDFDocumentLoadingTask */ + function PDFDocumentLoadingTask() { + this._capability = createPromiseCapability(); + + /** + * Callback to request a password if wrong or no password was provided. + * The callback receives two parameters: function that needs to be called + * with new password and reason (see {PasswordResponses}). + */ + this.onPassword = null; + + /** + * Callback to be able to monitor the loading progress of the PDF file + * (necessary to implement e.g. a loading bar). The callback receives + * an {Object} with the properties: {number} loaded and {number} total. + */ + this.onProgress = null; + } + + PDFDocumentLoadingTask.prototype = + /** @lends PDFDocumentLoadingTask.prototype */ { + /** + * @return {Promise} + */ + get promise() { + return this._capability.promise; + }, + + // TODO add cancel or abort method + + /** + * Registers callbacks to indicate the document loading completion. + * + * @param {function} onFulfilled The callback for the loading completion. + * @param {function} onRejected The callback for the loading failure. + * @return {Promise} A promise that is resolved after the onFulfilled or + * onRejected callback. + */ + then: function PDFDocumentLoadingTask_then(onFulfilled, onRejected) { + return this.promise.then.apply(this.promise, arguments); + } + }; + + return PDFDocumentLoadingTask; +})(); + +/** + * Abstract class to support range requests file loading. + * @class + */ +var PDFDataRangeTransport = (function pdfDataRangeTransportClosure() { + /** + * @constructs PDFDataRangeTransport + * @param {number} length + * @param {Uint8Array} initialData + */ + function PDFDataRangeTransport(length, initialData) { + this.length = length; + this.initialData = initialData; + + this._rangeListeners = []; + this._progressListeners = []; + this._progressiveReadListeners = []; + this._readyCapability = createPromiseCapability(); + } + PDFDataRangeTransport.prototype = + /** @lends PDFDataRangeTransport.prototype */ { + addRangeListener: + function PDFDataRangeTransport_addRangeListener(listener) { + this._rangeListeners.push(listener); + }, + + addProgressListener: + function PDFDataRangeTransport_addProgressListener(listener) { + this._progressListeners.push(listener); + }, + + addProgressiveReadListener: + function PDFDataRangeTransport_addProgressiveReadListener(listener) { + this._progressiveReadListeners.push(listener); + }, + + onDataRange: function PDFDataRangeTransport_onDataRange(begin, chunk) { + var listeners = this._rangeListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](begin, chunk); + } + }, + + onDataProgress: function PDFDataRangeTransport_onDataProgress(loaded) { + this._readyCapability.promise.then(function () { + var listeners = this._progressListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](loaded); + } + }.bind(this)); + }, + + onDataProgressiveRead: + function PDFDataRangeTransport_onDataProgress(chunk) { + this._readyCapability.promise.then(function () { + var listeners = this._progressiveReadListeners; + for (var i = 0, n = listeners.length; i < n; ++i) { + listeners[i](chunk); + } + }.bind(this)); + }, + + transportReady: function PDFDataRangeTransport_transportReady() { + this._readyCapability.resolve(); + }, + + requestDataRange: + function PDFDataRangeTransport_requestDataRange(begin, end) { + throw new Error('Abstract method PDFDataRangeTransport.requestDataRange'); + } + }; + return PDFDataRangeTransport; +})(); + +PDFJS.PDFDataRangeTransport = PDFDataRangeTransport; + /** * Proxy to a PDFDocument in the worker thread. Also, contains commonly used * properties that can be read synchronously. @@ -1639,7 +1809,7 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { return this.transport.downloadInfoCapability.promise; }, /** - * @returns {Promise} A promise this is resolved with current stats about + * @return {Promise} A promise this is resolved with current stats about * document structures (see {@link PDFDocumentStats}). */ getStats: function PDFDocumentProxy_getStats() { @@ -1703,7 +1873,7 @@ var PDFDocumentProxy = (function PDFDocumentProxyClosure() { * (default value is 'display'). * @property {Object} imageLayer - (optional) An object that has beginLayout, * endLayout and appendImage functions. - * @property {function} continueCallback - (optional) A function that will be + * @property {function} continueCallback - (deprecated) A function that will be * called each time the rendering is paused. To continue * rendering call the function that is the first argument * to the callback. @@ -1836,7 +2006,12 @@ var PDFPageProxy = (function PDFPageProxyClosure() { intentState.renderTasks = []; } intentState.renderTasks.push(internalRenderTask); - var renderTask = new RenderTask(internalRenderTask); + var renderTask = internalRenderTask.task; + + // Obsolete parameter support + if (params.continueCallback) { + renderTask.onContinue = params.continueCallback; + } var self = this; intentState.displayReadyCapability.promise.then( @@ -2001,19 +2176,16 @@ var PDFPageProxy = (function PDFPageProxyClosure() { * @ignore */ var WorkerTransport = (function WorkerTransportClosure() { - function WorkerTransport(workerInitializedCapability, workerReadyCapability, - pdfDataRangeTransport, progressCallback) { + function WorkerTransport(workerInitializedCapability, pdfDataRangeTransport) { this.pdfDataRangeTransport = pdfDataRangeTransport; - this.workerInitializedCapability = workerInitializedCapability; - this.workerReadyCapability = workerReadyCapability; - this.progressCallback = progressCallback; this.commonObjs = new PDFObjects(); + this.loadingTask = null; + this.pageCache = []; this.pagePromises = []; this.downloadInfoCapability = createPromiseCapability(); - this.passwordCallback = null; // If worker support isn't disabled explicit and the browser has worker // support, create a new web worker and test if it/the browser fullfills @@ -2152,48 +2324,50 @@ var WorkerTransport = (function WorkerTransportClosure() { this.numPages = data.pdfInfo.numPages; var pdfDocument = new PDFDocumentProxy(pdfInfo, this); this.pdfDocument = pdfDocument; - this.workerReadyCapability.resolve(pdfDocument); + this.loadingTask._capability.resolve(pdfDocument); }, this); messageHandler.on('NeedPassword', function transportNeedPassword(exception) { - if (this.passwordCallback) { - return this.passwordCallback(updatePassword, - PasswordResponses.NEED_PASSWORD); + var loadingTask = this.loadingTask; + if (loadingTask.onPassword) { + return loadingTask.onPassword(updatePassword, + PasswordResponses.NEED_PASSWORD); } - this.workerReadyCapability.reject( + loadingTask._capability.reject( new PasswordException(exception.message, exception.code)); }, this); messageHandler.on('IncorrectPassword', function transportIncorrectPassword(exception) { - if (this.passwordCallback) { - return this.passwordCallback(updatePassword, - PasswordResponses.INCORRECT_PASSWORD); + var loadingTask = this.loadingTask; + if (loadingTask.onPassword) { + return loadingTask.onPassword(updatePassword, + PasswordResponses.INCORRECT_PASSWORD); } - this.workerReadyCapability.reject( + loadingTask._capability.reject( new PasswordException(exception.message, exception.code)); }, this); messageHandler.on('InvalidPDF', function transportInvalidPDF(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new InvalidPDFException(exception.message)); }, this); messageHandler.on('MissingPDF', function transportMissingPDF(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new MissingPDFException(exception.message)); }, this); messageHandler.on('UnexpectedResponse', function transportUnexpectedResponse(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new UnexpectedResponseException(exception.message, exception.status)); }, this); messageHandler.on('UnknownError', function transportUnknownError(exception) { - this.workerReadyCapability.reject( + this.loadingTask._capability.reject( new UnknownErrorException(exception.message, exception.details)); }, this); @@ -2288,8 +2462,9 @@ var WorkerTransport = (function WorkerTransportClosure() { }, this); messageHandler.on('DocProgress', function transportDocProgress(data) { - if (this.progressCallback) { - this.progressCallback({ + var loadingTask = this.loadingTask; + if (loadingTask.onProgress) { + loadingTask.onProgress({ loaded: data.loaded, total: data.total }); @@ -2349,10 +2524,16 @@ var WorkerTransport = (function WorkerTransportClosure() { }); }, - fetchDocument: function WorkerTransport_fetchDocument(source) { + fetchDocument: function WorkerTransport_fetchDocument(loadingTask, source) { + this.loadingTask = loadingTask; + source.disableAutoFetch = PDFJS.disableAutoFetch; source.disableStream = PDFJS.disableStream; source.chunkedViewerLoading = !!this.pdfDataRangeTransport; + if (this.pdfDataRangeTransport) { + source.length = this.pdfDataRangeTransport.length; + source.initialData = this.pdfDataRangeTransport.initialData; + } this.messageHandler.send('GetDocRequest', { source: source, disableRange: PDFJS.disableRange, @@ -2563,26 +2744,37 @@ var PDFObjects = (function PDFObjectsClosure() { */ var RenderTask = (function RenderTaskClosure() { function RenderTask(internalRenderTask) { - this.internalRenderTask = internalRenderTask; + this._internalRenderTask = internalRenderTask; + /** - * Promise for rendering task completion. - * @type {Promise} + * Callback for incremental rendering -- a function that will be called + * each time the rendering is paused. To continue rendering call the + * function that is the first argument to the callback. + * @type {function} */ - this.promise = this.internalRenderTask.capability.promise; + this.onContinue = null; } RenderTask.prototype = /** @lends RenderTask.prototype */ { + /** + * Promise for rendering task completion. + * @return {Promise} + */ + get promise() { + return this._internalRenderTask.capability.promise; + }, + /** * Cancels the rendering task. If the task is currently rendering it will * not be cancelled until graphics pauses with a timeout. The promise that * this object extends will resolved when cancelled. */ cancel: function RenderTask_cancel() { - this.internalRenderTask.cancel(); + this._internalRenderTask.cancel(); }, /** - * Registers callback to indicate the rendering task completion. + * Registers callbacks to indicate the rendering task completion. * * @param {function} onFulfilled The callback for the rendering completion. * @param {function} onRejected The callback for the rendering failure. @@ -2590,7 +2782,7 @@ var RenderTask = (function RenderTaskClosure() { * onRejected callback. */ then: function RenderTask_then(onFulfilled, onRejected) { - return this.promise.then(onFulfilled, onRejected); + return this.promise.then.apply(this.promise, arguments); } }; @@ -2617,6 +2809,7 @@ var InternalRenderTask = (function InternalRenderTaskClosure() { this.graphicsReady = false; this.cancelled = false; this.capability = createPromiseCapability(); + this.task = new RenderTask(this); // caching this-bound methods this._continueBound = this._continue.bind(this); this._scheduleNextBound = this._scheduleNext.bind(this); @@ -2679,8 +2872,8 @@ var InternalRenderTask = (function InternalRenderTaskClosure() { if (this.cancelled) { return; } - if (this.params.continueCallback) { - this.params.continueCallback(this._scheduleNextBound); + if (this.task.onContinue) { + this.task.onContinue.call(this.task, this._scheduleNextBound); } else { this._scheduleNext(); } diff --git a/browser/extensions/pdfjs/content/build/pdf.worker.js b/browser/extensions/pdfjs/content/build/pdf.worker.js index 0db3119f503d..c69e081395ec 100644 --- a/browser/extensions/pdfjs/content/build/pdf.worker.js +++ b/browser/extensions/pdfjs/content/build/pdf.worker.js @@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') { (typeof window !== 'undefined' ? window : this).PDFJS = {}; } -PDFJS.version = '1.0.1040'; -PDFJS.build = '997096f'; +PDFJS.version = '1.0.1130'; +PDFJS.build = 'e4f0ae2'; (function pdfjsWrapper() { // Use strict in our context only - users might not want it @@ -341,6 +341,7 @@ function isValidUrl(url, allowRelative) { case 'https': case 'ftp': case 'mailto': + case 'tel': return true; default: return false; @@ -470,6 +471,8 @@ var XRefParseException = (function XRefParseExceptionClosure() { function bytesToString(bytes) { + assert(bytes !== null && typeof bytes === 'object' && + bytes.length !== undefined, 'Invalid argument for bytesToString'); var length = bytes.length; var MAX_ARGUMENT_COUNT = 8192; if (length < MAX_ARGUMENT_COUNT) { @@ -485,6 +488,7 @@ function bytesToString(bytes) { } function stringToBytes(str) { + assert(typeof str === 'string', 'Invalid argument for stringToBytes'); var length = str.length; var bytes = new Uint8Array(length); for (var i = 0; i < length; ++i) { @@ -1449,6 +1453,9 @@ var ChunkedStream = (function ChunkedStreamClosure() { getUint16: function ChunkedStream_getUint16() { var b0 = this.getByte(); var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } return (b0 << 8) + b1; }, @@ -14056,7 +14063,7 @@ var SpecialPUASymbols = { '63731': 0x23A9, // braceleftbt (0xF8F3) '63740': 0x23AB, // bracerighttp (0xF8FC) '63741': 0x23AC, // bracerightmid (0xF8FD) - '63742': 0x23AD, // bracerightmid (0xF8FE) + '63742': 0x23AD, // bracerightbt (0xF8FE) '63726': 0x23A1, // bracketlefttp (0xF8EE) '63727': 0x23A2, // bracketleftex (0xF8EF) '63728': 0x23A3, // bracketleftbt (0xF8F0) @@ -15996,7 +16003,7 @@ var Font = (function FontClosure() { // to be used with the canvas.font. var fontName = name.replace(/[,_]/g, '-'); var isStandardFont = !!stdFontMap[fontName] || - (nonStdFontMap[fontName] && !!stdFontMap[nonStdFontMap[fontName]]); + !!(nonStdFontMap[fontName] && stdFontMap[nonStdFontMap[fontName]]); fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName; this.bold = (fontName.search(/bold/gi) !== -1); @@ -29595,6 +29602,102 @@ var Parser = (function ParserClosure() { } return ((stream.pos - 4) - startPos); }, + /** + * Find the EOI (end-of-image) marker 0xFFD9 of the stream. + * @returns {number} The inline stream length. + */ + findDCTDecodeInlineStreamEnd: + function Parser_findDCTDecodeInlineStreamEnd(stream) { + var startPos = stream.pos, foundEOI = false, b, markerLength, length; + while ((b = stream.getByte()) !== -1) { + if (b !== 0xFF) { // Not a valid marker. + continue; + } + switch (stream.getByte()) { + case 0x00: // Byte stuffing. + // 0xFF00 appears to be a very common byte sequence in JPEG images. + break; + + case 0xFF: // Fill byte. + // Avoid skipping a valid marker, resetting the stream position. + stream.skip(-1); + break; + + case 0xD9: // EOI + foundEOI = true; + break; + + case 0xC0: // SOF0 + case 0xC1: // SOF1 + case 0xC2: // SOF2 + case 0xC3: // SOF3 + + case 0xC5: // SOF5 + case 0xC6: // SOF6 + case 0xC7: // SOF7 + + case 0xC9: // SOF9 + case 0xCA: // SOF10 + case 0xCB: // SOF11 + + case 0xCD: // SOF13 + case 0xCE: // SOF14 + case 0xCF: // SOF15 + + case 0xC4: // DHT + case 0xCC: // DAC + + case 0xDA: // SOS + case 0xDB: // DQT + case 0xDC: // DNL + case 0xDD: // DRI + case 0xDE: // DHP + case 0xDF: // EXP + + case 0xE0: // APP0 + case 0xE1: // APP1 + case 0xE2: // APP2 + case 0xE3: // APP3 + case 0xE4: // APP4 + case 0xE5: // APP5 + case 0xE6: // APP6 + case 0xE7: // APP7 + case 0xE8: // APP8 + case 0xE9: // APP9 + case 0xEA: // APP10 + case 0xEB: // APP11 + case 0xEC: // APP12 + case 0xED: // APP13 + case 0xEE: // APP14 + case 0xEF: // APP15 + + case 0xFE: // COM + // The marker should be followed by the length of the segment. + markerLength = stream.getUint16(); + if (markerLength > 2) { + // |markerLength| contains the byte length of the marker segment, + // including its own length (2 bytes) and excluding the marker. + stream.skip(markerLength - 2); // Jump to the next marker. + } else { + // The marker length is invalid, resetting the stream position. + stream.skip(-2); + } + break; + } + if (foundEOI) { + break; + } + } + length = stream.pos - startPos; + if (b === -1) { + warn('Inline DCTDecode image stream: ' + + 'EOI marker not found, searching for /EI/ instead.'); + stream.skip(-length); // Reset the stream position. + return this.findDefaultInlineStreamEnd(stream); + } + this.inlineStreamSkipEI(stream); + return length; + }, /** * Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream. * @returns {number} The inline stream length. @@ -29686,7 +29789,9 @@ var Parser = (function ParserClosure() { // Parse image stream. var startPos = stream.pos, length, i, ii; - if (filterName === 'ASCII85Decide' || filterName === 'A85') { + if (filterName === 'DCTDecode' || filterName === 'DCT') { + length = this.findDCTDecodeInlineStreamEnd(stream); + } else if (filterName === 'ASCII85Decide' || filterName === 'A85') { length = this.findASCII85DecodeInlineStreamEnd(stream); } else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') { length = this.findASCIIHexDecodeInlineStreamEnd(stream); @@ -30613,6 +30718,9 @@ var Stream = (function StreamClosure() { getUint16: function Stream_getUint16() { var b0 = this.getByte(); var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } return (b0 << 8) + b1; }, getInt32: function Stream_getInt32() { @@ -30740,6 +30848,9 @@ var DecodeStream = (function DecodeStreamClosure() { getUint16: function DecodeStream_getUint16() { var b0 = this.getByte(); var b1 = this.getByte(); + if (b0 === -1 || b1 === -1) { + return -1; + } return (b0 << 8) + b1; }, getInt32: function DecodeStream_getInt32() { @@ -34839,11 +34950,6 @@ var JpxImage = (function JpxImageClosure() { context.QCC = []; context.COC = []; break; - case 0xFF55: // Tile-part lengths, main header (TLM) - var Ltlm = readUint16(data, position); // Marker segment length - // Skip tile length markers - position += Ltlm; - break; case 0xFF5C: // Quantization default (QCD) length = readUint16(data, position); var qcd = {}; @@ -35033,6 +35139,9 @@ var JpxImage = (function JpxImageClosure() { length = tile.dataEnd - position; parseTilePackets(context, data, position, length); break; + case 0xFF55: // Tile-part lengths, main header (TLM) + case 0xFF57: // Packet length, main header (PLM) + case 0xFF58: // Packet length, tile-part header (PLT) case 0xFF64: // Comment (COM) length = readUint16(data, position); // skipping content @@ -35373,7 +35482,7 @@ var JpxImage = (function JpxImageClosure() { r = 0; c = 0; p = 0; - + this.nextPacket = function JpxImage_nextPacket() { // Section B.12.1.3 Resolution-position-component-layer for (; r <= maxDecompositionLevelsCount; r++) { @@ -35457,7 +35566,7 @@ var JpxImage = (function JpxImageClosure() { var componentsCount = siz.Csiz; var precinctsSizes = getPrecinctSizesInImageScale(tile); var l = 0, r = 0, c = 0, px = 0, py = 0; - + this.nextPacket = function JpxImage_nextPacket() { // Section B.12.1.5 Component-position-resolution-layer for (; c < componentsCount; ++c) { @@ -37030,10 +37139,9 @@ var Jbig2Image = (function Jbig2ImageClosure() { // At each pixel: Clear contextLabel pixels that are shifted // out of the context, then add new ones. - // If j + n is out of range at the right image border, then - // the undefined value of bitmap[i - 2][j + n] is shifted to 0 contextLabel = ((contextLabel & OLD_PIXEL_MASK) << 1) | - (row2[j + 3] << 11) | (row1[j + 4] << 4) | pixel; + (j + 3 < width ? row2[j + 3] << 11 : 0) | + (j + 4 < width ? row1[j + 4] << 4 : 0) | pixel; } } diff --git a/browser/extensions/pdfjs/content/web/viewer.css b/browser/extensions/pdfjs/content/web/viewer.css index 3ccd604aa81f..b51e608e9f42 100644 --- a/browser/extensions/pdfjs/content/web/viewer.css +++ b/browser/extensions/pdfjs/content/web/viewer.css @@ -20,6 +20,7 @@ right: 0; bottom: 0; overflow: hidden; + opacity: 0.2; } .textLayer > div { @@ -55,6 +56,9 @@ background-color: rgb(0, 100, 0); } +.textLayer ::selection { background: rgb(0,0,255); } +.textLayer ::-moz-selection { background: rgb(0,0,255); } + .pdfViewer .canvasWrapper { overflow: hidden; } @@ -1153,9 +1157,13 @@ html[dir='rtl'] .verticalToolbarSeparator { margin-bottom: 10px; } +#thumbnailView > a:last-of-type > .thumbnail:not([data-loaded]) { + margin-bottom: 9px; +} + .thumbnail:not([data-loaded]) { border: 1px dashed rgba(255, 255, 255, 0.5); - margin-bottom: 10px; + margin: -1px -1px 4px -1px; } .thumbnailImage { @@ -1164,6 +1172,8 @@ html[dir='rtl'] .verticalToolbarSeparator { box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(0, 0, 0, 0.3); opacity: 0.8; z-index: 99; + background-color: white; + background-clip: content-box; } .thumbnailSelectionRing { @@ -1294,19 +1304,12 @@ html[dir='rtl'] .attachmentsItem > button { cursor: default; } - /* TODO: file FF bug to support ::-moz-selection:window-inactive so we can override the opaque grey background when the window is inactive; see https://bugzilla.mozilla.org/show_bug.cgi?id=706209 */ ::selection { background: rgba(0,0,255,0.3); } ::-moz-selection { background: rgba(0,0,255,0.3); } -.textLayer ::selection { background: rgb(0,0,255); } -.textLayer ::-moz-selection { background: rgb(0,0,255); } -.textLayer { - opacity: 0.2; -} - #errorWrapper { background: none repeat scroll 0 0 #FF5555; color: white; diff --git a/browser/extensions/pdfjs/content/web/viewer.js b/browser/extensions/pdfjs/content/web/viewer.js index 16cb6113e5ee..991c191229d6 100644 --- a/browser/extensions/pdfjs/content/web/viewer.js +++ b/browser/extensions/pdfjs/content/web/viewer.js @@ -16,10 +16,10 @@ */ /* globals PDFJS, PDFBug, FirefoxCom, Stats, Cache, ProgressBar, DownloadManager, getFileName, scrollIntoView, getPDFFileNameFromURL, - PDFHistory, Preferences, SidebarView, ViewHistory, PageView, + PDFHistory, Preferences, SidebarView, ViewHistory, Stats, PDFThumbnailViewer, URL, noContextMenuHandler, SecondaryToolbar, PasswordPrompt, PresentationMode, HandTool, Promise, - DocumentProperties, DocumentOutlineView, DocumentAttachmentsView, + DocumentProperties, PDFOutlineView, PDFAttachmentView, OverlayManager, PDFFindController, PDFFindBar, getVisibleElements, watchScroll, PDFViewer, PDFRenderingQueue, PresentationModeState, RenderingStates, DEFAULT_SCALE, UNKNOWN_SCALE, @@ -51,7 +51,6 @@ var UNKNOWN_SCALE = 0; var MAX_AUTO_SCALE = 1.25; var SCROLLBAR_PADDING = 40; var VERTICAL_PADDING = 5; -var DEFAULT_CACHE_SIZE = 10; // optimised CSS custom property getter/setter var CustomStyle = (function CustomStyleClosure() { @@ -379,26 +378,6 @@ var ProgressBar = (function ProgressBarClosure() { return ProgressBar; })(); -var Cache = function cacheCache(size) { - var data = []; - this.push = function cachePush(view) { - var i = data.indexOf(view); - if (i >= 0) { - data.splice(i, 1); - } - data.push(view); - if (data.length > size) { - data.shift().destroy(); - } - }; - this.resize = function (newSize) { - size = newSize; - while (data.length > size) { - data.shift().destroy(); - } - }; -}; - var DEFAULT_PREFERENCES = { @@ -931,6 +910,9 @@ var FindStates = { FIND_PENDING: 3 }; +var FIND_SCROLL_OFFSET_TOP = -50; +var FIND_SCROLL_OFFSET_LEFT = -400; + /** * Provides "search" or "find" functionality for the PDF. * This object actually performs the search for a given string. @@ -1104,8 +1086,6 @@ var PDFFindController = (function PDFFindControllerClosure() { }, updatePage: function PDFFindController_updatePage(index) { - var page = this.pdfViewer.getPageView(index); - if (this.selected.pageIdx === index) { // If the page is selected, scroll the page into view, which triggers // rendering the page, which adds the textLayer. Once the textLayer is @@ -1113,6 +1093,7 @@ var PDFFindController = (function PDFFindControllerClosure() { this.pdfViewer.scrollPageIntoView(index + 1); } + var page = this.pdfViewer.getPageView(index); if (page.textLayer) { page.textLayer.updateMatches(); } @@ -1216,6 +1197,26 @@ var PDFFindController = (function PDFFindControllerClosure() { } }, + /** + * The method is called back from the text layer when match presentation + * is updated. + * @param {number} pageIndex - page index. + * @param {number} index - match index. + * @param {Array} elements - text layer div elements array. + * @param {number} beginIdx - start index of the div array for the match. + * @param {number} endIdx - end index of the div array for the match. + */ + updateMatchPosition: function PDFFindController_updateMatchPosition( + pageIndex, index, elements, beginIdx, endIdx) { + if (this.selected.matchIdx === index && + this.selected.pageIdx === pageIndex) { + scrollIntoView(elements[beginIdx], { + top: FIND_SCROLL_OFFSET_TOP, + left: FIND_SCROLL_OFFSET_LEFT + }); + } + }, + nextPageMatch: function PDFFindController_nextPageMatch() { if (this.resumePageIdx !== null) { console.error('There can only be one pending page.'); @@ -2672,6 +2673,7 @@ var PresentationModeState = { }; var IGNORE_CURRENT_POSITION_ON_ZOOM = false; +var DEFAULT_CACHE_SIZE = 10; var CLEANUP_TIMEOUT = 30000; @@ -2822,7 +2824,10 @@ var PDFRenderingQueue = (function PDFRenderingQueueClosure() { break; case RenderingStates.INITIAL: this.highestPriorityPage = view.renderingId; - view.draw(this.renderHighestPriority.bind(this)); + var continueRendering = function () { + this.renderHighestPriority(); + }.bind(this); + view.draw().then(continueRendering, continueRendering); break; } return true; @@ -2833,601 +2838,525 @@ var PDFRenderingQueue = (function PDFRenderingQueueClosure() { })(); +var TEXT_LAYER_RENDER_DELAY = 200; // ms + /** - * @constructor - * @param {HTMLDivElement} container - The viewer element. - * @param {number} id - The page unique ID (normally its number). - * @param {number} scale - The page scale display. - * @param {PageViewport} defaultViewport - The page viewport. - * @param {IPDFLinkService} linkService - The navigation/linking service. - * @param {PDFRenderingQueue} renderingQueue - The rendering queue object. - * @param {Cache} cache - The page cache. - * @param {PDFPageSource} pageSource - * @param {PDFViewer} viewer - * + * @typedef {Object} PDFPageViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {number} id - The page unique ID (normally its number). + * @property {number} scale - The page scale display. + * @property {PageViewport} defaultViewport - The page viewport. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + * @property {IPDFTextLayerFactory} textLayerFactory + * @property {IPDFAnnotationsLayerFactory} annotationsLayerFactory + */ + +/** + * @class * @implements {IRenderableView} */ -var PageView = function pageView(container, id, scale, defaultViewport, - linkService, renderingQueue, cache, - pageSource, viewer) { - this.id = id; - this.renderingId = 'page' + id; +var PDFPageView = (function PDFPageViewClosure() { + /** + * @constructs PDFPageView + * @param {PDFPageViewOptions} options + */ + function PDFPageView(options) { + var container = options.container; + var id = options.id; + var scale = options.scale; + var defaultViewport = options.defaultViewport; + var renderingQueue = options.renderingQueue; + var textLayerFactory = options.textLayerFactory; + var annotationsLayerFactory = options.annotationsLayerFactory; - this.rotation = 0; - this.scale = scale || 1.0; - this.viewport = defaultViewport; - this.pdfPageRotate = defaultViewport.rotation; - this.hasRestrictedScaling = false; + this.id = id; + this.renderingId = 'page' + id; - this.linkService = linkService; - this.renderingQueue = renderingQueue; - this.cache = cache; - this.pageSource = pageSource; - this.viewer = viewer; + this.rotation = 0; + this.scale = scale || 1.0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + this.hasRestrictedScaling = false; - this.renderingState = RenderingStates.INITIAL; - this.resume = null; + this.renderingQueue = renderingQueue; + this.textLayerFactory = textLayerFactory; + this.annotationsLayerFactory = annotationsLayerFactory; - this.textLayer = null; - - this.zoomLayer = null; - - this.annotationLayer = null; - - var anchor = document.createElement('a'); - anchor.name = '' + this.id; - - var div = this.el = document.createElement('div'); - div.id = 'pageContainer' + this.id; - div.className = 'page'; - div.style.width = Math.floor(this.viewport.width) + 'px'; - div.style.height = Math.floor(this.viewport.height) + 'px'; - - container.appendChild(anchor); - container.appendChild(div); - - this.setPdfPage = function pageViewSetPdfPage(pdfPage) { - this.pdfPage = pdfPage; - this.pdfPageRotate = pdfPage.rotate; - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, totalRotation); - this.stats = pdfPage.stats; - this.reset(); - }; - - this.destroy = function pageViewDestroy() { - this.zoomLayer = null; - this.reset(); - if (this.pdfPage) { - this.pdfPage.destroy(); - } - }; - - this.reset = function pageViewReset(keepAnnotations) { - if (this.renderTask) { - this.renderTask.cancel(); - } - this.resume = null; this.renderingState = RenderingStates.INITIAL; + this.resume = null; + this.onBeforeDraw = null; + this.onAfterDraw = null; + + this.textLayer = null; + + this.zoomLayer = null; + + this.annotationLayer = null; + + var div = document.createElement('div'); + div.id = 'pageContainer' + this.id; + div.className = 'page'; div.style.width = Math.floor(this.viewport.width) + 'px'; div.style.height = Math.floor(this.viewport.height) + 'px'; + this.el = div; // TODO replace 'el' property usage + this.div = div; - var childNodes = div.childNodes; - for (var i = div.childNodes.length - 1; i >= 0; i--) { - var node = childNodes[i]; - if ((this.zoomLayer && this.zoomLayer === node) || - (keepAnnotations && this.annotationLayer === node)) { - continue; - } - div.removeChild(node); - } - div.removeAttribute('data-loaded'); - - if (keepAnnotations) { - if (this.annotationLayer) { - // Hide annotationLayer until all elements are resized - // so they are not displayed on the already-resized page - this.annotationLayer.setAttribute('hidden', 'true'); - } - } else { - this.annotationLayer = null; - } - - if (this.canvas) { - // Zeroing the width and height causes Firefox to release graphics - // resources immediately, which can greatly reduce memory consumption. - this.canvas.width = 0; - this.canvas.height = 0; - delete this.canvas; - } - - this.loadingIconDiv = document.createElement('div'); - this.loadingIconDiv.className = 'loadingIcon'; - div.appendChild(this.loadingIconDiv); - }; - - this.update = function pageViewUpdate(scale, rotation) { - this.scale = scale || this.scale; - - if (typeof rotation !== 'undefined') { - this.rotation = rotation; - } - - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = this.viewport.clone({ - scale: this.scale * CSS_UNITS, - rotation: totalRotation - }); - - var isScalingRestricted = false; - if (this.canvas && PDFJS.maxCanvasPixels > 0) { - var ctx = this.canvas.getContext('2d'); - var outputScale = getOutputScale(ctx); - var pixelsInViewport = this.viewport.width * this.viewport.height; - var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); - if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * - ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > - PDFJS.maxCanvasPixels) { - isScalingRestricted = true; - } - } - - if (this.canvas && - (PDFJS.useOnlyCssZoom || - (this.hasRestrictedScaling && isScalingRestricted))) { - this.cssTransform(this.canvas, true); - return; - } else if (this.canvas && !this.zoomLayer) { - this.zoomLayer = this.canvas.parentNode; - this.zoomLayer.style.position = 'absolute'; - } - if (this.zoomLayer) { - this.cssTransform(this.zoomLayer.firstChild); - } - this.reset(true); - }; - - this.cssTransform = function pageCssTransform(canvas, redrawAnnotations) { - // Scale canvas, canvas wrapper, and page container. - var width = this.viewport.width; - var height = this.viewport.height; - canvas.style.width = canvas.parentNode.style.width = div.style.width = - Math.floor(width) + 'px'; - canvas.style.height = canvas.parentNode.style.height = div.style.height = - Math.floor(height) + 'px'; - // The canvas may have been originally rotated, so rotate relative to that. - var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; - var absRotation = Math.abs(relativeRotation); - var scaleX = 1, scaleY = 1; - if (absRotation === 90 || absRotation === 270) { - // Scale x and y because of the rotation. - scaleX = height / width; - scaleY = width / height; - } - var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + - 'scale(' + scaleX + ',' + scaleY + ')'; - CustomStyle.setProp('transform', canvas, cssTransform); - - if (this.textLayer) { - // Rotating the text layer is more complicated since the divs inside the - // the text layer are rotated. - // TODO: This could probably be simplified by drawing the text layer in - // one orientation then rotating overall. - var textLayerViewport = this.textLayer.viewport; - var textRelativeRotation = this.viewport.rotation - - textLayerViewport.rotation; - var textAbsRotation = Math.abs(textRelativeRotation); - var scale = width / textLayerViewport.width; - if (textAbsRotation === 90 || textAbsRotation === 270) { - scale = width / textLayerViewport.height; - } - var textLayerDiv = this.textLayer.textLayerDiv; - var transX, transY; - switch (textAbsRotation) { - case 0: - transX = transY = 0; - break; - case 90: - transX = 0; - transY = '-' + textLayerDiv.style.height; - break; - case 180: - transX = '-' + textLayerDiv.style.width; - transY = '-' + textLayerDiv.style.height; - break; - case 270: - transX = '-' + textLayerDiv.style.width; - transY = 0; - break; - default: - console.error('Bad rotation value.'); - break; - } - CustomStyle.setProp('transform', textLayerDiv, - 'rotate(' + textAbsRotation + 'deg) ' + - 'scale(' + scale + ', ' + scale + ') ' + - 'translate(' + transX + ', ' + transY + ')'); - CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); - } - - if (redrawAnnotations && this.annotationLayer) { - setupAnnotations(div, this.pdfPage, this.viewport); - } - }; - - Object.defineProperty(this, 'width', { - get: function PageView_getWidth() { - return this.viewport.width; - }, - enumerable: true - }); - - Object.defineProperty(this, 'height', { - get: function PageView_getHeight() { - return this.viewport.height; - }, - enumerable: true - }); - - var self = this; - - function setupAnnotations(pageDiv, pdfPage, viewport) { - - function bindLink(link, dest) { - link.href = linkService.getDestinationHash(dest); - link.onclick = function pageViewSetupLinksOnclick() { - if (dest) { - linkService.navigateTo(dest); - } - return false; - }; - if (dest) { - link.className = 'internalLink'; - } - } - - function bindNamedAction(link, action) { - link.href = linkService.getAnchorUrl(''); - link.onclick = function pageViewSetupNamedActionOnClick() { - linkService.executeNamedAction(action); - return false; - }; - link.className = 'internalLink'; - } - - pdfPage.getAnnotations().then(function(annotationsData) { - viewport = viewport.clone({ dontFlip: true }); - var transform = viewport.transform; - var transformStr = 'matrix(' + transform.join(',') + ')'; - var data, element, i, ii; - - if (self.annotationLayer) { - // If an annotationLayer already exists, refresh its children's - // transformation matrices - for (i = 0, ii = annotationsData.length; i < ii; i++) { - data = annotationsData[i]; - element = self.annotationLayer.querySelector( - '[data-annotation-id="' + data.id + '"]'); - if (element) { - CustomStyle.setProp('transform', element, transformStr); - } - } - // See this.reset() - self.annotationLayer.removeAttribute('hidden'); - } else { - for (i = 0, ii = annotationsData.length; i < ii; i++) { - data = annotationsData[i]; - if (!data || !data.hasHtml) { - continue; - } - - element = PDFJS.AnnotationUtils.getHtmlElement(data, - pdfPage.commonObjs); - element.setAttribute('data-annotation-id', data.id); - mozL10n.translate(element); - - var rect = data.rect; - var view = pdfPage.view; - rect = PDFJS.Util.normalizeRect([ - rect[0], - view[3] - rect[1] + view[1], - rect[2], - view[3] - rect[3] + view[1] - ]); - element.style.left = rect[0] + 'px'; - element.style.top = rect[1] + 'px'; - element.style.position = 'absolute'; - - CustomStyle.setProp('transform', element, transformStr); - var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; - CustomStyle.setProp('transformOrigin', element, transformOriginStr); - - if (data.subtype === 'Link' && !data.url) { - var link = element.getElementsByTagName('a')[0]; - if (link) { - if (data.action) { - bindNamedAction(link, data.action); - } else { - bindLink(link, ('dest' in data) ? data.dest : null); - } - } - } - - if (!self.annotationLayer) { - var annotationLayerDiv = document.createElement('div'); - annotationLayerDiv.className = 'annotationLayer'; - pageDiv.appendChild(annotationLayerDiv); - self.annotationLayer = annotationLayerDiv; - } - - self.annotationLayer.appendChild(element); - } - } - }); + container.appendChild(div); } - this.getPagePoint = function pageViewGetPagePoint(x, y) { - return this.viewport.convertToPdfPoint(x, y); - }; + PDFPageView.prototype = { + setPdfPage: function PDFPageView_setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(this.scale * CSS_UNITS, + totalRotation); + this.stats = pdfPage.stats; + this.reset(); + }, - this.draw = function pageviewDraw(callback) { - var pdfPage = this.pdfPage; - - if (this.pagePdfPromise) { - return; - } - if (!pdfPage) { - var promise = this.pageSource.getPage(); - promise.then(function(pdfPage) { - delete this.pagePdfPromise; - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - this.pagePdfPromise = promise; - return; - } - - if (this.renderingState !== RenderingStates.INITIAL) { - console.error('Must be in new state before drawing'); - } - - this.renderingState = RenderingStates.RUNNING; - - var viewport = this.viewport; - // Wrap the canvas so if it has a css transform for highdpi the overflow - // will be hidden in FF. - var canvasWrapper = document.createElement('div'); - canvasWrapper.style.width = div.style.width; - canvasWrapper.style.height = div.style.height; - canvasWrapper.classList.add('canvasWrapper'); - - var canvas = document.createElement('canvas'); - canvas.id = 'page' + this.id; - canvasWrapper.appendChild(canvas); - if (this.annotationLayer) { - // annotationLayer needs to stay on top - div.insertBefore(canvasWrapper, this.annotationLayer); - } else { - div.appendChild(canvasWrapper); - } - this.canvas = canvas; - - var ctx = canvas.getContext('2d'); - var outputScale = getOutputScale(ctx); - - if (PDFJS.useOnlyCssZoom) { - var actualSizeViewport = viewport.clone({ scale: CSS_UNITS }); - // Use a scale that will make the canvas be the original intended size - // of the page. - outputScale.sx *= actualSizeViewport.width / viewport.width; - outputScale.sy *= actualSizeViewport.height / viewport.height; - outputScale.scaled = true; - } - - if (PDFJS.maxCanvasPixels > 0) { - var pixelsInViewport = viewport.width * viewport.height; - var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); - if (outputScale.sx > maxScale || outputScale.sy > maxScale) { - outputScale.sx = maxScale; - outputScale.sy = maxScale; - outputScale.scaled = true; - this.hasRestrictedScaling = true; - } else { - this.hasRestrictedScaling = false; + destroy: function PDFPageView_destroy() { + this.zoomLayer = null; + this.reset(); + if (this.pdfPage) { + this.pdfPage.destroy(); } - } + }, - canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; - canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; - canvas.style.width = Math.floor(viewport.width) + 'px'; - canvas.style.height = Math.floor(viewport.height) + 'px'; - // Add the viewport so it's known what it was originally drawn with. - canvas._viewport = viewport; + reset: function PDFPageView_reset(keepAnnotations) { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.resume = null; + this.renderingState = RenderingStates.INITIAL; - var textLayerDiv = null; - var textLayer = null; - if (!PDFJS.disableTextLayer) { - textLayerDiv = document.createElement('div'); - textLayerDiv.className = 'textLayer'; - textLayerDiv.style.width = canvas.style.width; - textLayerDiv.style.height = canvas.style.height; + var div = this.div; + div.style.width = Math.floor(this.viewport.width) + 'px'; + div.style.height = Math.floor(this.viewport.height) + 'px'; + + var childNodes = div.childNodes; + var currentZoomLayer = this.zoomLayer || null; + var currentAnnotationNode = (keepAnnotations && this.annotationLayer && + this.annotationLayer.div) || null; + for (var i = childNodes.length - 1; i >= 0; i--) { + var node = childNodes[i]; + if (currentZoomLayer === node || currentAnnotationNode === node) { + continue; + } + div.removeChild(node); + } + div.removeAttribute('data-loaded'); + + if (keepAnnotations) { + if (this.annotationLayer) { + // Hide annotationLayer until all elements are resized + // so they are not displayed on the already-resized page + this.annotationLayer.hide(); + } + } else { + this.annotationLayer = null; + } + + if (this.canvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + this.canvas.width = 0; + this.canvas.height = 0; + delete this.canvas; + } + + this.loadingIconDiv = document.createElement('div'); + this.loadingIconDiv.className = 'loadingIcon'; + div.appendChild(this.loadingIconDiv); + }, + + update: function PDFPageView_update(scale, rotation) { + this.scale = scale || this.scale; + + if (typeof rotation !== 'undefined') { + this.rotation = rotation; + } + + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: this.scale * CSS_UNITS, + rotation: totalRotation + }); + + var isScalingRestricted = false; + if (this.canvas && PDFJS.maxCanvasPixels > 0) { + var ctx = this.canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + var pixelsInViewport = this.viewport.width * this.viewport.height; + var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); + if (((Math.floor(this.viewport.width) * outputScale.sx) | 0) * + ((Math.floor(this.viewport.height) * outputScale.sy) | 0) > + PDFJS.maxCanvasPixels) { + isScalingRestricted = true; + } + } + + if (this.canvas && + (PDFJS.useOnlyCssZoom || + (this.hasRestrictedScaling && isScalingRestricted))) { + this.cssTransform(this.canvas, true); + return; + } else if (this.canvas && !this.zoomLayer) { + this.zoomLayer = this.canvas.parentNode; + this.zoomLayer.style.position = 'absolute'; + } + if (this.zoomLayer) { + this.cssTransform(this.zoomLayer.firstChild); + } + this.reset(true); + }, + + /** + * Called when moved in the parent's container. + */ + updatePosition: function PDFPageView_updatePosition() { + if (this.textLayer) { + this.textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + }, + + cssTransform: function PDFPageView_transform(canvas, redrawAnnotations) { + // Scale canvas, canvas wrapper, and page container. + var width = this.viewport.width; + var height = this.viewport.height; + var div = this.div; + canvas.style.width = canvas.parentNode.style.width = div.style.width = + Math.floor(width) + 'px'; + canvas.style.height = canvas.parentNode.style.height = div.style.height = + Math.floor(height) + 'px'; + // The canvas may have been originally rotated, rotate relative to that. + var relativeRotation = this.viewport.rotation - canvas._viewport.rotation; + var absRotation = Math.abs(relativeRotation); + var scaleX = 1, scaleY = 1; + if (absRotation === 90 || absRotation === 270) { + // Scale x and y because of the rotation. + scaleX = height / width; + scaleY = width / height; + } + var cssTransform = 'rotate(' + relativeRotation + 'deg) ' + + 'scale(' + scaleX + ',' + scaleY + ')'; + CustomStyle.setProp('transform', canvas, cssTransform); + + if (this.textLayer) { + // Rotating the text layer is more complicated since the divs inside the + // the text layer are rotated. + // TODO: This could probably be simplified by drawing the text layer in + // one orientation then rotating overall. + var textLayerViewport = this.textLayer.viewport; + var textRelativeRotation = this.viewport.rotation - + textLayerViewport.rotation; + var textAbsRotation = Math.abs(textRelativeRotation); + var scale = width / textLayerViewport.width; + if (textAbsRotation === 90 || textAbsRotation === 270) { + scale = width / textLayerViewport.height; + } + var textLayerDiv = this.textLayer.textLayerDiv; + var transX, transY; + switch (textAbsRotation) { + case 0: + transX = transY = 0; + break; + case 90: + transX = 0; + transY = '-' + textLayerDiv.style.height; + break; + case 180: + transX = '-' + textLayerDiv.style.width; + transY = '-' + textLayerDiv.style.height; + break; + case 270: + transX = '-' + textLayerDiv.style.width; + transY = 0; + break; + default: + console.error('Bad rotation value.'); + break; + } + CustomStyle.setProp('transform', textLayerDiv, + 'rotate(' + textAbsRotation + 'deg) ' + + 'scale(' + scale + ', ' + scale + ') ' + + 'translate(' + transX + ', ' + transY + ')'); + CustomStyle.setProp('transformOrigin', textLayerDiv, '0% 0%'); + } + + if (redrawAnnotations && this.annotationLayer) { + this.annotationLayer.setupAnnotations(this.viewport); + } + }, + + get width() { + return this.viewport.width; + }, + + get height() { + return this.viewport.height; + }, + + getPagePoint: function PDFPageView_getPagePoint(x, y) { + return this.viewport.convertToPdfPoint(x, y); + }, + + draw: function PDFPageView_draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + + this.renderingState = RenderingStates.RUNNING; + + var pdfPage = this.pdfPage; + var viewport = this.viewport; + var div = this.div; + // Wrap the canvas so if it has a css transform for highdpi the overflow + // will be hidden in FF. + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = div.style.width; + canvasWrapper.style.height = div.style.height; + canvasWrapper.classList.add('canvasWrapper'); + + var canvas = document.createElement('canvas'); + canvas.id = 'page' + this.id; + canvasWrapper.appendChild(canvas); if (this.annotationLayer) { // annotationLayer needs to stay on top - div.insertBefore(textLayerDiv, this.annotationLayer); + div.insertBefore(canvasWrapper, this.annotationLayer.div); } else { - div.appendChild(textLayerDiv); + div.appendChild(canvasWrapper); + } + this.canvas = canvas; + + var ctx = canvas.getContext('2d'); + var outputScale = getOutputScale(ctx); + + if (PDFJS.useOnlyCssZoom) { + var actualSizeViewport = viewport.clone({ scale: CSS_UNITS }); + // Use a scale that will make the canvas be the original intended size + // of the page. + outputScale.sx *= actualSizeViewport.width / viewport.width; + outputScale.sy *= actualSizeViewport.height / viewport.height; + outputScale.scaled = true; } - textLayer = this.viewer.createTextLayerBuilder(textLayerDiv, this.id - 1, - this.viewport); - } - this.textLayer = textLayer; - - // TODO(mack): use data attributes to store these - ctx._scaleX = outputScale.sx; - ctx._scaleY = outputScale.sy; - if (outputScale.scaled) { - ctx.scale(outputScale.sx, outputScale.sy); - } - - // Rendering area - - var self = this; - function pageViewDrawCallback(error) { - // The renderTask may have been replaced by a new one, so only remove the - // reference to the renderTask if it matches the one that is triggering - // this callback. - if (renderTask === self.renderTask) { - self.renderTask = null; + if (PDFJS.maxCanvasPixels > 0) { + var pixelsInViewport = viewport.width * viewport.height; + var maxScale = Math.sqrt(PDFJS.maxCanvasPixels / pixelsInViewport); + if (outputScale.sx > maxScale || outputScale.sy > maxScale) { + outputScale.sx = maxScale; + outputScale.sy = maxScale; + outputScale.scaled = true; + this.hasRestrictedScaling = true; + } else { + this.hasRestrictedScaling = false; + } } - if (error === 'cancelled') { - return; + canvas.width = (Math.floor(viewport.width) * outputScale.sx) | 0; + canvas.height = (Math.floor(viewport.height) * outputScale.sy) | 0; + canvas.style.width = Math.floor(viewport.width) + 'px'; + canvas.style.height = Math.floor(viewport.height) + 'px'; + // Add the viewport so it's known what it was originally drawn with. + canvas._viewport = viewport; + + var textLayerDiv = null; + var textLayer = null; + if (this.textLayerFactory) { + textLayerDiv = document.createElement('div'); + textLayerDiv.className = 'textLayer'; + textLayerDiv.style.width = canvas.style.width; + textLayerDiv.style.height = canvas.style.height; + if (this.annotationLayer) { + // annotationLayer needs to stay on top + div.insertBefore(textLayerDiv, this.annotationLayer.div); + } else { + div.appendChild(textLayerDiv); + } + + textLayer = this.textLayerFactory.createTextLayerBuilder(textLayerDiv, + this.id - 1, + this.viewport); + } + this.textLayer = textLayer; + + // TODO(mack): use data attributes to store these + ctx._scaleX = outputScale.sx; + ctx._scaleY = outputScale.sy; + if (outputScale.scaled) { + ctx.scale(outputScale.sx, outputScale.sy); } - self.renderingState = RenderingStates.FINISHED; - - if (self.loadingIconDiv) { - div.removeChild(self.loadingIconDiv); - delete self.loadingIconDiv; - } - - if (self.zoomLayer) { - div.removeChild(self.zoomLayer); - self.zoomLayer = null; - } - - self.error = error; - self.stats = pdfPage.stats; - self.updateStats(); - if (self.onAfterDraw) { - self.onAfterDraw(); - } - - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagerender', true, true, { - pageNumber: pdfPage.pageNumber + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; }); - div.dispatchEvent(event); - callback(); - } + // Rendering area - var renderContext = { - canvasContext: ctx, - viewport: this.viewport, - // intent: 'default', // === 'display' - continueCallback: function pdfViewcContinueCallback(cont) { - if (!self.renderingQueue.isHighestPriority(self)) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function resumeCallback() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; + var self = this; + function pageViewDrawCallback(error) { + // The renderTask may have been replaced by a new one, so only remove + // the reference to the renderTask if it matches the one that is + // triggering this callback. + if (renderTask === self.renderTask) { + self.renderTask = null; + } + + if (error === 'cancelled') { + rejectRenderPromise(error); return; } - cont(); - } - }; - var renderTask = this.renderTask = this.pdfPage.render(renderContext); - this.renderTask.promise.then( - function pdfPageRenderCallback() { - pageViewDrawCallback(null); - if (textLayer) { - self.pdfPage.getTextContent().then( - function textContentResolved(textContent) { - textLayer.setTextContent(textContent); - } - ); + self.renderingState = RenderingStates.FINISHED; + + if (self.loadingIconDiv) { + div.removeChild(self.loadingIconDiv); + delete self.loadingIconDiv; + } + + if (self.zoomLayer) { + div.removeChild(self.zoomLayer); + self.zoomLayer = null; + } + + self.error = error; + self.stats = pdfPage.stats; + if (self.onAfterDraw) { + self.onAfterDraw(); + } + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('pagerendered', true, true, { + pageNumber: self.id + }); + div.dispatchEvent(event); + + if (!error) { + resolveRenderPromise(undefined); + } else { + rejectRenderPromise(error); } - }, - function pdfPageRenderError(error) { - pageViewDrawCallback(error); } - ); - setupAnnotations(div, pdfPage, this.viewport); - div.setAttribute('data-loaded', true); - - // Add the page to the cache at the start of drawing. That way it can be - // evicted from the cache and destroyed even if we pause its rendering. - cache.push(this); - }; - - this.beforePrint = function pageViewBeforePrint() { - var pdfPage = this.pdfPage; - - var viewport = pdfPage.getViewport(1); - // Use the same hack we use for high dpi displays for printing to get better - // output until bug 811002 is fixed in FF. - var PRINT_OUTPUT_SCALE = 2; - var canvas = document.createElement('canvas'); - canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; - canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; - canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; - canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; - var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + - (1 / PRINT_OUTPUT_SCALE) + ')'; - CustomStyle.setProp('transform' , canvas, cssScale); - CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); - - var printContainer = document.getElementById('printContainer'); - var canvasWrapper = document.createElement('div'); - canvasWrapper.style.width = viewport.width + 'pt'; - canvasWrapper.style.height = viewport.height + 'pt'; - canvasWrapper.appendChild(canvas); - printContainer.appendChild(canvasWrapper); - - canvas.mozPrintCallback = function(obj) { - var ctx = obj.context; - - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - ctx.restore(); - ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + var renderContinueCallback = null; + if (this.renderingQueue) { + renderContinueCallback = function renderContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + } var renderContext = { canvasContext: ctx, - viewport: viewport, - intent: 'print' + viewport: this.viewport, + // intent: 'default', // === 'display' + continueCallback: renderContinueCallback }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); - pdfPage.render(renderContext).promise.then(function() { - // Tell the printEngine that rendering this canvas/page has finished. - obj.done(); - }, function(error) { - console.error(error); - // Tell the printEngine that rendering this canvas/page has failed. - // This will make the print proces stop. - if ('abort' in obj) { - obj.abort(); - } else { - obj.done(); + this.renderTask.promise.then( + function pdfPageRenderCallback() { + pageViewDrawCallback(null); + if (textLayer) { + self.pdfPage.getTextContent().then( + function textContentResolved(textContent) { + textLayer.setTextContent(textContent); + textLayer.render(TEXT_LAYER_RENDER_DELAY); + } + ); + } + }, + function pdfPageRenderError(error) { + pageViewDrawCallback(error); } - }); - }; + ); + + if (this.annotationsLayerFactory) { + if (!this.annotationLayer) { + this.annotationLayer = this.annotationsLayerFactory. + createAnnotationsLayerBuilder(div, this.pdfPage); + } + this.annotationLayer.setupAnnotations(this.viewport); + } + div.setAttribute('data-loaded', true); + + if (self.onBeforeDraw) { + self.onBeforeDraw(); + } + return promise; + }, + + beforePrint: function PDFPageView_beforePrint() { + var pdfPage = this.pdfPage; + + var viewport = pdfPage.getViewport(1); + // Use the same hack we use for high dpi displays for printing to get + // better output until bug 811002 is fixed in FF. + var PRINT_OUTPUT_SCALE = 2; + var canvas = document.createElement('canvas'); + canvas.width = Math.floor(viewport.width) * PRINT_OUTPUT_SCALE; + canvas.height = Math.floor(viewport.height) * PRINT_OUTPUT_SCALE; + canvas.style.width = (PRINT_OUTPUT_SCALE * viewport.width) + 'pt'; + canvas.style.height = (PRINT_OUTPUT_SCALE * viewport.height) + 'pt'; + var cssScale = 'scale(' + (1 / PRINT_OUTPUT_SCALE) + ', ' + + (1 / PRINT_OUTPUT_SCALE) + ')'; + CustomStyle.setProp('transform' , canvas, cssScale); + CustomStyle.setProp('transformOrigin' , canvas, '0% 0%'); + + var printContainer = document.getElementById('printContainer'); + var canvasWrapper = document.createElement('div'); + canvasWrapper.style.width = viewport.width + 'pt'; + canvasWrapper.style.height = viewport.height + 'pt'; + canvasWrapper.appendChild(canvas); + printContainer.appendChild(canvasWrapper); + + canvas.mozPrintCallback = function(obj) { + var ctx = obj.context; + + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.restore(); + ctx.scale(PRINT_OUTPUT_SCALE, PRINT_OUTPUT_SCALE); + + var renderContext = { + canvasContext: ctx, + viewport: viewport, + intent: 'print' + }; + + pdfPage.render(renderContext).promise.then(function() { + // Tell the printEngine that rendering this canvas/page has finished. + obj.done(); + }, function(error) { + console.error(error); + // Tell the printEngine that rendering this canvas/page has failed. + // This will make the print proces stop. + if ('abort' in obj) { + obj.abort(); + } else { + obj.done(); + } + }); + }; + }, }; - this.updateStats = function pageViewUpdateStats() { - if (!this.stats) { - return; - } - - if (PDFJS.pdfBug && Stats.enabled) { - var stats = this.stats; - Stats.add(this.id, stats); - } - }; -}; + return PDFPageView; +})(); -var FIND_SCROLL_OFFSET_TOP = -50; -var FIND_SCROLL_OFFSET_LEFT = -400; var MAX_TEXT_DIVS_TO_RENDER = 100000; -var RENDER_DELAY = 200; // ms var NonWhitespaceRegexp = /\S/; @@ -3440,9 +3369,6 @@ function isAllWhitespace(str) { * @property {HTMLDivElement} textLayerDiv - The text layer container. * @property {number} pageIndex - The page index. * @property {PageViewport} viewport - The viewport of the text layer. - * @property {ILastScrollSource} lastScrollSource - The object that records when - * last time scroll happened. - * @property {boolean} isViewerInPresentationMode * @property {PDFFindController} findController */ @@ -3456,18 +3382,27 @@ function isAllWhitespace(str) { var TextLayerBuilder = (function TextLayerBuilderClosure() { function TextLayerBuilder(options) { this.textLayerDiv = options.textLayerDiv; - this.layoutDone = false; + this.renderingDone = false; this.divContentDone = false; this.pageIdx = options.pageIndex; + this.pageNumber = this.pageIdx + 1; this.matches = []; - this.lastScrollSource = options.lastScrollSource || null; this.viewport = options.viewport; - this.isViewerInPresentationMode = options.isViewerInPresentationMode; this.textDivs = []; this.findController = options.findController || null; } TextLayerBuilder.prototype = { + _finishRendering: function TextLayerBuilder_finishRendering() { + this.renderingDone = true; + + var event = document.createEvent('CustomEvent'); + event.initCustomEvent('textlayerrendered', true, true, { + pageNumber: this.pageNumber + }); + this.textLayerDiv.dispatchEvent(event); + }, + renderLayer: function TextLayerBuilder_renderLayer() { var textLayerFrag = document.createDocumentFragment(); var textDivs = this.textDivs; @@ -3478,6 +3413,7 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { // No point in rendering many divs as it would make the browser // unusable even after the divs are rendered. if (textDivsLength > MAX_TEXT_DIVS_TO_RENDER) { + this._finishRendering(); return; } @@ -3521,27 +3457,33 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { } this.textLayerDiv.appendChild(textLayerFrag); - this.renderingDone = true; + this._finishRendering(); this.updateMatches(); }, - setupRenderLayoutTimer: - function TextLayerBuilder_setupRenderLayoutTimer() { - // Schedule renderLayout() if the user has been scrolling, - // otherwise run it right away. - var self = this; - var lastScroll = (this.lastScrollSource === null ? - 0 : this.lastScrollSource.lastScroll); + /** + * Renders the text layer. + * @param {number} timeout (optional) if specified, the rendering waits + * for specified amount of ms. + */ + render: function TextLayerBuilder_render(timeout) { + if (!this.divContentDone || this.renderingDone) { + return; + } - if (Date.now() - lastScroll > RENDER_DELAY) { // Render right away + if (this.renderTimer) { + clearTimeout(this.renderTimer); + this.renderTimer = null; + } + + if (!timeout) { // Render right away this.renderLayer(); } else { // Schedule - if (this.renderTimer) { - clearTimeout(this.renderTimer); - } + var self = this; this.renderTimer = setTimeout(function() { - self.setupRenderLayoutTimer(); - }, RENDER_DELAY); + self.renderLayer(); + self.renderTimer = null; + }, timeout); } }, @@ -3611,7 +3553,6 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { this.appendText(textItems[i], textContent.styles); } this.divContentDone = true; - this.setupRenderLayoutTimer(); }, convertMatches: function TextLayerBuilder_convertMatches(matches) { @@ -3673,8 +3614,9 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { var bidiTexts = this.textContent.items; var textDivs = this.textDivs; var prevEnd = null; + var pageIdx = this.pageIdx; var isSelectedPage = (this.findController === null ? - false : (this.pageIdx === this.findController.selected.pageIdx)); + false : (pageIdx === this.findController.selected.pageIdx)); var selectedMatchIdx = (this.findController === null ? -1 : this.findController.selected.matchIdx); var highlightAll = (this.findController === null ? @@ -3720,10 +3662,9 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { var isSelected = (isSelectedPage && i === selectedMatchIdx); var highlightSuffix = (isSelected ? ' selected' : ''); - if (isSelected && !this.isViewerInPresentationMode) { - scrollIntoView(textDivs[begin.divIdx], - { top: FIND_SCROLL_OFFSET_TOP, - left: FIND_SCROLL_OFFSET_LEFT }); + if (this.findController) { + this.findController.updateMatchPosition(pageIdx, i, textDivs, + begin.divIdx, end.divIdx); } // Match inside new div. @@ -3795,6 +3736,186 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { return TextLayerBuilder; })(); +/** + * @constructor + * @implements IPDFTextLayerFactory + */ +function DefaultTextLayerFactory() {} +DefaultTextLayerFactory.prototype = { + /** + * @param {HTMLDivElement} textLayerDiv + * @param {number} pageIndex + * @param {PageViewport} viewport + * @returns {TextLayerBuilder} + */ + createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { + return new TextLayerBuilder({ + textLayerDiv: textLayerDiv, + pageIndex: pageIndex, + viewport: viewport + }); + } +}; + + +/** + * @typedef {Object} AnnotationsLayerBuilderOptions + * @property {HTMLDivElement} pageDiv + * @property {PDFPage} pdfPage + * @property {IPDFLinkService} linkService + */ + +/** + * @class + */ +var AnnotationsLayerBuilder = (function AnnotationsLayerBuilderClosure() { + /** + * @param {AnnotationsLayerBuilderOptions} options + * @constructs AnnotationsLayerBuilder + */ + function AnnotationsLayerBuilder(options) { + this.pageDiv = options.pageDiv; + this.pdfPage = options.pdfPage; + this.linkService = options.linkService; + + this.div = null; + } + AnnotationsLayerBuilder.prototype = + /** @lends AnnotationsLayerBuilder.prototype */ { + + /** + * @param {PageViewport} viewport + */ + setupAnnotations: + function AnnotationsLayerBuilder_setupAnnotations(viewport) { + function bindLink(link, dest) { + link.href = linkService.getDestinationHash(dest); + link.onclick = function annotationsLayerBuilderLinksOnclick() { + if (dest) { + linkService.navigateTo(dest); + } + return false; + }; + if (dest) { + link.className = 'internalLink'; + } + } + + function bindNamedAction(link, action) { + link.href = linkService.getAnchorUrl(''); + link.onclick = function annotationsLayerBuilderNamedActionOnClick() { + linkService.executeNamedAction(action); + return false; + }; + link.className = 'internalLink'; + } + + var linkService = this.linkService; + var pdfPage = this.pdfPage; + var self = this; + + pdfPage.getAnnotations().then(function (annotationsData) { + viewport = viewport.clone({ dontFlip: true }); + var transform = viewport.transform; + var transformStr = 'matrix(' + transform.join(',') + ')'; + var data, element, i, ii; + + if (self.div) { + // If an annotationLayer already exists, refresh its children's + // transformation matrices + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + element = self.div.querySelector( + '[data-annotation-id="' + data.id + '"]'); + if (element) { + CustomStyle.setProp('transform', element, transformStr); + } + } + // See PDFPageView.reset() + self.div.removeAttribute('hidden'); + } else { + for (i = 0, ii = annotationsData.length; i < ii; i++) { + data = annotationsData[i]; + if (!data || !data.hasHtml) { + continue; + } + + element = PDFJS.AnnotationUtils.getHtmlElement(data, + pdfPage.commonObjs); + element.setAttribute('data-annotation-id', data.id); + if (typeof mozL10n !== 'undefined') { + mozL10n.translate(element); + } + + var rect = data.rect; + var view = pdfPage.view; + rect = PDFJS.Util.normalizeRect([ + rect[0], + view[3] - rect[1] + view[1], + rect[2], + view[3] - rect[3] + view[1] + ]); + element.style.left = rect[0] + 'px'; + element.style.top = rect[1] + 'px'; + element.style.position = 'absolute'; + + CustomStyle.setProp('transform', element, transformStr); + var transformOriginStr = -rect[0] + 'px ' + -rect[1] + 'px'; + CustomStyle.setProp('transformOrigin', element, transformOriginStr); + + if (data.subtype === 'Link' && !data.url) { + var link = element.getElementsByTagName('a')[0]; + if (link) { + if (data.action) { + bindNamedAction(link, data.action); + } else { + bindLink(link, ('dest' in data) ? data.dest : null); + } + } + } + + if (!self.div) { + var annotationLayerDiv = document.createElement('div'); + annotationLayerDiv.className = 'annotationLayer'; + self.pageDiv.appendChild(annotationLayerDiv); + self.div = annotationLayerDiv; + } + + self.div.appendChild(element); + } + } + }); + }, + + hide: function () { + if (!this.div) { + return; + } + this.div.setAttribute('hidden', 'true'); + } + }; + return AnnotationsLayerBuilder; +})(); + +/** + * @constructor + * @implements IPDFAnnotationsLayerFactory + */ +function DefaultAnnotationsLayerFactory() {} +DefaultAnnotationsLayerFactory.prototype = { + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { + return new AnnotationsLayerBuilder({ + pageDiv: pageDiv, + pdfPage: pdfPage + }); + } +}; + /** * @typedef {Object} PDFViewerOptions @@ -3808,10 +3929,29 @@ var TextLayerBuilder = (function TextLayerBuilderClosure() { /** * Simple viewer control to display PDF content/pages. * @class - * @implements {ILastScrollSource} * @implements {IRenderableView} */ var PDFViewer = (function pdfViewer() { + function PDFPageViewBuffer(size) { + var data = []; + this.push = function cachePush(view) { + var i = data.indexOf(view); + if (i >= 0) { + data.splice(i, 1); + } + data.push(view); + if (data.length > size) { + data.shift().destroy(); + } + }; + this.resize = function (newSize) { + size = newSize; + while (data.length > size) { + data.shift().destroy(); + } + }; + } + /** * @constructs PDFViewer * @param {PDFViewerOptions} options @@ -3831,7 +3971,6 @@ var PDFViewer = (function pdfViewer() { } this.scroll = watchScroll(this.container, this._scrollUpdate.bind(this)); - this.lastScroll = 0; this.updateInProgress = false; this.presentationModeState = PresentationModeState.UNKNOWN; this._resetView(); @@ -3867,7 +4006,6 @@ var PDFViewer = (function pdfViewer() { return; } - this.pages[val - 1].updateStats(); event.previousPageNumber = this._currentPageNumber; this._currentPageNumber = val; event.pageNumber = val; @@ -3973,18 +4111,19 @@ var PDFViewer = (function pdfViewer() { }); this.onePageRendered = onePageRendered; - var bindOnAfterDraw = function (pageView) { + var bindOnAfterAndBeforeDraw = function (pageView) { + pageView.onBeforeDraw = function pdfViewLoadOnBeforeDraw() { + // Add the page to the buffer at the start of drawing. That way it can + // be evicted from the buffer and destroyed even if we pause its + // rendering. + self._buffer.push(this); + }; // when page is painted, using the image as thumbnail base pageView.onAfterDraw = function pdfViewLoadOnAfterDraw() { if (!isOnePageRenderedResolved) { isOnePageRenderedResolved = true; resolveOnePageRendered(); } - var event = document.createEvent('CustomEvent'); - event.initCustomEvent('pagerendered', true, true, { - pageNumber: pageView.id - }); - self.container.dispatchEvent(event); }; }; @@ -3997,12 +4136,20 @@ var PDFViewer = (function pdfViewer() { var scale = this._currentScale || 1.0; var viewport = pdfPage.getViewport(scale * CSS_UNITS); for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { - var pageSource = new PDFPageSource(pdfDocument, pageNum); - var pageView = new PageView(this.viewer, pageNum, scale, - viewport.clone(), this.linkService, - this.renderingQueue, this.cache, - pageSource, this); - bindOnAfterDraw(pageView); + var textLayerFactory = null; + if (!PDFJS.disableTextLayer) { + textLayerFactory = this; + } + var pageView = new PDFPageView({ + container: this.viewer, + id: pageNum, + scale: scale, + defaultViewport: viewport.clone(), + renderingQueue: this.renderingQueue, + textLayerFactory: textLayerFactory, + annotationsLayerFactory: this + }); + bindOnAfterAndBeforeDraw(pageView); this.pages.push(pageView); } @@ -4043,13 +4190,14 @@ var PDFViewer = (function pdfViewer() { }, _resetView: function () { - this.cache = new Cache(DEFAULT_CACHE_SIZE); this.pages = []; this._currentPageNumber = 1; this._currentScale = UNKNOWN_SCALE; this._currentScaleValue = null; + this._buffer = new PDFPageViewBuffer(DEFAULT_CACHE_SIZE); this.location = null; this._pagesRotation = 0; + this._pagesRequests = []; var container = this.viewer; while (container.hasChildNodes()) { @@ -4058,12 +4206,13 @@ var PDFViewer = (function pdfViewer() { }, _scrollUpdate: function () { - this.lastScroll = Date.now(); - if (this.pagesCount === 0) { return; } this.update(); + for (var i = 0, ii = this.pages.length; i < ii; i++) { + this.pages[i].updatePosition(); + } }, _setScaleUpdatePages: function pdfViewer_setScaleUpdatePages( @@ -4160,7 +4309,6 @@ var PDFViewer = (function pdfViewer() { scrollPageIntoView: function PDFViewer_scrollPageIntoView(pageNumber, dest) { var pageView = this.pages[pageNumber - 1]; - var pageViewDiv = pageView.el; if (this.presentationModeState === PresentationModeState.FULLSCREEN) { @@ -4174,7 +4322,7 @@ var PDFViewer = (function pdfViewer() { this._setScale(this.currentScaleValue, true); } if (!dest) { - scrollIntoView(pageViewDiv); + scrollIntoView(pageView.div); return; } @@ -4237,7 +4385,7 @@ var PDFViewer = (function pdfViewer() { } if (scale === 'page-fit' && !dest[4]) { - scrollIntoView(pageViewDiv); + scrollIntoView(pageView.div); return; } @@ -4248,7 +4396,7 @@ var PDFViewer = (function pdfViewer() { var left = Math.min(boundingRect[0][0], boundingRect[1][0]); var top = Math.min(boundingRect[0][1], boundingRect[1][1]); - scrollIntoView(pageViewDiv, { left: left, top: top }); + scrollIntoView(pageView.div, { left: left, top: top }); }, _updateLocation: function (firstPage) { @@ -4290,7 +4438,7 @@ var PDFViewer = (function pdfViewer() { var suggestedCacheSize = Math.max(DEFAULT_CACHE_SIZE, 2 * visiblePages.length + 1); - this.cache.resize(suggestedCacheSize); + this._buffer.resize(suggestedCacheSize); this.renderingQueue.renderHighestPriority(visible); @@ -4366,13 +4514,38 @@ var PDFViewer = (function pdfViewer() { } }, + /** + * @param {PDFPageView} pageView + * @returns {PDFPage} + * @private + */ + _ensurePdfPageLoaded: function (pageView) { + if (pageView.pdfPage) { + return Promise.resolve(pageView.pdfPage); + } + var pageNumber = pageView.id; + if (this._pagesRequests[pageNumber]) { + return this._pagesRequests[pageNumber]; + } + var promise = this.pdfDocument.getPage(pageNumber).then( + function (pdfPage) { + pageView.setPdfPage(pdfPage); + this._pagesRequests[pageNumber] = null; + return pdfPage; + }.bind(this)); + this._pagesRequests[pageNumber] = promise; + return promise; + }, + forceRendering: function (currentlyVisiblePages) { var visiblePages = currentlyVisiblePages || this._getVisiblePages(); var pageView = this.renderingQueue.getHighestPriority(visiblePages, this.pages, this.scroll.down); if (pageView) { - this.renderingQueue.renderView(pageView); + this._ensurePdfPageLoaded(pageView).then(function () { + this.renderingQueue.renderView(pageView); + }.bind(this)); return true; } return false; @@ -4385,9 +4558,9 @@ var PDFViewer = (function pdfViewer() { }, /** - * @param textLayerDiv {HTMLDivElement} - * @param pageIndex {number} - * @param viewport {PageViewport} + * @param {HTMLDivElement} textLayerDiv + * @param {number} pageIndex + * @param {PageViewport} viewport * @returns {TextLayerBuilder} */ createTextLayerBuilder: function (textLayerDiv, pageIndex, viewport) { @@ -4397,9 +4570,20 @@ var PDFViewer = (function pdfViewer() { textLayerDiv: textLayerDiv, pageIndex: pageIndex, viewport: viewport, - lastScrollSource: this, - isViewerInPresentationMode: isViewerInPresentationMode, - findController: this.findController + findController: isViewerInPresentationMode ? null : this.findController + }); + }, + + /** + * @param {HTMLDivElement} pageDiv + * @param {PDFPage} pdfPage + * @returns {AnnotationsLayerBuilder} + */ + createAnnotationsLayerBuilder: function (pageDiv, pdfPage) { + return new AnnotationsLayerBuilder({ + pageDiv: pageDiv, + pdfPage: pdfPage, + linkService: this.linkService }); }, @@ -4458,32 +4642,632 @@ var SimpleLinkService = (function SimpleLinkServiceClosure() { return SimpleLinkService; })(); + +var THUMBNAIL_SCROLL_MARGIN = -19; + + +var THUMBNAIL_WIDTH = 98; // px +var THUMBNAIL_CANVAS_BORDER_WIDTH = 1; // px + /** - * PDFPage object source. - * @class + * @typedef {Object} PDFThumbnailViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {number} id - The thumbnail's unique ID (normally its number). + * @property {PageViewport} defaultViewport - The page viewport. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. */ -var PDFPageSource = (function PDFPageSourceClosure() { - /** - * @constructs - * @param {PDFDocument} pdfDocument - * @param {number} pageNumber - * @constructor - */ - function PDFPageSource(pdfDocument, pageNumber) { - this.pdfDocument = pdfDocument; - this.pageNumber = pageNumber; + +/** + * @class + * @implements {IRenderableView} + */ +var PDFThumbnailView = (function PDFThumbnailViewClosure() { + function getTempCanvas(width, height) { + var tempCanvas = PDFThumbnailView.tempImageCache; + if (!tempCanvas) { + tempCanvas = document.createElement('canvas'); + PDFThumbnailView.tempImageCache = tempCanvas; + } + tempCanvas.width = width; + tempCanvas.height = height; + + // Since this is a temporary canvas, we need to fill the canvas with a white + // background ourselves. |_getPageDrawContext| uses CSS rules for this. + var ctx = tempCanvas.getContext('2d'); + ctx.save(); + ctx.fillStyle = 'rgb(255, 255, 255)'; + ctx.fillRect(0, 0, width, height); + ctx.restore(); + return tempCanvas; } - PDFPageSource.prototype = /** @lends PDFPageSource.prototype */ { + /** + * @constructs PDFThumbnailView + * @param {PDFThumbnailViewOptions} options + */ + function PDFThumbnailView(options) { + var container = options.container; + var id = options.id; + var defaultViewport = options.defaultViewport; + var linkService = options.linkService; + var renderingQueue = options.renderingQueue; + + this.id = id; + this.renderingId = 'thumbnail' + id; + + this.pdfPage = null; + this.rotation = 0; + this.viewport = defaultViewport; + this.pdfPageRotate = defaultViewport.rotation; + + this.linkService = linkService; + this.renderingQueue = renderingQueue; + + this.hasImage = false; + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + this.pageWidth = this.viewport.width; + this.pageHeight = this.viewport.height; + this.pageRatio = this.pageWidth / this.pageHeight; + + this.canvasWidth = THUMBNAIL_WIDTH; + this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; + this.scale = this.canvasWidth / this.pageWidth; + + var anchor = document.createElement('a'); + anchor.href = linkService.getAnchorUrl('#page=' + id); + anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); + anchor.onclick = function stopNavigation() { + linkService.page = id; + return false; + }; + + var div = document.createElement('div'); + div.id = 'thumbnailContainer' + id; + div.className = 'thumbnail'; + this.el = div; // TODO: replace 'el' property usage. + this.div = div; + + if (id === 1) { + // Highlight the thumbnail of the first page when no page number is + // specified (or exists in cache) when the document is loaded. + div.classList.add('selected'); + } + + var ring = document.createElement('div'); + ring.className = 'thumbnailSelectionRing'; + var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; + ring.style.width = this.canvasWidth + borderAdjustment + 'px'; + ring.style.height = this.canvasHeight + borderAdjustment + 'px'; + this.ring = ring; + + div.appendChild(ring); + anchor.appendChild(div); + container.appendChild(anchor); + } + + PDFThumbnailView.prototype = { + setPdfPage: function PDFThumbnailView_setPdfPage(pdfPage) { + this.pdfPage = pdfPage; + this.pdfPageRotate = pdfPage.rotate; + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = pdfPage.getViewport(1, totalRotation); + this.reset(); + }, + + reset: function PDFThumbnailView_reset() { + if (this.renderTask) { + this.renderTask.cancel(); + } + this.hasImage = false; + this.resume = null; + this.renderingState = RenderingStates.INITIAL; + + this.pageWidth = this.viewport.width; + this.pageHeight = this.viewport.height; + this.pageRatio = this.pageWidth / this.pageHeight; + + this.canvasHeight = (this.canvasWidth / this.pageRatio) | 0; + this.scale = (this.canvasWidth / this.pageWidth); + + this.div.removeAttribute('data-loaded'); + var ring = this.ring; + var childNodes = ring.childNodes; + for (var i = childNodes.length - 1; i >= 0; i--) { + ring.removeChild(childNodes[i]); + } + var borderAdjustment = 2 * THUMBNAIL_CANVAS_BORDER_WIDTH; + ring.style.width = this.canvasWidth + borderAdjustment + 'px'; + ring.style.height = this.canvasHeight + borderAdjustment + 'px'; + + if (this.canvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + this.canvas.width = 0; + this.canvas.height = 0; + delete this.canvas; + } + }, + + update: function PDFThumbnailView_update(rotation) { + if (typeof rotation !== 'undefined') { + this.rotation = rotation; + } + var totalRotation = (this.rotation + this.pdfPageRotate) % 360; + this.viewport = this.viewport.clone({ + scale: 1, + rotation: totalRotation + }); + this.reset(); + }, + /** - * @returns {Promise} + * @private */ - getPage: function () { - return this.pdfDocument.getPage(this.pageNumber); + _getPageDrawContext: function PDFThumbnailView_getPageDrawContext() { + var canvas = document.createElement('canvas'); + canvas.id = this.renderingId; + + canvas.width = this.canvasWidth; + canvas.height = this.canvasHeight; + canvas.className = 'thumbnailImage'; + canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', + {page: this.id}, 'Thumbnail of Page {{page}}')); + + this.canvas = canvas; + this.div.setAttribute('data-loaded', true); + this.ring.appendChild(canvas); + + return canvas.getContext('2d'); + }, + + draw: function PDFThumbnailView_draw() { + if (this.renderingState !== RenderingStates.INITIAL) { + console.error('Must be in new state before drawing'); + } + if (this.hasImage) { + return Promise.resolve(undefined); + } + this.hasImage = true; + this.renderingState = RenderingStates.RUNNING; + + var resolveRenderPromise, rejectRenderPromise; + var promise = new Promise(function (resolve, reject) { + resolveRenderPromise = resolve; + rejectRenderPromise = reject; + }); + + var self = this; + function thumbnailDrawCallback(error) { + // The renderTask may have been replaced by a new one, so only remove + // the reference to the renderTask if it matches the one that is + // triggering this callback. + if (renderTask === self.renderTask) { + self.renderTask = null; + } + if (error === 'cancelled') { + rejectRenderPromise(error); + return; + } + self.renderingState = RenderingStates.FINISHED; + + if (!error) { + resolveRenderPromise(undefined); + } else { + rejectRenderPromise(error); + } + } + + var ctx = this._getPageDrawContext(); + var drawViewport = this.viewport.clone({ scale: this.scale }); + var renderContinueCallback = function renderContinueCallback(cont) { + if (!self.renderingQueue.isHighestPriority(self)) { + self.renderingState = RenderingStates.PAUSED; + self.resume = function resumeCallback() { + self.renderingState = RenderingStates.RUNNING; + cont(); + }; + return; + } + cont(); + }; + + var renderContext = { + canvasContext: ctx, + viewport: drawViewport, + continueCallback: renderContinueCallback + }; + var renderTask = this.renderTask = this.pdfPage.render(renderContext); + + renderTask.promise.then( + function pdfPageRenderCallback() { + thumbnailDrawCallback(null); + }, + function pdfPageRenderError(error) { + thumbnailDrawCallback(error); + } + ); + return promise; + }, + + setImage: function PDFThumbnailView_setImage(pageView) { + var img = pageView.canvas; + if (this.hasImage || !img) { + return; + } + if (!this.pdfPage) { + this.setPdfPage(pageView.pdfPage); + } + this.hasImage = true; + this.renderingState = RenderingStates.FINISHED; + + var ctx = this._getPageDrawContext(); + var canvas = ctx.canvas; + + if (img.width <= 2 * canvas.width) { + ctx.drawImage(img, 0, 0, img.width, img.height, + 0, 0, canvas.width, canvas.height); + return; + } + // drawImage does an awful job of rescaling the image, doing it gradually. + var MAX_NUM_SCALING_STEPS = 3; + var reducedWidth = canvas.width << MAX_NUM_SCALING_STEPS; + var reducedHeight = canvas.height << MAX_NUM_SCALING_STEPS; + var reducedImage = getTempCanvas(reducedWidth, reducedHeight); + var reducedImageCtx = reducedImage.getContext('2d'); + + while (reducedWidth > img.width || reducedHeight > img.height) { + reducedWidth >>= 1; + reducedHeight >>= 1; + } + reducedImageCtx.drawImage(img, 0, 0, img.width, img.height, + 0, 0, reducedWidth, reducedHeight); + while (reducedWidth > 2 * canvas.width) { + reducedImageCtx.drawImage(reducedImage, + 0, 0, reducedWidth, reducedHeight, + 0, 0, reducedWidth >> 1, reducedHeight >> 1); + reducedWidth >>= 1; + reducedHeight >>= 1; + } + ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, + 0, 0, canvas.width, canvas.height); } }; - return PDFPageSource; + return PDFThumbnailView; +})(); + +PDFThumbnailView.tempImageCache = null; + + +/** + * @typedef {Object} PDFThumbnailViewerOptions + * @property {HTMLDivElement} container - The container for the thumbnail + * elements. + * @property {IPDFLinkService} linkService - The navigation/linking service. + * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. + */ + +/** + * Simple viewer control to display thumbnails for pages. + * @class + * @implements {IRenderableView} + */ +var PDFThumbnailViewer = (function PDFThumbnailViewerClosure() { + /** + * @constructs PDFThumbnailViewer + * @param {PDFThumbnailViewerOptions} options + */ + function PDFThumbnailViewer(options) { + this.container = options.container; + this.renderingQueue = options.renderingQueue; + this.linkService = options.linkService; + + this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this)); + this._resetView(); + } + + PDFThumbnailViewer.prototype = { + /** + * @private + */ + _scrollUpdated: function PDFThumbnailViewer_scrollUpdated() { + this.renderingQueue.renderHighestPriority(); + }, + + getThumbnail: function PDFThumbnailViewer_getThumbnail(index) { + return this.thumbnails[index]; + }, + + /** + * @private + */ + _getVisibleThumbs: function PDFThumbnailViewer_getVisibleThumbs() { + return getVisibleElements(this.container, this.thumbnails); + }, + + scrollThumbnailIntoView: + function PDFThumbnailViewer_scrollThumbnailIntoView(page) { + var selected = document.querySelector('.thumbnail.selected'); + if (selected) { + selected.classList.remove('selected'); + } + var thumbnail = document.getElementById('thumbnailContainer' + page); + thumbnail.classList.add('selected'); + var visibleThumbs = this._getVisibleThumbs(); + var numVisibleThumbs = visibleThumbs.views.length; + + // If the thumbnail isn't currently visible, scroll it into view. + if (numVisibleThumbs > 0) { + var first = visibleThumbs.first.id; + // Account for only one thumbnail being visible. + var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first); + if (page <= first || page >= last) { + scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN }); + } + } + }, + + get pagesRotation() { + return this._pagesRotation; + }, + + set pagesRotation(rotation) { + this._pagesRotation = rotation; + for (var i = 0, l = this.thumbnails.length; i < l; i++) { + var thumb = this.thumbnails[i]; + thumb.update(rotation); + } + }, + + cleanup: function PDFThumbnailViewer_cleanup() { + var tempCanvas = PDFThumbnailView.tempImageCache; + if (tempCanvas) { + // Zeroing the width and height causes Firefox to release graphics + // resources immediately, which can greatly reduce memory consumption. + tempCanvas.width = 0; + tempCanvas.height = 0; + } + PDFThumbnailView.tempImageCache = null; + }, + + /** + * @private + */ + _resetView: function PDFThumbnailViewer_resetView() { + this.thumbnails = []; + this._pagesRotation = 0; + this._pagesRequests = []; + }, + + setDocument: function PDFThumbnailViewer_setDocument(pdfDocument) { + if (this.pdfDocument) { + // cleanup of the elements and views + var thumbsView = this.container; + while (thumbsView.hasChildNodes()) { + thumbsView.removeChild(thumbsView.lastChild); + } + this._resetView(); + } + + this.pdfDocument = pdfDocument; + if (!pdfDocument) { + return Promise.resolve(); + } + + return pdfDocument.getPage(1).then(function (firstPage) { + var pagesCount = pdfDocument.numPages; + var viewport = firstPage.getViewport(1.0); + for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { + var thumbnail = new PDFThumbnailView({ + container: this.container, + id: pageNum, + defaultViewport: viewport.clone(), + linkService: this.linkService, + renderingQueue: this.renderingQueue + }); + this.thumbnails.push(thumbnail); + } + }.bind(this)); + }, + + /** + * @param {PDFPageView} pageView + * @returns {PDFPage} + * @private + */ + _ensurePdfPageLoaded: + function PDFThumbnailViewer_ensurePdfPageLoaded(thumbView) { + if (thumbView.pdfPage) { + return Promise.resolve(thumbView.pdfPage); + } + var pageNumber = thumbView.id; + if (this._pagesRequests[pageNumber]) { + return this._pagesRequests[pageNumber]; + } + var promise = this.pdfDocument.getPage(pageNumber).then( + function (pdfPage) { + thumbView.setPdfPage(pdfPage); + this._pagesRequests[pageNumber] = null; + return pdfPage; + }.bind(this)); + this._pagesRequests[pageNumber] = promise; + return promise; + }, + + ensureThumbnailVisible: + function PDFThumbnailViewer_ensureThumbnailVisible(page) { + // Ensure that the thumbnail of the current page is visible + // when switching from another view. + scrollIntoView(document.getElementById('thumbnailContainer' + page)); + }, + + forceRendering: function () { + var visibleThumbs = this._getVisibleThumbs(); + var thumbView = this.renderingQueue.getHighestPriority(visibleThumbs, + this.thumbnails, + this.scroll.down); + if (thumbView) { + this._ensurePdfPageLoaded(thumbView).then(function () { + this.renderingQueue.renderView(thumbView); + }.bind(this)); + return true; + } + return false; + } + }; + + return PDFThumbnailViewer; +})(); + + +/** + * @typedef {Object} PDFOutlineViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {Array} outline - An array of outline objects. + * @property {IPDFLinkService} linkService - The navigation/linking service. + */ + +/** + * @class + */ +var PDFOutlineView = (function PDFOutlineViewClosure() { + /** + * @constructs PDFOutlineView + * @param {PDFOutlineViewOptions} options + */ + function PDFOutlineView(options) { + this.container = options.container; + this.outline = options.outline; + this.linkService = options.linkService; + } + + PDFOutlineView.prototype = { + reset: function PDFOutlineView_reset() { + var container = this.container; + while (container.firstChild) { + container.removeChild(container.firstChild); + } + }, + + /** + * @private + */ + _bindLink: function PDFOutlineView_bindLink(element, item) { + var linkService = this.linkService; + element.href = linkService.getDestinationHash(item.dest); + element.onclick = function goToDestination(e) { + linkService.navigateTo(item.dest); + return false; + }; + }, + + render: function PDFOutlineView_render() { + var outline = this.outline; + + this.reset(); + + if (!outline) { + return; + } + + var queue = [{ parent: this.container, items: this.outline }]; + while (queue.length > 0) { + var levelData = queue.shift(); + for (var i = 0, len = levelData.items.length; i < len; i++) { + var item = levelData.items[i]; + var div = document.createElement('div'); + div.className = 'outlineItem'; + var element = document.createElement('a'); + this._bindLink(element, item); + element.textContent = item.title; + div.appendChild(element); + + if (item.items.length > 0) { + var itemsDiv = document.createElement('div'); + itemsDiv.className = 'outlineItems'; + div.appendChild(itemsDiv); + queue.push({ parent: itemsDiv, items: item.items }); + } + + levelData.parent.appendChild(div); + } + } + } + }; + + return PDFOutlineView; +})(); + + +/** + * @typedef {Object} PDFAttachmentViewOptions + * @property {HTMLDivElement} container - The viewer element. + * @property {Array} attachments - An array of attachment objects. + * @property {DownloadManager} downloadManager - The download manager. + */ + +/** + * @class + */ +var PDFAttachmentView = (function PDFAttachmentViewClosure() { + /** + * @constructs PDFAttachmentView + * @param {PDFAttachmentViewOptions} options + */ + function PDFAttachmentView(options) { + this.container = options.container; + this.attachments = options.attachments; + this.downloadManager = options.downloadManager; + } + + PDFAttachmentView.prototype = { + reset: function PDFAttachmentView_reset() { + var container = this.container; + while (container.firstChild) { + container.removeChild(container.firstChild); + } + }, + + /** + * @private + */ + _bindLink: function PDFAttachmentView_bindLink(button, content, filename) { + button.onclick = function downloadFile(e) { + this.downloadManager.downloadData(content, filename, ''); + return false; + }.bind(this); + }, + + render: function PDFAttachmentView_render() { + var attachments = this.attachments; + + this.reset(); + + if (!attachments) { + return; + } + + var names = Object.keys(attachments).sort(function(a, b) { + return a.toLowerCase().localeCompare(b.toLowerCase()); + }); + for (var i = 0, len = names.length; i < len; i++) { + var item = attachments[names[i]]; + var filename = getFileName(item.filename); + var div = document.createElement('div'); + div.className = 'attachmentsItem'; + var button = document.createElement('button'); + this._bindLink(button, item.content, filename); + button.textContent = filename; + div.appendChild(button); + this.container.appendChild(div); + } + } + }; + + return PDFAttachmentView; })(); @@ -4764,67 +5548,18 @@ var PDFViewerApplication = { }, initPassiveLoading: function pdfViewInitPassiveLoading() { - var pdfDataRangeTransportReadyResolve; - var pdfDataRangeTransportReady = new Promise(function (resolve) { - pdfDataRangeTransportReadyResolve = resolve; - }); - var pdfDataRangeTransport = { - rangeListeners: [], - progressListeners: [], - progressiveReadListeners: [], - ready: pdfDataRangeTransportReady, - - addRangeListener: function PdfDataRangeTransport_addRangeListener( - listener) { - this.rangeListeners.push(listener); - }, - - addProgressListener: function PdfDataRangeTransport_addProgressListener( - listener) { - this.progressListeners.push(listener); - }, - - addProgressiveReadListener: - function PdfDataRangeTransport_addProgressiveReadListener(listener) { - this.progressiveReadListeners.push(listener); - }, - - onDataRange: function PdfDataRangeTransport_onDataRange(begin, chunk) { - var listeners = this.rangeListeners; - for (var i = 0, n = listeners.length; i < n; ++i) { - listeners[i](begin, chunk); - } - }, - - onDataProgress: function PdfDataRangeTransport_onDataProgress(loaded) { - this.ready.then(function () { - var listeners = this.progressListeners; - for (var i = 0, n = listeners.length; i < n; ++i) { - listeners[i](loaded); - } - }.bind(this)); - }, - - onDataProgressiveRead: - function PdfDataRangeTransport_onDataProgress(chunk) { - this.ready.then(function () { - var listeners = this.progressiveReadListeners; - for (var i = 0, n = listeners.length; i < n; ++i) { - listeners[i](chunk); - } - }.bind(this)); - }, - - transportReady: function PdfDataRangeTransport_transportReady() { - pdfDataRangeTransportReadyResolve(); - }, - - requestDataRange: function PdfDataRangeTransport_requestDataRange( - begin, end) { - FirefoxCom.request('requestDataRange', { begin: begin, end: end }); - } + function FirefoxComDataRangeTransport(length, initialData) { + PDFJS.PDFDataRangeTransport.call(this, length, initialData); + } + FirefoxComDataRangeTransport.prototype = + Object.create(PDFJS.PDFDataRangeTransport.prototype); + FirefoxComDataRangeTransport.prototype.requestDataRange = + function FirefoxComDataRangeTransport_requestDataRange(begin, end) { + FirefoxCom.request('requestDataRange', { begin: begin, end: end }); }; + var pdfDataRangeTransport; + window.addEventListener('message', function windowMessage(e) { if (e.source !== null) { // The message MUST originate from Chrome code. @@ -4838,11 +5573,15 @@ var PDFViewerApplication = { } switch (args.pdfjsLoadAction) { case 'supportsRangedLoading': + pdfDataRangeTransport = + new FirefoxComDataRangeTransport(args.length, args.data); + PDFViewerApplication.open(args.pdfUrl, 0, undefined, - pdfDataRangeTransport, { - length: args.length, - initialData: args.data - }); + pdfDataRangeTransport); + + if (args.length) { + DocumentProperties.setFileSize(args.length); + } break; case 'range': pdfDataRangeTransport.onDataRange(args.begin, args.chunk); @@ -5344,15 +6083,16 @@ var PDFViewerApplication = { var promises = [pagesPromise, this.animationStartedPromise]; Promise.all(promises).then(function() { pdfDocument.getOutline().then(function(outline) { - var outlineView = document.getElementById('outlineView'); - self.outline = new DocumentOutlineView({ + var container = document.getElementById('outlineView'); + self.outline = new PDFOutlineView({ + container: container, outline: outline, - outlineView: outlineView, linkService: self }); + self.outline.render(); document.getElementById('viewOutline').disabled = !outline; - if (!outline && !outlineView.classList.contains('hidden')) { + if (!outline && !container.classList.contains('hidden')) { self.switchSidebarView('thumbs'); } if (outline && @@ -5361,14 +6101,16 @@ var PDFViewerApplication = { } }); pdfDocument.getAttachments().then(function(attachments) { - var attachmentsView = document.getElementById('attachmentsView'); - self.attachments = new DocumentAttachmentsView({ + var container = document.getElementById('attachmentsView'); + self.attachments = new PDFAttachmentView({ + container: container, attachments: attachments, - attachmentsView: attachmentsView + downloadManager: new DownloadManager() }); + self.attachments.render(); document.getElementById('viewAttachments').disabled = !attachments; - if (!attachments && !attachmentsView.classList.contains('hidden')) { + if (!attachments && !container.classList.contains('hidden')) { self.switchSidebarView('thumbs'); } if (attachments && @@ -5684,7 +6426,6 @@ var PDFViewerApplication = { rotatePages: function pdfViewRotatePages(delta) { var pageNumber = this.page; - this.pageRotation = (this.pageRotation + 360 + delta) % 360; this.pdfViewer.pagesRotation = this.pageRotation; this.pdfThumbnailViewer.pagesRotation = this.pageRotation; @@ -5764,455 +6505,6 @@ var PDFViewerApplication = { }; -var THUMBNAIL_SCROLL_MARGIN = -19; - -/** - * @constructor - * @param container - * @param id - * @param defaultViewport - * @param linkService - * @param renderingQueue - * @param pageSource - * - * @implements {IRenderableView} - */ -var ThumbnailView = function thumbnailView(container, id, defaultViewport, - linkService, renderingQueue, - pageSource) { - var anchor = document.createElement('a'); - anchor.href = linkService.getAnchorUrl('#page=' + id); - anchor.title = mozL10n.get('thumb_page_title', {page: id}, 'Page {{page}}'); - anchor.onclick = function stopNavigation() { - linkService.page = id; - return false; - }; - - this.pdfPage = undefined; - this.viewport = defaultViewport; - this.pdfPageRotate = defaultViewport.rotation; - - this.rotation = 0; - this.pageWidth = this.viewport.width; - this.pageHeight = this.viewport.height; - this.pageRatio = this.pageWidth / this.pageHeight; - this.id = id; - this.renderingId = 'thumbnail' + id; - - this.canvasWidth = 98; - this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; - this.scale = (this.canvasWidth / this.pageWidth); - - var div = this.el = document.createElement('div'); - div.id = 'thumbnailContainer' + id; - div.className = 'thumbnail'; - - if (id === 1) { - // Highlight the thumbnail of the first page when no page number is - // specified (or exists in cache) when the document is loaded. - div.classList.add('selected'); - } - - var ring = document.createElement('div'); - ring.className = 'thumbnailSelectionRing'; - ring.style.width = this.canvasWidth + 'px'; - ring.style.height = this.canvasHeight + 'px'; - - div.appendChild(ring); - anchor.appendChild(div); - container.appendChild(anchor); - - this.hasImage = false; - this.renderingState = RenderingStates.INITIAL; - this.renderingQueue = renderingQueue; - this.pageSource = pageSource; - - this.setPdfPage = function thumbnailViewSetPdfPage(pdfPage) { - this.pdfPage = pdfPage; - this.pdfPageRotate = pdfPage.rotate; - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = pdfPage.getViewport(1, totalRotation); - this.update(); - }; - - this.update = function thumbnailViewUpdate(rotation) { - if (rotation !== undefined) { - this.rotation = rotation; - } - var totalRotation = (this.rotation + this.pdfPageRotate) % 360; - this.viewport = this.viewport.clone({ - scale: 1, - rotation: totalRotation - }); - this.pageWidth = this.viewport.width; - this.pageHeight = this.viewport.height; - this.pageRatio = this.pageWidth / this.pageHeight; - - this.canvasHeight = this.canvasWidth / this.pageWidth * this.pageHeight; - this.scale = (this.canvasWidth / this.pageWidth); - - div.removeAttribute('data-loaded'); - ring.textContent = ''; - ring.style.width = this.canvasWidth + 'px'; - ring.style.height = this.canvasHeight + 'px'; - - this.hasImage = false; - this.renderingState = RenderingStates.INITIAL; - this.resume = null; - }; - - this.getPageDrawContext = function thumbnailViewGetPageDrawContext() { - var canvas = document.createElement('canvas'); - canvas.id = 'thumbnail' + id; - - canvas.width = this.canvasWidth; - canvas.height = this.canvasHeight; - canvas.className = 'thumbnailImage'; - canvas.setAttribute('aria-label', mozL10n.get('thumb_page_canvas', - {page: id}, 'Thumbnail of Page {{page}}')); - - div.setAttribute('data-loaded', true); - - ring.appendChild(canvas); - - var ctx = canvas.getContext('2d'); - ctx.save(); - ctx.fillStyle = 'rgb(255, 255, 255)'; - ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight); - ctx.restore(); - return ctx; - }; - - this.drawingRequired = function thumbnailViewDrawingRequired() { - return !this.hasImage; - }; - - this.draw = function thumbnailViewDraw(callback) { - if (!this.pdfPage) { - var promise = this.pageSource.getPage(this.id); - promise.then(function(pdfPage) { - this.setPdfPage(pdfPage); - this.draw(callback); - }.bind(this)); - return; - } - - if (this.renderingState !== RenderingStates.INITIAL) { - console.error('Must be in new state before drawing'); - } - - this.renderingState = RenderingStates.RUNNING; - if (this.hasImage) { - callback(); - return; - } - - var self = this; - var ctx = this.getPageDrawContext(); - var drawViewport = this.viewport.clone({ scale: this.scale }); - var renderContext = { - canvasContext: ctx, - viewport: drawViewport, - continueCallback: function(cont) { - if (!self.renderingQueue.isHighestPriority(self)) { - self.renderingState = RenderingStates.PAUSED; - self.resume = function() { - self.renderingState = RenderingStates.RUNNING; - cont(); - }; - return; - } - cont(); - } - }; - this.pdfPage.render(renderContext).promise.then( - function pdfPageRenderCallback() { - self.renderingState = RenderingStates.FINISHED; - callback(); - }, - function pdfPageRenderError(error) { - self.renderingState = RenderingStates.FINISHED; - callback(); - } - ); - this.hasImage = true; - }; - - function getTempCanvas(width, height) { - var tempCanvas = ThumbnailView.tempImageCache; - if (!tempCanvas) { - tempCanvas = document.createElement('canvas'); - ThumbnailView.tempImageCache = tempCanvas; - } - tempCanvas.width = width; - tempCanvas.height = height; - return tempCanvas; - } - - this.setImage = function thumbnailViewSetImage(img) { - if (!this.pdfPage) { - var promise = this.pageSource.getPage(); - promise.then(function(pdfPage) { - this.setPdfPage(pdfPage); - this.setImage(img); - }.bind(this)); - return; - } - if (this.hasImage || !img) { - return; - } - this.renderingState = RenderingStates.FINISHED; - var ctx = this.getPageDrawContext(); - - var reducedImage = img; - var reducedWidth = img.width; - var reducedHeight = img.height; - - // drawImage does an awful job of rescaling the image, doing it gradually - var MAX_SCALE_FACTOR = 2.0; - if (Math.max(img.width / ctx.canvas.width, - img.height / ctx.canvas.height) > MAX_SCALE_FACTOR) { - reducedWidth >>= 1; - reducedHeight >>= 1; - reducedImage = getTempCanvas(reducedWidth, reducedHeight); - var reducedImageCtx = reducedImage.getContext('2d'); - reducedImageCtx.drawImage(img, 0, 0, img.width, img.height, - 0, 0, reducedWidth, reducedHeight); - while (Math.max(reducedWidth / ctx.canvas.width, - reducedHeight / ctx.canvas.height) > MAX_SCALE_FACTOR) { - reducedImageCtx.drawImage(reducedImage, - 0, 0, reducedWidth, reducedHeight, - 0, 0, reducedWidth >> 1, reducedHeight >> 1); - reducedWidth >>= 1; - reducedHeight >>= 1; - } - } - - ctx.drawImage(reducedImage, 0, 0, reducedWidth, reducedHeight, - 0, 0, ctx.canvas.width, ctx.canvas.height); - - this.hasImage = true; - }; -}; - -ThumbnailView.tempImageCache = null; - -/** - * @typedef {Object} PDFThumbnailViewerOptions - * @property {HTMLDivElement} container - The container for the thumbs elements. - * @property {IPDFLinkService} linkService - The navigation/linking service. - * @property {PDFRenderingQueue} renderingQueue - The rendering queue object. - */ - -/** - * Simple viewer control to display thumbs for pages. - * @class - */ -var PDFThumbnailViewer = (function pdfThumbnailViewer() { - /** - * @constructs - * @param {PDFThumbnailViewerOptions} options - */ - function PDFThumbnailViewer(options) { - this.container = options.container; - this.renderingQueue = options.renderingQueue; - this.linkService = options.linkService; - - this.scroll = watchScroll(this.container, this._scrollUpdated.bind(this)); - this._resetView(); - } - - PDFThumbnailViewer.prototype = { - _scrollUpdated: function PDFThumbnailViewer_scrollUpdated() { - this.renderingQueue.renderHighestPriority(); - }, - - getThumbnail: function PDFThumbnailViewer_getThumbnail(index) { - return this.thumbnails[index]; - }, - - _getVisibleThumbs: function PDFThumbnailViewer_getVisibleThumbs() { - return getVisibleElements(this.container, this.thumbnails); - }, - - scrollThumbnailIntoView: function (page) { - var selected = document.querySelector('.thumbnail.selected'); - if (selected) { - selected.classList.remove('selected'); - } - var thumbnail = document.getElementById('thumbnailContainer' + page); - thumbnail.classList.add('selected'); - var visibleThumbs = this._getVisibleThumbs(); - var numVisibleThumbs = visibleThumbs.views.length; - - // If the thumbnail isn't currently visible, scroll it into view. - if (numVisibleThumbs > 0) { - var first = visibleThumbs.first.id; - // Account for only one thumbnail being visible. - var last = (numVisibleThumbs > 1 ? visibleThumbs.last.id : first); - if (page <= first || page >= last) { - scrollIntoView(thumbnail, { top: THUMBNAIL_SCROLL_MARGIN }); - } - } - }, - - get pagesRotation() { - return this._pagesRotation; - }, - - set pagesRotation(rotation) { - this._pagesRotation = rotation; - for (var i = 0, l = this.thumbnails.length; i < l; i++) { - var thumb = this.thumbnails[i]; - thumb.update(rotation); - } - }, - - cleanup: function PDFThumbnailViewer_cleanup() { - ThumbnailView.tempImageCache = null; - }, - - _resetView: function () { - this.thumbnails = []; - this._pagesRotation = 0; - }, - - setDocument: function (pdfDocument) { - if (this.pdfDocument) { - // cleanup of the elements and views - var thumbsView = this.container; - while (thumbsView.hasChildNodes()) { - thumbsView.removeChild(thumbsView.lastChild); - } - this._resetView(); - } - - this.pdfDocument = pdfDocument; - if (!pdfDocument) { - return Promise.resolve(); - } - - return pdfDocument.getPage(1).then(function (firstPage) { - var pagesCount = pdfDocument.numPages; - var viewport = firstPage.getViewport(1.0); - for (var pageNum = 1; pageNum <= pagesCount; ++pageNum) { - var pageSource = new PDFPageSource(pdfDocument, pageNum); - var thumbnail = new ThumbnailView(this.container, pageNum, - viewport.clone(), this.linkService, - this.renderingQueue, pageSource); - this.thumbnails.push(thumbnail); - } - }.bind(this)); - }, - - ensureThumbnailVisible: - function PDFThumbnailViewer_ensureThumbnailVisible(page) { - // Ensure that the thumbnail of the current page is visible - // when switching from another view. - scrollIntoView(document.getElementById('thumbnailContainer' + page)); - }, - - forceRendering: function () { - var visibleThumbs = this._getVisibleThumbs(); - var thumbView = this.renderingQueue.getHighestPriority(visibleThumbs, - this.thumbnails, - this.scroll.down); - if (thumbView) { - this.renderingQueue.renderView(thumbView); - return true; - } - return false; - } - }; - - return PDFThumbnailViewer; -})(); - - -var DocumentOutlineView = function documentOutlineView(options) { - var outline = options.outline; - var outlineView = options.outlineView; - while (outlineView.firstChild) { - outlineView.removeChild(outlineView.firstChild); - } - - if (!outline) { - return; - } - - var linkService = options.linkService; - - function bindItemLink(domObj, item) { - domObj.href = linkService.getDestinationHash(item.dest); - domObj.onclick = function documentOutlineViewOnclick(e) { - linkService.navigateTo(item.dest); - return false; - }; - } - - var queue = [{parent: outlineView, items: outline}]; - while (queue.length > 0) { - var levelData = queue.shift(); - var i, n = levelData.items.length; - for (i = 0; i < n; i++) { - var item = levelData.items[i]; - var div = document.createElement('div'); - div.className = 'outlineItem'; - var a = document.createElement('a'); - bindItemLink(a, item); - a.textContent = item.title; - div.appendChild(a); - - if (item.items.length > 0) { - var itemsDiv = document.createElement('div'); - itemsDiv.className = 'outlineItems'; - div.appendChild(itemsDiv); - queue.push({parent: itemsDiv, items: item.items}); - } - - levelData.parent.appendChild(div); - } - } -}; - - -var DocumentAttachmentsView = function documentAttachmentsView(options) { - var attachments = options.attachments; - var attachmentsView = options.attachmentsView; - while (attachmentsView.firstChild) { - attachmentsView.removeChild(attachmentsView.firstChild); - } - - if (!attachments) { - return; - } - - function bindItemLink(domObj, item) { - domObj.onclick = function documentAttachmentsViewOnclick(e) { - var downloadManager = new DownloadManager(); - downloadManager.downloadData(item.content, getFileName(item.filename), - ''); - return false; - }; - } - - var names = Object.keys(attachments).sort(function(a,b) { - return a.toLowerCase().localeCompare(b.toLowerCase()); - }); - for (var i = 0, ii = names.length; i < ii; i++) { - var item = attachments[names[i]]; - var div = document.createElement('div'); - div.className = 'attachmentsItem'; - var button = document.createElement('button'); - bindItemLink(button, item); - button.textContent = getFileName(item.filename); - div.appendChild(button); - attachmentsView.appendChild(div); - } -}; - - - function webViewerLoad(evt) { PDFViewerApplication.initialize().then(webViewerInitialized); } @@ -6409,20 +6701,15 @@ function webViewerInitialized() { document.addEventListener('DOMContentLoaded', webViewerLoad, true); document.addEventListener('pagerendered', function (e) { - var pageIndex = e.detail.pageNumber - 1; + var pageNumber = e.detail.pageNumber; + var pageIndex = pageNumber - 1; var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex); var thumbnailView = PDFViewerApplication.pdfThumbnailViewer. getThumbnail(pageIndex); - thumbnailView.setImage(pageView.canvas); + thumbnailView.setImage(pageView); - if (pageView.textLayer && pageView.textLayer.textDivs && - pageView.textLayer.textDivs.length > 0 && - !PDFViewerApplication.supportsDocumentColors) { - console.error(mozL10n.get('document_colors_disabled', null, - 'PDF documents are not allowed to use their own colors: ' + - '\'Allow pages to choose their own colors\' ' + - 'is deactivated in the browser.')); - PDFViewerApplication.fallback(); + if (PDFJS.pdfBug && Stats.enabled && pageView.stats) { + Stats.add(pageNumber, pageView.stats); } if (pageView.error) { @@ -6443,12 +6730,27 @@ document.addEventListener('pagerendered', function (e) { // If the page is still visible when it has finished rendering, // ensure that the page number input loading indicator is hidden. - if ((pageIndex + 1) === PDFViewerApplication.page) { + if (pageNumber === PDFViewerApplication.page) { var pageNumberInput = document.getElementById('pageNumber'); pageNumberInput.classList.remove(PAGE_NUMBER_LOADING_INDICATOR); } }, true); +document.addEventListener('textlayerrendered', function (e) { + var pageIndex = e.detail.pageNumber - 1; + var pageView = PDFViewerApplication.pdfViewer.getPageView(pageIndex); + + if (pageView.textLayer && pageView.textLayer.textDivs && + pageView.textLayer.textDivs.length > 0 && + !PDFViewerApplication.supportsDocumentColors) { + console.error(mozL10n.get('document_colors_disabled', null, + 'PDF documents are not allowed to use their own colors: ' + + '\'Allow pages to choose their own colors\' ' + + 'is deactivated in the browser.')); + PDFViewerApplication.fallback(); + } +}, true); + window.addEventListener('presentationmodechanged', function (e) { var active = e.detail.active; var switchInProgress = e.detail.switchInProgress; @@ -6608,6 +6910,14 @@ window.addEventListener('pagechange', function pagechange(evt) { document.getElementById('firstPage').disabled = (page <= 1); document.getElementById('lastPage').disabled = (page >= numPages); + // we need to update stats + if (PDFJS.pdfBug && Stats.enabled) { + var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1); + if (pageView.stats) { + Stats.add(page, pageView.stats); + } + } + // checking if the this.page was called from the updateViewarea function if (evt.updateInProgress) { return; diff --git a/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd b/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd new file mode 100644 index 000000000000..609e001989e4 --- /dev/null +++ b/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/browser/locales/en-US/chrome/browser/browser.dtd b/browser/locales/en-US/chrome/browser/browser.dtd index 0553b443f669..5eb959c4d21f 100644 --- a/browser/locales/en-US/chrome/browser/browser.dtd +++ b/browser/locales/en-US/chrome/browser/browser.dtd @@ -795,11 +795,6 @@ just addresses the organization to follow, e.g. "This site is run by " --> a CSS length value. --> - - - - - diff --git a/browser/locales/jar.mn b/browser/locales/jar.mn index fc739544d9ba..06c914a862c4 100644 --- a/browser/locales/jar.mn +++ b/browser/locales/jar.mn @@ -18,6 +18,7 @@ locale/browser/aboutHealthReport.dtd (%chrome/browser/aboutHealthReport.dtd) #endif locale/browser/aboutSessionRestore.dtd (%chrome/browser/aboutSessionRestore.dtd) + locale/browser/aboutTabCrashed.dtd (%chrome/browser/aboutTabCrashed.dtd) #ifdef MOZ_SERVICES_SYNC locale/browser/syncProgress.dtd (%chrome/browser/syncProgress.dtd) locale/browser/syncCustomize.dtd (%chrome/browser/syncCustomize.dtd) diff --git a/browser/modules/TabCrashReporter.jsm b/browser/modules/TabCrashReporter.jsm index 677688c1465e..030e9c96f327 100644 --- a/browser/modules/TabCrashReporter.jsm +++ b/browser/modules/TabCrashReporter.jsm @@ -82,6 +82,7 @@ this.TabCrashReporter = { if (this.browserMap.get(browser) == childID) { this.browserMap.delete(browser); browser.contentDocument.documentElement.classList.remove("crashDumpAvailable"); + browser.contentDocument.documentElement.classList.add("crashDumpSubmitted"); } } } diff --git a/browser/themes/linux/aboutTabCrashed.css b/browser/themes/linux/aboutTabCrashed.css deleted file mode 100644 index 54d6e8f7f4f0..000000000000 --- a/browser/themes/linux/aboutTabCrashed.css +++ /dev/null @@ -1,108 +0,0 @@ -body { - background-color: rgb(241, 244, 248); - margin-top: 2em; - font: message-box; - font-size: 100%; -} - -p { - font-size: .8em; -} - -#error-box { - background: url('chrome://global/skin/icons/information-24.png') no-repeat left 4px; - -moz-padding-start: 30px; -} - -#error-box:-moz-locale-dir(rtl) { - background-position: right 4px; -} - -#main-error-msg { - color: #4b4b4b; - font-weight: bold; -} - -#report-box { - text-align: center; - width: 75%; - margin: 0 auto; - display: none; -} - -.crashDumpAvailable #report-box { - display: block -} - -#button-box { - text-align: center; - width: 75%; - margin: 0 auto; -} - -@media all and (min-width: 300px) { - #error-box { - max-width: 50%; - margin: 0 auto; - background-image: url('chrome://global/skin/icons/information-32.png'); - min-height: 36px; - -moz-padding-start: 38px; - } - - button { - width: auto !important; - min-width: 150px; - } -} - -@media all and (min-width: 780px) { - #error-box { - max-width: 30%; - } -} - -button { - font: message-box; - font-size: 0.6875em; - -moz-appearance: none; - -moz-user-select: none; - width: 100%; - margin: 2px 0; - padding: 2px 6px; - line-height: 1.2; - background-color: hsla(210,30%,95%,.1); - background-image: linear-gradient(hsla(0,0%,100%,.6), hsla(0,0%,100%,.1)); - background-clip: padding-box; - border: 1px solid hsla(210,15%,25%,.4); - border-color: hsla(210,15%,25%,.3) hsla(210,15%,25%,.35) hsla(210,15%,25%,.4); - border-radius: 3px; - box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset, - 0 0 0 1px hsla(0,0%,100%,.3) inset, - 0 1px 0 hsla(0,0%,100%,.1); - - transition-property: background-color, border-color, box-shadow; - transition-duration: 150ms; - transition-timing-function: ease; - -} - -button:hover { - background-color: hsla(210,30%,95%,.8); - border-color: hsla(210,15%,25%,.45) hsla(210,15%,25%,.5) hsla(210,15%,25%,.55); - box-shadow: 0 1px 0 hsla(0,0%,100%,.3) inset, - 0 0 0 1px hsla(0,0%,100%,.3) inset, - 0 1px 0 hsla(0,0%,100%,.1), - 0 0 3px hsla(210,15%,25%,.1); - transition-property: background-color, border-color, box-shadow; - transition-duration: 150ms; - transition-timing-function: ease; -} - -button:hover:active { - background-color: hsla(210,15%,25%,.2); - box-shadow: 0 1px 1px hsla(210,15%,25%,.2) inset, - 0 0 2px hsla(210,15%,25%,.4) inset; - transition-property: background-color, border-color, box-shadow; - transition-duration: 10ms; - transition-timing-function: linear; -} diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn index a99dc4e786e3..1bfa3112a83b 100644 --- a/browser/themes/linux/jar.mn +++ b/browser/themes/linux/jar.mn @@ -191,6 +191,7 @@ browser.jar: skin/classic/browser/tabbrowser/alltabs.png (tabbrowser/alltabs.png) skin/classic/browser/tabbrowser/alltabs-inverted.png (tabbrowser/alltabs-inverted.png) skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png) + skin/classic/browser/tabbrowser/crashed.svg (../shared/tabbrowser/crashed.svg) skin/classic/browser/tabbrowser/loading.png (tabbrowser/loading.png) skin/classic/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png) skin/classic/browser/tabbrowser/tab-arrow-left.png (tabbrowser/tab-arrow-left.png) diff --git a/browser/themes/osx/browser.css b/browser/themes/osx/browser.css index 38aed4bf54ab..0312b384bb61 100644 --- a/browser/themes/osx/browser.css +++ b/browser/themes/osx/browser.css @@ -3092,6 +3092,14 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker { opacity: .9; } +/* + * Force the overlay to create a new stacking context so it always appears on + * top of the icon. + */ +.tab-icon-overlay { + opacity: 0.9999; +} + .tab-label:not([selected="true"]) { opacity: .7; } diff --git a/browser/themes/osx/jar.mn b/browser/themes/osx/jar.mn index 64bb99f4c3e8..3437e71fbed6 100644 --- a/browser/themes/osx/jar.mn +++ b/browser/themes/osx/jar.mn @@ -301,6 +301,7 @@ browser.jar: skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon.png (tabbrowser/alltabs-box-bkgnd-icon.png) skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon-inverted.png (tabbrowser/alltabs-box-bkgnd-icon-inverted.png) skin/classic/browser/tabbrowser/alltabs-box-bkgnd-icon-inverted@2x.png (tabbrowser/alltabs-box-bkgnd-icon-inverted@2x.png) + skin/classic/browser/tabbrowser/crashed.svg (../shared/tabbrowser/crashed.svg) skin/classic/browser/tabbrowser/newtab.png (tabbrowser/newtab.png) skin/classic/browser/tabbrowser/newtab@2x.png (tabbrowser/newtab@2x.png) skin/classic/browser/tabbrowser/newtab-inverted.png (tabbrowser/newtab-inverted.png) diff --git a/browser/themes/shared/aboutTabCrashed.css b/browser/themes/shared/aboutTabCrashed.css index 2ae76e112c58..2ef767eb8b24 100644 --- a/browser/themes/shared/aboutTabCrashed.css +++ b/browser/themes/shared/aboutTabCrashed.css @@ -1,11 +1,11 @@ +/* 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/. */ + .title { background-image: url("chrome://browser/skin/tab-crashed.svg"); } -#report-box { - display: none; +#reportSent { + font-weight: bold; } - -.crashDumpAvailable #report-box { - display: block -} \ No newline at end of file diff --git a/browser/themes/shared/devedition.inc.css b/browser/themes/shared/devedition.inc.css index c6e9de00d05b..4d6610b39a09 100644 --- a/browser/themes/shared/devedition.inc.css +++ b/browser/themes/shared/devedition.inc.css @@ -157,7 +157,8 @@ /* End override @tabCurveHalfWidth@ and @tabCurveWidth@ */ #urlbar ::-moz-selection, -#navigator-toolbox .searchbar-textbox ::-moz-selection { +#navigator-toolbox .searchbar-textbox ::-moz-selection, +.browserContainer > findbar ::-moz-selection { background-color: var(--chrome-selection-background-color); color: var(--chrome-selection-color); } @@ -182,6 +183,16 @@ color: var(--chrome-color); } +.browserContainer > findbar { + background-image: none; +} + +/* Default findbar text color doesn't look good - Bug 1125677 */ +.browserContainer > findbar .findbar-find-status, +.browserContainer > findbar .found-matches { + color: inherit; +} + #navigator-toolbox .toolbarbutton-1, .browserContainer > findbar .findbar-button, #PlacesToolbar toolbarbutton.bookmark-item { diff --git a/browser/themes/shared/tabbrowser/crashed.svg b/browser/themes/shared/tabbrowser/crashed.svg new file mode 100644 index 000000000000..28a73750dbb3 --- /dev/null +++ b/browser/themes/shared/tabbrowser/crashed.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/browser/themes/shared/tabs.inc.css b/browser/themes/shared/tabs.inc.css index 2fd3f05aa101..11fd26df1cbb 100644 --- a/browser/themes/shared/tabs.inc.css +++ b/browser/themes/shared/tabs.inc.css @@ -65,6 +65,10 @@ -moz-padding-start: 9px; } +.tab-content[pinned] { + -moz-padding-end: 3px; +} + .tab-throbber, .tab-icon-image, .tab-close-button { @@ -75,12 +79,26 @@ .tab-icon-image { height: 16px; width: 16px; + -moz-margin-end: 6px; } .tab-icon-image { list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png"); } +.tab-icon-overlay { + width: 16px; + height: 16px; + margin-top: 10px; + -moz-margin-start: -16px; + display: none; +} + +.tab-icon-overlay[crashed] { + display: -moz-box; + list-style-image: url("chrome://browser/skin/tabbrowser/crashed.svg"); +} + .tab-throbber[busy] { list-style-image: url("chrome://browser/skin/tabbrowser/connecting.png"); } @@ -89,11 +107,6 @@ list-style-image: url("chrome://browser/skin/tabbrowser/loading.png"); } -.tab-throbber:not([pinned]), -.tab-icon-image:not([pinned]) { - -moz-margin-end: 6px; -} - .tab-label { -moz-margin-end: 0; -moz-margin-start: 0; diff --git a/browser/themes/windows/jar.mn b/browser/themes/windows/jar.mn index e96e9c8f85d6..8cd019284f33 100644 --- a/browser/themes/windows/jar.mn +++ b/browser/themes/windows/jar.mn @@ -219,6 +219,7 @@ browser.jar: skin/classic/browser/tabbrowser/newtab.png (tabbrowser/newtab-XPVista7.png) skin/classic/browser/tabbrowser/newtab-inverted.png (tabbrowser/newtab-inverted.png) skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png) + skin/classic/browser/tabbrowser/crashed.svg (../shared/tabbrowser/crashed.svg) skin/classic/browser/tabbrowser/loading.png (tabbrowser/loading.png) skin/classic/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png) skin/classic/browser/tabbrowser/tab-active-middle@2x.png (tabbrowser/tab-active-middle@2x.png) @@ -689,6 +690,7 @@ browser.jar: skin/classic/aero/browser/tabbrowser/newtab-XPVista7.png (tabbrowser/newtab-XPVista7.png) skin/classic/aero/browser/tabbrowser/newtab-inverted.png (tabbrowser/newtab-inverted.png) skin/classic/aero/browser/tabbrowser/connecting.png (tabbrowser/connecting.png) + skin/classic/aero/browser/tabbrowser/crashed.svg (../shared/tabbrowser/crashed.svg) skin/classic/aero/browser/tabbrowser/loading.png (tabbrowser/loading.png) skin/classic/aero/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png) skin/classic/aero/browser/tabbrowser/tab-active-middle@2x.png (tabbrowser/tab-active-middle@2x.png) diff --git a/mobile/android/base/toolbar/BrowserToolbar.java b/mobile/android/base/toolbar/BrowserToolbar.java index c2128847eb5c..fced287bf358 100644 --- a/mobile/android/base/toolbar/BrowserToolbar.java +++ b/mobile/android/base/toolbar/BrowserToolbar.java @@ -546,7 +546,11 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout private void updateProgressVisibility() { final Tab selectedTab = Tabs.getInstance().getSelectedTab(); - updateProgressVisibility(selectedTab, selectedTab.getLoadProgress()); + // The selected tab may be null if GeckoApp (and thus the + // selected tab) are not yet initialized (bug 1090287). + if (selectedTab != null) { + updateProgressVisibility(selectedTab, selectedTab.getLoadProgress()); + } } private void updateProgressVisibility(Tab selectedTab, int progress) { diff --git a/python/mozboot/mozboot/debian.py b/python/mozboot/mozboot/debian.py index 68dbb540cf62..d92aac74accc 100644 --- a/python/mozboot/mozboot/debian.py +++ b/python/mozboot/mozboot/debian.py @@ -121,7 +121,9 @@ class DebianBootstrapper(BaseBootstrapper): def suggest_mobile_android_mozconfig(self): import android - android.suggest_mozconfig(sdk_path=self.sdk_path, + # The SDK path that mozconfig wants includes platforms/android-21. + sdk_path = os.path.join(self.sdk_path, 'platforms', android.ANDROID_PLATFORM) + android.suggest_mozconfig(sdk_path=sdk_path, ndk_path=self.ndk_path) def _update_package_manager(self): diff --git a/toolkit/components/addoncompat/RemoteAddonsChild.jsm b/toolkit/components/addoncompat/RemoteAddonsChild.jsm index 3623244017e9..ecce33c92373 100644 --- a/toolkit/components/addoncompat/RemoteAddonsChild.jsm +++ b/toolkit/components/addoncompat/RemoteAddonsChild.jsm @@ -287,7 +287,7 @@ AboutProtocolChannel.prototype = { function AboutProtocolInstance(contractID) { this._contractID = contractID; - this._uriFlags = null; + this._uriFlags = undefined; } AboutProtocolInstance.prototype = { diff --git a/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js index 1b0d68028187..6f3cc98f62e3 100644 --- a/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js +++ b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js @@ -36,7 +36,7 @@ add_task(function* test_same_date_same_hash() { converter.charset = "UTF-8"; let result = yield OS.File.read(mostRecentBackupFile); let jsonString = converter.convertFromByteArray(result, result.length); - do_log_info("Check is valid JSON"); + do_print("Check is valid JSON"); JSON.parse(jsonString); // Cleanup @@ -66,7 +66,7 @@ add_task(function* test_same_date_diff_hash() { converter.charset = "UTF-8"; let result = yield OS.File.read(mostRecentBackupFile, { compression: "lz4" }); let jsonString = converter.convertFromByteArray(result, result.length); - do_log_info("Check is valid JSON"); + do_print("Check is valid JSON"); JSON.parse(jsonString); // Cleanup diff --git a/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js index 04d90d66c1c1..0dedf8794116 100644 --- a/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js +++ b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js @@ -30,15 +30,15 @@ add_task(function() { PlacesUtils.bookmarks.removeItem(f1); yield BookmarkJSONUtils.importFromFile((yield PlacesBackups.getMostRecentBackup()), true); - do_log_info("Checking first level"); + do_print("Checking first level"); let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; let level1 = root.getChild(0); do_check_eq(level1.title, "f1"); - do_log_info("Checking second level"); + do_print("Checking second level"); PlacesUtils.asContainer(level1).containerOpen = true let level2 = level1.getChild(0); do_check_eq(level2.title, "f2"); - do_log_info("Checking bookmark"); + do_print("Checking bookmark"); PlacesUtils.asContainer(level2).containerOpen = true let bookmark = level2.getChild(0); do_check_eq(bookmark.title, "bookmark"); diff --git a/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js index 0fdb943775ae..f149be4a12bd 100644 --- a/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js +++ b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js @@ -27,7 +27,7 @@ add_task(function() { PlacesUtils.bookmarks.removeItem(bm); yield BookmarkHTMLUtils.importFromFile(file, true); - do_log_info("Checking first level"); + do_print("Checking first level"); let root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root; let node = root.getChild(0); do_check_eq(node.uri, uri.spec); diff --git a/toolkit/components/places/tests/bookmarks/test_async_observers.js b/toolkit/components/places/tests/bookmarks/test_async_observers.js index 2420a87a0ad6..877ffdf8212f 100644 --- a/toolkit/components/places/tests/bookmarks/test_async_observers.js +++ b/toolkit/components/places/tests/bookmarks/test_async_observers.js @@ -33,7 +33,7 @@ let observer = { onItemChanged: function(aItemId, aProperty, aIsAnnotation, aNewValue, aLastModified, aItemType) { - do_log_info("Check that we got the correct change information."); + do_print("Check that we got the correct change information."); do_check_neq(this.bookmarks.indexOf(aItemId), -1); if (aProperty == "favicon") { do_check_false(aIsAnnotation); @@ -57,7 +57,7 @@ let observer = { }, onItemVisited: function(aItemId, aVisitId, aTime) { - do_log_info("Check that we got the correct visit information."); + do_print("Check that we got the correct visit information."); do_check_neq(this.bookmarks.indexOf(aItemId), -1); this.observedVisitId = aVisitId; do_check_eq(aTime, NOW); diff --git a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js index 377067aa434c..428420c8d8b1 100644 --- a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js +++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js @@ -11,7 +11,7 @@ let gBookmarksObserver = { let args = this.expected.shift().args; do_check_eq(aArguments.length, args.length); for (let i = 0; i < aArguments.length; i++) { - do_log_info(aMethodName + "(args[" + i + "]: " + args[i].name + ")"); + do_print(aMethodName + "(args[" + i + "]: " + args[i].name + ")"); do_check_true(args[i].check(aArguments[i])); } diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js index 24148ca3ce9a..eae3cb497c92 100644 --- a/toolkit/components/places/tests/favicons/test_replaceFaviconData.js +++ b/toolkit/components/places/tests/favicons/test_replaceFaviconData.js @@ -59,7 +59,7 @@ function run_test() { }; add_task(function test_replaceFaviconData_validHistoryURI() { - do_log_info("test replaceFaviconData for valid history uri"); + do_print("test replaceFaviconData for valid history uri"); let pageURI = uri("http://test1.bar/"); yield promiseAddVisits(pageURI); @@ -87,7 +87,7 @@ add_task(function test_replaceFaviconData_validHistoryURI() { }); add_task(function test_replaceFaviconData_overrideDefaultFavicon() { - do_log_info("test replaceFaviconData to override a later setAndFetchFaviconForPage"); + do_print("test replaceFaviconData to override a later setAndFetchFaviconForPage"); let pageURI = uri("http://test2.bar/"); yield promiseAddVisits(pageURI); @@ -119,7 +119,7 @@ add_task(function test_replaceFaviconData_overrideDefaultFavicon() { }); add_task(function test_replaceFaviconData_replaceExisting() { - do_log_info("test replaceFaviconData to override a previous setAndFetchFaviconForPage"); + do_print("test replaceFaviconData to override a previous setAndFetchFaviconForPage"); let pageURI = uri("http://test3.bar"); yield promiseAddVisits(pageURI); @@ -156,7 +156,7 @@ add_task(function test_replaceFaviconData_replaceExisting() { }); add_task(function test_replaceFaviconData_unrelatedReplace() { - do_log_info("test replaceFaviconData to not make unrelated changes"); + do_print("test replaceFaviconData to not make unrelated changes"); let pageURI = uri("http://test4.bar/"); yield promiseAddVisits(pageURI); @@ -188,7 +188,7 @@ add_task(function test_replaceFaviconData_unrelatedReplace() { }); add_task(function test_replaceFaviconData_badInputs() { - do_log_info("test replaceFaviconData to throw on bad inputs"); + do_print("test replaceFaviconData to throw on bad inputs"); let favicon = createFavicon("favicon8.png"); @@ -228,7 +228,7 @@ add_task(function test_replaceFaviconData_badInputs() { }); add_task(function test_replaceFaviconData_twiceReplace() { - do_log_info("test replaceFaviconData on multiple replacements"); + do_print("test replaceFaviconData on multiple replacements"); let pageURI = uri("http://test5.bar/"); yield promiseAddVisits(pageURI); diff --git a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js index 5d1d1d395718..6dae7e751c77 100644 --- a/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js +++ b/toolkit/components/places/tests/favicons/test_replaceFaviconDataFromDataURL.js @@ -63,7 +63,7 @@ function run_test() { }; add_task(function test_replaceFaviconDataFromDataURL_validHistoryURI() { - do_log_info("test replaceFaviconDataFromDataURL for valid history uri"); + do_print("test replaceFaviconDataFromDataURL for valid history uri"); let pageURI = uri("http://test1.bar/"); yield promiseAddVisits(pageURI); @@ -89,7 +89,7 @@ add_task(function test_replaceFaviconDataFromDataURL_validHistoryURI() { }); add_task(function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() { - do_log_info("test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage"); + do_print("test replaceFaviconDataFromDataURL to override a later setAndFetchFaviconForPage"); let pageURI = uri("http://test2.bar/"); yield promiseAddVisits(pageURI); @@ -119,7 +119,7 @@ add_task(function test_replaceFaviconDataFromDataURL_overrideDefaultFavicon() { }); add_task(function test_replaceFaviconDataFromDataURL_replaceExisting() { - do_log_info("test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage"); + do_print("test replaceFaviconDataFromDataURL to override a previous setAndFetchFaviconForPage"); let pageURI = uri("http://test3.bar"); yield promiseAddVisits(pageURI); @@ -152,7 +152,7 @@ add_task(function test_replaceFaviconDataFromDataURL_replaceExisting() { }); add_task(function test_replaceFaviconDataFromDataURL_unrelatedReplace() { - do_log_info("test replaceFaviconDataFromDataURL to not make unrelated changes"); + do_print("test replaceFaviconDataFromDataURL to not make unrelated changes"); let pageURI = uri("http://test4.bar/"); yield promiseAddVisits(pageURI); @@ -182,7 +182,7 @@ add_task(function test_replaceFaviconDataFromDataURL_unrelatedReplace() { }); add_task(function test_replaceFaviconDataFromDataURL_badInputs() { - do_log_info("test replaceFaviconDataFromDataURL to throw on bad inputs"); + do_print("test replaceFaviconDataFromDataURL to throw on bad inputs"); let favicon = createFavicon("favicon8.png"); @@ -210,7 +210,7 @@ add_task(function test_replaceFaviconDataFromDataURL_badInputs() { }); add_task(function test_replaceFaviconDataFromDataURL_twiceReplace() { - do_log_info("test replaceFaviconDataFromDataURL on multiple replacements"); + do_print("test replaceFaviconDataFromDataURL on multiple replacements"); let pageURI = uri("http://test5.bar/"); yield promiseAddVisits(pageURI); @@ -241,7 +241,7 @@ add_task(function test_replaceFaviconDataFromDataURL_twiceReplace() { }); add_task(function test_replaceFaviconDataFromDataURL_afterRegularAssign() { - do_log_info("test replaceFaviconDataFromDataURL after replaceFaviconData"); + do_print("test replaceFaviconDataFromDataURL after replaceFaviconData"); let pageURI = uri("http://test6.bar/"); yield promiseAddVisits(pageURI); @@ -274,7 +274,7 @@ add_task(function test_replaceFaviconDataFromDataURL_afterRegularAssign() { }); add_task(function test_replaceFaviconDataFromDataURL_beforeRegularAssign() { - do_log_info("test replaceFaviconDataFromDataURL before replaceFaviconData"); + do_print("test replaceFaviconDataFromDataURL before replaceFaviconData"); let pageURI = uri("http://test7.bar/"); yield promiseAddVisits(pageURI); diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js index a1b31a8fbb10..f292b89a5ed6 100644 --- a/toolkit/components/places/tests/head_common.js +++ b/toolkit/components/places/tests/head_common.js @@ -760,17 +760,6 @@ function do_check_guid_for_bookmark(aId, } } -/** - * Logs info to the console in the standard way (includes the filename). - * - * @param aMessage - * The message to log to the console. - */ -function do_log_info(aMessage) -{ - print("TEST-INFO | " + _TEST_FILE + " | " + aMessage); -} - /** * Compares 2 arrays returning whether they contains the same elements. * diff --git a/toolkit/components/places/tests/inline/head_autocomplete.js b/toolkit/components/places/tests/inline/head_autocomplete.js index 13895cef9247..7dbbe259070a 100644 --- a/toolkit/components/places/tests/inline/head_autocomplete.js +++ b/toolkit/components/places/tests/inline/head_autocomplete.js @@ -139,7 +139,7 @@ function ensure_results(aSearchString, aExpectedValue) { waitForCleanup(run_next_test); }; - do_log_info("Searching for: '" + aSearchString + "'"); + do_print("Searching for: '" + aSearchString + "'"); controller.startSearch(aSearchString); } @@ -153,7 +153,7 @@ function run_test() { gAutoCompleteTests.forEach(function (testData) { let [description, searchString, expectedValue, setupFunc] = testData; add_test(function () { - do_log_info(description); + do_print(description); Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true); Services.prefs.setBoolPref("browser.urlbar.autoFill", true); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); diff --git a/toolkit/components/places/tests/network/test_history_redirects.js b/toolkit/components/places/tests/network/test_history_redirects.js index 56c82701f77a..13717fb82bf5 100644 --- a/toolkit/components/places/tests/network/test_history_redirects.js +++ b/toolkit/components/places/tests/network/test_history_redirects.js @@ -92,8 +92,8 @@ function continue_test() { try { while(stmt.executeStep()) { let comparator = EXPECTED.shift(); - do_log_info("Checking that '" + comparator.url + - "' was entered into the DB correctly"); + do_print("Checking that '" + comparator.url + + "' was entered into the DB correctly"); do_check_eq(stmt.row.id, comparator.id); do_check_eq(stmt.row.url, comparator.url); do_check_eq(stmt.row.from_visit, comparator.from_visit); @@ -155,7 +155,7 @@ ChannelListener.prototype = { }, onStartRequest: function(request, context) { - do_log_info("onStartRequest"); + do_print("onStartRequest"); this._got_onstartrequest = true; }, @@ -164,7 +164,7 @@ ChannelListener.prototype = { }, onStopRequest: function(request, context, status) { - do_log_info("onStopRequest"); + do_print("onStopRequest"); this._got_onstoprequest++; let success = Components.isSuccessCode(status); do_check_true(success); @@ -177,7 +177,7 @@ ChannelListener.prototype = { // nsIChannelEventSink asyncOnChannelRedirect: function (aOldChannel, aNewChannel, aFlags, callback) { - do_log_info("onChannelRedirect"); + do_print("onChannelRedirect"); this._got_onchannelredirect = true; callback.onRedirectVerifyCallback(Components.results.NS_OK); }, diff --git a/toolkit/components/places/tests/queries/test_tags.js b/toolkit/components/places/tests/queries/test_tags.js index 7be4f35f9c60..85d99de77651 100644 --- a/toolkit/components/places/tests/queries/test_tags.js +++ b/toolkit/components/places/tests/queries/test_tags.js @@ -13,29 +13,29 @@ function tags_getter_setter() { - do_log_info("Tags getter/setter should work correctly"); - do_log_info("Without setting tags, tags getter should return empty array"); + do_print("Tags getter/setter should work correctly"); + do_print("Without setting tags, tags getter should return empty array"); var [query, dummy] = makeQuery(); do_check_eq(query.tags.length, 0); - do_log_info("Setting tags to an empty array, tags getter should return "+ - "empty array"); + do_print("Setting tags to an empty array, tags getter should return "+ + "empty array"); [query, dummy] = makeQuery([]); do_check_eq(query.tags.length, 0); - do_log_info("Setting a few tags, tags getter should return correct array"); + do_print("Setting a few tags, tags getter should return correct array"); var tags = ["bar", "baz", "foo"]; [query, dummy] = makeQuery(tags); setsAreEqual(query.tags, tags, true); - do_log_info("Setting some dupe tags, tags getter return unique tags"); + do_print("Setting some dupe tags, tags getter return unique tags"); [query, dummy] = makeQuery(["foo", "foo", "bar", "foo", "baz", "bar"]); setsAreEqual(query.tags, ["bar", "baz", "foo"], true); }, function invalid_setter_calls() { - do_log_info("Invalid calls to tags setter should fail"); + do_print("Invalid calls to tags setter should fail"); try { var query = PlacesUtils.history.getNewQuery(); query.tags = null; @@ -105,19 +105,19 @@ function not_setting_tags() { - do_log_info("Not setting tags at all should not affect query URI"); + do_print("Not setting tags at all should not affect query URI"); checkQueryURI(); }, function empty_array_tags() { - do_log_info("Setting tags with an empty array should not affect query URI"); + do_print("Setting tags with an empty array should not affect query URI"); checkQueryURI([]); }, function set_tags() { - do_log_info("Setting some tags should result in correct query URI"); + do_print("Setting some tags should result in correct query URI"); checkQueryURI([ "foo", "七難", @@ -133,22 +133,22 @@ function no_tags_tagsAreNot() { - do_log_info("Not setting tags at all but setting tagsAreNot should " + - "affect query URI"); + do_print("Not setting tags at all but setting tagsAreNot should " + + "affect query URI"); checkQueryURI(null, true); }, function empty_array_tags_tagsAreNot() { - do_log_info("Setting tags with an empty array and setting tagsAreNot " + - "should affect query URI"); + do_print("Setting tags with an empty array and setting tagsAreNot " + + "should affect query URI"); checkQueryURI([], true); }, function () { - do_log_info("Setting some tags and setting tagsAreNot should result in " + - "correct query URI"); + do_print("Setting some tags and setting tagsAreNot should result in " + + "correct query URI"); checkQueryURI([ "foo", "七難", @@ -164,8 +164,8 @@ function tag_to_uri() { - do_log_info("Querying history on tag associated with a URI should return " + - "that URI"); + do_print("Querying history on tag associated with a URI should return " + + "that URI"); yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo"]); executeAndCheckQueryResults(query, opts, [aURI.spec]); @@ -178,8 +178,8 @@ function tags_to_uri() { - do_log_info("Querying history on many tags associated with a URI should " + - "return that URI"); + do_print("Querying history on many tags associated with a URI should " + + "return that URI"); yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo", "bar"]); executeAndCheckQueryResults(query, opts, [aURI.spec]); @@ -194,8 +194,8 @@ function repeated_tag() { - do_log_info("Specifying the same tag multiple times in a history query " + - "should not matter"); + do_print("Specifying the same tag multiple times in a history query " + + "should not matter"); yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo", "foo"]); executeAndCheckQueryResults(query, opts, [aURI.spec]); @@ -206,8 +206,8 @@ function many_tags_no_uri() { - do_log_info("Querying history on many tags associated with a URI and " + - "tags not associated with that URI should not return that URI"); + do_print("Querying history on many tags associated with a URI and " + + "tags not associated with that URI should not return that URI"); yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo", "bogus"]); executeAndCheckQueryResults(query, opts, []); @@ -220,7 +220,7 @@ function nonexistent_tags() { - do_log_info("Querying history on nonexistent tags should return no results"); + do_print("Querying history on nonexistent tags should return no results"); yield task_doWithVisit(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["bogus"]); executeAndCheckQueryResults(query, opts, []); @@ -231,8 +231,8 @@ function tag_to_bookmark() { - do_log_info("Querying bookmarks on tag associated with a URI should " + - "return that URI"); + do_print("Querying bookmarks on tag associated with a URI should " + + "return that URI"); yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; @@ -248,8 +248,8 @@ function many_tags_to_bookmark() { - do_log_info("Querying bookmarks on many tags associated with a URI " + - "should return that URI"); + do_print("Querying bookmarks on many tags associated with a URI " + + "should return that URI"); yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo", "bar"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; @@ -268,8 +268,8 @@ function repeated_tag_to_bookmarks() { - do_log_info("Specifying the same tag multiple times in a bookmark query " + - "should not matter"); + do_print("Specifying the same tag multiple times in a bookmark query " + + "should not matter"); yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo", "foo"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; @@ -282,8 +282,8 @@ function many_tags_no_bookmark() { - do_log_info("Querying bookmarks on many tags associated with a URI and " + - "tags not associated with that URI should not return that URI"); + do_print("Querying bookmarks on many tags associated with a URI and " + + "tags not associated with that URI should not return that URI"); yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["foo", "bogus"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; @@ -299,7 +299,7 @@ function nonexistent_tags_bookmark() { - do_log_info("Querying bookmarks on nonexistent tag should return no results"); + do_print("Querying bookmarks on nonexistent tag should return no results"); yield task_doWithBookmark(["foo", "bar", "baz"], function (aURI) { var [query, opts] = makeQuery(["bogus"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; @@ -312,14 +312,14 @@ function tagsAreNot_history() { - do_log_info("Querying history using tagsAreNot should work correctly"); + do_print("Querying history using tagsAreNot should work correctly"); var urisAndTags = { "http://example.com/1": ["foo", "bar"], "http://example.com/2": ["baz", "qux"], "http://example.com/3": null }; - do_log_info("Add visits and tag the URIs"); + do_print("Add visits and tag the URIs"); for (let [pURI, tags] in Iterator(urisAndTags)) { let nsiuri = uri(pURI); yield promiseAddVisits(nsiuri); @@ -327,27 +327,27 @@ PlacesUtils.tagging.tagURI(nsiuri, tags); } - do_log_info(' Querying for "foo" should match only /2 and /3'); + do_print(' Querying for "foo" should match only /2 and /3'); var [query, opts] = makeQuery(["foo"], true); queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/2", "http://example.com/3"]); - do_log_info(' Querying for "foo" and "bar" should match only /2 and /3'); + do_print(' Querying for "foo" and "bar" should match only /2 and /3'); [query, opts] = makeQuery(["foo", "bar"], true); queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/2", "http://example.com/3"]); - do_log_info(' Querying for "foo" and "bogus" should match only /2 and /3'); + do_print(' Querying for "foo" and "bogus" should match only /2 and /3'); [query, opts] = makeQuery(["foo", "bogus"], true); queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/2", "http://example.com/3"]); - do_log_info(' Querying for "foo" and "baz" should match only /3'); + do_print(' Querying for "foo" and "baz" should match only /3'); [query, opts] = makeQuery(["foo", "baz"], true); queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/3"]); - do_log_info(' Querying for "bogus" should match all'); + do_print(' Querying for "bogus" should match all'); [query, opts] = makeQuery(["bogus"], true); queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/1", @@ -365,14 +365,14 @@ function tagsAreNot_bookmarks() { - do_log_info("Querying bookmarks using tagsAreNot should work correctly"); + do_print("Querying bookmarks using tagsAreNot should work correctly"); var urisAndTags = { "http://example.com/1": ["foo", "bar"], "http://example.com/2": ["baz", "qux"], "http://example.com/3": null }; - do_log_info("Add bookmarks and tag the URIs"); + do_print("Add bookmarks and tag the URIs"); for (let [pURI, tags] in Iterator(urisAndTags)) { let nsiuri = uri(pURI); addBookmark(nsiuri); @@ -380,31 +380,31 @@ PlacesUtils.tagging.tagURI(nsiuri, tags); } - do_log_info(' Querying for "foo" should match only /2 and /3'); + do_print(' Querying for "foo" should match only /2 and /3'); var [query, opts] = makeQuery(["foo"], true); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/2", "http://example.com/3"]); - do_log_info(' Querying for "foo" and "bar" should match only /2 and /3'); + do_print(' Querying for "foo" and "bar" should match only /2 and /3'); [query, opts] = makeQuery(["foo", "bar"], true); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/2", "http://example.com/3"]); - do_log_info(' Querying for "foo" and "bogus" should match only /2 and /3'); + do_print(' Querying for "foo" and "bogus" should match only /2 and /3'); [query, opts] = makeQuery(["foo", "bogus"], true); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/2", "http://example.com/3"]); - do_log_info(' Querying for "foo" and "baz" should match only /3'); + do_print(' Querying for "foo" and "baz" should match only /3'); [query, opts] = makeQuery(["foo", "baz"], true); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, ["http://example.com/3"]); - do_log_info(' Querying for "bogus" should match all'); + do_print(' Querying for "bogus" should match all'); [query, opts] = makeQuery(["bogus"], true); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, @@ -422,16 +422,16 @@ }, function duplicate_tags() { - do_log_info("Duplicate existing tags (i.e., multiple tag folders with " + - "same name) should not throw off query results"); + do_print("Duplicate existing tags (i.e., multiple tag folders with " + + "same name) should not throw off query results"); var tagName = "foo"; - do_log_info("Add bookmark and tag it normally"); + do_print("Add bookmark and tag it normally"); addBookmark(TEST_URI); PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); - do_log_info("Manually create tag folder with same name as tag and insert " + - "bookmark"); + do_print("Manually create tag folder with same name as tag and insert " + + "bookmark"); var dupTagId = PlacesUtils.bookmarks.createFolder(PlacesUtils.tagsFolderId, tagName, Ci.nsINavBookmarksService.DEFAULT_INDEX); @@ -442,7 +442,7 @@ "title"); do_check_true(bmId > 0); - do_log_info("Querying for tag should match URI"); + do_print("Querying for tag should match URI"); var [query, opts] = makeQuery([tagName]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); @@ -453,21 +453,21 @@ function folder_named_as_tag() { - do_log_info("Regular folders with the same name as tag should not throw " + - "off query results"); + do_print("Regular folders with the same name as tag should not throw " + + "off query results"); var tagName = "foo"; - do_log_info("Add bookmark and tag it"); + do_print("Add bookmark and tag it"); addBookmark(TEST_URI); PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); - do_log_info("Create folder with same name as tag"); + do_print("Create folder with same name as tag"); var folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, tagName, Ci.nsINavBookmarksService.DEFAULT_INDEX); do_check_true(folderId > 0); - do_log_info("Querying for tag should match URI"); + do_print("Querying for tag should match URI"); var [query, opts] = makeQuery([tagName]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); @@ -477,7 +477,7 @@ }, function ORed_queries() { - do_log_info("Multiple queries ORed together should work"); + do_print("Multiple queries ORed together should work"); var urisAndTags = { "http://example.com/1": [], "http://example.com/2": [] @@ -490,7 +490,7 @@ urisAndTags["http://example.com/2"].push("/2 tag " + i); } - do_log_info("Add visits and tag the URIs"); + do_print("Add visits and tag the URIs"); for (let [pURI, tags] in Iterator(urisAndTags)) { let nsiuri = uri(pURI); yield promiseAddVisits(nsiuri); @@ -498,40 +498,40 @@ PlacesUtils.tagging.tagURI(nsiuri, tags); } - do_log_info("Query for /1 OR query for /2 should match both /1 and /2"); + do_print("Query for /1 OR query for /2 should match both /1 and /2"); var [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); var [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"]); var root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); - do_log_info("Query for /1 OR query on bogus tag should match only /1"); + do_print("Query for /1 OR query on bogus tag should match only /1"); [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); [query2, dummy] = makeQuery(["bogus"]); root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; queryResultsAre(root, ["http://example.com/1"]); - do_log_info("Query for /1 OR query for /1 should match only /1"); + do_print("Query for /1 OR query for /1 should match only /1"); [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); [query2, dummy] = makeQuery(urisAndTags["http://example.com/1"]); root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; queryResultsAre(root, ["http://example.com/1"]); - do_log_info("Query for /1 with tagsAreNot OR query for /2 with tagsAreNot " + - "should match both /1 and /2"); + do_print("Query for /1 with tagsAreNot OR query for /2 with tagsAreNot " + + "should match both /1 and /2"); [query1, opts] = makeQuery(urisAndTags["http://example.com/1"], true); [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"], true); root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); - do_log_info("Query for /1 OR query for /2 with tagsAreNot should match " + - "only /1"); + do_print("Query for /1 OR query for /2 with tagsAreNot should match " + + "only /1"); [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"], true); root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; queryResultsAre(root, ["http://example.com/1"]); - do_log_info("Query for /1 OR query for /1 with tagsAreNot should match " + - "both URIs"); + do_print("Query for /1 OR query for /1 with tagsAreNot should match " + + "both URIs"); [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); [query2, dummy] = makeQuery(urisAndTags["http://example.com/1"], true); root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; @@ -568,7 +568,7 @@ function addBookmark(aURI) { aURI, Ci.nsINavBookmarksService.DEFAULT_INDEX, aURI.spec); - do_log_info("Sanity check: insertBookmark should not fail"); + do_print("Sanity check: insertBookmark should not fail"); do_check_true(bmId > 0); } @@ -596,7 +596,7 @@ function checkQueryURI(aTags, aTagsAreNot) { var expURI = "place:" + pairs.join("&"); var [query, opts] = makeQuery(aTags, aTagsAreNot); var actualURI = queryURI(query, opts); - do_log_info("Query URI should be what we expect for the given tags"); + do_print("Query URI should be what we expect for the given tags"); do_check_eq(actualURI, expURI); } @@ -683,7 +683,7 @@ function executeAndCheckQueryResults(aQuery, aQueryOpts, aExpectedURIs) { */ function makeQuery(aTags, aTagsAreNot) { aTagsAreNot = !!aTagsAreNot; - do_log_info("Making a query " + + do_print("Making a query " + (aTags ? "with tags " + aTags.toSource() : "without calling setTags() at all") + @@ -701,7 +701,7 @@ function makeQuery(aTags, aTagsAreNot) { uniqueTags.sort(); } - do_log_info("Made query should be correct for tags and tagsAreNot"); + do_print("Made query should be correct for tags and tagsAreNot"); if (uniqueTags) setsAreEqual(query.tags, uniqueTags, true); var expCount = uniqueTags ? uniqueTags.length : 0; diff --git a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js index 5aed5ee6cb7a..3613f9f06306 100644 --- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js +++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js @@ -122,12 +122,12 @@ function* check_autocomplete(test) { let numSearchesStarted = 0; input.onSearchBegin = () => { - do_log_info("onSearchBegin received"); + do_print("onSearchBegin received"); numSearchesStarted++; }; let deferred = Promise.defer(); input.onSearchComplete = () => { - do_log_info("onSearchComplete received"); + do_print("onSearchComplete received"); deferred.resolve(); } @@ -136,7 +136,7 @@ function* check_autocomplete(test) { controller.startSearch(test.incompleteSearch); expectedSearches++; } - do_log_info("Searching for: '" + test.search + "'"); + do_print("Searching for: '" + test.search + "'"); controller.startSearch(test.search); yield deferred.promise; @@ -150,7 +150,7 @@ function* check_autocomplete(test) { for (let i = 0; i < controller.matchCount; i++) { let value = controller.getValueAt(i); let comment = controller.getCommentAt(i); - do_log_info("Looking for '" + value + "', '" + comment + "' in expected results..."); + do_print("Looking for '" + value + "', '" + comment + "' in expected results..."); let j; for (j = 0; j < matches.length; j++) { // Skip processed expected results @@ -167,10 +167,10 @@ function* check_autocomplete(test) { else style = ["favicon"]; - do_log_info("Checking against expected '" + uri.spec + "', '" + title + "'..."); + do_print("Checking against expected '" + uri.spec + "', '" + title + "'..."); // Got a match on both uri and title? if (stripPrefix(uri.spec) == stripPrefix(value) && title == comment) { - do_log_info("Got a match at index " + j + "!"); + do_print("Got a match at index " + j + "!"); let actualStyle = controller.getStyleAt(i).split(/\s+/).sort(); if (style) Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style"); @@ -254,7 +254,7 @@ function changeRestrict(aType, aChar) { else branch += "restrict."; - do_log_info("changing restrict for " + aType + " to '" + aChar + "'"); + do_print("changing restrict for " + aType + " to '" + aChar + "'"); Services.prefs.setCharPref(branch + aType, aChar); } diff --git a/toolkit/components/places/tests/unifiedcomplete/test_416211.js b/toolkit/components/places/tests/unifiedcomplete/test_416211.js index 0592cd83b0aa..014563f6b21c 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_416211.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_416211.js @@ -8,7 +8,7 @@ */ add_task(function* test_tag_match_has_bookmark_title() { - do_log_info("Make sure the tag match gives the bookmark title"); + do_print("Make sure the tag match gives the bookmark title"); let uri = NetUtil.newURI("http://theuri/"); yield promiseAddVisits({ uri: uri, title: "Page title" }); addBookmark({ uri: uri, diff --git a/toolkit/components/places/tests/unifiedcomplete/test_416214.js b/toolkit/components/places/tests/unifiedcomplete/test_416214.js index 8fe1267d7a04..35030f16031b 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_416214.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_416214.js @@ -15,7 +15,7 @@ */ add_task(function* test_tag_match_url() { - do_log_info("Make sure tag matches return the right url as well as '+' remain escaped"); + do_print("Make sure tag matches return the right url as well as '+' remain escaped"); let uri1 = NetUtil.newURI("http://escaped/ユニコード"); let uri2 = NetUtil.newURI("http://asciiescaped/blocking-firefox3%2B"); yield promiseAddVisits([ { uri: uri1, title: "title" }, diff --git a/toolkit/components/places/tests/unifiedcomplete/test_417798.js b/toolkit/components/places/tests/unifiedcomplete/test_417798.js index aa5555b5b685..4f307b589bd8 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_417798.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_417798.js @@ -16,32 +16,32 @@ add_task(function* test_javascript_match() { addBookmark({ uri: uri2, title: "Title with javascript:" }); - do_log_info("Match non-javascript: with plain search"); + do_print("Match non-javascript: with plain search"); yield check_autocomplete({ search: "a", matches: [ { uri: uri1, title: "Title with javascript:" } ] }); - do_log_info("Match non-javascript: with almost javascript:"); + do_print("Match non-javascript: with almost javascript:"); yield check_autocomplete({ search: "javascript", matches: [ { uri: uri1, title: "Title with javascript:" } ] }); - do_log_info("Match javascript:"); + do_print("Match javascript:"); yield check_autocomplete({ search: "javascript:", matches: [ { uri: uri1, title: "Title with javascript:" }, { uri: uri2, title: "Title with javascript:", style: [ "bookmark" ]} ] }); - do_log_info("Match nothing with non-first javascript:"); + do_print("Match nothing with non-first javascript:"); yield check_autocomplete({ search: "5 javascript:", matches: [ ] }); - do_log_info("Match javascript: with multi-word search"); + do_print("Match javascript: with multi-word search"); yield check_autocomplete({ search: "javascript: 5", matches: [ { uri: uri2, title: "Title with javascript:", style: [ "bookmark" ]} ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_418257.js b/toolkit/components/places/tests/unifiedcomplete/test_418257.js index 64bbdaf51958..b6064ee024fe 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_418257.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_418257.js @@ -30,31 +30,31 @@ add_task(function* test_javascript_match() { title: "tagged", tags: [ "tag1", "tag2", "tag3" ] }); - do_log_info("Make sure tags come back in the title when matching tags"); + do_print("Make sure tags come back in the title when matching tags"); yield check_autocomplete({ search: "page1 tag", matches: [ { uri: uri1, title: "tagged", tags: [ "tag1" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Check tags in title for page2"); + do_print("Check tags in title for page2"); yield check_autocomplete({ search: "page2 tag", matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Make sure tags appear even when not matching the tag"); + do_print("Make sure tags appear even when not matching the tag"); yield check_autocomplete({ search: "page3", matches: [ { uri: uri3, title: "tagged", tags: [ "tag1", "tag3" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Multiple tags come in commas for page4"); + do_print("Multiple tags come in commas for page4"); yield check_autocomplete({ search: "page4", matches: [ { uri: uri4, title: "tagged", tags: [ "tag1", "tag2", "tag3" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Extra test just to make sure we match the title"); + do_print("Extra test just to make sure we match the title"); yield check_autocomplete({ search: "tag2", matches: [ { uri: uri2, title: "tagged", tags: [ "tag1", "tag2" ], style: [ "bookmark-tag" ] }, diff --git a/toolkit/components/places/tests/unifiedcomplete/test_422277.js b/toolkit/components/places/tests/unifiedcomplete/test_422277.js index a738c2a82ee8..b6e7b5e5f549 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_422277.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_422277.js @@ -8,7 +8,7 @@ */ add_task(function* test_javascript_match() { - do_log_info("Bad escaped uri stays escaped"); + do_print("Bad escaped uri stays escaped"); let uri1 = NetUtil.newURI("http://site/%EAid"); yield promiseAddVisits([ { uri: uri1, title: "title" } ]); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autoFill_default_behavior.js b/toolkit/components/places/tests/unifiedcomplete/test_autoFill_default_behavior.js index 57d6cbf26f65..57e759e17cb2 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_autoFill_default_behavior.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_autoFill_default_behavior.js @@ -27,7 +27,7 @@ add_task(function* test_default_behavior_host() { Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false); Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); - do_log_info("Restrict history, common visit, should not autoFill"); + do_print("Restrict history, common visit, should not autoFill"); yield check_autocomplete({ search: "vi", matches: [ { uri: uri2, title: "visited" } ], @@ -35,7 +35,7 @@ add_task(function* test_default_behavior_host() { completed: "vi" }); - do_log_info("Restrict history, typed visit, should autoFill"); + do_print("Restrict history, typed visit, should autoFill"); yield check_autocomplete({ search: "ty", matches: [ { uri: uri1, title: "typed", style: [ "autofill" ] } ], @@ -44,7 +44,7 @@ add_task(function* test_default_behavior_host() { }); // Don't autoFill this one cause it's not typed. - do_log_info("Restrict history, bookmark, should not autoFill"); + do_print("Restrict history, bookmark, should not autoFill"); yield check_autocomplete({ search: "bo", matches: [ ], @@ -53,7 +53,7 @@ add_task(function* test_default_behavior_host() { }); // Note we don't show this one cause it's not typed. - do_log_info("Restrict history, typed bookmark, should autoFill"); + do_print("Restrict history, typed bookmark, should autoFill"); yield check_autocomplete({ search: "tp", matches: [ { uri: uri4, title: "tpbk", style: [ "autofill" ] } ], @@ -66,7 +66,7 @@ add_task(function* test_default_behavior_host() { // We are not restricting on typed, so we autoFill the bookmark even if we // are restricted to history. We accept that cause not doing that // would be a perf hit and the privacy implications are very weak. - do_log_info("Restrict history, bookmark, autoFill.typed = false, should autoFill"); + do_print("Restrict history, bookmark, autoFill.typed = false, should autoFill"); yield check_autocomplete({ search: "bo", matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ], style: [ "autofill" ] } ], @@ -74,7 +74,7 @@ add_task(function* test_default_behavior_host() { completed: "bookmarked/" }); - do_log_info("Restrict history, common visit, autoFill.typed = false, should autoFill"); + do_print("Restrict history, common visit, autoFill.typed = false, should autoFill"); yield check_autocomplete({ search: "vi", matches: [ { uri: uri2, title: "visited", style: [ "autofill" ] } ], @@ -87,7 +87,7 @@ add_task(function* test_default_behavior_host() { Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true); // Typed behavior basically acts like history, but filters on typed. - do_log_info("Restrict typed, common visit, autoFill.typed = false, should not autoFill"); + do_print("Restrict typed, common visit, autoFill.typed = false, should not autoFill"); yield check_autocomplete({ search: "vi", matches: [ ], @@ -95,7 +95,7 @@ add_task(function* test_default_behavior_host() { completed: "vi" }); - do_log_info("Restrict typed, typed visit, autofill.typed = false, should autoFill"); + do_print("Restrict typed, typed visit, autofill.typed = false, should autoFill"); yield check_autocomplete({ search: "ty", matches: [ { uri: uri1, title: "typed", style: [ "autofill" ] } ], @@ -103,7 +103,7 @@ add_task(function* test_default_behavior_host() { completed: "typed/" }); - do_log_info("Restrict typed, bookmark, autofill.typed = false, should not autoFill"); + do_print("Restrict typed, bookmark, autofill.typed = false, should not autoFill"); yield check_autocomplete({ search: "bo", matches: [ ], @@ -111,7 +111,7 @@ add_task(function* test_default_behavior_host() { completed: "bo" }); - do_log_info("Restrict typed, typed bookmark, autofill.typed = false, should autoFill"); + do_print("Restrict typed, typed bookmark, autofill.typed = false, should autoFill"); yield check_autocomplete({ search: "tp", matches: [ { uri: uri4, title: "tpbk", style: [ "autofill" ] } ], @@ -124,7 +124,7 @@ add_task(function* test_default_behavior_host() { Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true); - do_log_info("Restrict bookmarks, common visit, should not autoFill"); + do_print("Restrict bookmarks, common visit, should not autoFill"); yield check_autocomplete({ search: "vi", matches: [ ], @@ -132,7 +132,7 @@ add_task(function* test_default_behavior_host() { completed: "vi" }); - do_log_info("Restrict bookmarks, typed visit, should not autoFill"); + do_print("Restrict bookmarks, typed visit, should not autoFill"); yield check_autocomplete({ search: "ty", matches: [ ], @@ -141,7 +141,7 @@ add_task(function* test_default_behavior_host() { }); // Don't autoFill this one cause it's not typed. - do_log_info("Restrict bookmarks, bookmark, should not autoFill"); + do_print("Restrict bookmarks, bookmark, should not autoFill"); yield check_autocomplete({ search: "bo", matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ] } ], @@ -150,7 +150,7 @@ add_task(function* test_default_behavior_host() { }); // Note we don't show this one cause it's not typed. - do_log_info("Restrict bookmarks, typed bookmark, should autoFill"); + do_print("Restrict bookmarks, typed bookmark, should autoFill"); yield check_autocomplete({ search: "tp", matches: [ { uri: uri4, title: "tpbk", style: [ "autofill" ] } ], @@ -160,7 +160,7 @@ add_task(function* test_default_behavior_host() { Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); - do_log_info("Restrict bookmarks, bookmark, autofill.typed = false, should autoFill"); + do_print("Restrict bookmarks, bookmark, autofill.typed = false, should autoFill"); yield check_autocomplete({ search: "bo", matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill" ] } ], @@ -169,7 +169,7 @@ add_task(function* test_default_behavior_host() { }); // Don't autofill because it's a title. - do_log_info("Restrict bookmarks, title, autofill.typed = false, should not autoFill"); + do_print("Restrict bookmarks, title, autofill.typed = false, should not autoFill"); yield check_autocomplete({ search: "# ta", matches: [ ], @@ -178,7 +178,7 @@ add_task(function* test_default_behavior_host() { }); // Don't autofill because it's a tag. - do_log_info("Restrict bookmarks, tag, autofill.typed = false, should not autoFill"); + do_print("Restrict bookmarks, tag, autofill.typed = false, should not autoFill"); yield check_autocomplete({ search: "+ ta", matches: [ { uri: uri5, title: "title", tags: [ "foo" ], style: [ "tag" ] } ], @@ -210,7 +210,7 @@ add_task(function* test_default_behavior_url() { Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true); Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); - do_log_info("URL: Restrict history, common visit, should not autoFill"); + do_print("URL: Restrict history, common visit, should not autoFill"); yield check_autocomplete({ search: "visited/v", matches: [ { uri: uri2, title: "visited" } ], @@ -218,7 +218,7 @@ add_task(function* test_default_behavior_url() { completed: "visited/v" }); - do_log_info("URL: Restrict history, typed visit, should autoFill"); + do_print("URL: Restrict history, typed visit, should autoFill"); yield check_autocomplete({ search: "typed/t", matches: [ { uri: uri1, title: "typed/ty/", style: [ "autofill" ] } ], @@ -227,7 +227,7 @@ add_task(function* test_default_behavior_url() { }); // Don't autoFill this one cause it's not typed. - do_log_info("URL: Restrict history, bookmark, should not autoFill"); + do_print("URL: Restrict history, bookmark, should not autoFill"); yield check_autocomplete({ search: "bookmarked/b", matches: [ ], @@ -236,7 +236,7 @@ add_task(function* test_default_behavior_url() { }); // Note we don't show this one cause it's not typed. - do_log_info("URL: Restrict history, typed bookmark, should autoFill"); + do_print("URL: Restrict history, typed bookmark, should autoFill"); yield check_autocomplete({ search: "tpbk/t", matches: [ { uri: uri4, title: "tpbk/tp/", style: [ "autofill" ] } ], @@ -248,7 +248,7 @@ add_task(function* test_default_behavior_url() { Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); - do_log_info("URL: Restrict bookmarks, common visit, should not autoFill"); + do_print("URL: Restrict bookmarks, common visit, should not autoFill"); yield check_autocomplete({ search: "visited/v", matches: [ ], @@ -256,7 +256,7 @@ add_task(function* test_default_behavior_url() { completed: "visited/v" }); - do_log_info("URL: Restrict bookmarks, typed visit, should not autoFill"); + do_print("URL: Restrict bookmarks, typed visit, should not autoFill"); yield check_autocomplete({ search: "typed/t", matches: [ ], @@ -265,7 +265,7 @@ add_task(function* test_default_behavior_url() { }); // Don't autoFill this one cause it's not typed. - do_log_info("URL: Restrict bookmarks, bookmark, should not autoFill"); + do_print("URL: Restrict bookmarks, bookmark, should not autoFill"); yield check_autocomplete({ search: "bookmarked/b", matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ] } ], @@ -274,7 +274,7 @@ add_task(function* test_default_behavior_url() { }); // Note we don't show this one cause it's not typed. - do_log_info("URL: Restrict bookmarks, typed bookmark, should autoFill"); + do_print("URL: Restrict bookmarks, typed bookmark, should autoFill"); yield check_autocomplete({ search: "tpbk/t", matches: [ { uri: uri4, title: "tpbk/tp/", style: [ "autofill" ] } ], @@ -284,7 +284,7 @@ add_task(function* test_default_behavior_url() { Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); - do_log_info("URL: Restrict bookmarks, bookmark, autofill.typed = false, should autoFill"); + do_print("URL: Restrict bookmarks, bookmark, autofill.typed = false, should autoFill"); yield check_autocomplete({ search: "bookmarked/b", matches: [ { uri: uri3, title: "bookmarked/bo/", style: [ "autofill" ] } ], diff --git a/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js index bae14734ddf2..f77bb7555a2f 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_autocomplete_functional.js @@ -7,7 +7,7 @@ const PREF_AUTOCOMPLETE_ENABLED = "browser.urlbar.autocomplete.enabled"; add_task(function* test_disabling_autocomplete() { - do_log_info("Check disabling autocomplete disables autofill"); + do_print("Check disabling autocomplete disables autofill"); Services.prefs.setBoolPref(PREF_AUTOCOMPLETE_ENABLED, false); yield promiseAddVisits({ uri: NetUtil.newURI("http://visit.mozilla.org"), transition: TRANSITION_TYPED }); @@ -20,7 +20,7 @@ add_task(function* test_disabling_autocomplete() { }); add_task(function* test_urls_order() { - do_log_info("Add urls, check for correct order"); + do_print("Add urls, check for correct order"); let places = [{ uri: NetUtil.newURI("http://visit1.mozilla.org") }, { uri: NetUtil.newURI("http://visit2.mozilla.org"), transition: TRANSITION_TYPED }]; @@ -34,7 +34,7 @@ add_task(function* test_urls_order() { }); add_task(function* test_ignore_prefix() { - do_log_info("Add urls, make sure www and http are ignored"); + do_print("Add urls, make sure www and http are ignored"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://www.visit1.mozilla.org")); yield check_autocomplete({ @@ -46,7 +46,7 @@ add_task(function* test_ignore_prefix() { }); add_task(function* test_after_host() { - do_log_info("Autocompleting after an existing host completes to the url"); + do_print("Autocompleting after an existing host completes to the url"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://www.visit3.mozilla.org")); yield check_autocomplete({ @@ -58,7 +58,7 @@ add_task(function* test_after_host() { }); add_task(function* test_respect_www() { - do_log_info("Searching for www.me should yield www.me.mozilla.org/"); + do_print("Searching for www.me should yield www.me.mozilla.org/"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://www.me.mozilla.org")); yield check_autocomplete({ @@ -70,7 +70,7 @@ add_task(function* test_respect_www() { }); add_task(function* test_bookmark_first() { - do_log_info("With a bookmark and history, the query result should be the bookmark"); + do_print("With a bookmark and history, the query result should be the bookmark"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); addBookmark({ uri: NetUtil.newURI("http://bookmark1.mozilla.org/") }); yield promiseAddVisits(NetUtil.newURI("http://bookmark1.mozilla.org/foo")); @@ -83,7 +83,7 @@ add_task(function* test_bookmark_first() { }); add_task(function* test_full_path() { - do_log_info("Check to make sure we get the proper results with full paths"); + do_print("Check to make sure we get the proper results with full paths"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") }, { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }]; @@ -97,7 +97,7 @@ add_task(function* test_full_path() { }); add_task(function* test_complete_to_slash() { - do_log_info("Check to make sure we autocomplete to the following '/'"); + do_print("Check to make sure we autocomplete to the following '/'"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); let places = [{ uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=delicious") }, { uri: NetUtil.newURI("http://smokey.mozilla.org/foo/bar/baz?bacon=smokey") }]; @@ -111,7 +111,7 @@ add_task(function* test_complete_to_slash() { }); add_task(function* test_complete_to_slash_with_www() { - do_log_info("Check to make sure we autocomplete to the following '/'"); + do_print("Check to make sure we autocomplete to the following '/'"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); let places = [{ uri: NetUtil.newURI("http://www.smokey.mozilla.org/foo/bar/baz?bacon=delicious") }, { uri: NetUtil.newURI("http://www.smokey.mozilla.org/foo/bar/baz?bacon=smokey") }]; @@ -125,7 +125,7 @@ add_task(function* test_complete_to_slash_with_www() { }); add_task(function* test_complete_querystring() { - do_log_info("Check to make sure we autocomplete after ?"); + do_print("Check to make sure we autocomplete after ?"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious")); yield check_autocomplete({ @@ -137,7 +137,7 @@ add_task(function* test_complete_querystring() { }); add_task(function* test_complete_fragment() { - do_log_info("Check to make sure we autocomplete after #"); + do_print("Check to make sure we autocomplete after #"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://smokey.mozilla.org/foo?bacon=delicious#bar")); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js b/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js index 1fb62e9c6f0b..ed552c42414a 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_avoid_middle_complete.js @@ -6,7 +6,7 @@ add_task(function* test_prefix_space_noautofill() { yield promiseAddVisits({ uri: NetUtil.newURI("http://moz.org/test/"), transition: TRANSITION_TYPED }); - do_log_info("Should not try to autoFill if search string contains a space"); + do_print("Should not try to autoFill if search string contains a space"); yield check_autocomplete({ search: " mo", autofilled: " mo", @@ -20,7 +20,7 @@ add_task(function* test_trailing_space_noautofill() { yield promiseAddVisits({ uri: NetUtil.newURI("http://moz.org/test/"), transition: TRANSITION_TYPED }); - do_log_info("Should not try to autoFill if search string contains a space"); + do_print("Should not try to autoFill if search string contains a space"); yield check_autocomplete({ search: "mo ", autofilled: "mo ", @@ -38,7 +38,7 @@ add_task(function* test_searchEngine_autofill() { engine.addParam("q", "{searchTerms}", null); do_register_cleanup(() => Services.search.removeEngine(engine)); - do_log_info("Should autoFill search engine if search string does not contains a space"); + do_print("Should autoFill search engine if search string does not contains a space"); yield check_autocomplete({ search: "ca", autofilled: "cake.search", @@ -56,7 +56,7 @@ add_task(function* test_searchEngine_prefix_space_noautofill() { engine.addParam("q", "{searchTerms}", null); do_register_cleanup(() => Services.search.removeEngine(engine)); - do_log_info("Should not try to autoFill search engine if search string contains a space"); + do_print("Should not try to autoFill search engine if search string contains a space"); yield check_autocomplete({ search: " cu", autofilled: " cu", @@ -74,7 +74,7 @@ add_task(function* test_searchEngine_trailing_space_noautofill() { engine.addParam("q", "{searchTerms}", null); do_register_cleanup(() => Services.search.removeEngine(engine)); - do_log_info("Should not try to autoFill search engine if search string contains a space"); + do_print("Should not try to autoFill search engine if search string contains a space"); yield check_autocomplete({ search: "ba ", autofilled: "ba ", @@ -92,7 +92,7 @@ add_task(function* test_searchEngine_www_noautofill() { engine.addParam("q", "{searchTerms}", null); do_register_cleanup(() => Services.search.removeEngine(engine)); - do_log_info("Should not autoFill search engine if search string contains www. but engine doesn't"); + do_print("Should not autoFill search engine if search string contains www. but engine doesn't"); yield check_autocomplete({ search: "www.ham", autofilled: "www.ham", @@ -110,7 +110,7 @@ add_task(function* test_searchEngine_different_scheme_noautofill() { engine.addParam("q", "{searchTerms}", null); do_register_cleanup(() => Services.search.removeEngine(engine)); - do_log_info("Should not autoFill search engine if search string has a different scheme."); + do_print("Should not autoFill search engine if search string has a different scheme."); yield check_autocomplete({ search: "http://pie", autofilled: "http://pie", @@ -129,21 +129,21 @@ add_task(function* test_searchEngine_matching_prefix_autofill() { do_register_cleanup(() => Services.search.removeEngine(engine)); - do_log_info("Should autoFill search engine if search string has matching prefix."); + do_print("Should autoFill search engine if search string has matching prefix."); yield check_autocomplete({ search: "http://www.be", autofilled: "http://www.bean.search", completed: "http://www.bean.search" }) - do_log_info("Should autoFill search engine if search string has www prefix."); + do_print("Should autoFill search engine if search string has www prefix."); yield check_autocomplete({ search: "www.be", autofilled: "www.bean.search", completed: "http://www.bean.search" }); - do_log_info("Should autoFill search engine if search string has matching scheme."); + do_print("Should autoFill search engine if search string has matching scheme."); yield check_autocomplete({ search: "http://be", autofilled: "http://bean.search", @@ -159,7 +159,7 @@ add_task(function* test_prefix_autofill() { yield promiseAddVisits({ uri: NetUtil.newURI("http://moz.org/test/"), transition: TRANSITION_TYPED }); - do_log_info("Should not try to autoFill in-the-middle if a search is canceled immediately"); + do_print("Should not try to autoFill in-the-middle if a search is canceled immediately"); yield check_autocomplete({ incompleteSearch: "moz", search: "mozi", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js b/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js index ec62a160cc5d..917bfad60185 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_avoid_stripping_to_empty_tokens.js @@ -28,7 +28,7 @@ add_task(function* test_protocol_trimming() { "www.mo te" ]; for (let input of inputs) { - do_log_info("Searching for: " + input); + do_print("Searching for: " + input); yield check_autocomplete({ search: input, matches: matches diff --git a/toolkit/components/places/tests/unifiedcomplete/test_casing.js b/toolkit/components/places/tests/unifiedcomplete/test_casing.js index 8a2156d3e7b9..e31548d6b1da 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_casing.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_casing.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ add_task(function* test_casing_1() { - do_log_info("Searching for cased entry 1"); + do_print("Searching for cased entry 1"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -15,7 +15,7 @@ add_task(function* test_casing_1() { }); add_task(function* test_casing_2() { - do_log_info("Searching for cased entry 2"); + do_print("Searching for cased entry 2"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -27,7 +27,7 @@ add_task(function* test_casing_2() { }); add_task(function* test_casing_3() { - do_log_info("Searching for cased entry 3"); + do_print("Searching for cased entry 3"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -39,7 +39,7 @@ add_task(function* test_casing_3() { }); add_task(function* test_casing_4() { - do_log_info("Searching for cased entry 4"); + do_print("Searching for cased entry 4"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -51,7 +51,7 @@ add_task(function* test_casing_4() { }); add_task(function* test_casing_5() { - do_log_info("Searching for cased entry 5"); + do_print("Searching for cased entry 5"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -63,7 +63,7 @@ add_task(function* test_casing_5() { }); add_task(function* test_untrimmed_casing() { - do_log_info("Searching for untrimmed cased entry"); + do_print("Searching for untrimmed cased entry"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -75,7 +75,7 @@ add_task(function* test_untrimmed_casing() { }); add_task(function* test_untrimmed_www_casing() { - do_log_info("Searching for untrimmed cased entry with www"); + do_print("Searching for untrimmed cased entry with www"); yield promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -87,7 +87,7 @@ add_task(function* test_untrimmed_www_casing() { }); add_task(function* test_untrimmed_path_casing() { - do_log_info("Searching for untrimmed cased entry with path"); + do_print("Searching for untrimmed cased entry with path"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -99,7 +99,7 @@ add_task(function* test_untrimmed_path_casing() { }); add_task(function* test_untrimmed_path_casing_2() { - do_log_info("Searching for untrimmed cased entry with path 2"); + do_print("Searching for untrimmed cased entry with path 2"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -111,7 +111,7 @@ add_task(function* test_untrimmed_path_casing_2() { }); add_task(function* test_untrimmed_path_www_casing() { - do_log_info("Searching for untrimmed cased entry with www and path"); + do_print("Searching for untrimmed cased entry with www and path"); yield promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -123,7 +123,7 @@ add_task(function* test_untrimmed_path_www_casing() { }); add_task(function* test_untrimmed_path_www_casing_2() { - do_log_info("Searching for untrimmed cased entry with www and path 2"); + do_print("Searching for untrimmed cased entry with www and path 2"); yield promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/Test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js b/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js index 7c1862ef1516..e92acef3c651 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_do_not_trim.js @@ -6,7 +6,7 @@ // that largely confuses completeDefaultIndex add_task(function* test_not_autofill_ws_1() { - do_log_info("Do not autofill whitespaced entry 1"); + do_print("Do not autofill whitespaced entry 1"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -18,7 +18,7 @@ add_task(function* test_not_autofill_ws_1() { }); add_task(function* test_not_autofill_ws_2() { - do_log_info("Do not autofill whitespaced entry 2"); + do_print("Do not autofill whitespaced entry 2"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -30,7 +30,7 @@ add_task(function* test_not_autofill_ws_2() { }); add_task(function* test_not_autofill_ws_3() { - do_log_info("Do not autofill whitespaced entry 3"); + do_print("Do not autofill whitespaced entry 3"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -42,7 +42,7 @@ add_task(function* test_not_autofill_ws_3() { }); add_task(function* test_not_autofill_ws_4() { - do_log_info("Do not autofill whitespaced entry 4"); + do_print("Do not autofill whitespaced entry 4"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -55,7 +55,7 @@ add_task(function* test_not_autofill_ws_4() { add_task(function* test_not_autofill_ws_5() { - do_log_info("Do not autofill whitespaced entry 5"); + do_print("Do not autofill whitespaced entry 5"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -67,7 +67,7 @@ add_task(function* test_not_autofill_ws_5() { }); add_task(function* test_not_autofill_ws_6() { - do_log_info("Do not autofill whitespaced entry 6"); + do_print("Do not autofill whitespaced entry 6"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js b/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js index 4b30406a6f01..e2033436c69c 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_download_embed_bookmarks.js @@ -35,37 +35,37 @@ add_task(function* test_download_embed_bookmarks() { addBookmark({ uri: uri3, title: "framed-bookmark" }); - do_log_info("Searching for bookmarked download uri matches"); + do_print("Searching for bookmarked download uri matches"); yield check_autocomplete({ search: "download-bookmark", matches: [ { uri: uri1, title: "download-bookmark", style: [ "bookmark" ] } ] }); - do_log_info("Searching for bookmarked embed uri matches"); + do_print("Searching for bookmarked embed uri matches"); yield check_autocomplete({ search: "embed-bookmark", matches: [ { uri: uri2, title: "embed-bookmark", style: [ "bookmark" ] } ] }); - do_log_info("Searching for bookmarked framed uri matches"); + do_print("Searching for bookmarked framed uri matches"); yield check_autocomplete({ search: "framed-bookmark", matches: [ { uri: uri3, title: "framed-bookmark", style: [ "bookmark" ] } ] }); - do_log_info("Searching for download uri does not match"); + do_print("Searching for download uri does not match"); yield check_autocomplete({ search: "download2", matches: [ ] }); - do_log_info("Searching for embed uri does not match"); + do_print("Searching for embed uri does not match"); yield check_autocomplete({ search: "embed2", matches: [ ] }); - do_log_info("Searching for framed uri does not match"); + do_print("Searching for framed uri does not match"); yield check_autocomplete({ search: "framed2", matches: [ ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js b/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js index 7415dca8ae63..9ba6ef9a94aa 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_dupe_urls.js @@ -4,7 +4,7 @@ // Ensure inline autocomplete doesn't return zero frecency pages. add_task(function* test_dupe_urls() { - do_log_info("Searching for urls with dupes should only show one"); + do_print("Searching for urls with dupes should only show one"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("http://mozilla.org/?") }); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js index ed52c6d60b7b..d8ac6a89647c 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_empty_search.js @@ -40,7 +40,7 @@ add_task(function* test_javascript_match() { // Now remove page 6 from history, so it is an unvisited bookmark. PlacesUtils.history.removePage(uri6); - do_log_info("Match everything"); + do_print("Match everything"); yield check_autocomplete({ search: "foo", searchParam: "enable-actions", @@ -53,21 +53,21 @@ add_task(function* test_javascript_match() { { uri: makeActionURI("switchtab", {url: "http://t.foo/6"}), title: "title", style: [ "action,switchtab" ] }, ] }); - do_log_info("Match only typed history"); + do_print("Match only typed history"); yield check_autocomplete({ search: "foo ^ ~", matches: [ { uri: uri3, title: "title" }, { uri: uri4, title: "title" } ] }); - do_log_info("Drop-down empty search matches only typed history"); + do_print("Drop-down empty search matches only typed history"); yield check_autocomplete({ search: "", matches: [ { uri: uri3, title: "title" }, { uri: uri4, title: "title" } ] }); - do_log_info("Drop-down empty search matches only bookmarks"); + do_print("Drop-down empty search matches only bookmarks"); Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); yield check_autocomplete({ @@ -78,7 +78,7 @@ add_task(function* test_javascript_match() { { uri: uri6, title: "title", style: ["bookmark"] } ] }); - do_log_info("Drop-down empty search matches only open tabs"); + do_print("Drop-down empty search matches only open tabs"); Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false); yield check_autocomplete({ search: "", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js index 4e83f99ac860..69b15c2e0acf 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js @@ -11,20 +11,20 @@ add_task(function* test_enabled() { let uri = NetUtil.newURI("http://url/0"); yield promiseAddVisits([ { uri: uri, title: "title" } ]); - do_log_info("plain search"); + do_print("plain search"); yield check_autocomplete({ search: "url", matches: [ { uri: uri, title: "title" } ] }); - do_log_info("search disabled"); + do_print("search disabled"); Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false); yield check_autocomplete({ search: "url", matches: [ ] }); - do_log_info("resume normal search"); + do_print("resume normal search"); Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true); yield check_autocomplete({ search: "url", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js b/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js index a4830ecbdffe..1ef51a286635 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_escape_self.js @@ -13,13 +13,13 @@ add_task(function* test_escape() { yield promiseAddVisits([ { uri: uri1, title: "title" }, { uri: uri2, title: "title" } ]); - do_log_info("Unescaped location matches itself"); + do_print("Unescaped location matches itself"); yield check_autocomplete({ search: "http://unescapeduri/", matches: [ { uri: uri1, title: "title" } ] }); - do_log_info("Escaped location matches itself"); + do_print("Escaped location matches itself"); yield check_autocomplete({ search: "http://escapeduri/%40/", matches: [ { uri: uri2, title: "title" } ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js b/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js index 53070e005a1c..8b670f5d2fc2 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_ignore_protocol.js @@ -12,7 +12,7 @@ add_task(function* test_escape() { yield promiseAddVisits([ { uri: uri1, title: "title" }, { uri: uri2, title: "title" } ]); - do_log_info("Searching for h matches site and not http://"); + do_print("Searching for h matches site and not http://"); yield check_autocomplete({ search: "h", matches: [ { uri: uri2, title: "title" } ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js index d9b3923b8853..4d40b69adbf1 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js @@ -19,43 +19,43 @@ add_task(function* test_keyword_searc() { { uri: uri2, title: "Generic page title" } ]); addBookmark({ uri: uri1, title: "Keyword title", keyword: "key"}); - do_log_info("Plain keyword query"); + do_print("Plain keyword query"); yield check_autocomplete({ search: "key term", matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "Keyword title", style: ["keyword"] } ] }); - do_log_info("Multi-word keyword query"); + do_print("Multi-word keyword query"); yield check_autocomplete({ search: "key multi word", matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "Keyword title", style: ["keyword"] } ] }); - do_log_info("Keyword query with +"); + do_print("Keyword query with +"); yield check_autocomplete({ search: "key blocking+", matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "Keyword title", style: ["keyword"] } ] }); - do_log_info("Unescaped term in query"); + do_print("Unescaped term in query"); yield check_autocomplete({ search: "key ユニコード", matches: [ { uri: NetUtil.newURI("http://abc/?search=ユニコード"), title: "Keyword title", style: ["keyword"] } ] }); - do_log_info("Keyword that happens to match a page"); + do_print("Keyword that happens to match a page"); yield check_autocomplete({ search: "key ThisPageIsInHistory", matches: [ { uri: NetUtil.newURI("http://abc/?search=ThisPageIsInHistory"), title: "Generic page title", style: ["bookmark"] } ] }); - do_log_info("Keyword without query (without space)"); + do_print("Keyword without query (without space)"); yield check_autocomplete({ search: "key", matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Keyword title", style: ["keyword"] } ] }); - do_log_info("Keyword without query (with space)"); + do_print("Keyword without query (with space)"); yield check_autocomplete({ search: "key ", matches: [ { uri: NetUtil.newURI("http://abc/?search="), title: "Keyword title", style: ["keyword"] } ] @@ -66,7 +66,7 @@ add_task(function* test_keyword_searc() { yield promiseAddVisits([ { uri: uri3, title: "Generic page title" } ]); addBookmark({ uri: uri3, title: "Keyword title", keyword: "key", style: ["keyword"] }); - do_log_info("Two keywords matched"); + do_print("Two keywords matched"); yield check_autocomplete({ search: "key twoKey", matches: [ { uri: NetUtil.newURI("http://abc/?search=twoKey"), title: "Keyword title", style: ["keyword"] }, diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js index fa80fbae7674..aba1631ae279 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js @@ -19,49 +19,49 @@ add_task(function* test_keyword_search() { { uri: uri2, title: "Generic page title" } ]); addBookmark({ uri: uri1, title: "Keyword title", keyword: "key"}); - do_log_info("Plain keyword query"); + do_print("Plain keyword query"); yield check_autocomplete({ search: "key term", searchParam: "enable-actions", matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "Keyword title", style: [ "action", "keyword" ] } ] }); - do_log_info("Multi-word keyword query"); + do_print("Multi-word keyword query"); yield check_autocomplete({ search: "key multi word", searchParam: "enable-actions", matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "Keyword title", style: [ "action", "keyword" ] } ] }); - do_log_info("Keyword query with +"); + do_print("Keyword query with +"); yield check_autocomplete({ search: "key blocking+", searchParam: "enable-actions", matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "Keyword title", style: [ "action", "keyword" ] } ] }); - do_log_info("Unescaped term in query"); + do_print("Unescaped term in query"); yield check_autocomplete({ search: "key ユニコード", searchParam: "enable-actions", matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ユニコード", input: "key ユニコード"}), title: "Keyword title", style: [ "action", "keyword" ] } ] }); - do_log_info("Keyword that happens to match a page"); + do_print("Keyword that happens to match a page"); yield check_autocomplete({ search: "key ThisPageIsInHistory", searchParam: "enable-actions", matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "Keyword title", style: [ "action", "keyword" ] } ] }); - do_log_info("Keyword without query (without space)"); + do_print("Keyword without query (without space)"); yield check_autocomplete({ search: "key", searchParam: "enable-actions", matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "Keyword title", style: [ "action", "keyword" ] } ] }); - do_log_info("Keyword without query (with space)"); + do_print("Keyword without query (with space)"); yield check_autocomplete({ search: "key ", searchParam: "enable-actions", @@ -73,7 +73,7 @@ add_task(function* test_keyword_search() { yield promiseAddVisits([ { uri: uri3, title: "Generic page title" } ]); addBookmark({ uri: uri3, title: "Keyword title", keyword: "key"}); - do_log_info("Two keywords matched"); + do_print("Two keywords matched"); yield check_autocomplete({ search: "key twoKey", searchParam: "enable-actions", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_keywords.js b/toolkit/components/places/tests/unifiedcomplete/test_keywords.js index d14ec2bc690f..248a9b51d489 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_keywords.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_keywords.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ add_task(function* test_non_keyword() { - do_log_info("Searching for non-keyworded entry should autoFill it"); + do_print("Searching for non-keyworded entry should autoFill it"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/") }); @@ -16,7 +16,7 @@ add_task(function* test_non_keyword() { }); add_task(function* test_keyword() { - do_log_info("Searching for keyworded entry should not autoFill it"); + do_print("Searching for keyworded entry should not autoFill it"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); @@ -29,7 +29,7 @@ add_task(function* test_keyword() { }); add_task(function* test_more_than_keyword() { - do_log_info("Searching for more than keyworded entry should autoFill it"); + do_print("Searching for more than keyworded entry should autoFill it"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); @@ -42,7 +42,7 @@ add_task(function* test_more_than_keyword() { }); add_task(function* test_less_than_keyword() { - do_log_info("Searching for less than keyworded entry should autoFill it"); + do_print("Searching for less than keyworded entry should autoFill it"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); @@ -55,7 +55,7 @@ add_task(function* test_less_than_keyword() { }); add_task(function* test_keyword_casing() { - do_log_info("Searching for keyworded entry is case-insensitive"); + do_print("Searching for keyworded entry is case-insensitive"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/test/"), transition: TRANSITION_TYPED }); addBookmark({ uri: NetUtil.newURI("http://mozilla.org/test/"), keyword: "moz" }); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js b/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js index e08c195a37f6..84b2c5bf5751 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_match_beginning.js @@ -15,32 +15,32 @@ add_task(function* test_match_beginning() { yield promiseAddVisits([ { uri: uri1, title: "a b" }, { uri: uri2, title: "b a" } ]); - do_log_info("Match at the beginning of titles"); + do_print("Match at the beginning of titles"); Services.prefs.setIntPref("browser.urlbar.matchBehavior", 3); yield check_autocomplete({ search: "a", matches: [ { uri: uri1, title: "a b" } ] }); - do_log_info("Match at the beginning of titles"); + do_print("Match at the beginning of titles"); yield check_autocomplete({ search: "b", matches: [ { uri: uri2, title: "b a" } ] }); - do_log_info("Match at the beginning of urls"); + do_print("Match at the beginning of urls"); yield check_autocomplete({ search: "x", matches: [ { uri: uri1, title: "a b" } ] }); - do_log_info("Match at the beginning of urls"); + do_print("Match at the beginning of urls"); yield check_autocomplete({ search: "y", matches: [ { uri: uri2, title: "b a" } ] }); - do_log_info("Sanity check that matching anywhere finds more"); + do_print("Sanity check that matching anywhere finds more"); Services.prefs.setIntPref("browser.urlbar.matchBehavior", 1); yield check_autocomplete({ search: "a", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js b/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js index e63ed78c7345..e7b86cf6f037 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_multi_word_search.js @@ -24,39 +24,39 @@ add_task(function* test_match_beginning() { addBookmark({ uri: uri3, title: "f(o)o br" }); addBookmark({ uri: uri4, title: "b(a)r bz" }); - do_log_info("Match 2 terms all in url"); + do_print("Match 2 terms all in url"); yield check_autocomplete({ search: "c d", matches: [ { uri: uri1, title: "f(o)o br" } ] }); - do_log_info("Match 1 term in url and 1 term in title"); + do_print("Match 1 term in url and 1 term in title"); yield check_autocomplete({ search: "b e", matches: [ { uri: uri1, title: "f(o)o br" }, { uri: uri2, title: "b(a)r bz" } ] }); - do_log_info("Match 3 terms all in title; display bookmark title if matched"); + do_print("Match 3 terms all in title; display bookmark title if matched"); yield check_autocomplete({ search: "b a z", matches: [ { uri: uri2, title: "b(a)r bz" }, { uri: uri4, title: "b(a)r bz", style: [ "bookmark" ] } ] }); - do_log_info("Match 2 terms in url and 1 in title; make sure bookmark title is used for search"); + do_print("Match 2 terms in url and 1 in title; make sure bookmark title is used for search"); yield check_autocomplete({ search: "k f t", matches: [ { uri: uri3, title: "f(o)o br", style: [ "bookmark" ] } ] }); - do_log_info("Match 3 terms in url and 1 in title"); + do_print("Match 3 terms in url and 1 in title"); yield check_autocomplete({ search: "d i g z", matches: [ { uri: uri2, title: "b(a)r bz" } ] }); - do_log_info("Match nothing"); + do_print("Match nothing"); yield check_autocomplete({ search: "m o z i", matches: [ ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_queryurl.js b/toolkit/components/places/tests/unifiedcomplete/test_queryurl.js index 2d25d5ceba26..e74d8b1e55ea 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_queryurl.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_queryurl.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ add_task(function* test_no_slash() { - do_log_info("Searching for host match without slash should match host"); + do_print("Searching for host match without slash should match host"); yield promiseAddVisits({ uri: NetUtil.newURI("http://file.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("file:///c:/test.html"), @@ -17,7 +17,7 @@ add_task(function* test_no_slash() { }); add_task(function* test_w_slash() { - do_log_info("Searching match with slash at the end should do nothing"); + do_print("Searching match with slash at the end should do nothing"); yield promiseAddVisits({ uri: NetUtil.newURI("http://file.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("file:///c:/test.html"), @@ -31,7 +31,7 @@ add_task(function* test_w_slash() { }); add_task(function* test_middle() { - do_log_info("Searching match with slash in the middle should match url"); + do_print("Searching match with slash in the middle should match url"); yield promiseAddVisits({ uri: NetUtil.newURI("http://file.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("file:///c:/test.html"), @@ -45,7 +45,7 @@ add_task(function* test_middle() { }); add_task(function* test_nonhost() { - do_log_info("Searching for non-host match without slash should not match url"); + do_print("Searching for non-host match without slash should not match url"); yield promiseAddVisits({ uri: NetUtil.newURI("file:///c:/test.html"), transition: TRANSITION_TYPED }); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_current.js b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_current.js index 8369ffde3d77..0977fe8ebf5a 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_current.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_current.js @@ -10,28 +10,28 @@ add_task(function*() { Services.search.addEngineWithDetails("AliasedMozSearch", "", "doit", "", "GET", "http://s.example.com/search"); - do_log_info("search engine"); + do_print("search engine"); yield check_autocomplete({ search: "mozilla", searchParam: "enable-actions", matches: [ { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "mozilla", searchQuery: "mozilla"}), title: "MozSearch", style: [ "action", "searchengine" ] }, ] }); - do_log_info("search engine, uri-like input"); + do_print("search engine, uri-like input"); yield check_autocomplete({ search: "http:///", searchParam: "enable-actions", matches: [ { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "http:///", searchQuery: "http:///"}), title: "MozSearch", style: [ "action", "searchengine" ] }, ] }); - do_log_info("search engine, multiple words"); + do_print("search engine, multiple words"); yield check_autocomplete({ search: "mozzarella cheese", searchParam: "enable-actions", matches: [ { uri: makeActionURI("searchengine", {engineName: "MozSearch", input: "mozzarella cheese", searchQuery: "mozzarella cheese"}), title: "MozSearch", style: [ "action", "searchengine" ] }, ] }); - do_log_info("search engine, after current engine has changed"); + do_print("search engine, after current engine has changed"); Services.search.addEngineWithDetails("MozSearch2", "", "", "", "GET", "http://s.example.com/search2"); engine = Services.search.getEngineByName("MozSearch2"); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_host.js b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_host.js index ec20ffa4429b..af604bdb1456 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_host.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_host.js @@ -55,7 +55,7 @@ add_task(function* test_searchEngine_autoFill() { yield promiseAsyncUpdates(); ok(frecencyForUrl(uri) > 10000, "Added URI should have expected high frecency"); - do_log_info("Check search domain is autoFilled even if there's an higher frecency match"); + do_print("Check search domain is autoFilled even if there's an higher frecency match"); yield check_autocomplete({ search: "my", autofilled: "my.search.com", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js index 4c2ddcd80ca3..065a1973fb78 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_restyle.js @@ -14,7 +14,7 @@ add_task(function* test_searchEngine() { yield promiseAddVisits({ uri: uri1, title: "Terms - SearchEngine Search" }); addBookmark({ uri: uri2, title: "Terms - SearchEngine Search" }); - do_log_info("Past search terms should be styled, unless bookmarked"); + do_print("Past search terms should be styled, unless bookmarked"); Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true); yield check_autocomplete({ search: "term", @@ -22,7 +22,7 @@ add_task(function* test_searchEngine() { { uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ] }); - do_log_info("Past search terms should not be styled if restyling is disabled"); + do_print("Past search terms should not be styled if restyling is disabled"); Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false); yield check_autocomplete({ search: "term", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_special_search.js b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js index 40287993d51d..6a9fa430ca18 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_special_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_special_search.js @@ -45,7 +45,7 @@ add_task(function* test_special_searches() { addBookmark( { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ] } ); // Test restricting searches - do_log_info("History restrict"); + do_print("History restrict"); yield check_autocomplete({ search: "^", matches: [ { uri: uri1, title: "title" }, @@ -56,7 +56,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("Star restrict"); + do_print("Star restrict"); yield check_autocomplete({ search: "*", matches: [ { uri: uri5, title: "title", style: [ "bookmark" ] }, @@ -69,7 +69,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Tag restrict"); + do_print("Tag restrict"); yield check_autocomplete({ search: "+", matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, @@ -79,7 +79,7 @@ add_task(function* test_special_searches() { }); // Test specials as any word position - do_log_info("Special as first word"); + do_print("Special as first word"); yield check_autocomplete({ search: "^ foo bar", matches: [ { uri: uri2, title: "foo.bar" }, @@ -89,7 +89,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("Special as middle word"); + do_print("Special as middle word"); yield check_autocomplete({ search: "foo ^ bar", matches: [ { uri: uri2, title: "foo.bar" }, @@ -99,7 +99,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("Special as last word"); + do_print("Special as last word"); yield check_autocomplete({ search: "foo bar ^", matches: [ { uri: uri2, title: "foo.bar" }, @@ -110,7 +110,7 @@ add_task(function* test_special_searches() { }); // Test restricting and matching searches with a term - do_log_info("foo ^ -> history"); + do_print("foo ^ -> history"); yield check_autocomplete({ search: "foo ^", matches: [ { uri: uri2, title: "foo.bar" }, @@ -120,7 +120,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo | -> history (change pref)"); + do_print("foo | -> history (change pref)"); changeRestrict("history", "|"); yield check_autocomplete({ search: "foo |", @@ -131,7 +131,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo * -> is star"); + do_print("foo * -> is star"); resetRestrict("history"); yield check_autocomplete({ search: "foo *", @@ -144,7 +144,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo | -> is star (change pref)"); + do_print("foo | -> is star (change pref)"); changeRestrict("bookmark", "|"); yield check_autocomplete({ search: "foo |", @@ -157,7 +157,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo # -> in title"); + do_print("foo # -> in title"); resetRestrict("bookmark"); yield check_autocomplete({ search: "foo #", @@ -171,7 +171,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo | -> in title (change pref)"); + do_print("foo | -> in title (change pref)"); changeRestrict("title", "|"); yield check_autocomplete({ search: "foo |", @@ -185,7 +185,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo @ -> in url"); + do_print("foo @ -> in url"); resetRestrict("title"); yield check_autocomplete({ search: "foo @", @@ -197,7 +197,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo | -> in url (change pref)"); + do_print("foo | -> in url (change pref)"); changeRestrict("url", "|"); yield check_autocomplete({ search: "foo |", @@ -209,7 +209,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo + -> is tag"); + do_print("foo + -> is tag"); resetRestrict("url"); yield check_autocomplete({ search: "foo +", @@ -219,7 +219,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo | -> is tag (change pref)"); + do_print("foo | -> is tag (change pref)"); changeRestrict("tag", "|"); yield check_autocomplete({ search: "foo |", @@ -229,7 +229,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo ~ -> is typed"); + do_print("foo ~ -> is typed"); resetRestrict("tag"); yield check_autocomplete({ search: "foo ~", @@ -237,7 +237,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo | -> is typed (change pref)"); + do_print("foo | -> is typed (change pref)"); changeRestrict("typed", "|"); yield check_autocomplete({ search: "foo |", @@ -246,7 +246,7 @@ add_task(function* test_special_searches() { }); // Test various pairs of special searches - do_log_info("foo ^ * -> history, is star"); + do_print("foo ^ * -> history, is star"); resetRestrict("typed"); yield check_autocomplete({ search: "foo ^ *", @@ -254,7 +254,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo ^ # -> history, in title"); + do_print("foo ^ # -> history, in title"); yield check_autocomplete({ search: "foo ^ #", matches: [ { uri: uri2, title: "foo.bar" }, @@ -263,7 +263,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo ^ @ -> history, in url"); + do_print("foo ^ @ -> history, in url"); yield check_autocomplete({ search: "foo ^ @", matches: [ { uri: uri3, title: "title" }, @@ -271,20 +271,20 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo ^ + -> history, is tag"); + do_print("foo ^ + -> history, is tag"); yield check_autocomplete({ search: "foo ^ +", matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo ^ ~ -> history, is typed"); + do_print("foo ^ ~ -> history, is typed"); yield check_autocomplete({ search: "foo ^ ~", matches: [ { uri: uri4, title: "foo.bar" }, { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo * # -> is star, in title"); + do_print("foo * # -> is star, in title"); yield check_autocomplete({ search: "foo * #", matches: [ { uri: uri6, title: "foo.bar", style: [ "bookmark" ] }, @@ -295,7 +295,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo * @ -> is star, in url"); + do_print("foo * @ -> is star, in url"); yield check_autocomplete({ search: "foo * @", matches: [ { uri: uri7, title: "title", style: [ "bookmark" ] }, @@ -304,7 +304,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo * + -> same as +"); + do_print("foo * + -> same as +"); yield check_autocomplete({ search: "foo * +", matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] }, @@ -313,13 +313,13 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo * ~ -> is star, is typed"); + do_print("foo * ~ -> is star, is typed"); yield check_autocomplete({ search: "foo * ~", matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo # @ -> in title, in url"); + do_print("foo # @ -> in title, in url"); yield check_autocomplete({ search: "foo # @", matches: [ { uri: uri4, title: "foo.bar" }, @@ -328,7 +328,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo # + -> in title, is tag"); + do_print("foo # + -> in title, is tag"); yield check_autocomplete({ search: "foo # +", matches: [ { uri: uri9, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, @@ -337,28 +337,28 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo # ~ -> in title, is typed"); + do_print("foo # ~ -> in title, is typed"); yield check_autocomplete({ search: "foo # ~", matches: [ { uri: uri4, title: "foo.bar" }, { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo @ + -> in url, is tag"); + do_print("foo @ + -> in url, is tag"); yield check_autocomplete({ search: "foo @ +", matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] }, { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo @ ~ -> in url, is typed"); + do_print("foo @ ~ -> in url, is typed"); yield check_autocomplete({ search: "foo @ ~", matches: [ { uri: uri4, title: "foo.bar" }, { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] }); - do_log_info("foo + ~ -> is tag, is typed"); + do_print("foo + ~ -> is tag, is typed"); yield check_autocomplete({ search: "foo + ~", matches: [ { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "tag" ] } ] @@ -369,7 +369,7 @@ add_task(function* test_special_searches() { Services.prefs.setBoolPref("browser.urlbar.autoFill", false); // Test default usage by setting certain browser.urlbar.suggest.* prefs - do_log_info("foo -> default history"); + do_print("foo -> default history"); setSuggestPrefsToFalse(); Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); yield check_autocomplete({ @@ -381,7 +381,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: ["foo.bar"], style: [ "tag" ] } ] }); - do_log_info("foo -> default history, is star"); + do_print("foo -> default history, is star"); setSuggestPrefsToFalse(); Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); @@ -399,7 +399,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo -> default history, is star, is typed"); + do_print("foo -> default history, is star, is typed"); setSuggestPrefsToFalse(); Services.prefs.setBoolPref("browser.urlbar.suggest.history", true); Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true); @@ -410,7 +410,7 @@ add_task(function* test_special_searches() { { uri: uri11, title: "title", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo -> is star"); + do_print("foo -> is star"); setSuggestPrefsToFalse(); Services.prefs.setBoolPref("browser.urlbar.suggest.history", false); Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", true); @@ -425,7 +425,7 @@ add_task(function* test_special_searches() { { uri: uri12, title: "foo.bar", tags: [ "foo.bar" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("foo -> is star, is typed"); + do_print("foo -> is star, is typed"); setSuggestPrefsToFalse(); // only typed should be ignored Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", true); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js b/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js index ed11f7bdcb58..cf1d6c90c5e6 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_swap_protocol.js @@ -42,106 +42,106 @@ add_task(function* test_swap_protocol() { Services.prefs.setBoolPref("browser.urlbar.autoFill", "false"); Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false); - do_log_info("http://www.site matches all site"); + do_print("http://www.site matches all site"); yield check_autocomplete({ search: "http://www.site", matches: allMatches }); - do_log_info("http://site matches all site"); + do_print("http://site matches all site"); yield check_autocomplete({ search: "http://site", matches: allMatches }); - do_log_info("ftp://ftp.site matches itself"); + do_print("ftp://ftp.site matches itself"); yield check_autocomplete({ search: "ftp://ftp.site", matches: [ { uri: uri3, title: "title" } ] }); - do_log_info("ftp://site matches all site"); + do_print("ftp://site matches all site"); yield check_autocomplete({ search: "ftp://site", matches: allMatches }); - do_log_info("https://www.site matches all site"); + do_print("https://www.site matches all site"); yield check_autocomplete({ search: "https://www.site", matches: allMatches }); - do_log_info("https://site matches all site"); + do_print("https://site matches all site"); yield check_autocomplete({ search: "https://site", matches: allMatches }); - do_log_info("www.site matches all site"); + do_print("www.site matches all site"); yield check_autocomplete({ search: "www.site", matches: allMatches }); - do_log_info("w matches none of www."); + do_print("w matches none of www."); yield check_autocomplete({ search: "w", matches: [ { uri: uri7, title: "title" }, { uri: uri8, title: "title" } ] }); - do_log_info("http://w matches none of www."); + do_print("http://w matches none of www."); yield check_autocomplete({ search: "http://w", matches: [ { uri: uri7, title: "title" }, { uri: uri8, title: "title" } ] }); - do_log_info("http://w matches none of www."); + do_print("http://w matches none of www."); yield check_autocomplete({ search: "http://www.w", matches: [ { uri: uri7, title: "title" }, { uri: uri8, title: "title" } ] }); - do_log_info("ww matches none of www."); + do_print("ww matches none of www."); yield check_autocomplete({ search: "ww", matches: [ { uri: uri8, title: "title" } ] }); - do_log_info("ww matches none of www."); + do_print("ww matches none of www."); yield check_autocomplete({ search: "ww", matches: [ { uri: uri8, title: "title" } ] }); - do_log_info("http://ww matches none of www."); + do_print("http://ww matches none of www."); yield check_autocomplete({ search: "http://ww", matches: [ { uri: uri8, title: "title" } ] }); - do_log_info("http://www.ww matches none of www."); + do_print("http://www.ww matches none of www."); yield check_autocomplete({ search: "http://www.ww", matches: [ { uri: uri8, title: "title" } ] }); - do_log_info("www matches none of www."); + do_print("www matches none of www."); yield check_autocomplete({ search: "www", matches: [ { uri: uri8, title: "title" } ] }); - do_log_info("http://www matches none of www."); + do_print("http://www matches none of www."); yield check_autocomplete({ search: "http://www", matches: [ { uri: uri8, title: "title" } ] }); - do_log_info("http://www.www matches none of www."); + do_print("http://www.www matches none of www."); yield check_autocomplete({ search: "http://www.www", matches: [ { uri: uri8, title: "title" } ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_tabmatches.js b/toolkit/components/places/tests/unifiedcomplete/test_tabmatches.js index 53e35b57bd7c..13185eaac51c 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_tabmatches.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_tabmatches.js @@ -23,7 +23,7 @@ add_task(function* test_tab_matches() { addOpenPages(uri3, 1); addOpenPages(uri4, 1); - do_log_info("two results, normal result is a tab match"); + do_print("two results, normal result is a tab match"); yield check_autocomplete({ search: "abc.com", searchParam: "enable-actions", @@ -31,7 +31,7 @@ add_task(function* test_tab_matches() { { uri: makeActionURI("switchtab", {url: "http://abc.com/"}), title: "ABC rocks", style: [ "action", "switchtab" ] } ] }); - do_log_info("three results, one tab match"); + do_print("three results, one tab match"); yield check_autocomplete({ search: "abc", searchParam: "enable-actions", @@ -40,7 +40,7 @@ add_task(function* test_tab_matches() { { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] } ] }); - do_log_info("three results, both normal results are tab matches"); + do_print("three results, both normal results are tab matches"); addOpenPages(uri2, 1); yield check_autocomplete({ search: "abc", @@ -50,7 +50,7 @@ add_task(function* test_tab_matches() { { uri: makeActionURI("switchtab", {url: "http://xyz.net/"}), title: "xyz.net - we're better than ABC", style: [ "action", "switchtab" ] } ] }); - do_log_info("three results, both normal results are tab matches, one has multiple tabs"); + do_print("three results, both normal results are tab matches, one has multiple tabs"); addOpenPages(uri2, 5); yield check_autocomplete({ search: "abc", @@ -60,7 +60,7 @@ add_task(function* test_tab_matches() { { uri: makeActionURI("switchtab", {url: "http://xyz.net/"}), title: "xyz.net - we're better than ABC", style: [ "action", "switchtab" ] } ] }); - do_log_info("three results, no tab matches (disable-private-actions)"); + do_print("three results, no tab matches (disable-private-actions)"); yield check_autocomplete({ search: "abc", searchParam: "enable-actions disable-private-actions", @@ -69,7 +69,7 @@ add_task(function* test_tab_matches() { { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] } ] }); - do_log_info("two results (actions disabled)"); + do_print("two results (actions disabled)"); yield check_autocomplete({ search: "abc", searchParam: "", @@ -77,7 +77,7 @@ add_task(function* test_tab_matches() { { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] } ] }); - do_log_info("three results, no tab matches"); + do_print("three results, no tab matches"); removeOpenPages(uri1, 1); removeOpenPages(uri2, 6); yield check_autocomplete({ @@ -88,7 +88,7 @@ add_task(function* test_tab_matches() { { uri: uri2, title: "xyz.net - we're better than ABC", style: [ "favicon" ] } ] }); - do_log_info("tab match search with restriction character"); + do_print("tab match search with restriction character"); addOpenPages(uri1, 1); yield check_autocomplete({ search: gTabRestrictChar + " abc", @@ -97,7 +97,7 @@ add_task(function* test_tab_matches() { { uri: makeActionURI("switchtab", {url: "http://abc.com/"}), title: "ABC rocks", style: [ "action", "switchtab" ] } ] }); - do_log_info("tab match with not-addable pages"); + do_print("tab match with not-addable pages"); yield check_autocomplete({ search: "mozilla", searchParam: "enable-actions", @@ -105,7 +105,7 @@ add_task(function* test_tab_matches() { { uri: makeActionURI("switchtab", {url: "about:mozilla"}), title: "about:mozilla", style: [ "action", "switchtab" ] } ] }); - do_log_info("tab match with not-addable pages and restriction character"); + do_print("tab match with not-addable pages and restriction character"); yield check_autocomplete({ search: gTabRestrictChar + " mozilla", searchParam: "enable-actions", @@ -113,7 +113,7 @@ add_task(function* test_tab_matches() { { uri: makeActionURI("switchtab", {url: "about:mozilla"}), title: "about:mozilla", style: [ "action", "switchtab" ] } ] }); - do_log_info("tab match with not-addable pages and only restriction character"); + do_print("tab match with not-addable pages and only restriction character"); yield check_autocomplete({ search: gTabRestrictChar, searchParam: "enable-actions", diff --git a/toolkit/components/places/tests/unifiedcomplete/test_trimming.js b/toolkit/components/places/tests/unifiedcomplete/test_trimming.js index 3a0eda2f4464..d7f32b3966e2 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_trimming.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_trimming.js @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ add_task(function* test_untrimmed_secure_www() { - do_log_info("Searching for untrimmed https://www entry"); + do_print("Searching for untrimmed https://www entry"); yield promiseAddVisits({ uri: NetUtil.newURI("https://www.mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -15,7 +15,7 @@ add_task(function* test_untrimmed_secure_www() { }); add_task(function* test_untrimmed_secure_www_path() { - do_log_info("Searching for untrimmed https://www entry with path"); + do_print("Searching for untrimmed https://www entry with path"); yield promiseAddVisits({ uri: NetUtil.newURI("https://www.mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -27,7 +27,7 @@ add_task(function* test_untrimmed_secure_www_path() { }); add_task(function* test_untrimmed_secure() { - do_log_info("Searching for untrimmed https:// entry"); + do_print("Searching for untrimmed https:// entry"); yield promiseAddVisits({ uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -39,7 +39,7 @@ add_task(function* test_untrimmed_secure() { }); add_task(function* test_untrimmed_secure_path() { - do_log_info("Searching for untrimmed https:// entry with path"); + do_print("Searching for untrimmed https:// entry with path"); yield promiseAddVisits({ uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -51,7 +51,7 @@ add_task(function* test_untrimmed_secure_path() { }); add_task(function* test_untrimmed_www() { - do_log_info("Searching for untrimmed http://www entry"); + do_print("Searching for untrimmed http://www entry"); yield promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -63,7 +63,7 @@ add_task(function* test_untrimmed_www() { }); add_task(function* test_untrimmed_www_path() { - do_log_info("Searching for untrimmed http://www entry with path"); + do_print("Searching for untrimmed http://www entry with path"); yield promiseAddVisits({ uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -75,7 +75,7 @@ add_task(function* test_untrimmed_www_path() { }); add_task(function* test_untrimmed_ftp() { - do_log_info("Searching for untrimmed ftp:// entry"); + do_print("Searching for untrimmed ftp:// entry"); yield promiseAddVisits({ uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -87,7 +87,7 @@ add_task(function* test_untrimmed_ftp() { }); add_task(function* test_untrimmed_ftp_path() { - do_log_info("Searching for untrimmed ftp:// entry with path"); + do_print("Searching for untrimmed ftp:// entry with path"); yield promiseAddVisits({ uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -99,7 +99,7 @@ add_task(function* test_untrimmed_ftp_path() { }); add_task(function* test_priority_1() { - do_log_info("Ensuring correct priority 1"); + do_print("Ensuring correct priority 1"); yield promiseAddVisits([{ uri: NetUtil.newURI("https://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("https://mozilla.org/test/"), @@ -119,7 +119,7 @@ add_task(function* test_priority_1() { }); add_task(function* test_periority_2() { - do_log_info( "Ensuring correct priority 2"); + do_print( "Ensuring correct priority 2"); yield promiseAddVisits([{ uri: NetUtil.newURI("https://mozilla.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("ftp://mozilla.org/test/"), @@ -137,7 +137,7 @@ add_task(function* test_periority_2() { }); add_task(function* test_periority_3() { - do_log_info("Ensuring correct priority 3"); + do_print("Ensuring correct priority 3"); yield promiseAddVisits([{ uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("http://www.mozilla.org/test/"), @@ -153,7 +153,7 @@ add_task(function* test_periority_3() { }); add_task(function* test_periority_4() { - do_log_info("Ensuring correct priority 4"); + do_print("Ensuring correct priority 4"); yield promiseAddVisits([{ uri: NetUtil.newURI("http://www.mozilla.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("http://mozilla.org/test/"), @@ -167,7 +167,7 @@ add_task(function* test_periority_4() { }); add_task(function* test_priority_5() { - do_log_info("Ensuring correct priority 5"); + do_print("Ensuring correct priority 5"); yield promiseAddVisits([{ uri: NetUtil.newURI("ftp://mozilla.org/test/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("ftp://www.mozilla.org/test/"), @@ -181,7 +181,7 @@ add_task(function* test_priority_5() { }); add_task(function* test_priority_6() { - do_log_info("Ensuring correct priority 6"); + do_print("Ensuring correct priority 6"); yield promiseAddVisits([{ uri: NetUtil.newURI("http://www.mozilla.org/test1/"), transition: TRANSITION_TYPED }, { uri: NetUtil.newURI("http://www.mozilla.org/test2/"), @@ -195,7 +195,7 @@ add_task(function* test_priority_6() { }); add_task(function* test_longer_domain() { - do_log_info("Ensuring longer domain can't match"); + do_print("Ensuring longer domain can't match"); // The .co should be preferred, but should not get the https from the .com. // The .co domain must be added later to activate the trigger bug. yield promiseAddVisits([{ uri: NetUtil.newURI("https://mozilla.com/"), @@ -214,7 +214,7 @@ add_task(function* test_longer_domain() { }); add_task(function* test_escaped_chars() { - do_log_info("Searching for URL with characters that are normally escaped"); + do_print("Searching for URL with characters that are normally escaped"); yield promiseAddVisits({ uri: NetUtil.newURI("https://www.mozilla.org/啊-test"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -226,7 +226,7 @@ add_task(function* test_escaped_chars() { }); add_task(function* test_unsecure_secure() { - do_log_info("Don't return unsecure URL when searching for secure ones"); + do_print("Don't return unsecure URL when searching for secure ones"); yield promiseAddVisits({ uri: NetUtil.newURI("http://test.moz.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -238,7 +238,7 @@ add_task(function* test_unsecure_secure() { }); add_task(function* test_unsecure_secure_domain() { - do_log_info("Don't return unsecure domain when searching for secure ones"); + do_print("Don't return unsecure domain when searching for secure ones"); yield promiseAddVisits({ uri: NetUtil.newURI("http://test.moz.org/test/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -250,7 +250,7 @@ add_task(function* test_unsecure_secure_domain() { }); add_task(function* test_untyped_www() { - do_log_info("Untyped is not accounted for www"); + do_print("Untyped is not accounted for www"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits({ uri: NetUtil.newURI("http://www.moz.org/test/") }); yield check_autocomplete({ @@ -262,7 +262,7 @@ add_task(function* test_untyped_www() { }); add_task(function* test_untyped_ftp() { - do_log_info("Untyped is not accounted for ftp"); + do_print("Untyped is not accounted for ftp"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits({ uri: NetUtil.newURI("ftp://moz.org/test/") }); yield check_autocomplete({ @@ -274,7 +274,7 @@ add_task(function* test_untyped_ftp() { }); add_task(function* test_untyped_secure() { - do_log_info("Untyped is not accounted for https"); + do_print("Untyped is not accounted for https"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits({ uri: NetUtil.newURI("https://moz.org/test/") }); yield check_autocomplete({ @@ -286,7 +286,7 @@ add_task(function* test_untyped_secure() { }); add_task(function* test_untyped_secure_www() { - do_log_info("Untyped is not accounted for https://www"); + do_print("Untyped is not accounted for https://www"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits({ uri: NetUtil.newURI("https://www.moz.org/test/") }); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_typed.js b/toolkit/components/places/tests/unifiedcomplete/test_typed.js index 930524b3655e..bd28fc795d80 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_typed.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_typed.js @@ -6,7 +6,7 @@ // ensure autocomplete is able to dinamically switch behavior. add_task(function* test_domain() { - do_log_info("Searching for domain should autoFill it"); + do_print("Searching for domain should autoFill it"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://mozilla.org/link/")); yield check_autocomplete({ @@ -18,7 +18,7 @@ add_task(function* test_domain() { }); add_task(function* test_url() { - do_log_info("Searching for url should autoFill it"); + do_print("Searching for url should autoFill it"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits(NetUtil.newURI("http://mozilla.org/link/")); yield check_autocomplete({ @@ -32,7 +32,7 @@ add_task(function* test_url() { // Now do searches with typed behavior forced to true. add_task(function* test_untyped_domain() { - do_log_info("Searching for non-typed domain should not autoFill it"); + do_print("Searching for non-typed domain should not autoFill it"); yield promiseAddVisits(NetUtil.newURI("http://mozilla.org/link/")); yield check_autocomplete({ search: "moz", @@ -43,7 +43,7 @@ add_task(function* test_untyped_domain() { }); add_task(function* test_typed_domain() { - do_log_info("Searching for typed domain should autoFill it"); + do_print("Searching for typed domain should autoFill it"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/typed/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ @@ -55,7 +55,7 @@ add_task(function* test_typed_domain() { }); add_task(function* test_untyped_url() { - do_log_info("Searching for non-typed url should not autoFill it"); + do_print("Searching for non-typed url should not autoFill it"); yield promiseAddVisits(NetUtil.newURI("http://mozilla.org/link/")); yield check_autocomplete({ search: "mozilla.org/li", @@ -66,7 +66,7 @@ add_task(function* test_untyped_url() { }); add_task(function* test_typed_url() { - do_log_info("Searching for typed url should autoFill it"); + do_print("Searching for typed url should autoFill it"); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/link/"), transition: TRANSITION_TYPED }); yield check_autocomplete({ diff --git a/toolkit/components/places/tests/unifiedcomplete/test_visiturl.js b/toolkit/components/places/tests/unifiedcomplete/test_visiturl.js index af6ef4a4504b..952a031c1752 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_visiturl.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_visiturl.js @@ -3,21 +3,21 @@ add_task(function*() { - do_log_info("visit url, no protocol"); + do_print("visit url, no protocol"); yield check_autocomplete({ search: "mozilla.org", searchParam: "enable-actions", matches: [ { uri: makeActionURI("visiturl", {url: "http://mozilla.org/", input: "mozilla.org"}), title: "http://mozilla.org/", style: [ "action", "visiturl" ] } ] }); - do_log_info("visit url, with protocol"); + do_print("visit url, with protocol"); yield check_autocomplete({ search: "https://mozilla.org", searchParam: "enable-actions", matches: [ { uri: makeActionURI("visiturl", {url: "https://mozilla.org/", input: "https://mozilla.org"}), title: "https://mozilla.org/", style: [ "action", "visiturl" ] } ] }); - do_log_info("visit url, about: protocol (no host)"); + do_print("visit url, about: protocol (no host)"); yield check_autocomplete({ search: "about:config", searchParam: "enable-actions", @@ -26,7 +26,7 @@ add_task(function*() { // This is distinct because of how we predict being able to url autofill via // host lookups. - do_log_info("visit url, host matching visited host but not visited url"); + do_print("visit url, host matching visited host but not visited url"); yield promiseAddVisits([ { uri: NetUtil.newURI("http://mozilla.org/wine/"), title: "Mozilla Wine", transition: TRANSITION_TYPED }, ]); @@ -37,7 +37,7 @@ add_task(function*() { }); // And hosts with no dot in them are special, due to requiring whitelisting. - do_log_info("visit url, host matching visited host but not visited url, non-whitelisted host"); + do_print("visit url, host matching visited host but not visited url, non-whitelisted host"); Services.search.addEngineWithDetails("MozSearch", "", "", "", "GET", "http://s.example.com/search"); let engine = Services.search.getEngineByName("MozSearch"); diff --git a/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js index 56828adb3732..af43e3910a3a 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_word_boundary_search.js @@ -47,7 +47,7 @@ add_task(function* test_escape() { // match only on word boundaries Services.prefs.setIntPref("browser.urlbar.matchBehavior", 2); - do_log_info("Match 'match' at the beginning or after / or on a CamelCase"); + do_print("Match 'match' at the beginning or after / or on a CamelCase"); yield check_autocomplete({ search: "match", matches: [ { uri: uri1, title: "title1" }, @@ -56,7 +56,7 @@ add_task(function* test_escape() { { uri: uri10, title: "title1" } ] }); - do_log_info("Match 'dont' at the beginning or after /"); + do_print("Match 'dont' at the beginning or after /"); yield check_autocomplete({ search: "dont", matches: [ { uri: uri2, title: "title1" }, @@ -64,7 +64,7 @@ add_task(function* test_escape() { { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Match 'match' at the beginning or after / or on a CamelCase"); + do_print("Match 'match' at the beginning or after / or on a CamelCase"); yield check_autocomplete({ search: "2", matches: [ { uri: uri3, title: "matchme2" }, @@ -73,7 +73,7 @@ add_task(function* test_escape() { { uri: uri6, title: "title1", tags: [ "dontmatchme3" ], style: [ "bookmark-tag" ] } ] }); - do_log_info("Match 't' at the beginning or after /"); + do_print("Match 't' at the beginning or after /"); yield check_autocomplete({ search: "t", matches: [ { uri: uri1, title: "title1" }, @@ -85,13 +85,13 @@ add_task(function* test_escape() { { uri: uri10, title: "title1" } ] }); - do_log_info("Match 'word' after many consecutive word boundaries"); + do_print("Match 'word' after many consecutive word boundaries"); yield check_autocomplete({ search: "word", matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ] }); - do_log_info("Match a word boundary '/' for everything"); + do_print("Match a word boundary '/' for everything"); yield check_autocomplete({ search: "/", matches: [ { uri: uri1, title: "title1" }, @@ -106,50 +106,50 @@ add_task(function* test_escape() { { uri: uri10, title: "title1" } ] }); - do_log_info("Match word boundaries '()_+' that are among word boundaries"); + do_print("Match word boundaries '()_+' that are among word boundaries"); yield check_autocomplete({ search: "()_+", matches: [ { uri: uri7, title: "!@#$%^&*()_+{}|:<>?word" } ] }); - do_log_info("Katakana characters form a string, so match the beginning"); + do_print("Katakana characters form a string, so match the beginning"); yield check_autocomplete({ search: katakana[0], matches: [ { uri: uri8, title: katakana.join("") } ] }); /* - do_log_info("Middle of a katakana word shouldn't be matched"); + do_print("Middle of a katakana word shouldn't be matched"); yield check_autocomplete({ search: katakana[1], matches: [ ] }); */ - do_log_info("Ideographs are treated as words so 'nin' is one word"); + do_print("Ideographs are treated as words so 'nin' is one word"); yield check_autocomplete({ search: ideograph[0], matches: [ { uri: uri9, title: ideograph.join("") } ] }); - do_log_info("Ideographs are treated as words so 'ten' is another word"); + do_print("Ideographs are treated as words so 'ten' is another word"); yield check_autocomplete({ search: ideograph[1], matches: [ { uri: uri9, title: ideograph.join("") } ] }); - do_log_info("Ideographs are treated as words so 'do' is yet another word"); + do_print("Ideographs are treated as words so 'do' is yet another word"); yield check_autocomplete({ search: ideograph[2], matches: [ { uri: uri9, title: ideograph.join("") } ] }); - do_log_info("Extra negative assert that we don't match in the middle"); + do_print("Extra negative assert that we don't match in the middle"); yield check_autocomplete({ search: "ch", matches: [ ] }); - do_log_info("Don't match one character after a camel-case word boundary (bug 429498)"); + do_print("Don't match one character after a camel-case word boundary (bug 429498)"); yield check_autocomplete({ search: "atch", matches: [ ] diff --git a/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js b/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js index 20a2eb0facf1..fa5c80ce2019 100644 --- a/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js +++ b/toolkit/components/places/tests/unifiedcomplete/test_zero_frecency.js @@ -5,7 +5,7 @@ // Ensure inline autocomplete doesn't return zero frecency pages. add_task(function* test_zzero_frec_domain() { - do_log_info("Searching for zero frecency domain should not autoFill it"); + do_print("Searching for zero frecency domain should not autoFill it"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/framed_link/"), transition: TRANSITION_FRAMED_LINK }); @@ -18,7 +18,7 @@ add_task(function* test_zzero_frec_domain() { }); add_task(function* test_zzero_frec_url() { - do_log_info("Searching for zero frecency url should not autoFill it"); + do_print("Searching for zero frecency url should not autoFill it"); Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false); yield promiseAddVisits({ uri: NetUtil.newURI("http://mozilla.org/framed_link/"), transition: TRANSITION_FRAMED_LINK }); diff --git a/toolkit/components/places/tests/unit/test_412132.js b/toolkit/components/places/tests/unit/test_412132.js index 305fd36ec36c..f2aed44ad40e 100644 --- a/toolkit/components/places/tests/unit/test_412132.js +++ b/toolkit/components/places/tests/unit/test_412132.js @@ -13,9 +13,9 @@ add_task(function changeuri_unvisited_bookmark() { - do_log_info("After changing URI of bookmark, frecency of bookmark's " + - "original URI should be zero if original URI is unvisited and " + - "no longer bookmarked."); + do_print("After changing URI of bookmark, frecency of bookmark's " + + "original URI should be zero if original URI is unvisited and " + + "no longer bookmarked."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -23,14 +23,14 @@ add_task(function changeuri_unvisited_bookmark() "bookmark title"); yield promiseAsyncUpdates(); - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2")); yield promiseAsyncUpdates(); - do_log_info("Unvisited URI no longer bookmarked => frecency should = 0"); + do_print("Unvisited URI no longer bookmarked => frecency should = 0"); do_check_eq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -39,8 +39,8 @@ add_task(function changeuri_unvisited_bookmark() add_task(function changeuri_visited_bookmark() { - do_log_info("After changing URI of bookmark, frecency of bookmark's " + - "original URI should not be zero if original URI is visited."); + do_print("After changing URI of bookmark, frecency of bookmark's " + + "original URI should not be zero if original URI is visited."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -49,7 +49,7 @@ add_task(function changeuri_visited_bookmark() yield promiseAsyncUpdates(); - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); yield promiseAddVisits(TEST_URI); @@ -60,7 +60,7 @@ add_task(function changeuri_visited_bookmark() yield promiseAsyncUpdates(); - do_log_info("*Visited* URI no longer bookmarked => frecency should != 0"); + do_print("*Visited* URI no longer bookmarked => frecency should != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -69,9 +69,9 @@ add_task(function changeuri_visited_bookmark() add_task(function changeuri_bookmark_still_bookmarked() { - do_log_info("After changing URI of bookmark, frecency of bookmark's " + - "original URI should not be zero if original URI is still " + - "bookmarked."); + do_print("After changing URI of bookmark, frecency of bookmark's " + + "original URI should not be zero if original URI is still " + + "bookmarked."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -84,14 +84,14 @@ add_task(function changeuri_bookmark_still_bookmarked() yield promiseAsyncUpdates(); - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); PlacesUtils.bookmarks.changeBookmarkURI(id1, uri("http://example.com/2")); yield promiseAsyncUpdates(); - do_log_info("URI still bookmarked => frecency should != 0"); + do_print("URI still bookmarked => frecency should != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -100,7 +100,7 @@ add_task(function changeuri_bookmark_still_bookmarked() add_task(function changeuri_nonexistent_bookmark() { - do_log_info("Changing the URI of a nonexistent bookmark should fail."); + do_print("Changing the URI of a nonexistent bookmark should fail."); function tryChange(itemId) { try { diff --git a/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js index fef11c34560e..45de3556827d 100644 --- a/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js +++ b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js @@ -89,7 +89,7 @@ function promiseDefaultSearchEngine() { function promiseSearchTopic(expectedVerb) { let deferred = Promise.defer(); Services.obs.addObserver( function observe(subject, topic, verb) { - do_log_info("browser-search-engine-modified: " + verb); + do_print("browser-search-engine-modified: " + verb); if (verb == expectedVerb) { Services.obs.removeObserver(observe, "browser-search-engine-modified"); deferred.resolve(); diff --git a/toolkit/components/places/tests/unit/test_async_history_api.js b/toolkit/components/places/tests/unit/test_async_history_api.js index 41a00e6cec62..b4492a342421 100644 --- a/toolkit/components/places/tests/unit/test_async_history_api.js +++ b/toolkit/components/places/tests/unit/test_async_history_api.js @@ -69,7 +69,7 @@ function TitleChangedObserver(aURI, TitleChangedObserver.prototype = { __proto__: NavHistoryObserver.prototype, onTitleChanged(aURI, aTitle, aGUID) { - do_log_info("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")"); + do_print("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")"); if (!this.uri.equals(aURI)) { return; } @@ -106,9 +106,9 @@ VisitObserver.prototype = { aTransitionType, aGUID) { - do_log_info("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime + - ", " + aSessionId + ", " + aReferringId + ", " + - aTransitionType + ", " + aGUID + ")"); + do_print("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime + + ", " + aSessionId + ", " + aReferringId + ", " + + aTransitionType + ", " + aGUID + ")"); if (!this.uri.equals(aURI) || this.guid != aGUID) { return; } @@ -252,7 +252,7 @@ add_task(function* test_no_visits_throws() { (aPlace.uri ? "uri" : "no uri") + ", " + (aPlace.guid ? "guid" : "no guid") + ", " + (aPlace.visits ? "visits array" : "no visits array"); - do_log_info(str); + do_print(str); }; // Loop through every possible case. Note that we don't actually care about @@ -375,7 +375,7 @@ add_task(function* test_non_addable_uri_errors() { // NetUtil.newURI() can throw if e.g. our app knows about imap:// // but the account is not set up and so the URL is invalid for us. // Note this in the log but ignore as it's not the subject of this test. - do_log_info("Could not construct URI for '" + url + "'; ignoring"); + do_print("Could not construct URI for '" + url + "'; ignoring"); } }); @@ -384,7 +384,7 @@ add_task(function* test_non_addable_uri_errors() { do_throw("Unexpected success."); } for (let place of placesResult.errors) { - do_log_info("Checking '" + place.info.uri.spec + "'"); + do_print("Checking '" + place.info.uri.spec + "'"); do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG); do_check_false(yield promiseIsURIVisited(place.info.uri)); } @@ -1046,7 +1046,7 @@ add_task(function* test_visit_notifies() { }); PlacesUtils.history.addObserver(visitObserver, false); let observer = function(aSubject, aTopic, aData) { - do_log_info("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); + do_print("observe(" + aSubject + ", " + aTopic + ", " + aData + ")"); do_check_true(aSubject instanceof Ci.nsIURI); do_check_true(aSubject.equals(place.uri)); @@ -1084,7 +1084,7 @@ add_task(function* test_callbacks_not_supplied() { // NetUtil.newURI() can throw if e.g. our app knows about imap:// // but the account is not set up and so the URL is invalid for us. // Note this in the log but ignore as it's not the subject of this test. - do_log_info("Could not construct URI for '" + url + "'; ignoring"); + do_print("Could not construct URI for '" + url + "'; ignoring"); } }); diff --git a/toolkit/components/places/tests/unit/test_isURIVisited.js b/toolkit/components/places/tests/unit/test_isURIVisited.js index 696b86fc3777..a7f2f68500a6 100644 --- a/toolkit/components/places/tests/unit/test_isURIVisited.js +++ b/toolkit/components/places/tests/unit/test_isURIVisited.js @@ -48,10 +48,10 @@ function step() .getService(Ci.mozIAsyncHistory); for (let scheme in SCHEMES) { - do_log_info("Testing scheme " + scheme); + do_print("Testing scheme " + scheme); for (let i = 0; i < TRANSITIONS.length; i++) { let transition = TRANSITIONS[i]; - do_log_info("With transition " + transition); + do_print("With transition " + transition); let uri = NetUtil.newURI(scheme + "mozilla.org/"); @@ -63,7 +63,7 @@ function step() handleError: function () {}, handleResult: function () {}, handleCompletion: function () { - do_log_info("Added visit to " + uri.spec); + do_print("Added visit to " + uri.spec); history.isURIVisited(uri, function (aURI, aIsVisited) { do_check_true(uri.equals(aURI)); diff --git a/toolkit/components/places/tests/unit/test_isvisited.js b/toolkit/components/places/tests/unit/test_isvisited.js index 866254028aa1..630febbad229 100644 --- a/toolkit/components/places/tests/unit/test_isvisited.js +++ b/toolkit/components/places/tests/unit/test_isvisited.js @@ -59,7 +59,7 @@ add_task(function test_execute() // nsIIOService.newURI() can throw if e.g. our app knows about imap:// // but the account is not set up and so the URL is invalid for us. // Note this in the log but ignore as it's not the subject of this test. - do_log_info("Could not construct URI for '" + currentURL + "'; ignoring"); + do_print("Could not construct URI for '" + currentURL + "'; ignoring"); } if (cantAddUri) { try { diff --git a/toolkit/components/places/tests/unit/test_removeVisitsByTimeframe.js b/toolkit/components/places/tests/unit/test_removeVisitsByTimeframe.js index 15e84a542f09..3ae85350c78d 100644 --- a/toolkit/components/places/tests/unit/test_removeVisitsByTimeframe.js +++ b/toolkit/components/places/tests/unit/test_removeVisitsByTimeframe.js @@ -16,23 +16,23 @@ function* cleanup() { } add_task(function remove_visits_outside_unbookmarked_uri() { - do_log_info("*** TEST: Remove some visits outside valid timeframe from an unbookmarked URI"); + do_print("*** TEST: Remove some visits outside valid timeframe from an unbookmarked URI"); - do_log_info("Add 10 visits for the URI from way in the past."); + do_print("Add 10 visits for the URI from way in the past."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: TEST_URI, visitDate: NOW - 1000 - i }); } yield promiseAddVisits(visits); - do_log_info("Remove visits using timerange outside the URI's visits."); + do_print("Remove visits using timerange outside the URI's visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 10, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(TEST_URI.spec)); - do_log_info("Run a history query and check that all visits still exist."); + do_print("Run a history query and check that all visits still exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -46,40 +46,40 @@ add_task(function remove_visits_outside_unbookmarked_uri() { } root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return true."); + do_print("asyncHistory.isURIVisited should return true."); do_check_true(yield promiseIsURIVisited(TEST_URI)); yield promiseAsyncUpdates(); - do_log_info("Frecency should be positive.") + do_print("Frecency should be positive.") do_check_true(frecencyForUrl(TEST_URI) > 0); yield cleanup(); }); add_task(function remove_visits_outside_bookmarked_uri() { - do_log_info("*** TEST: Remove some visits outside valid timeframe from a bookmarked URI"); + do_print("*** TEST: Remove some visits outside valid timeframe from a bookmarked URI"); - do_log_info("Add 10 visits for the URI from way in the past."); + do_print("Add 10 visits for the URI from way in the past."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: TEST_URI, visitDate: NOW - 1000 - i }); } yield promiseAddVisits(visits); - do_log_info("Bookmark the URI."); + do_print("Bookmark the URI."); PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "bookmark title"); yield promiseAsyncUpdates(); - do_log_info("Remove visits using timerange outside the URI's visits."); + do_print("Remove visits using timerange outside the URI's visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 10, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(TEST_URI.spec)); - do_log_info("Run a history query and check that all visits still exist."); + do_print("Run a history query and check that all visits still exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -93,34 +93,34 @@ add_task(function remove_visits_outside_bookmarked_uri() { } root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return true."); + do_print("asyncHistory.isURIVisited should return true."); do_check_true(yield promiseIsURIVisited(TEST_URI)); yield promiseAsyncUpdates(); - do_log_info("Frecency should be positive.") + do_print("Frecency should be positive.") do_check_true(frecencyForUrl(TEST_URI) > 0); yield cleanup(); }); add_task(function remove_visits_unbookmarked_uri() { - do_log_info("*** TEST: Remove some visits from an unbookmarked URI"); + do_print("*** TEST: Remove some visits from an unbookmarked URI"); - do_log_info("Add 10 visits for the URI from now to 9 usecs in the past."); + do_print("Add 10 visits for the URI from now to 9 usecs in the past."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: TEST_URI, visitDate: NOW - i }); } yield promiseAddVisits(visits); - do_log_info("Remove the 5 most recent visits."); + do_print("Remove the 5 most recent visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 4, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(TEST_URI.spec)); - do_log_info("Run a history query and check that only the older 5 visits still exist."); + do_print("Run a history query and check that only the older 5 visits still exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -134,40 +134,40 @@ add_task(function remove_visits_unbookmarked_uri() { } root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return true."); + do_print("asyncHistory.isURIVisited should return true."); do_check_true(yield promiseIsURIVisited(TEST_URI)); yield promiseAsyncUpdates(); - do_log_info("Frecency should be positive.") + do_print("Frecency should be positive.") do_check_true(frecencyForUrl(TEST_URI) > 0); yield cleanup(); }); add_task(function remove_visits_bookmarked_uri() { - do_log_info("*** TEST: Remove some visits from a bookmarked URI"); + do_print("*** TEST: Remove some visits from a bookmarked URI"); - do_log_info("Add 10 visits for the URI from now to 9 usecs in the past."); + do_print("Add 10 visits for the URI from now to 9 usecs in the past."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: TEST_URI, visitDate: NOW - i }); } yield promiseAddVisits(visits); - do_log_info("Bookmark the URI."); + do_print("Bookmark the URI."); PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "bookmark title"); yield promiseAsyncUpdates(); - do_log_info("Remove the 5 most recent visits."); + do_print("Remove the 5 most recent visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 4, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(TEST_URI.spec)); - do_log_info("Run a history query and check that only the older 5 visits still exist."); + do_print("Run a history query and check that only the older 5 visits still exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -181,34 +181,34 @@ add_task(function remove_visits_bookmarked_uri() { } root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return true."); + do_print("asyncHistory.isURIVisited should return true."); do_check_true(yield promiseIsURIVisited(TEST_URI)); yield promiseAsyncUpdates() - do_log_info("Frecency should be positive.") + do_print("Frecency should be positive.") do_check_true(frecencyForUrl(TEST_URI) > 0); yield cleanup(); }); add_task(function remove_all_visits_unbookmarked_uri() { - do_log_info("*** TEST: Remove all visits from an unbookmarked URI"); + do_print("*** TEST: Remove all visits from an unbookmarked URI"); - do_log_info("Add some visits for the URI."); + do_print("Add some visits for the URI."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: TEST_URI, visitDate: NOW - i }); } yield promiseAddVisits(visits); - do_log_info("Remove all visits."); + do_print("Remove all visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 10, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should no longer exist in moz_places."); + do_print("URI should no longer exist in moz_places."); do_check_false(page_in_database(TEST_URI.spec)); - do_log_info("Run a history query and check that no visits exist."); + do_print("Run a history query and check that no visits exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -218,29 +218,29 @@ add_task(function remove_all_visits_unbookmarked_uri() { do_check_eq(root.childCount, 0); root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return false."); + do_print("asyncHistory.isURIVisited should return false."); do_check_false(yield promiseIsURIVisited(TEST_URI)); yield cleanup(); }); add_task(function remove_all_visits_unbookmarked_place_uri() { - do_log_info("*** TEST: Remove all visits from an unbookmarked place: URI"); - do_log_info("Add some visits for the URI."); + do_print("*** TEST: Remove all visits from an unbookmarked place: URI"); + do_print("Add some visits for the URI."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: PLACE_URI, visitDate: NOW - i }); } yield promiseAddVisits(visits); - do_log_info("Remove all visits."); + do_print("Remove all visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 10, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(PLACE_URI.spec)); - do_log_info("Run a history query and check that no visits exist."); + do_print("Run a history query and check that no visits exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -250,40 +250,40 @@ add_task(function remove_all_visits_unbookmarked_place_uri() { do_check_eq(root.childCount, 0); root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return false."); + do_print("asyncHistory.isURIVisited should return false."); do_check_false(yield promiseIsURIVisited(PLACE_URI)); yield promiseAsyncUpdates(); - do_log_info("Frecency should be zero.") + do_print("Frecency should be zero.") do_check_eq(frecencyForUrl(PLACE_URI.spec), 0); yield cleanup(); }); add_task(function remove_all_visits_bookmarked_uri() { - do_log_info("*** TEST: Remove all visits from a bookmarked URI"); + do_print("*** TEST: Remove all visits from a bookmarked URI"); - do_log_info("Add some visits for the URI."); + do_print("Add some visits for the URI."); let visits = []; for (let i = 0; i < 10; i++) { visits.push({ uri: TEST_URI, visitDate: NOW - i }); } yield promiseAddVisits(visits); - do_log_info("Bookmark the URI."); + do_print("Bookmark the URI."); PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, PlacesUtils.bookmarks.DEFAULT_INDEX, "bookmark title"); yield promiseAsyncUpdates(); - do_log_info("Remove all visits."); + do_print("Remove all visits."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 10, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(TEST_URI.spec)); - do_log_info("Run a history query and check that no visits exist."); + do_print("Run a history query and check that no visits exist."); let query = PlacesUtils.history.getNewQuery(); let opts = PlacesUtils.history.getNewQueryOptions(); opts.resultType = opts.RESULTS_AS_VISIT; @@ -293,35 +293,35 @@ add_task(function remove_all_visits_bookmarked_uri() { do_check_eq(root.childCount, 0); root.containerOpen = false; - do_log_info("asyncHistory.isURIVisited should return false."); + do_print("asyncHistory.isURIVisited should return false."); do_check_false(yield promiseIsURIVisited(TEST_URI)); - do_log_info("nsINavBookmarksService.isBookmarked should return true."); + do_print("nsINavBookmarksService.isBookmarked should return true."); do_check_true(PlacesUtils.bookmarks.isBookmarked(TEST_URI)); yield promiseAsyncUpdates(); - do_log_info("Frecency should be negative.") + do_print("Frecency should be negative.") do_check_true(frecencyForUrl(TEST_URI) < 0); yield cleanup(); }); add_task(function remove_all_visits_bookmarked_uri() { - do_log_info("*** TEST: Remove some visits from a zero frecency URI retains zero frecency"); + do_print("*** TEST: Remove some visits from a zero frecency URI retains zero frecency"); - do_log_info("Add some visits for the URI."); + do_print("Add some visits for the URI."); yield promiseAddVisits([{ uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: (NOW - 86400000000) }, { uri: TEST_URI, transition: TRANSITION_FRAMED_LINK, visitDate: NOW }]); - do_log_info("Remove newer visit."); + do_print("Remove newer visit."); PlacesUtils.history.removeVisitsByTimeframe(NOW - 10, NOW); yield promiseAsyncUpdates(); - do_log_info("URI should still exist in moz_places."); + do_print("URI should still exist in moz_places."); do_check_true(page_in_database(TEST_URI.spec)); - do_log_info("Frecency should be zero.") + do_print("Frecency should be zero.") do_check_eq(frecencyForUrl(TEST_URI), 0); yield cleanup(); diff --git a/toolkit/components/places/tests/unit/test_telemetry.js b/toolkit/components/places/tests/unit/test_telemetry.js index b286e41fdb71..0c0d13da4c20 100644 --- a/toolkit/components/places/tests/unit/test_telemetry.js +++ b/toolkit/components/places/tests/unit/test_telemetry.js @@ -117,7 +117,7 @@ add_task(function test_execute() yield promiseTopicObserved("places-maintenance-finished"); for (let histogramId in histograms) { - do_log_info("checking histogram " + histogramId); + do_print("checking histogram " + histogramId); let validate = histograms[histogramId]; let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot(); validate(snapshot.sum); diff --git a/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js index 5d0835e62a29..88610bccba29 100644 --- a/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js +++ b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js @@ -14,8 +14,8 @@ add_test(function removed_bookmark() { - do_log_info("After removing bookmark, frecency of bookmark's URI should be " + - "zero if URI is unvisited and no longer bookmarked."); + do_print("After removing bookmark, frecency of bookmark's URI should be " + + "zero if URI is unvisited and no longer bookmarked."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -23,14 +23,14 @@ add_test(function removed_bookmark() "bookmark title"); promiseAsyncUpdates().then(function () { - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); PlacesUtils.bookmarks.removeItem(id); promiseAsyncUpdates().then(function () { - do_log_info("Unvisited URI no longer bookmarked => frecency should = 0"); + do_print("Unvisited URI no longer bookmarked => frecency should = 0"); do_check_eq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -41,8 +41,8 @@ add_test(function removed_bookmark() add_test(function removed_but_visited_bookmark() { - do_log_info("After removing bookmark, frecency of bookmark's URI should " + - "not be zero if URI is visited."); + do_print("After removing bookmark, frecency of bookmark's URI should " + + "not be zero if URI is visited."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -50,7 +50,7 @@ add_test(function removed_but_visited_bookmark() "bookmark title"); promiseAsyncUpdates().then(function () { - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); promiseAddVisits(TEST_URI).then(function () { @@ -58,7 +58,7 @@ add_test(function removed_but_visited_bookmark() promiseAsyncUpdates().then(function () { - do_log_info("*Visited* URI no longer bookmarked => frecency should != 0"); + do_print("*Visited* URI no longer bookmarked => frecency should != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -70,8 +70,8 @@ add_test(function removed_but_visited_bookmark() add_test(function remove_bookmark_still_bookmarked() { - do_log_info("After removing bookmark, frecency of bookmark's URI should ", - "not be zero if URI is still bookmarked."); + do_print("After removing bookmark, frecency of bookmark's URI should " + + "not be zero if URI is still bookmarked."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -83,14 +83,14 @@ add_test(function remove_bookmark_still_bookmarked() "bookmark 2 title"); promiseAsyncUpdates().then(function () { - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); PlacesUtils.bookmarks.removeItem(id1); promiseAsyncUpdates().then(function () { - do_log_info("URI still bookmarked => frecency should != 0"); + do_print("URI still bookmarked => frecency should != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -101,8 +101,8 @@ add_test(function remove_bookmark_still_bookmarked() add_test(function cleared_parent_of_visited_bookmark() { - do_log_info("After removing all children from bookmark's parent, frecency " + - "of bookmark's URI should not be zero if URI is visited."); + do_print("After removing all children from bookmark's parent, frecency " + + "of bookmark's URI should not be zero if URI is visited."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, TEST_URI, @@ -110,7 +110,7 @@ add_test(function cleared_parent_of_visited_bookmark() "bookmark title"); promiseAsyncUpdates().then(function () { - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); promiseAddVisits(TEST_URI).then(function () { @@ -118,7 +118,7 @@ add_test(function cleared_parent_of_visited_bookmark() promiseAsyncUpdates().then(function () { - do_log_info("*Visited* URI no longer bookmarked => frecency should != 0"); + do_print("*Visited* URI no longer bookmarked => frecency should != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); remove_all_bookmarks(); @@ -130,9 +130,9 @@ add_test(function cleared_parent_of_visited_bookmark() add_test(function cleared_parent_of_bookmark_still_bookmarked() { - do_log_info("After removing all children from bookmark's parent, frecency " + - "of bookmark's URI should not be zero if URI is still " + - "bookmarked."); + do_print("After removing all children from bookmark's parent, frecency " + + "of bookmark's URI should not be zero if URI is still " + + "bookmarked."); const TEST_URI = NetUtil.newURI("http://example.com/1"); let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, TEST_URI, @@ -145,7 +145,7 @@ add_test(function cleared_parent_of_bookmark_still_bookmarked() "bookmark 2 title"); promiseAsyncUpdates().then(function () { - do_log_info("Bookmarked => frecency of URI should be != 0"); + do_print("Bookmarked => frecency of URI should be != 0"); do_check_neq(frecencyForUrl(TEST_URI), 0); PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId); diff --git a/toolkit/components/places/tests/unit/test_utils_backups_create.js b/toolkit/components/places/tests/unit/test_utils_backups_create.js index 98b791cec24c..20539304260e 100644 --- a/toolkit/components/places/tests/unit/test_utils_backups_create.js +++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js @@ -42,7 +42,7 @@ add_task(function () { let backupFile = bookmarksBackupDir.clone(); backupFile.append(backupFilename); backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8)); - do_log_info("Creating fake backup " + backupFile.leafName); + do_print("Creating fake backup " + backupFile.leafName); if (!backupFile.exists()) do_throw("Unable to create fake backup " + backupFile.leafName); } diff --git a/toolkit/themes/linux/global/in-content/common.css b/toolkit/themes/linux/global/in-content/common.css index 0daf602755e7..92df1b3d4f77 100644 --- a/toolkit/themes/linux/global/in-content/common.css +++ b/toolkit/themes/linux/global/in-content/common.css @@ -13,6 +13,7 @@ xul|tab[selected] { } xul|button, +html|button, xul|colorpicker[type="button"], xul|menulist { margin: 2px 4px; diff --git a/toolkit/themes/osx/global/in-content/common.css b/toolkit/themes/osx/global/in-content/common.css index a03c0cfe5fd4..705cb2278aa0 100644 --- a/toolkit/themes/osx/global/in-content/common.css +++ b/toolkit/themes/osx/global/in-content/common.css @@ -15,12 +15,14 @@ xul|tab[selected] { } xul|button, +html|button, xul|colorpicker[type="button"], xul|menulist { margin-top: 3px; } -xul|button { +xul|button, +html|button { /* use the same margin of other elements for the alignment */ margin-left: 4px; margin-right: 4px; diff --git a/toolkit/themes/shared/in-content/common.inc.css b/toolkit/themes/shared/in-content/common.inc.css index 64df7b884cef..9b34c4d9e0fd 100644 --- a/toolkit/themes/shared/in-content/common.inc.css +++ b/toolkit/themes/shared/in-content/common.inc.css @@ -29,6 +29,11 @@ html|h1 { margin-bottom: .5em; } +html|hr { + border-style: solid none none none; + border-color: #c1c1c1; +} + xul|caption { -moz-appearance: none; margin: 0; @@ -406,11 +411,29 @@ html|a:hover:active, /* Checkboxes and radio buttons */ +/* Hide the actual checkbox */ +html|input[type="checkbox"] { + opacity: 0; + position: absolute; +} + +/* Create a box to style as the checkbox */ +html|input[type="checkbox"] + html|label:before { + display: inline-block; + content: ""; + vertical-align: middle; +} + +html|input[type="checkbox"] + html|label { + line-height: 0px; +} + xul|checkbox { -moz-margin-start: 0; } -xul|*.checkbox-check { +xul|*.checkbox-check, +html|input[type="checkbox"] + html|label:before { -moz-appearance: none; width: 23px; height: 23px; @@ -425,7 +448,8 @@ xul|*.checkbox-check { box-shadow: 0 1px 1px 0 #fff, inset 0 2px 0 0 rgba(0,0,0,0.03); } -xul|checkbox:not([disabled="true"]):hover > xul|*.checkbox-check { +xul|checkbox:not([disabled="true"]):hover > xul|*.checkbox-check, +html|input[type="checkbox"]:not(:disabled) + html|label:hover:before { border-color: #0095dd; } @@ -433,7 +457,12 @@ xul|*.checkbox-check[checked] { list-style-image: url("chrome://global/skin/in-content/check.svg#check"); } -xul|checkbox[disabled="true"] > xul|*.checkbox-check { +html|input[type="checkbox"]:checked + html|label:before { + background-image: url("chrome://global/skin/in-content/check.svg#check"), linear-gradient(#fff, rgba(255,255,255,0.8)) !important; +} + +xul|checkbox[disabled="true"] > xul|*.checkbox-check, +html|input[type="checkbox"]:disabled + html|label { opacity: 0.5; } diff --git a/toolkit/themes/windows/global/in-content/common.css b/toolkit/themes/windows/global/in-content/common.css index 429d2e0cef36..d8dc3bc952fa 100644 --- a/toolkit/themes/windows/global/in-content/common.css +++ b/toolkit/themes/windows/global/in-content/common.css @@ -9,6 +9,7 @@ xul|caption { } xul|button, +html|button, xul|colorpicker[type="button"], xul|menulist { margin: 2px 4px;