From 5a77c77b1c91c83f829197d6211a13245fb44bfd Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Fri, 30 Jan 2015 16:01:42 +0000 Subject: [PATCH 01/16] Bug 1093780 Part 1 - Update OpenTok library to v2.4.0 for Loop. r=dmose --- .../loop/content/shared/css/conversation.css | 10 +- .../loop/content/shared/js/mixins.js | 8 +- .../shared/libs/sdk-content/css/ot.css | 440 +- .../libs/sdk-content/js/dynamic_config.min.js | 12 +- .../loop/content/shared/libs/sdk.js | 37438 ++++++++-------- .../test/functional/test_1_browser_call.py | 2 +- 6 files changed, 19883 insertions(+), 18027 deletions(-) diff --git a/browser/components/loop/content/shared/css/conversation.css b/browser/components/loop/content/shared/css/conversation.css index 661a680b6669..15acdda5e6a7 100644 --- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -510,7 +510,7 @@ */ .local-stream.local-stream-audio, .standalone .OT_subscriber .OT_video-poster, -.fx-embedded .OT_video-container .OT_video-poster, +.fx-embedded .OT_subscriber .OT_video-poster, .local-stream-audio .OT_publisher .OT_video-poster { background-image: url("../img/audio-call-avatar.svg"); background-repeat: no-repeat; @@ -537,7 +537,7 @@ * Another less ugly possibility would be to work with Ted Mielczarek to use * the fake camera drivers he has for Linux. */ -.room-conversation .OT_publisher .OT_video-container { +.room-conversation .OT_publisher .OT_widget-container { height: 100% !important; width: 100% !important; top: 0 !important; @@ -545,12 +545,12 @@ background-color: transparent; /* avoid visually obvious letterboxing */ } -.room-conversation .OT_publisher .OT_video-container video { +.room-conversation .OT_publisher .OT_widget-container video { background-color: transparent; /* avoid visually obvious letterboxing */ } -.fx-embedded .room-conversation .room-preview .OT_publisher .OT_video-container, -.fx-embedded .room-conversation .room-preview .OT_publisher .OT_video-container video { +.fx-embedded .room-conversation .room-preview .OT_publisher .OT_widget-container, +.fx-embedded .room-conversation .room-preview .OT_publisher .OT_widget-container video { /* Desktop conversation window room preview local stream actually wants a black background */ background-color: #000; diff --git a/browser/components/loop/content/shared/js/mixins.js b/browser/components/loop/content/shared/js/mixins.js index 78b7fa701eea..93dc79e425df 100644 --- a/browser/components/loop/content/shared/js/mixins.js +++ b/browser/components/loop/content/shared/js/mixins.js @@ -198,15 +198,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/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/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") From db8a1ac1c2b415afe1c2eed85868b073a4681521 Mon Sep 17 00:00:00 2001 From: Mike de Boer Date: Fri, 30 Jan 2015 16:01:42 +0000 Subject: [PATCH 02/16] Bug 1093780 Part 2 - Add support for using 'contain' mode for all video streams Loop publishes and resize/ position the elements based on their aspect ratio. r=Standard8 --- .../loop/content/shared/css/conversation.css | 16 -- .../loop/content/shared/js/actions.js | 9 + .../loop/content/shared/js/activeRoomStore.js | 24 +- .../loop/content/shared/js/mixins.js | 210 +++++++++++++++++- .../loop/content/shared/js/otSdkDriver.js | 45 +++- .../loop/content/shared/js/utils.js | 7 + .../content/js/standaloneRoomViews.js | 39 +++- .../content/js/standaloneRoomViews.jsx | 39 +++- 8 files changed, 356 insertions(+), 33 deletions(-) diff --git a/browser/components/loop/content/shared/css/conversation.css b/browser/components/loop/content/shared/css/conversation.css index 15acdda5e6a7..fd2007483c2b 100644 --- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -3,11 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Shared conversation window styles */ -.standalone .video-layout-wrapper, -.conversation .media video { - background-color: #444; -} - .conversation { position: relative; } @@ -673,7 +668,6 @@ html, .fx-embedded, #main, } .standalone { - max-width: 1000px; margin: 0 auto; } } @@ -905,11 +899,6 @@ html, .fx-embedded, #main, width: 75%; } -.standalone .room-conversation .local-stream { - width: 33%; - height: 26.5%; -} - .standalone .room-conversation .conversation-toolbar { background: #000; border: none; @@ -945,11 +934,6 @@ html, .fx-embedded, #main, .standalone .room-conversation .video_wrapper.remote_wrapper { width: 100%; } - .standalone .room-conversation .local-stream { - /* Assumes 4:3 aspect ratio */ - width: 180px; - height: 135px; - } .standalone .conversation-toolbar { height: 38px; padding: 8px; diff --git a/browser/components/loop/content/shared/js/actions.js b/browser/components/loop/content/shared/js/actions.js index 6b1f95ee55df..7e6a1fa38742 100644 --- a/browser/components/loop/content/shared/js/actions.js +++ b/browser/components/loop/content/shared/js/actions.js @@ -176,6 +176,15 @@ loop.shared.actions = (function() { MediaConnected: Action.define("mediaConnected", { }), + /** + * Used for notifying that the dimensions of a stream just changed. Also + * dispatched when a stream connects for the first time. + */ + VideoDimensionsChanged: Action.define("videoDimensionsChanged", { + videoType: String, + dimensions: Object + }), + /** * Used to mute or unmute a stream */ diff --git a/browser/components/loop/content/shared/js/activeRoomStore.js b/browser/components/loop/content/shared/js/activeRoomStore.js index 83a263173049..cf0196b9685c 100644 --- a/browser/components/loop/content/shared/js/activeRoomStore.js +++ b/browser/components/loop/content/shared/js/activeRoomStore.js @@ -68,7 +68,9 @@ loop.store.ActiveRoomStore = (function() { // session. 'Used' means at least one call has been placed // with it. Entering and leaving the room without seeing // anyone is not considered as 'used' - used: false + used: false, + localVideoDimensions: {}, + remoteVideoDimensions: {} }; }, @@ -119,7 +121,8 @@ loop.store.ActiveRoomStore = (function() { "remotePeerConnected", "windowUnload", "leaveRoom", - "feedbackComplete" + "feedbackComplete", + "videoDimensionsChanged" ]); }, @@ -477,6 +480,23 @@ loop.store.ActiveRoomStore = (function() { // Note, that we want some values, such as the windowId, so we don't // do a full reset here. this.setStoreState(this.getInitialStoreState()); + }, + + /** + * Handles a change in dimensions of a video stream and updates the store data + * with the new dimensions of a local or remote stream. + * + * @param {sharedActions.VideoDimensionsChanged} actionData + */ + videoDimensionsChanged: function(actionData) { + // NOTE: in the future, when multiple remote video streams are supported, + // we'll need to make this support multiple remotes as well. Good + // starting point for video tiling. + var storeProp = (actionData.isLocal ? "local" : "remote") + "VideoDimensions"; + var nextState = {}; + nextState[storeProp] = this.getStoreState()[storeProp]; + nextState[storeProp][actionData.videoType] = actionData.dimensions; + this.setStoreState(nextState); } }); diff --git a/browser/components/loop/content/shared/js/mixins.js b/browser/components/loop/content/shared/js/mixins.js index 93dc79e425df..6595193e41d9 100644 --- a/browser/components/loop/content/shared/js/mixins.js +++ b/browser/components/loop/content/shared/js/mixins.js @@ -157,29 +157,217 @@ loop.shared.mixins = (function() { * elements and handling updates of the media containers. */ var MediaSetupMixin = { + _videoDimensionsCache: { + local: {}, + remote: {} + }, + componentDidMount: function() { - rootObject.addEventListener('orientationchange', this.updateVideoContainer); - rootObject.addEventListener('resize', this.updateVideoContainer); + rootObject.addEventListener("orientationchange", this.updateVideoContainer); + rootObject.addEventListener("resize", this.updateVideoContainer); }, componentWillUnmount: function() { - rootObject.removeEventListener('orientationchange', this.updateVideoContainer); - rootObject.removeEventListener('resize', this.updateVideoContainer); + rootObject.removeEventListener("orientationchange", this.updateVideoContainer); + rootObject.removeEventListener("resize", this.updateVideoContainer); + }, + + /** + * Whenever the dimensions change of a video stream, this function is called + * by `updateVideoDimensions` to store the new values and notifies the callee + * if the dimensions changed compared to the currently stored values. + * + * @param {String} which Type of video stream. May be 'local' or 'remote' + * @param {Object} newDimensions Object containing 'width' and 'height' properties + * @return {Boolean} `true` when the dimensions have changed, + * `false` if not + */ + _updateDimensionsCache: function(which, newDimensions) { + var cache = this._videoDimensionsCache[which]; + var cacheKeys = Object.keys(cache); + var changed = false; + Object.keys(newDimensions).forEach(function(videoType) { + if (cacheKeys.indexOf(videoType) === -1) { + cache[videoType] = newDimensions[videoType]; + cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]); + changed = true; + return; + } + if (cache[videoType].width !== newDimensions[videoType].width) { + cache[videoType].width = newDimensions[videoType].width; + changed = true; + } + if (cache[videoType].height !== newDimensions[videoType].height) { + cache[videoType].height = newDimensions[videoType].height; + changed = true; + } + if (changed) { + cache[videoType].aspectRatio = this.getAspectRatio(cache[videoType]); + } + }, this); + return changed; + }, + + /** + * Whenever the dimensions change of a video stream, this function is called + * to process these changes and possibly trigger an update to the video + * container elements. + * + * @param {Object} localVideoDimensions Object containing 'width' and 'height' + * properties grouped by stream name + * @param {Object} remoteVideoDimensions Object containing 'width' and 'height' + * properties grouped by stream name + */ + updateVideoDimensions: function(localVideoDimensions, remoteVideoDimensions) { + var localChanged = this._updateDimensionsCache("local", localVideoDimensions); + var remoteChanged = this._updateDimensionsCache("remote", remoteVideoDimensions); + if (localChanged || remoteChanged) { + this.updateVideoContainer(); + } + }, + + /** + * Get the aspect ratio of a width/ height pair, which should be the dimensions + * of a stream. The returned object is an aspect ratio indexed by 1; the leading + * size has a value smaller than 1 and the slave size has a value of 1. + * this is exactly the same as notations like 4:3 and 16:9, which are merely + * human-readable forms of their fractional counterparts. 4:3 === 1:0.75 and + * 16:9 === 1:0.5625. + * So we're using the aspect ratios in their original form, because that's + * easier to do calculus with. + * + * Example: + * A stream with dimensions `{ width: 640, height: 480 }` yields an indexed + * aspect ratio of `{ width: 1, height: 0.75 }`. This means that the 'height' + * will determine the value of 'width' when the stream is stretched or shrunk + * to fit inside its container element at the maximum size. + * + * @param {Object} dimensions Object containing 'width' and 'height' properties + * @return {Object} Contains the indexed aspect ratio for 'width' + * and 'height' assigned to the corresponding + * properties. + */ + getAspectRatio: function(dimensions) { + if (dimensions.width === dimensions.height) { + return {width: 1, height: 1}; + } + var denominator = Math.max(dimensions.width, dimensions.height); + return { + width: dimensions.width / denominator, + height: dimensions.height / denominator + }; + }, + + /** + * Retrieve the dimensions of the remote video stream. + * Example output: + * { + * width: 680, + * height: 480, + * streamWidth: 640, + * streamHeight: 480, + * offsetX: 20, + * offsetY: 0 + * } + * + * Note: Once we support multiple remote video streams, this function will + * need to be updated. + * @return {Object} contains the remote stream dimension properties of its + * container node, the stream itself and offset of the stream + * relative to its container node in pixels. + */ + getRemoteVideoDimensions: function() { + var remoteVideoDimensions; + + Object.keys(this._videoDimensionsCache.remote).forEach(function(videoType) { + var node = this._getElement("." + (videoType === "camera" ? "remote" : videoType)); + var width = node.offsetWidth; + // If the width > 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); }, /** 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/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. * From ad6f3a7866c2c464ebabce76f8b7c95908bec178 Mon Sep 17 00:00:00 2001 From: Mike de Boer Date: Fri, 30 Jan 2015 16:01:42 +0000 Subject: [PATCH 03/16] Bug 1093780 Part 3 - add tests for contain mode functionality in the MediaSetup mixin. r=Standard8 --- .../loop/test/desktop-local/roomViews_test.js | 4 +- .../loop/test/shared/activeRoomStore_test.js | 25 +++++++ .../loop/test/shared/mixins_test.js | 71 +++++++++++++++++++ .../loop/test/shared/otSdkDriver_test.js | 39 ++++++++++ 4 files changed, 138 insertions(+), 1 deletion(-) 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/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 = { From b2809b66eace4a28fb124eb3f2ee64da756eeae4 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Fri, 30 Jan 2015 16:01:42 +0000 Subject: [PATCH 04/16] Bug 1093780 Part 4 - Fix the audio-only display of avatars for the new sdk. r=mikedeboer --- .../loop/content/shared/css/conversation.css | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/browser/components/loop/content/shared/css/conversation.css b/browser/components/loop/content/shared/css/conversation.css index fd2007483c2b..620d3fed34b4 100644 --- a/browser/components/loop/content/shared/css/conversation.css +++ b/browser/components/loop/content/shared/css/conversation.css @@ -503,10 +503,11 @@ * XXX this approach is fragile because it makes assumptions * about the generated OT markup, any change will break it */ -.local-stream.local-stream-audio, -.standalone .OT_subscriber .OT_video-poster, -.fx-embedded .OT_subscriber .OT_video-poster, -.local-stream-audio .OT_publisher .OT_video-poster { + +/* + * For any audio-only streams, we want to display our own background + */ +.OT_audio-only .OT_widget-container .OT_video-poster { background-image: url("../img/audio-call-avatar.svg"); background-repeat: no-repeat; background-color: #4BA6E7; @@ -514,6 +515,22 @@ background-position: center; } +/* + * Audio-only. For local streams, cancel out the SDK's opacity of 0.25. + * For remote streams we leave them shaded, as otherwise its too bright. + */ +.local-stream-audio .OT_publisher .OT_video-poster { + opacity: 1 +} + +/* + * In audio-only mode, don't display the video element, doing so interferes + * with the background opacity of the video-poster element. + */ +.OT_audio-only .OT_widget-container .OT_video-element { + display: none; +} + /* * Ensure that the publisher (i.e. local) video is never cropped, so that it's * not possible for someone to be presented with a picture that displays From 106d1f364f5ee1395044ded8062491b8e07896a2 Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Fri, 30 Jan 2015 08:16:24 -0800 Subject: [PATCH 05/16] Bug 1127351 - Override background-image on findbar in DevEdition to fix dark theme styling;r=Gijs --- browser/themes/shared/devedition.inc.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/browser/themes/shared/devedition.inc.css b/browser/themes/shared/devedition.inc.css index c6e9de00d05b..c511f2a97b7e 100644 --- a/browser/themes/shared/devedition.inc.css +++ b/browser/themes/shared/devedition.inc.css @@ -182,6 +182,10 @@ color: var(--chrome-color); } +.browserContainer > findbar { + background-image: none; +} + #navigator-toolbox .toolbarbutton-1, .browserContainer > findbar .findbar-button, #PlacesToolbar toolbarbutton.bookmark-item { From 11bf7412e7c9762dfea8a466947f2cc7edbaf2ad Mon Sep 17 00:00:00 2001 From: Brian Grinstead Date: Fri, 30 Jan 2015 08:18:36 -0800 Subject: [PATCH 06/16] Bug 1125677 - Update find bar styling in DevEdition theme;r=Gijs --- browser/themes/shared/devedition.inc.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/browser/themes/shared/devedition.inc.css b/browser/themes/shared/devedition.inc.css index c511f2a97b7e..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); } @@ -186,6 +187,12 @@ 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 { From 2c9c86be97c9c5d8d6f66217355179e1e21b7c29 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Tue, 13 Jan 2015 12:33:26 -0800 Subject: [PATCH 07/16] Bug 1112304: Update about:tabcrashed to match the new UX spec. r=dao Uses in-content styles for the tab crash page and adds an overlay to the favicon for crashed tabs. Adds support for closing the crashed tab. The strings here also refer to being able to restore all tabs, that will be implemented and landed at the same time in bug 1109650 to avoid l10n churn. --HG-- extra : rebase_source : 7afc65aff19c4da16959adb09f82006ec541fa31 extra : amend_source : 4a8323e5c1a66fb4e2fafa96e4e5c8cf9814acba --- browser/base/content/aboutTabCrashed.css | 8 ++ browser/base/content/aboutTabCrashed.js | 36 +++--- browser/base/content/aboutTabCrashed.xhtml | 20 ++-- browser/base/content/browser.js | 12 +- browser/base/content/tabbrowser.css | 5 +- browser/base/content/tabbrowser.xml | 12 +- browser/base/jar.mn | 1 + .../sessionstore/test/browser_crashedTabs.js | 75 +++++++++++- browser/components/sessionstore/test/head.js | 14 ++- .../en-US/chrome/browser/aboutTabCrashed.dtd | 10 ++ .../locales/en-US/chrome/browser/browser.dtd | 5 - browser/locales/jar.mn | 1 + browser/modules/TabCrashReporter.jsm | 1 + browser/themes/linux/aboutTabCrashed.css | 108 ------------------ browser/themes/linux/jar.mn | 1 + browser/themes/osx/browser.css | 8 ++ browser/themes/osx/jar.mn | 1 + browser/themes/shared/aboutTabCrashed.css | 12 +- browser/themes/shared/tabbrowser/crashed.svg | 16 +++ browser/themes/shared/tabs.inc.css | 23 +++- browser/themes/windows/jar.mn | 2 + .../themes/linux/global/in-content/common.css | 1 + .../themes/osx/global/in-content/common.css | 4 +- .../themes/shared/in-content/common.inc.css | 35 +++++- .../windows/global/in-content/common.css | 1 + 25 files changed, 250 insertions(+), 162 deletions(-) create mode 100644 browser/base/content/aboutTabCrashed.css create mode 100644 browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd delete mode 100644 browser/themes/linux/aboutTabCrashed.css create mode 100644 browser/themes/shared/tabbrowser/crashed.svg 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..989aa8d14684 100644 --- a/browser/base/content/aboutTabCrashed.js +++ b/browser/base/content/aboutTabCrashed.js @@ -12,21 +12,31 @@ 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"); +} // 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..d15c662623be 100644 --- a/browser/base/content/aboutTabCrashed.xhtml +++ b/browser/base/content/aboutTabCrashed.xhtml @@ -12,18 +12,19 @@ %globalDTD; - - %browserDTD; %brandDTD; - + + %tabCrashedDTD; ]> + @@ -36,12 +37,17 @@

&tabCrashed.message;

- - + +
+

&tabCrashed.reportSent;

+
- + +
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 8bf3e15c39cc..113e80c834c0 100644 --- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1116,7 +1116,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 +1134,16 @@ 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; + } }, false, true); let uriToLoad = this._getUriToLoad(); 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..a419c9929ad8 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"); @@ -3579,6 +3583,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 +4985,14 @@ class="tab-throbber" role="presentation" layer="true" /> - + { + Services.prefs.clearUserPref("browser.tabs.animate"); +}); + /** * 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 +64,7 @@ function crashBrowser(browser) { } Services.obs.removeObserver(observer, 'ipc:content-shutdown'); + info("Crash cleaned up"); resolve(); }; @@ -67,6 +74,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 +83,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); } /** @@ -232,6 +255,7 @@ add_task(function test_revived_history_from_remote() { // 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); @@ -272,6 +296,7 @@ add_task(function test_revived_history_from_non_remote() { // 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 +326,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 +341,17 @@ add_task(function test_revive_tab_from_session_store() { // Crash the tab yield crashBrowser(browser); + + is(newTab2.getAttribute("crashed"), "true", "Second tab should be crashed too."); + // Flush out any notifications from the crashed browser. TabState.flush(browser); // Use SessionStore to revive the tab - SessionStore.reviveCrashedTab(newTab); + clickButton(browser, "restoreTab"); yield promiseBrowserLoaded(browser); + 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 +363,35 @@ 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 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); + // Flush out any notifications from the crashed browser. + TabState.flush(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/locales/en-US/chrome/browser/aboutTabCrashed.dtd b/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd new file mode 100644 index 000000000000..fa1f02c4396d --- /dev/null +++ b/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd @@ -0,0 +1,10 @@ + + + + + + + + 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/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/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; From 1596720bbf37092f5cadeda2c38522de21f6c9c9 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Tue, 13 Jan 2015 12:35:57 -0800 Subject: [PATCH 08/16] Bug 1109650: Add a button to restore all crashed tabs to about:tabcrashed. r=ttaubert --HG-- extra : rebase_source : 105c07b05f25935b6101346f8860770f75b76cff extra : amend_source : e6875f05af24575461a9fa19c551e1870bfe3d52 --- browser/base/content/aboutTabCrashed.js | 4 + browser/base/content/aboutTabCrashed.xhtml | 2 + browser/base/content/browser.js | 21 ++++-- browser/base/content/tabbrowser.xml | 4 + .../sessionstore/test/browser_crashedTabs.js | 74 +++++++++++++++---- .../en-US/chrome/browser/aboutTabCrashed.dtd | 1 + 6 files changed, 87 insertions(+), 19 deletions(-) diff --git a/browser/base/content/aboutTabCrashed.js b/browser/base/content/aboutTabCrashed.js index 989aa8d14684..a0abedc0e58f 100644 --- a/browser/base/content/aboutTabCrashed.js +++ b/browser/base/content/aboutTabCrashed.js @@ -38,6 +38,10 @@ 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}); document.dispatchEvent(event); diff --git a/browser/base/content/aboutTabCrashed.xhtml b/browser/base/content/aboutTabCrashed.xhtml index d15c662623be..ec1f37161bf1 100644 --- a/browser/base/content/aboutTabCrashed.xhtml +++ b/browser/base/content/aboutTabCrashed.xhtml @@ -48,6 +48,8 @@ &tabCrashed.closeTab; + diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js index 113e80c834c0..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. @@ -1143,6 +1149,13 @@ var gBrowserInit = { 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); @@ -6479,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; @@ -7582,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.xml b/browser/base/content/tabbrowser.xml index a419c9929ad8..963467ee37c3 100644 --- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1488,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 }) diff --git a/browser/components/sessionstore/test/browser_crashedTabs.js b/browser/components/sessionstore/test/browser_crashedTabs.js index 995bc14bf196..e64c304573ad 100644 --- a/browser/components/sessionstore/test/browser_crashedTabs.js +++ b/browser/components/sessionstore/test/browser_crashedTabs.js @@ -12,6 +12,9 @@ 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. @@ -217,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. @@ -248,8 +249,6 @@ 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. @@ -289,8 +288,6 @@ 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. @@ -341,15 +338,11 @@ add_task(function test_revive_tab_from_session_store() { // Crash the tab yield crashBrowser(browser); - is(newTab2.getAttribute("crashed"), "true", "Second tab should be crashed too."); - // Flush out any notifications from the crashed browser. - TabState.flush(browser); - // Use SessionStore to revive the tab clickButton(browser, "restoreTab"); - yield promiseBrowserLoaded(browser); + 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."); @@ -366,6 +359,63 @@ add_task(function test_revive_tab_from_session_store() { 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 @@ -384,8 +434,6 @@ add_task(function test_close_tab_after_crash() { // Crash the tab yield crashBrowser(browser); - // Flush out any notifications from the crashed browser. - TabState.flush(browser); let promise = promiseEvent(gBrowser.tabContainer, "TabClose"); diff --git a/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd b/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd index fa1f02c4396d..609e001989e4 100644 --- a/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd +++ b/browser/locales/en-US/chrome/browser/aboutTabCrashed.dtd @@ -8,3 +8,4 @@ + From 0f22a9962b84fd113ef244c2ee4ba45a173209ba Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Fri, 30 Jan 2015 09:14:13 -0800 Subject: [PATCH 09/16] Bug 1127026: Shimmed AboutProtocolInstance.getURIFlags always returns null. r=billm --- toolkit/components/addoncompat/RemoteAddonsChild.jsm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = { From 413230539d65ebf2f505449234f98fbc66b16d7c Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Mon, 26 Jan 2015 11:13:49 -0800 Subject: [PATCH 10/16] Bug 1123824 - Include platforms/android-VERSION in suggested mozconfig. r=me,f=psd DONTBUILD NPOTB --HG-- extra : rebase_source : a273c9939b937ddbb0be387041eb645ec726bd26 --- python/mozboot/mozboot/debian.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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): From 2a24c47a3063b3b97b6f3ae6bd207d52f6c33a34 Mon Sep 17 00:00:00 2001 From: Michael Comella Date: Thu, 29 Jan 2015 17:44:07 -0800 Subject: [PATCH 11/16] Bug 1090287 - Check that the selected tab is not null before updating progress visibility. r=rnewman --HG-- extra : rebase_source : 2f0b2824dd1192b6c57273309f8f8b97b479ceff --- mobile/android/base/toolbar/BrowserToolbar.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) { From 8381ac874543907f32fe0f038146abb38f0d00ea Mon Sep 17 00:00:00 2001 From: Ryan VanderMeulen Date: Fri, 30 Jan 2015 15:08:06 -0500 Subject: [PATCH 12/16] Bug 1126941 - Update pdf.js to version 1.0.1130. r=Mossop, r=yury --- browser/extensions/pdfjs/README.mozilla | 2 +- .../pdfjs/content/PdfStreamConverter.jsm | 6 +- browser/extensions/pdfjs/content/build/pdf.js | 329 +- .../pdfjs/content/build/pdf.worker.js | 138 +- .../extensions/pdfjs/content/web/viewer.css | 19 +- .../extensions/pdfjs/content/web/viewer.js | 2668 +++++++++-------- 6 files changed, 1890 insertions(+), 1272 deletions(-) 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; From 047b066e28d88642383ae099cbcdb6ff7992fbd1 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Fri, 30 Jan 2015 20:49:33 +0000 Subject: [PATCH 13/16] Follow-up to bug 1093780 to fix an uncovered intermittent failure. Make sure we're in offline mode when opening the chat window to stop it accessing the network. rs=MattN over irc --- .../uitour/test/browser_UITour_loop.js | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) 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 From d674965080044eb76172ca96b1e4a22cc01c5e91 Mon Sep 17 00:00:00 2001 From: Swapnil R Patil Date: Fri, 30 Jan 2015 22:36:34 +0100 Subject: [PATCH 14/16] Bug 1085428 - Replaced do_log_info with do_print. r=mak --- .../places/tests/unit/test_421483.js | 12 +- .../tests/unit/test_browserGlue_prefs.js | 34 ++-- ...erGlue_urlbar_defaultbehavior_migration.js | 16 +- .../test_1016953-renaming-uncompressed.js | 4 +- .../test_992901-backup-unsorted-hierarchy.js | 6 +- .../test_997030-bookmarks-html-encode.js | 2 +- .../tests/bookmarks/test_async_observers.js | 4 +- .../bookmarks/test_nsINavBookmarkObserver.js | 2 +- .../tests/favicons/test_replaceFaviconData.js | 12 +- .../test_replaceFaviconDataFromDataURL.js | 16 +- .../components/places/tests/head_common.js | 11 -- .../places/tests/inline/head_autocomplete.js | 4 +- .../tests/network/test_history_redirects.js | 10 +- .../places/tests/queries/test_tags.js | 148 +++++++++--------- .../unifiedcomplete/head_autocomplete.js | 14 +- .../tests/unifiedcomplete/test_416211.js | 2 +- .../tests/unifiedcomplete/test_416214.js | 2 +- .../tests/unifiedcomplete/test_417798.js | 10 +- .../tests/unifiedcomplete/test_418257.js | 10 +- .../tests/unifiedcomplete/test_422277.js | 2 +- .../test_autoFill_default_behavior.js | 52 +++--- .../test_autocomplete_functional.js | 22 +-- .../test_avoid_middle_complete.js | 22 +-- .../test_avoid_stripping_to_empty_tokens.js | 2 +- .../tests/unifiedcomplete/test_casing.js | 22 +-- .../tests/unifiedcomplete/test_do_not_trim.js | 12 +- .../test_download_embed_bookmarks.js | 12 +- .../tests/unifiedcomplete/test_dupe_urls.js | 2 +- .../unifiedcomplete/test_empty_search.js | 10 +- .../tests/unifiedcomplete/test_enabled.js | 6 +- .../tests/unifiedcomplete/test_escape_self.js | 4 +- .../unifiedcomplete/test_ignore_protocol.js | 2 +- .../unifiedcomplete/test_keyword_search.js | 16 +- .../test_keyword_search_actions.js | 16 +- .../tests/unifiedcomplete/test_keywords.js | 10 +- .../unifiedcomplete/test_match_beginning.js | 10 +- .../unifiedcomplete/test_multi_word_search.js | 12 +- .../tests/unifiedcomplete/test_queryurl.js | 8 +- .../test_searchEngine_current.js | 8 +- .../unifiedcomplete/test_searchEngine_host.js | 2 +- .../test_searchEngine_restyle.js | 4 +- .../unifiedcomplete/test_special_search.js | 76 ++++----- .../unifiedcomplete/test_swap_protocol.js | 34 ++-- .../tests/unifiedcomplete/test_tabmatches.js | 22 +-- .../tests/unifiedcomplete/test_trimming.js | 44 +++--- .../tests/unifiedcomplete/test_typed.js | 12 +- .../tests/unifiedcomplete/test_visiturl.js | 10 +- .../test_word_boundary_search.js | 28 ++-- .../unifiedcomplete/test_zero_frecency.js | 4 +- .../places/tests/unit/test_412132.js | 30 ++-- .../test_PlacesSearchAutocompleteProvider.js | 2 +- .../tests/unit/test_async_history_api.js | 18 +-- .../places/tests/unit/test_isURIVisited.js | 6 +- .../places/tests/unit/test_isvisited.js | 2 +- .../unit/test_removeVisitsByTimeframe.js | 114 +++++++------- .../places/tests/unit/test_telemetry.js | 2 +- .../unit/test_update_frecency_after_delete.js | 40 ++--- .../tests/unit/test_utils_backups_create.js | 2 +- 58 files changed, 505 insertions(+), 516 deletions(-) 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/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); } From 9fed33dfb81af91a99f7ba4792d1a6b7e9eb1a65 Mon Sep 17 00:00:00 2001 From: Brian Nicholson Date: Fri, 30 Jan 2015 16:53:27 -0800 Subject: [PATCH 15/16] Bug 1126514 - Add tile IDs to Fennec tiles. r=mfinkle --- mobile/locales/en-US/chrome/region.properties | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mobile/locales/en-US/chrome/region.properties b/mobile/locales/en-US/chrome/region.properties index 5deefc056452..546930c4bdff 100644 --- a/mobile/locales/en-US/chrome/region.properties +++ b/mobile/locales/en-US/chrome/region.properties @@ -54,15 +54,19 @@ browser.suggestedsites.list.3=fxsupport browser.suggestedsites.mozilla.title=The Mozilla Project browser.suggestedsites.mozilla.url=https://www.mozilla.org/en-US/ browser.suggestedsites.mozilla.bgcolor=#ce4e41 +browser.suggestedsites.mozilla.trackingid=632 browser.suggestedsites.fxmarketplace.title=Firefox Marketplace browser.suggestedsites.fxmarketplace.url=https://marketplace.firefox.com/ browser.suggestedsites.fxmarketplace.bgcolor=#0096dd +browser.suggestedsites.fxmarketplace.trackingid=629 browser.suggestedsites.fxaddons.title=Add-ons: Customize Firefox browser.suggestedsites.fxaddons.url=https://addons.mozilla.org/en-US/android/ browser.suggestedsites.fxaddons.bgcolor=#62be06 +browser.suggestedsites.fxaddons.trackingid=630 browser.suggestedsites.fxsupport.title=Firefox Help and Support browser.suggestedsites.fxsupport.url=https://support.mozilla.org/en-US/products/mobile browser.suggestedsites.fxsupport.bgcolor=#f37c00 +browser.suggestedsites.fxsupport.trackingid=631 From 0d4bb858d091f6f42d08e1afa9418bc479e00de5 Mon Sep 17 00:00:00 2001 From: Phil Ringnalda Date: Fri, 30 Jan 2015 19:13:32 -0800 Subject: [PATCH 16/16] Back out cebdafba3a85 (bug 1126514) for robocop bustage --- mobile/locales/en-US/chrome/region.properties | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mobile/locales/en-US/chrome/region.properties b/mobile/locales/en-US/chrome/region.properties index 546930c4bdff..5deefc056452 100644 --- a/mobile/locales/en-US/chrome/region.properties +++ b/mobile/locales/en-US/chrome/region.properties @@ -54,19 +54,15 @@ browser.suggestedsites.list.3=fxsupport browser.suggestedsites.mozilla.title=The Mozilla Project browser.suggestedsites.mozilla.url=https://www.mozilla.org/en-US/ browser.suggestedsites.mozilla.bgcolor=#ce4e41 -browser.suggestedsites.mozilla.trackingid=632 browser.suggestedsites.fxmarketplace.title=Firefox Marketplace browser.suggestedsites.fxmarketplace.url=https://marketplace.firefox.com/ browser.suggestedsites.fxmarketplace.bgcolor=#0096dd -browser.suggestedsites.fxmarketplace.trackingid=629 browser.suggestedsites.fxaddons.title=Add-ons: Customize Firefox browser.suggestedsites.fxaddons.url=https://addons.mozilla.org/en-US/android/ browser.suggestedsites.fxaddons.bgcolor=#62be06 -browser.suggestedsites.fxaddons.trackingid=630 browser.suggestedsites.fxsupport.title=Firefox Help and Support browser.suggestedsites.fxsupport.url=https://support.mozilla.org/en-US/products/mobile browser.suggestedsites.fxsupport.bgcolor=#f37c00 -browser.suggestedsites.fxsupport.trackingid=631